├── gradle.properties
├── settings.gradle
├── .gitmodules
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── lib
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ └── strings.xml
│ │ ├── drawable
│ │ │ ├── seek_line.xml
│ │ │ ├── seek_thumb.xml
│ │ │ ├── page_indicator.xml
│ │ │ ├── ic_chevron_left_white_24dp.xml
│ │ │ ├── ic_chevron_right_white_24dp.xml
│ │ │ ├── ic_format_size_white_24dp.xml
│ │ │ ├── ic_close_white_24dp.xml
│ │ │ ├── ic_toc_white_24dp.xml
│ │ │ ├── ic_link_white_24dp.xml
│ │ │ ├── ic_search_white_24dp.xml
│ │ │ ├── button.xml
│ │ │ └── ic_error_red_24dp.xml
│ │ ├── menu
│ │ │ └── layout_menu.xml
│ │ └── layout
│ │ │ └── document_activity.xml
│ │ ├── java
│ │ └── com
│ │ │ └── artifex
│ │ │ └── mupdf
│ │ │ └── viewer
│ │ │ ├── CancellableTaskDefinition.java
│ │ │ ├── SearchTaskResult.java
│ │ │ ├── MuPDFCancellableTaskDefinition.java
│ │ │ ├── Stepper.java
│ │ │ ├── Pallet.java
│ │ │ ├── OutlineActivity.java
│ │ │ ├── CancellableAsyncTask.java
│ │ │ ├── ContentInputStream.java
│ │ │ ├── PageAdapter.java
│ │ │ ├── SearchTask.java
│ │ │ ├── MuPDFCore.java
│ │ │ ├── PageView.java
│ │ │ ├── DocumentActivity.java
│ │ │ └── ReaderView.java
│ │ └── AndroidManifest.xml
└── build.gradle
├── .gitignore
├── app
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── com
│ │ │ └── artifex
│ │ │ └── mupdf
│ │ │ └── viewer
│ │ │ └── app
│ │ │ └── LibraryActivity.java
│ │ └── res
│ │ └── drawable
│ │ └── ic_mupdf.xml
└── build.gradle
├── Makefile
├── gradlew.bat
├── README
├── gradlew
└── COPYING
/gradle.properties:
--------------------------------------------------------------------------------
1 | android.useAndroidX=true
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':jni'
2 | include ':lib'
3 | include ':app'
4 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "jni"]
2 | path = jni
3 | url = ../mupdf-android-fitz.git
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArtifexSoftware/mupdf-android-viewer/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/lib/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #C0202020
4 | #C0202020
5 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .externalNativeBuild
2 | .gradle
3 | .idea
4 | *.iml
5 | android.keystore
6 | build
7 | local.properties
8 | MAVEN
9 | tags
10 | mupdf-*-android-viewer.apk
11 | mupdf-*-android-viewer--app-release.aab
12 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/lib/src/main/res/drawable/seek_line.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/CancellableTaskDefinition.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | public interface CancellableTaskDefinition
4 | {
5 | public Result doInBackground(Params ... params);
6 | public void doCancel();
7 | public void doCleanup();
8 | }
9 |
--------------------------------------------------------------------------------
/lib/src/main/res/drawable/seek_thumb.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/lib/src/main/res/drawable/page_indicator.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/lib/src/main/res/drawable/ic_chevron_left_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/lib/src/main/res/drawable/ic_chevron_right_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/lib/src/main/res/drawable/ic_format_size_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/lib/src/main/res/drawable/ic_close_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/lib/src/main/res/drawable/ic_toc_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/lib/src/main/res/drawable/ic_link_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/lib/src/main/res/drawable/ic_search_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/lib/src/main/res/drawable/button.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 |
9 | -
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/SearchTaskResult.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | import com.artifex.mupdf.fitz.Quad;
4 |
5 | public class SearchTaskResult {
6 | public final String txt;
7 | public final int pageNumber;
8 | public final Quad searchBoxes[][];
9 | static private SearchTaskResult singleton;
10 |
11 | SearchTaskResult(String _txt, int _pageNumber, Quad _searchBoxes[][]) {
12 | txt = _txt;
13 | pageNumber = _pageNumber;
14 | searchBoxes = _searchBoxes;
15 | }
16 |
17 | static public SearchTaskResult get() {
18 | return singleton;
19 | }
20 |
21 | static public void set(SearchTaskResult r) {
22 | singleton = r;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/src/main/res/drawable/ic_error_red_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/lib/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Cancel
4 | Cannot open document
5 | Cannot open document: %1$s
6 | Dismiss
7 | Enter password
8 | No further occurrences found
9 | Not supported
10 | Okay
11 | Search…
12 | Searching…
13 | Text not found
14 | Android does not allow following file:// link:
15 |
16 |
--------------------------------------------------------------------------------
/lib/src/main/res/menu/layout_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCancellableTaskDefinition.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | import com.artifex.mupdf.fitz.Cookie;
4 |
5 | public abstract class MuPDFCancellableTaskDefinition implements CancellableTaskDefinition
6 | {
7 | private Cookie cookie;
8 |
9 | public MuPDFCancellableTaskDefinition()
10 | {
11 | this.cookie = new Cookie();
12 | }
13 |
14 | @Override
15 | public void doCancel()
16 | {
17 | if (cookie == null)
18 | return;
19 |
20 | cookie.abort();
21 | }
22 |
23 | @Override
24 | public void doCleanup()
25 | {
26 | if (cookie == null)
27 | return;
28 |
29 | cookie.destroy();
30 | cookie = null;
31 | }
32 |
33 | @Override
34 | public final Result doInBackground(Params ... params)
35 | {
36 | return doInBackground(cookie, params);
37 | }
38 |
39 | public abstract Result doInBackground(Cookie cookie, Params ... params);
40 | }
41 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/Stepper.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.os.Build;
5 | import android.util.Log;
6 | import android.view.View;
7 |
8 | public class Stepper {
9 | private final String APP = "MuPDF";
10 | protected final View mPoster;
11 | protected final Runnable mTask;
12 | protected boolean mPending;
13 |
14 | public Stepper(View v, Runnable r) {
15 | mPoster = v;
16 | mTask = r;
17 | mPending = false;
18 | }
19 |
20 | @SuppressLint("NewApi")
21 | public void prod() {
22 | if (!mPending) {
23 | mPending = true;
24 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
25 | mPoster.postOnAnimation(new Runnable() {
26 | @Override
27 | public void run() {
28 | mPending = false;
29 | mTask.run();
30 | }
31 | });
32 | } else {
33 | mPoster.post(new Runnable() {
34 | @Override
35 | public void run() {
36 | mPending = false;
37 | mTask.run();
38 | }
39 | });
40 |
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | group = 'com.artifex.mupdf'
4 | version = '1.27.0a'
5 |
6 | dependencies {
7 | if (file('../lib/build.gradle').isFile())
8 | api project(':lib')
9 | else
10 | api 'com.artifex.mupdf:viewer:1.27.0a'
11 | }
12 |
13 | android {
14 | namespace 'com.artifex.mupdf.viewer.app'
15 | compileSdkVersion 33
16 | defaultConfig {
17 | minSdkVersion 21
18 | targetSdkVersion 35
19 | versionName '1.27.0a'
20 | versionCode 200
21 | }
22 |
23 | splits {
24 | abi {
25 | enable true
26 | universalApk true
27 | }
28 | }
29 |
30 | bundle {
31 | abi {
32 | enableSplit true
33 | }
34 | }
35 |
36 | if (project.hasProperty('release_storeFile')) {
37 | signingConfigs {
38 | release {
39 | storeFile file(release_storeFile)
40 | storePassword release_storePassword
41 | keyAlias release_keyAlias
42 | keyPassword release_keyPassword
43 | }
44 | }
45 | buildTypes {
46 | release {
47 | signingConfig signingConfigs.release
48 | ndk {
49 | debugSymbolLevel 'FULL'
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/Pallet.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | import android.os.Bundle;
4 |
5 | import java.util.HashMap;
6 | import java.util.Map;
7 |
8 | public class Pallet {
9 | private static Pallet instance = new Pallet();
10 | private final Map pallet = new HashMap<>();
11 | private int sequenceNumber = 0;
12 |
13 | private Pallet() {
14 | }
15 |
16 | private static Pallet getInstance() {
17 | return instance;
18 | }
19 |
20 | public static int sendBundle(Bundle bundle) {
21 | Pallet instance = getInstance();
22 | int i = instance.sequenceNumber++;
23 | if (instance.sequenceNumber < 0)
24 | instance.sequenceNumber = 0;
25 | instance.pallet.put(new Integer(i), bundle);
26 | return i;
27 | }
28 |
29 | public static Bundle receiveBundle(int number) {
30 | Bundle bundle = (Bundle) getInstance().pallet.get(new Integer(number));
31 | if (bundle != null)
32 | getInstance().pallet.remove(new Integer(number));
33 | return bundle;
34 | }
35 |
36 | public static boolean hasBundle(int number) {
37 | return getInstance().pallet.containsKey(new Integer(number));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/lib/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'maven-publish'
3 |
4 | group = 'com.artifex.mupdf'
5 | version = '1.27.0a'
6 |
7 | dependencies {
8 | implementation 'androidx.appcompat:appcompat:1.1.+'
9 | if (file('../jni/build.gradle').isFile())
10 | api project(':jni')
11 | else
12 | api 'com.artifex.mupdf:fitz:1.27.0'
13 | }
14 |
15 | android {
16 | namespace 'com.artifex.mupdf.viewer'
17 | compileSdkVersion 33
18 | defaultConfig {
19 | minSdkVersion 21
20 | targetSdkVersion 35
21 | }
22 | publishing {
23 | singleVariant("release") {
24 | withSourcesJar()
25 | }
26 | }
27 | }
28 |
29 | project.afterEvaluate {
30 | publishing {
31 | publications {
32 | release(MavenPublication) {
33 | artifactId 'viewer'
34 | artifact(bundleReleaseAar)
35 |
36 | pom {
37 | name = 'viewer'
38 | url = 'http://www.mupdf.com'
39 | licenses {
40 | license {
41 | name = 'GNU Affero General Public License'
42 | url = 'https://www.gnu.org/licenses/agpl-3.0.html'
43 | }
44 | }
45 | }
46 | pom.withXml {
47 | final dependenciesNode = asNode().appendNode('dependencies')
48 | configurations.implementation.allDependencies.each {
49 | def dependencyNode = dependenciesNode.appendNode('dependency')
50 | dependencyNode.appendNode('groupId', it.group)
51 | dependencyNode.appendNode('artifactId', it.name)
52 | dependencyNode.appendNode('version', it.version)
53 | }
54 | }
55 | }
56 | }
57 | repositories {
58 | maven {
59 | name 'Local'
60 | if (project.hasProperty('MAVEN_REPO')) {
61 | url = MAVEN_REPO
62 | } else {
63 | url = "file://${System.properties['user.home']}/MAVEN"
64 | }
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/OutlineActivity.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | import android.app.ListActivity;
4 | import android.os.Bundle;
5 | import android.util.Log;
6 | import android.view.View;
7 | import android.view.Window;
8 | import android.view.WindowManager;
9 | import android.widget.ArrayAdapter;
10 | import android.widget.ListView;
11 |
12 | import java.io.Serializable;
13 | import java.util.ArrayList;
14 |
15 | public class OutlineActivity extends ListActivity
16 | {
17 | private final String APP = "MuPDF";
18 |
19 | public static class Item implements Serializable {
20 | public String title;
21 | public int page;
22 | public Item(String title, int page) {
23 | this.title = title;
24 | this.page = page;
25 | }
26 | public String toString() {
27 | return title;
28 | }
29 | }
30 |
31 | protected ArrayAdapter- adapter;
32 |
33 | public void onCreate(Bundle savedInstanceState) {
34 | super.onCreate(savedInstanceState);
35 | requestWindowFeature(Window.FEATURE_NO_TITLE);
36 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
37 |
38 | adapter = new ArrayAdapter
- (this, android.R.layout.simple_list_item_1);
39 | setListAdapter(adapter);
40 |
41 | int idx = getIntent().getIntExtra("PALLETBUNDLE", -1);
42 | Bundle bundle = Pallet.receiveBundle(idx);
43 | if (bundle != null) {
44 | int currentPage = bundle.getInt("POSITION");
45 | ArrayList
- outline = (ArrayList
- )bundle.getSerializable("OUTLINE");
46 | int found = -1;
47 | for (int i = 0; i < outline.size(); ++i) {
48 | Item item = outline.get(i);
49 | if (found < 0 && item.page >= currentPage)
50 | found = i;
51 | adapter.add(item);
52 | }
53 | if (found >= 0)
54 | setSelection(found);
55 | }
56 | }
57 |
58 | protected void onListItemClick(ListView l, View v, int position, long id) {
59 | Item item = adapter.getItem(position);
60 | setResult(RESULT_FIRST_USER + item.page);
61 | finish();
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/CancellableAsyncTask.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | import android.os.AsyncTask;
4 | import android.util.Log;
5 |
6 | import java.util.concurrent.CancellationException;
7 | import java.util.concurrent.ExecutionException;
8 |
9 | // Ideally this would be a subclass of AsyncTask, however the cancel() method is final, and cannot
10 | // be overridden. I felt that having two different, but similar cancel methods was a bad idea.
11 | public class CancellableAsyncTask
12 | {
13 | private final String APP = "MuPDF";
14 |
15 | private final AsyncTask asyncTask;
16 | private final CancellableTaskDefinition ourTask;
17 |
18 | public void onPreExecute()
19 | {
20 |
21 | }
22 |
23 | public void onPostExecute(Result result)
24 | {
25 |
26 | }
27 |
28 | public CancellableAsyncTask(final CancellableTaskDefinition task)
29 | {
30 | if (task == null)
31 | throw new IllegalArgumentException();
32 |
33 | this.ourTask = task;
34 | asyncTask = new AsyncTask()
35 | {
36 | @Override
37 | protected Result doInBackground(Params... params)
38 | {
39 | return task.doInBackground(params);
40 | }
41 |
42 | @Override
43 | protected void onPreExecute()
44 | {
45 | CancellableAsyncTask.this.onPreExecute();
46 | }
47 |
48 | @Override
49 | protected void onPostExecute(Result result)
50 | {
51 | CancellableAsyncTask.this.onPostExecute(result);
52 | task.doCleanup();
53 | }
54 |
55 | @Override
56 | protected void onCancelled(Result result)
57 | {
58 | task.doCleanup();
59 | }
60 | };
61 | }
62 |
63 | public void cancel()
64 | {
65 | this.asyncTask.cancel(true);
66 | ourTask.doCancel();
67 |
68 | try
69 | {
70 | this.asyncTask.get();
71 | }
72 | catch (InterruptedException e)
73 | {
74 | }
75 | catch (ExecutionException e)
76 | {
77 | }
78 | catch (CancellationException e)
79 | {
80 | }
81 | }
82 |
83 | public void execute(Params ... params)
84 | {
85 | asyncTask.execute(params);
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # This is a very simple Makefile that calls 'gradlew' to do the heavy lifting.
2 |
3 | default: debug
4 |
5 | debug:
6 | ./gradlew --warning-mode=all assembleDebug bundleDebug
7 | release:
8 | ./gradlew --warning-mode=all assembleRelease bundleRelease
9 | install:
10 | ./gradlew --warning-mode=all installDebug
11 | install-release:
12 | ./gradlew --warning-mode=all installRelease
13 | lint:
14 | ./gradlew --warning-mode=all lint
15 | archive:
16 | ./gradlew --warning-mode=all publishReleasePublicationToLocalRepository
17 | sync: archive
18 | rsync -av --chmod=g+w --chown=:gs-web \
19 | $(HOME)/MAVEN/com/artifex/mupdf/viewer/$(shell git describe --tags)/ \
20 | ghostscript.com:/var/www/maven.ghostscript.com/com/artifex/mupdf/viewer/$(shell git describe --tags)/
21 | rsync -av --chmod=g+w --chown=:gs-web \
22 | $(HOME)/MAVEN/com/artifex/mupdf/viewer/maven-metadata.xml* \
23 | ghostscript.com:/var/www/maven.ghostscript.com/com/artifex/mupdf/viewer/
24 |
25 | tarball: release
26 | cp app/build/outputs/apk/release/app-universal-release.apk \
27 | mupdf-$(shell git describe --tags)-android-viewer.apk
28 | cp app/build/outputs/bundle/release/app-release.aab \
29 | mupdf-$(shell git describe --tags)-android-viewer-app-release.aab
30 | synctarball: tarball
31 | rsync -av --chmod=g+w --chown=:gs-web \
32 | mupdf-$(shell git describe --tags)-android-viewer.apk \
33 | ghostscript.com:/var/www/mupdf.com/downloads/archive/mupdf-$(shell git describe --tags)-android-viewer.apk
34 | rsync -av --chmod=g+w --chown=:gs-web \
35 | mupdf-$(shell git describe --tags)-android-viewer-app-release.aab \
36 | ghostscript.com:/var/www/mupdf.com/downloads/archive/mupdf-$(shell git describe --tags)-android-viewer-app-release.aab
37 |
38 | run: install
39 | adb shell am start -n com.artifex.mupdf.viewer.app/.LibraryActivity
40 | run-release: install-release
41 | adb shell am start -n com.artifex.mupdf.viewer.app/.LibraryActivity
42 |
43 | clean:
44 | rm -rf .gradle build
45 | rm -rf jni/.cxx jni/.externalNativeBuild jni/.gradle jni/build
46 | rm -rf lib/.gradle lib/build
47 | rm -rf app/.gradle app/build/generated app/build/intermediates app/build/tmp app/build/outputs/logs app/build/outputs/bundle
48 |
49 | nuke: clean
50 | rm -rf app/build
51 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/ContentInputStream.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | import com.artifex.mupdf.fitz.SeekableInputStream;
4 |
5 | import android.util.Log;
6 | import android.content.ContentResolver;
7 | import android.net.Uri;
8 |
9 | import java.io.FileNotFoundException;
10 | import java.io.IOException;
11 | import java.io.InputStream;
12 | import java.io.PrintWriter;
13 | import java.io.StringWriter;
14 |
15 | public class ContentInputStream implements SeekableInputStream {
16 | private final String APP = "MuPDF";
17 |
18 | protected ContentResolver cr;
19 | protected Uri uri;
20 | protected InputStream is;
21 | protected long length, p;
22 | protected boolean mustReopenStream;
23 |
24 | public ContentInputStream(ContentResolver cr, Uri uri, long size) throws IOException {
25 | this.cr = cr;
26 | this.uri = uri;
27 | length = size;
28 | mustReopenStream = false;
29 | reopenStream();
30 | }
31 |
32 | public long seek(long offset, int whence) throws IOException {
33 | long newp = p;
34 | switch (whence) {
35 | case SEEK_SET:
36 | newp = offset;
37 | break;
38 | case SEEK_CUR:
39 | newp = p + offset;
40 | break;
41 | case SEEK_END:
42 | if (length < 0) {
43 | byte[] buf = new byte[16384];
44 | int k;
45 | while ((k = is.read(buf)) != -1)
46 | p += k;
47 | length = p;
48 | }
49 | newp = length + offset;
50 | break;
51 | }
52 |
53 | if (newp < p) {
54 | if (!mustReopenStream) {
55 | try {
56 | is.skip(newp - p);
57 | } catch (IOException x) {
58 | Log.i(APP, "Unable to skip backwards, reopening input stream");
59 | mustReopenStream = true;
60 | }
61 | }
62 | if (mustReopenStream) {
63 | reopenStream();
64 | is.skip(newp);
65 | }
66 | } else if (newp > p) {
67 | is.skip(newp - p);
68 | }
69 | return p = newp;
70 | }
71 |
72 | public long position() throws IOException {
73 | return p;
74 | }
75 |
76 | public int read(byte[] buf) throws IOException {
77 | int n = is.read(buf);
78 | if (n > 0)
79 | p += n;
80 | else if (n < 0 && length < 0)
81 | length = p;
82 | return n;
83 | }
84 |
85 | public void reopenStream() throws IOException {
86 | if (is != null)
87 | {
88 | is.close();
89 | is = null;
90 | }
91 | is = cr.openInputStream(uri);
92 | p = 0;
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/app/src/main/java/com/artifex/mupdf/viewer/app/LibraryActivity.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer.app;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.net.Uri;
6 | import android.os.Bundle;
7 | import android.util.Log;
8 |
9 | import com.artifex.mupdf.fitz.Document; /* for file name recognition */
10 | import com.artifex.mupdf.viewer.DocumentActivity;
11 |
12 | public class LibraryActivity extends Activity
13 | {
14 | private final String APP = "MuPDF";
15 |
16 | protected final int FILE_REQUEST = 42;
17 | protected boolean selectingDocument;
18 |
19 | public void onCreate(Bundle savedInstanceState) {
20 | super.onCreate(savedInstanceState);
21 | selectingDocument = false;
22 | }
23 |
24 | public void onStart() {
25 | super.onStart();
26 | if (!selectingDocument)
27 | {
28 | Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
29 | intent.addCategory(Intent.CATEGORY_OPENABLE);
30 | intent.setType("*/*");
31 | intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {
32 | // open the mime-types we know about
33 | "application/pdf",
34 | "application/vnd.ms-xpsdocument",
35 | "application/oxps",
36 | "application/x-cbz",
37 | "application/vnd.comicbook+zip",
38 | "application/epub+zip",
39 | "application/x-fictionbook",
40 | "application/x-mobipocket-ebook",
41 | // ... and the ones android doesn't know about
42 | "application/octet-stream"
43 | });
44 |
45 | startActivityForResult(intent, FILE_REQUEST);
46 | selectingDocument = true;
47 | }
48 | }
49 |
50 | public void onActivityResult(int request, int result, Intent data) {
51 | if (request == FILE_REQUEST && result == Activity.RESULT_OK) {
52 | if (data != null) {
53 | Intent intent = new Intent(this, DocumentActivity.class);
54 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
55 | intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
56 | intent.setAction(Intent.ACTION_VIEW);
57 | intent.setDataAndType(data.getData(), data.getType());
58 | intent.putExtra(getComponentName().getPackageName() + ".ReturnToLibraryActivity", 1);
59 | startActivity(intent);
60 | }
61 | if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.S_V2)
62 | finish();
63 | } else if (request == FILE_REQUEST && result == Activity.RESULT_CANCELED) {
64 | finish();
65 | }
66 | selectingDocument = false;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_mupdf.xml:
--------------------------------------------------------------------------------
1 |
6 |
8 |
10 |
16 |
19 |
22 |
23 |
24 |
25 |
28 |
31 |
34 |
37 |
40 |
43 |
46 |
49 |
52 |
53 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/PageAdapter.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.Point;
6 | import android.graphics.PointF;
7 | import android.util.Log;
8 | import android.util.SparseArray;
9 | import android.view.View;
10 | import android.view.ViewGroup;
11 | import android.widget.BaseAdapter;
12 | import android.os.AsyncTask;
13 |
14 | public class PageAdapter extends BaseAdapter {
15 | private final String APP = "MuPDF";
16 | private final Context mContext;
17 | private final MuPDFCore mCore;
18 | private final SparseArray mPageSizes = new SparseArray();
19 | private Bitmap mSharedHqBm;
20 |
21 | public PageAdapter(Context c, MuPDFCore core) {
22 | mContext = c;
23 | mCore = core;
24 | }
25 |
26 | public int getCount() {
27 | try {
28 | return mCore.countPages();
29 | } catch (RuntimeException e) {
30 | return 0;
31 | }
32 | }
33 |
34 | public Object getItem(int position) {
35 | return null;
36 | }
37 |
38 | public long getItemId(int position) {
39 | return 0;
40 | }
41 |
42 | public synchronized void releaseBitmaps()
43 | {
44 | // recycle and release the shared bitmap.
45 | if (mSharedHqBm!=null)
46 | mSharedHqBm.recycle();
47 | mSharedHqBm = null;
48 | }
49 |
50 | public void refresh() {
51 | mPageSizes.clear();
52 | }
53 |
54 | public synchronized View getView(final int position, View convertView, ViewGroup parent) {
55 | final PageView pageView;
56 | if (convertView == null) {
57 | if (mSharedHqBm == null || mSharedHqBm.getWidth() != parent.getWidth() || mSharedHqBm.getHeight() != parent.getHeight())
58 | {
59 | if (parent.getWidth() > 0 && parent.getHeight() > 0)
60 | mSharedHqBm = Bitmap.createBitmap(parent.getWidth(), parent.getHeight(), Bitmap.Config.ARGB_8888);
61 | else
62 | mSharedHqBm = null;
63 | }
64 |
65 | pageView = new PageView(mContext, mCore, new Point(parent.getWidth(), parent.getHeight()), mSharedHqBm);
66 | } else {
67 | pageView = (PageView) convertView;
68 | }
69 |
70 | PointF pageSize = mPageSizes.get(position);
71 | if (pageSize != null) {
72 | // We already know the page size. Set it up
73 | // immediately
74 | pageView.setPage(position, pageSize);
75 | } else {
76 | // Page size as yet unknown. Blank it for now, and
77 | // start a background task to find the size
78 | pageView.blank(position);
79 | AsyncTask sizingTask = new AsyncTask() {
80 | @Override
81 | protected PointF doInBackground(Void... arg0) {
82 | try {
83 | return mCore.getPageSize(position);
84 | } catch (RuntimeException e) {
85 | return null;
86 | }
87 | }
88 |
89 | @Override
90 | protected void onPostExecute(PointF result) {
91 | super.onPostExecute(result);
92 | // We now know the page size
93 | mPageSizes.put(position, result);
94 | // Check that this view hasn't been reused for
95 | // another page since we started
96 | if (pageView.getPage() == position)
97 | pageView.setPage(position, result);
98 | }
99 | };
100 |
101 | sizingTask.execute((Void)null);
102 | }
103 | return pageView;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
34 |
35 | @rem Find java.exe
36 | if defined JAVA_HOME goto findJavaFromJavaHome
37 |
38 | set JAVA_EXE=java.exe
39 | %JAVA_EXE% -version >NUL 2>&1
40 | if "%ERRORLEVEL%" == "0" goto init
41 |
42 | echo.
43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
44 | echo.
45 | echo Please set the JAVA_HOME variable in your environment to match the
46 | echo location of your Java installation.
47 |
48 | goto fail
49 |
50 | :findJavaFromJavaHome
51 | set JAVA_HOME=%JAVA_HOME:"=%
52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
53 |
54 | if exist "%JAVA_EXE%" goto init
55 |
56 | echo.
57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
58 | echo.
59 | echo Please set the JAVA_HOME variable in your environment to match the
60 | echo location of your Java installation.
61 |
62 | goto fail
63 |
64 | :init
65 | @rem Get command-line arguments, handling Windows variants
66 |
67 | if not "%OS%" == "Windows_NT" goto win9xME_args
68 |
69 | :win9xME_args
70 | @rem Slurp the command line arguments.
71 | set CMD_LINE_ARGS=
72 | set _SKIP=2
73 |
74 | :win9xME_args_slurp
75 | if "x%~1" == "x" goto execute
76 |
77 | set CMD_LINE_ARGS=%*
78 |
79 | :execute
80 | @rem Setup the command line
81 |
82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
83 |
84 | @rem Execute Gradle
85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
86 |
87 | :end
88 | @rem End local scope for the variables with windows NT shell
89 | if "%ERRORLEVEL%"=="0" goto mainEnd
90 |
91 | :fail
92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
93 | rem the _cmd.exe /c_ return code!
94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
95 | exit /b 1
96 |
97 | :mainEnd
98 | if "%OS%"=="Windows_NT" endlocal
99 |
100 | :omega
101 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/SearchTask.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | import com.artifex.mupdf.fitz.Quad;
4 |
5 | import android.app.AlertDialog;
6 | import android.app.ProgressDialog;
7 | import android.content.Context;
8 | import android.content.DialogInterface;
9 | import android.os.Handler;
10 | import android.os.AsyncTask;
11 | import android.util.Log;
12 |
13 | class ProgressDialogX extends ProgressDialog {
14 | public ProgressDialogX(Context context) {
15 | super(context);
16 | }
17 |
18 | private boolean mCancelled = false;
19 |
20 | public boolean isCancelled() {
21 | return mCancelled;
22 | }
23 |
24 | @Override
25 | public void cancel() {
26 | mCancelled = true;
27 | super.cancel();
28 | }
29 | }
30 |
31 | public abstract class SearchTask {
32 | private final String APP = "MuPDF";
33 |
34 | private static final int SEARCH_PROGRESS_DELAY = 200;
35 | private final Context mContext;
36 | private final MuPDFCore mCore;
37 | private final Handler mHandler;
38 | private final AlertDialog.Builder mAlertBuilder;
39 | private AsyncTask mSearchTask;
40 |
41 | public SearchTask(Context context, MuPDFCore core) {
42 | mContext = context;
43 | mCore = core;
44 | mHandler = new Handler();
45 | mAlertBuilder = new AlertDialog.Builder(context);
46 | }
47 |
48 | protected abstract void onTextFound(SearchTaskResult result);
49 |
50 | public void stop() {
51 | if (mSearchTask != null) {
52 | mSearchTask.cancel(true);
53 | mSearchTask = null;
54 | }
55 | }
56 |
57 | public void go(final String text, int direction, int displayPage, int searchPage) {
58 | if (mCore == null)
59 | return;
60 | stop();
61 |
62 | final int increment = direction;
63 | final int startIndex = searchPage == -1 ? displayPage : searchPage + increment;
64 |
65 | final ProgressDialogX progressDialog = new ProgressDialogX(mContext);
66 | progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
67 | progressDialog.setTitle(mContext.getString(R.string.searching_));
68 | progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
69 | public void onCancel(DialogInterface dialog) {
70 | stop();
71 | }
72 | });
73 | progressDialog.setMax(mCore.countPages());
74 |
75 | mSearchTask = new AsyncTask() {
76 | @Override
77 | protected SearchTaskResult doInBackground(Void... params) {
78 | int index = startIndex;
79 |
80 | while (0 <= index && index < mCore.countPages() && !isCancelled()) {
81 | publishProgress(index);
82 | Quad searchHits[][] = mCore.searchPage(index, text);
83 |
84 | if (searchHits != null && searchHits.length > 0)
85 | return new SearchTaskResult(text, index, searchHits);
86 |
87 | index += increment;
88 | }
89 | return null;
90 | }
91 |
92 | @Override
93 | protected void onPostExecute(SearchTaskResult result) {
94 | progressDialog.cancel();
95 | if (result != null) {
96 | onTextFound(result);
97 | } else {
98 | mAlertBuilder.setTitle(SearchTaskResult.get() == null ? R.string.text_not_found : R.string.no_further_occurrences_found);
99 | AlertDialog alert = mAlertBuilder.create();
100 | alert.setButton(AlertDialog.BUTTON_POSITIVE, mContext.getString(R.string.dismiss),
101 | (DialogInterface.OnClickListener)null);
102 | alert.show();
103 | }
104 | }
105 |
106 | @Override
107 | protected void onCancelled() {
108 | progressDialog.cancel();
109 | }
110 |
111 | @Override
112 | protected void onProgressUpdate(Integer... values) {
113 | progressDialog.setProgress(values[0].intValue());
114 | }
115 |
116 | @Override
117 | protected void onPreExecute() {
118 | super.onPreExecute();
119 | mHandler.postDelayed(new Runnable() {
120 | public void run() {
121 | if (!progressDialog.isCancelled())
122 | {
123 | progressDialog.show();
124 | progressDialog.setProgress(startIndex);
125 | }
126 | }
127 | }, SEARCH_PROGRESS_DELAY);
128 | }
129 | };
130 |
131 | mSearchTask.execute();
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | # MuPDF Android Viewer
2 |
3 | This project is a simplified variant of the full MuPDF Android app that only
4 | supports viewing documents. The annotation editing and form filling features
5 | are not present here.
6 |
7 | This project builds both a viewer library and a viewer application.
8 | The viewer library can be used to view PDF and other documents.
9 |
10 | The application is a simple file chooser that shows a list of documents on the
11 | external storage on your device, and hands off the selected file to the viewer
12 | library.
13 |
14 | ## License
15 |
16 | MuPDF is Copyright (c) 2006-2017 Artifex Software, Inc.
17 |
18 | This program is free software: you can redistribute it and/or modify it under
19 | the terms of the GNU Affero General Public License as published by the Free
20 | Software Foundation, either version 3 of the License, or (at your option) any
21 | later version.
22 |
23 | This program is distributed in the hope that it will be useful, but WITHOUT ANY
24 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
25 | PARTICULAR PURPOSE. See the GNU General Public License for more details.
26 |
27 | You should have received a copy of the GNU Affero General Public License along
28 | with this program. If not, see .
29 |
30 | ## Prerequisites
31 |
32 | You need a working Android development environment, consisting of the Android
33 | SKD and the Android NDK. The easiest way is to use Android Studio to download
34 | and install the SDK and NDK.
35 |
36 | ## Building
37 |
38 | Download the project using Git:
39 |
40 | $ git clone git://git.ghostscript.com/mupdf-android-viewer.git
41 |
42 | Edit the local.properties file to point to your Android SDK directory:
43 |
44 | $ echo sdk.dir=$HOME/Android/Sdk > local.properties
45 |
46 | If all tools have been installed as per the prerequisites, build the app using
47 | the gradle wrapper:
48 |
49 | $ ./gradlew assembleRelease
50 |
51 | If all has gone well, you can now find the app APKs in app/build/outputs/apk/,
52 | with one APK for each ABI. There is also a universal APK which supports all
53 | ABIs.
54 |
55 | The library component can be found in lib/build/outputs/aar/lib-release.aar.
56 |
57 | ## Running
58 |
59 | To run the app in the android emulator, first you'll need to set up an "Android
60 | Virtual Device" for the emulator. Run "android avd" and create a new device.
61 | You can also use Android Studio to set up a virtual device. Use the x86 ABI for
62 | best emulator performance.
63 |
64 | Then launch the emulator, or connect a device with USB debugging enabled:
65 |
66 | $ emulator -avd MyVirtualDevice &
67 |
68 | Then copy some test files to the device:
69 |
70 | $ adb push file.pdf /mnt/sdcard/Download
71 |
72 | Then install the app on the device:
73 |
74 | $ ./gradlew installDebug
75 |
76 | To start the installed app on the device:
77 |
78 | $ adb shell am start -n com.artifex.mupdf.viewer.app/.LibraryActivity
79 |
80 | To see the error and debugging message log:
81 |
82 | $ adb logcat
83 |
84 | ## Building the JNI library locally
85 |
86 | The viewer library here is 100% pure java, but it uses the MuPDF fitz library,
87 | which provides access to PDF rendering and other low level functionality.
88 | The default is to use the JNI library artifact from the Ghostscript Maven
89 | repository.
90 |
91 | If you want to build the JNI code yourself, you will need to check out the
92 | 'jni' submodule recursively. You will also need a working host development
93 | environment with a C compiler and GNU Make.
94 |
95 | Either clone the original project with the --recursive flag, or initialize all
96 | the submodules recursively by hand:
97 |
98 | mupdf-mini $ git submodule update --init
99 | mupdf-mini $ cd jni
100 | mupdf-mini/jni $ git submodule update --init
101 | mupdf-mini/jni $ cd libmupdf
102 | mupdf-mini/jni/libmupdf $ git submodule update --init
103 |
104 | Then you need to run the 'make generate' step in the libmupdf directory:
105 |
106 | mupdf-mini/jni/libmupdf $ make generate
107 |
108 | Once this is done, the build system should pick up the local JNI library
109 | instead of using the Maven artifact.
110 |
111 | ## Release
112 |
113 | To do a release you MUST first change the package name!
114 |
115 | Do NOT use the com.artifex domain for your custom app!
116 |
117 | In order to sign a release build, you will need to create a key and a key
118 | store.
119 |
120 | $ keytool -genkey -v -keystore app/android.keystore -alias MyKey \
121 | -validity 3650 -keysize 2048 -keyalg RSA
122 |
123 | Then add the following entries to app/gradle.properties:
124 |
125 | release_storeFile=android.keystore
126 | release_storePassword=
127 | release_keyAlias=MyKey
128 | release_keyPassword=
129 |
130 | If your keystore has been set up properly, you can now build a signed release.
131 |
132 | ## Maven
133 |
134 | The library component of this project can be packaged as a Maven artifact.
135 |
136 | The default is to create the Maven artifact in the 'MAVEN' directory. You can
137 | copy thoes files to the distribution site manually, or you can change the
138 | uploadArchives repository in build.gradle before running the uploadArchives
139 | task.
140 |
141 | $ ./gradlew uploadArchives
142 |
143 | Good Luck!
144 |
--------------------------------------------------------------------------------
/lib/src/main/res/layout/document_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
16 |
17 |
24 |
25 |
31 |
32 |
45 |
46 |
53 |
54 |
61 |
62 |
70 |
71 |
78 |
79 |
80 |
81 |
87 |
88 |
95 |
96 |
112 |
113 |
120 |
121 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
142 |
143 |
152 |
153 |
165 |
166 |
167 |
168 |
179 |
180 |
181 |
182 |
183 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 | # Determine the Java command to use to start the JVM.
86 | if [ -n "$JAVA_HOME" ] ; then
87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
88 | # IBM's JDK on AIX uses strange locations for the executables
89 | JAVACMD="$JAVA_HOME/jre/sh/java"
90 | else
91 | JAVACMD="$JAVA_HOME/bin/java"
92 | fi
93 | if [ ! -x "$JAVACMD" ] ; then
94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
95 |
96 | Please set the JAVA_HOME variable in your environment to match the
97 | location of your Java installation."
98 | fi
99 | else
100 | JAVACMD="java"
101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
102 |
103 | Please set the JAVA_HOME variable in your environment to match the
104 | location of your Java installation."
105 | fi
106 |
107 | # Increase the maximum file descriptors if we can.
108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
109 | MAX_FD_LIMIT=`ulimit -H -n`
110 | if [ $? -eq 0 ] ; then
111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
112 | MAX_FD="$MAX_FD_LIMIT"
113 | fi
114 | ulimit -n $MAX_FD
115 | if [ $? -ne 0 ] ; then
116 | warn "Could not set maximum file descriptor limit: $MAX_FD"
117 | fi
118 | else
119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
120 | fi
121 | fi
122 |
123 | # For Darwin, add options to specify how the application appears in the dock
124 | if $darwin; then
125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
126 | fi
127 |
128 | # For Cygwin or MSYS, switch paths to Windows format before running java
129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
132 | JAVACMD=`cygpath --unix "$JAVACMD"`
133 |
134 | # We build the pattern for arguments to be converted via cygpath
135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
136 | SEP=""
137 | for dir in $ROOTDIRSRAW ; do
138 | ROOTDIRS="$ROOTDIRS$SEP$dir"
139 | SEP="|"
140 | done
141 | OURCYGPATTERN="(^($ROOTDIRS))"
142 | # Add a user-defined pattern to the cygpath arguments
143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
145 | fi
146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
147 | i=0
148 | for arg in "$@" ; do
149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
151 |
152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
154 | else
155 | eval `echo args$i`="\"$arg\""
156 | fi
157 | i=$((i+1))
158 | done
159 | case $i in
160 | (0) set -- ;;
161 | (1) set -- "$args0" ;;
162 | (2) set -- "$args0" "$args1" ;;
163 | (3) set -- "$args0" "$args1" "$args2" ;;
164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
170 | esac
171 | fi
172 |
173 | # Escape application args
174 | save () {
175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
176 | echo " "
177 | }
178 | APP_ARGS=$(save "$@")
179 |
180 | # Collect all arguments for the java command, following the shell quoting and substitution rules
181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
182 |
183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
185 | cd "$(dirname "$0")"
186 | fi
187 |
188 | exec "$JAVACMD" "$@"
189 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCore.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | import com.artifex.mupdf.fitz.Cookie;
4 | import com.artifex.mupdf.fitz.DisplayList;
5 | import com.artifex.mupdf.fitz.Document;
6 | import com.artifex.mupdf.fitz.Link;
7 | import com.artifex.mupdf.fitz.Matrix;
8 | import com.artifex.mupdf.fitz.Outline;
9 | import com.artifex.mupdf.fitz.Page;
10 | import com.artifex.mupdf.fitz.Quad;
11 | import com.artifex.mupdf.fitz.Rect;
12 | import com.artifex.mupdf.fitz.RectI;
13 | import com.artifex.mupdf.fitz.SeekableInputStream;
14 | import com.artifex.mupdf.fitz.android.AndroidDrawDevice;
15 |
16 | import android.content.Context;
17 | import android.graphics.Bitmap;
18 | import android.graphics.PointF;
19 | import android.util.Log;
20 |
21 | import java.util.ArrayList;
22 |
23 | public class MuPDFCore
24 | {
25 | private final String APP = "MuPDF";
26 |
27 | private final int MAXIMUM_OUTLINE_ITEMS = 1000;
28 | private final int MAXIMUM_OUTLINE_DEPTH = 4;
29 |
30 | private int resolution;
31 | private Document doc;
32 | private Outline[] outline;
33 | private int pageCount = -1;
34 | private boolean reflowable = false;
35 | private int currentPage;
36 | private Page page;
37 | private float pageWidth;
38 | private float pageHeight;
39 | private DisplayList displayList;
40 |
41 | /* Default to "A Format" pocket book size. */
42 | private int layoutW = 312;
43 | private int layoutH = 504;
44 | private int layoutEM = 10;
45 |
46 | private boolean outlineTruncated;
47 |
48 | private MuPDFCore(Document doc) {
49 | this.doc = doc;
50 | doc.layout(layoutW, layoutH, layoutEM);
51 | pageCount = doc.countPages();
52 | reflowable = doc.isReflowable();
53 | resolution = 160;
54 | currentPage = -1;
55 | }
56 |
57 | public MuPDFCore(byte buffer[], String magic) {
58 | this(Document.openDocument(buffer, magic));
59 | }
60 |
61 | public MuPDFCore(SeekableInputStream stm, String magic) {
62 | this(Document.openDocument(stm, magic));
63 | }
64 |
65 | public String getTitle() {
66 | return doc.getMetaData(Document.META_INFO_TITLE);
67 | }
68 |
69 | public int countPages() {
70 | return pageCount;
71 | }
72 |
73 | public boolean isReflowable() {
74 | return reflowable;
75 | }
76 |
77 | public synchronized int layout(int oldPage, int w, int h, int em) {
78 | if (w != layoutW || h != layoutH || em != layoutEM) {
79 | System.out.println("LAYOUT: " + w + "," + h);
80 | layoutW = w;
81 | layoutH = h;
82 | layoutEM = em;
83 | long mark = doc.makeBookmark(doc.locationFromPageNumber(oldPage));
84 | doc.layout(layoutW, layoutH, layoutEM);
85 | currentPage = -1;
86 | pageCount = doc.countPages();
87 | outline = null;
88 | try {
89 | outline = doc.loadOutline();
90 | } catch (Exception ex) {
91 | /* ignore error */
92 | }
93 | return doc.pageNumberFromLocation(doc.findBookmark(mark));
94 | }
95 | return oldPage;
96 | }
97 |
98 | private synchronized void gotoPage(int pageNum) {
99 | /* TODO: page cache */
100 | if (pageNum > pageCount-1)
101 | pageNum = pageCount-1;
102 | else if (pageNum < 0)
103 | pageNum = 0;
104 | if (pageNum != currentPage) {
105 | if (page != null)
106 | page.destroy();
107 | page = null;
108 | if (displayList != null)
109 | displayList.destroy();
110 | displayList = null;
111 | page = null;
112 | pageWidth = 0;
113 | pageHeight = 0;
114 | currentPage = -1;
115 |
116 | if (doc != null) {
117 | page = doc.loadPage(pageNum);
118 | Rect b = page.getBounds();
119 | pageWidth = b.x1 - b.x0;
120 | pageHeight = b.y1 - b.y0;
121 | }
122 |
123 | currentPage = pageNum;
124 | }
125 | }
126 |
127 | public synchronized PointF getPageSize(int pageNum) {
128 | gotoPage(pageNum);
129 | return new PointF(pageWidth, pageHeight);
130 | }
131 |
132 | public synchronized void onDestroy() {
133 | if (displayList != null)
134 | displayList.destroy();
135 | displayList = null;
136 | if (page != null)
137 | page.destroy();
138 | page = null;
139 | if (doc != null)
140 | doc.destroy();
141 | doc = null;
142 | }
143 |
144 | public synchronized void drawPage(Bitmap bm, int pageNum,
145 | int pageW, int pageH,
146 | int patchX, int patchY,
147 | int patchW, int patchH,
148 | Cookie cookie) {
149 | gotoPage(pageNum);
150 |
151 | if (displayList == null && page != null)
152 | try {
153 | displayList = page.toDisplayList();
154 | } catch (Exception ex) {
155 | displayList = null;
156 | }
157 |
158 | if (displayList == null || page == null)
159 | return;
160 |
161 | float zoom = resolution / 72;
162 | Matrix ctm = new Matrix(zoom, zoom);
163 | RectI bbox = new RectI(page.getBounds().transform(ctm));
164 | float xscale = (float)pageW / (float)(bbox.x1-bbox.x0);
165 | float yscale = (float)pageH / (float)(bbox.y1-bbox.y0);
166 | ctm.scale(xscale, yscale);
167 |
168 | AndroidDrawDevice dev = new AndroidDrawDevice(bm, patchX, patchY);
169 | try {
170 | displayList.run(dev, ctm, cookie);
171 | dev.close();
172 | } finally {
173 | dev.destroy();
174 | }
175 | }
176 |
177 | public synchronized void updatePage(Bitmap bm, int pageNum,
178 | int pageW, int pageH,
179 | int patchX, int patchY,
180 | int patchW, int patchH,
181 | Cookie cookie) {
182 | drawPage(bm, pageNum, pageW, pageH, patchX, patchY, patchW, patchH, cookie);
183 | }
184 |
185 | public synchronized Link[] getPageLinks(int pageNum) {
186 | gotoPage(pageNum);
187 | return page != null ? page.getLinks() : null;
188 | }
189 |
190 | public synchronized int resolveLink(Link link) {
191 | return doc.pageNumberFromLocation(doc.resolveLink(link));
192 | }
193 |
194 | public synchronized Quad[][] searchPage(int pageNum, String text) {
195 | gotoPage(pageNum);
196 | return page.search(text);
197 | }
198 |
199 | public synchronized boolean hasOutline() {
200 | if (outline == null) {
201 | try {
202 | outline = doc.loadOutline();
203 | } catch (Exception ex) {
204 | /* ignore error */
205 | }
206 | }
207 | return outline != null;
208 | }
209 |
210 | private void flattenOutlineNodes(ArrayList result, Outline list[], String indent, int depth) {
211 | for (Outline node : list) {
212 | if (node.title != null) {
213 | int page = doc.pageNumberFromLocation(doc.resolveLink(node));
214 | if (result.size() >= MAXIMUM_OUTLINE_ITEMS)
215 | outlineTruncated = true;
216 | else
217 | result.add(new OutlineActivity.Item(indent + node.title, page));
218 | }
219 | if (node.down != null)
220 | {
221 | if (depth >= MAXIMUM_OUTLINE_DEPTH || result.size() >= MAXIMUM_OUTLINE_ITEMS)
222 | outlineTruncated = true;
223 | else
224 | flattenOutlineNodes(result, node.down, indent + " ", depth + 1);
225 | }
226 | }
227 | }
228 |
229 | public synchronized ArrayList getOutline() {
230 | outlineTruncated = false;
231 | ArrayList result = new ArrayList();
232 | flattenOutlineNodes(result, outline, "", 0);
233 | return result;
234 | }
235 |
236 | public synchronized boolean wasOutlineTruncated() {
237 | return outlineTruncated;
238 | }
239 |
240 | public synchronized boolean needsPassword() {
241 | return doc.needsPassword();
242 | }
243 |
244 | public synchronized boolean authenticatePassword(String password) {
245 | boolean authenticated = doc.authenticatePassword(password);
246 | pageCount = doc.countPages();
247 | reflowable = doc.isReflowable();
248 | return authenticated;
249 | }
250 | }
251 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/PageView.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | import com.artifex.mupdf.fitz.Cookie;
4 | import com.artifex.mupdf.fitz.Link;
5 | import com.artifex.mupdf.fitz.Quad;
6 |
7 | import java.util.ArrayList;
8 | import java.util.Iterator;
9 |
10 | import android.annotation.TargetApi;
11 | import android.app.AlertDialog;
12 | import android.content.ClipData;
13 | import android.content.Context;
14 | import android.content.DialogInterface;
15 | import android.content.Intent;
16 | import android.graphics.Bitmap.Config;
17 | import android.graphics.Bitmap;
18 | import android.graphics.Canvas;
19 | import android.graphics.Color;
20 | import android.graphics.Matrix;
21 | import android.graphics.Paint;
22 | import android.graphics.Path;
23 | import android.graphics.Point;
24 | import android.graphics.PointF;
25 | import android.graphics.Rect;
26 | import android.graphics.drawable.Drawable;
27 | import android.net.Uri;
28 | import android.os.Build;
29 | import android.os.FileUriExposedException;
30 | import android.os.Handler;
31 | import android.text.method.PasswordTransformationMethod;
32 | import android.util.Log;
33 | import android.view.LayoutInflater;
34 | import android.view.View;
35 | import android.view.ViewGroup;
36 | import android.view.WindowManager;
37 | import android.view.inputmethod.EditorInfo;
38 | import android.widget.EditText;
39 | import android.widget.ImageView;
40 | import android.widget.ProgressBar;
41 | import android.widget.Toast;
42 | import android.os.AsyncTask;
43 |
44 | // Make our ImageViews opaque to optimize redraw
45 | class OpaqueImageView extends ImageView {
46 |
47 | public OpaqueImageView(Context context) {
48 | super(context);
49 | }
50 |
51 | @Override
52 | public boolean isOpaque() {
53 | return true;
54 | }
55 | }
56 |
57 | public class PageView extends ViewGroup {
58 | private final String APP = "MuPDF";
59 | private final MuPDFCore mCore;
60 |
61 | private static final int HIGHLIGHT_COLOR = 0x80cc6600;
62 | private static final int LINK_COLOR = 0x800066cc;
63 | private static final int BOX_COLOR = 0xFF4444FF;
64 | private static final int BACKGROUND_COLOR = 0xFFFFFFFF;
65 | private static final int PROGRESS_DIALOG_DELAY = 200;
66 |
67 | protected final Context mContext;
68 |
69 | protected int mPageNumber;
70 | private Point mParentSize;
71 | protected Point mSize; // Size of page at minimum zoom
72 | protected float mSourceScale;
73 |
74 | private ImageView mEntire; // Image rendered at minimum zoom
75 | private Bitmap mEntireBm;
76 | private Matrix mEntireMat;
77 | private AsyncTask mGetLinkInfo;
78 | private CancellableAsyncTask mDrawEntire;
79 |
80 | private Point mPatchViewSize; // View size on the basis of which the patch was created
81 | private Rect mPatchArea;
82 | private ImageView mPatch;
83 | private Bitmap mPatchBm;
84 | private CancellableAsyncTask mDrawPatch;
85 | private Quad mSearchBoxes[][];
86 | protected Link mLinks[];
87 | private View mSearchView;
88 | private boolean mIsBlank;
89 | private boolean mHighlightLinks;
90 |
91 | private ImageView mErrorIndicator;
92 |
93 | private ProgressBar mBusyIndicator;
94 | private final Handler mHandler = new Handler();
95 |
96 | public PageView(Context c, MuPDFCore core, Point parentSize, Bitmap sharedHqBm) {
97 | super(c);
98 | mContext = c;
99 | mCore = core;
100 | mParentSize = parentSize;
101 | setBackgroundColor(BACKGROUND_COLOR);
102 | mEntireBm = Bitmap.createBitmap(parentSize.x, parentSize.y, Config.ARGB_8888);
103 | mPatchBm = sharedHqBm;
104 | mEntireMat = new Matrix();
105 | }
106 |
107 | private void reinit() {
108 | // Cancel pending render task
109 | if (mDrawEntire != null) {
110 | mDrawEntire.cancel();
111 | mDrawEntire = null;
112 | }
113 |
114 | if (mDrawPatch != null) {
115 | mDrawPatch.cancel();
116 | mDrawPatch = null;
117 | }
118 |
119 | if (mGetLinkInfo != null) {
120 | mGetLinkInfo.cancel(true);
121 | mGetLinkInfo = null;
122 | }
123 |
124 | mIsBlank = true;
125 | mPageNumber = 0;
126 |
127 | if (mSize == null)
128 | mSize = mParentSize;
129 |
130 | if (mEntire != null) {
131 | mEntire.setImageBitmap(null);
132 | mEntire.invalidate();
133 | }
134 |
135 | if (mPatch != null) {
136 | mPatch.setImageBitmap(null);
137 | mPatch.invalidate();
138 | }
139 |
140 | mPatchViewSize = null;
141 | mPatchArea = null;
142 |
143 | mSearchBoxes = null;
144 | mLinks = null;
145 |
146 | clearRenderError();
147 | }
148 |
149 | public void releaseResources() {
150 | reinit();
151 |
152 | if (mBusyIndicator != null) {
153 | removeView(mBusyIndicator);
154 | mBusyIndicator = null;
155 | }
156 | clearRenderError();
157 | }
158 |
159 | public void releaseBitmaps() {
160 | reinit();
161 |
162 | // recycle bitmaps before releasing them.
163 |
164 | if (mEntireBm!=null)
165 | mEntireBm.recycle();
166 | mEntireBm = null;
167 |
168 | if (mPatchBm!=null)
169 | mPatchBm.recycle();
170 | mPatchBm = null;
171 | }
172 |
173 | public void blank(int page) {
174 | reinit();
175 | mPageNumber = page;
176 |
177 | if (mBusyIndicator == null) {
178 | mBusyIndicator = new ProgressBar(mContext);
179 | mBusyIndicator.setIndeterminate(true);
180 | addView(mBusyIndicator);
181 | }
182 |
183 | setBackgroundColor(BACKGROUND_COLOR);
184 | }
185 |
186 | protected void clearRenderError() {
187 | if (mErrorIndicator == null)
188 | return;
189 |
190 | removeView(mErrorIndicator);
191 | mErrorIndicator = null;
192 | invalidate();
193 | }
194 |
195 | protected void setRenderError(String why) {
196 |
197 | int page = mPageNumber;
198 | reinit();
199 | mPageNumber = page;
200 |
201 | if (mBusyIndicator != null) {
202 | removeView(mBusyIndicator);
203 | mBusyIndicator = null;
204 | }
205 | if (mSearchView != null) {
206 | removeView(mSearchView);
207 | mSearchView = null;
208 | }
209 |
210 | if (mErrorIndicator == null) {
211 | mErrorIndicator = new OpaqueImageView(mContext);
212 | mErrorIndicator.setScaleType(ImageView.ScaleType.CENTER);
213 | addView(mErrorIndicator);
214 | Drawable mErrorIcon = getResources().getDrawable(R.drawable.ic_error_red_24dp);
215 | mErrorIndicator.setImageDrawable(mErrorIcon);
216 | mErrorIndicator.setBackgroundColor(BACKGROUND_COLOR);
217 | }
218 |
219 | setBackgroundColor(Color.TRANSPARENT);
220 | mErrorIndicator.bringToFront();
221 | mErrorIndicator.invalidate();
222 | }
223 |
224 | public void setPage(int page, PointF size) {
225 | // Cancel pending render task
226 | if (mDrawEntire != null) {
227 | mDrawEntire.cancel();
228 | mDrawEntire = null;
229 | }
230 |
231 | mIsBlank = false;
232 | // Highlights may be missing because mIsBlank was true on last draw
233 | if (mSearchView != null)
234 | mSearchView.invalidate();
235 |
236 | mPageNumber = page;
237 |
238 | if (size == null) {
239 | setRenderError("Error loading page");
240 | size = new PointF(612, 792);
241 | }
242 |
243 | // Calculate scaled size that fits within the screen limits
244 | // This is the size at minimum zoom
245 | mSourceScale = Math.min(mParentSize.x/size.x, mParentSize.y/size.y);
246 | Point newSize = new Point((int)(size.x*mSourceScale), (int)(size.y*mSourceScale));
247 | mSize = newSize;
248 |
249 | if (mErrorIndicator != null)
250 | return;
251 |
252 | if (mEntire == null) {
253 | mEntire = new OpaqueImageView(mContext);
254 | mEntire.setScaleType(ImageView.ScaleType.MATRIX);
255 | addView(mEntire);
256 | }
257 |
258 | mEntire.setImageBitmap(null);
259 | mEntire.invalidate();
260 |
261 | // Get the link info in the background
262 | mGetLinkInfo = new AsyncTask() {
263 | protected Link[] doInBackground(Void... v) {
264 | return getLinkInfo();
265 | }
266 |
267 | protected void onPostExecute(Link[] v) {
268 | mLinks = v;
269 | if (mSearchView != null)
270 | mSearchView.invalidate();
271 | }
272 | };
273 |
274 | mGetLinkInfo.execute();
275 |
276 | // Render the page in the background
277 | mDrawEntire = new CancellableAsyncTask(getDrawPageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) {
278 |
279 | @Override
280 | public void onPreExecute() {
281 | setBackgroundColor(BACKGROUND_COLOR);
282 | mEntire.setImageBitmap(null);
283 | mEntire.invalidate();
284 |
285 | if (mBusyIndicator == null) {
286 | mBusyIndicator = new ProgressBar(mContext);
287 | mBusyIndicator.setIndeterminate(true);
288 | addView(mBusyIndicator);
289 | mBusyIndicator.setVisibility(INVISIBLE);
290 | mHandler.postDelayed(new Runnable() {
291 | public void run() {
292 | if (mBusyIndicator != null)
293 | mBusyIndicator.setVisibility(VISIBLE);
294 | }
295 | }, PROGRESS_DIALOG_DELAY);
296 | }
297 | }
298 |
299 | @Override
300 | public void onPostExecute(Boolean result) {
301 | removeView(mBusyIndicator);
302 | mBusyIndicator = null;
303 | if (result.booleanValue()) {
304 | clearRenderError();
305 | mEntire.setImageBitmap(mEntireBm);
306 | mEntire.invalidate();
307 | } else {
308 | setRenderError("Error rendering page");
309 | }
310 | setBackgroundColor(Color.TRANSPARENT);
311 | }
312 | };
313 |
314 | mDrawEntire.execute();
315 |
316 | if (mSearchView == null) {
317 | mSearchView = new View(mContext) {
318 | @Override
319 | protected void onDraw(final Canvas canvas) {
320 | super.onDraw(canvas);
321 | // Work out current total scale factor
322 | // from source to view
323 | final float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
324 | final Paint paint = new Paint();
325 |
326 | if (!mIsBlank && mSearchBoxes != null) {
327 | paint.setColor(HIGHLIGHT_COLOR);
328 | for (Quad[] searchBox : mSearchBoxes) {
329 | for (Quad q : searchBox) {
330 | Path path = new Path();
331 | path.moveTo(q.ul_x * scale, q.ul_y * scale);
332 | path.lineTo(q.ll_x * scale, q.ll_y * scale);
333 | path.lineTo(q.lr_x * scale, q.lr_y * scale);
334 | path.lineTo(q.ur_x * scale, q.ur_y * scale);
335 | path.close();
336 | canvas.drawPath(path, paint);
337 | }
338 | }
339 | }
340 |
341 | if (!mIsBlank && mLinks != null && mHighlightLinks) {
342 | paint.setColor(LINK_COLOR);
343 | for (Link link : mLinks)
344 | canvas.drawRect(link.getBounds().x0*scale, link.getBounds().y0*scale,
345 | link.getBounds().x1*scale, link.getBounds().y1*scale,
346 | paint);
347 | }
348 | }
349 | };
350 |
351 | addView(mSearchView);
352 | }
353 | requestLayout();
354 | }
355 |
356 | public void setSearchBoxes(Quad searchBoxes[][]) {
357 | mSearchBoxes = searchBoxes;
358 | if (mSearchView != null)
359 | mSearchView.invalidate();
360 | }
361 |
362 | public void setLinkHighlighting(boolean f) {
363 | mHighlightLinks = f;
364 | if (mSearchView != null)
365 | mSearchView.invalidate();
366 | }
367 |
368 | @Override
369 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
370 | int x, y;
371 | switch(View.MeasureSpec.getMode(widthMeasureSpec)) {
372 | case View.MeasureSpec.UNSPECIFIED:
373 | x = mSize.x;
374 | break;
375 | default:
376 | x = View.MeasureSpec.getSize(widthMeasureSpec);
377 | }
378 | switch(View.MeasureSpec.getMode(heightMeasureSpec)) {
379 | case View.MeasureSpec.UNSPECIFIED:
380 | y = mSize.y;
381 | break;
382 | default:
383 | y = View.MeasureSpec.getSize(heightMeasureSpec);
384 | }
385 |
386 | setMeasuredDimension(x, y);
387 |
388 | if (mBusyIndicator != null) {
389 | int limit = Math.min(mParentSize.x, mParentSize.y)/2;
390 | mBusyIndicator.measure(View.MeasureSpec.AT_MOST | limit, View.MeasureSpec.AT_MOST | limit);
391 | }
392 | if (mErrorIndicator != null) {
393 | int limit = Math.min(mParentSize.x, mParentSize.y)/2;
394 | mErrorIndicator.measure(View.MeasureSpec.AT_MOST | limit, View.MeasureSpec.AT_MOST | limit);
395 | }
396 | }
397 |
398 | @Override
399 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
400 | int w = right-left;
401 | int h = bottom-top;
402 |
403 | if (mEntire != null) {
404 | if (mEntire.getWidth() != w || mEntire.getHeight() != h) {
405 | mEntireMat.setScale(w/(float)mSize.x, h/(float)mSize.y);
406 | mEntire.setImageMatrix(mEntireMat);
407 | mEntire.invalidate();
408 | }
409 | mEntire.layout(0, 0, w, h);
410 | }
411 |
412 | if (mSearchView != null) {
413 | mSearchView.layout(0, 0, w, h);
414 | }
415 |
416 | if (mPatchViewSize != null) {
417 | if (mPatchViewSize.x != w || mPatchViewSize.y != h) {
418 | // Zoomed since patch was created
419 | mPatchViewSize = null;
420 | mPatchArea = null;
421 | if (mPatch != null) {
422 | mPatch.setImageBitmap(null);
423 | mPatch.invalidate();
424 | }
425 | } else {
426 | mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom);
427 | }
428 | }
429 |
430 | if (mBusyIndicator != null) {
431 | int bw = mBusyIndicator.getMeasuredWidth();
432 | int bh = mBusyIndicator.getMeasuredHeight();
433 |
434 | mBusyIndicator.layout((w-bw)/2, (h-bh)/2, (w+bw)/2, (h+bh)/2);
435 | }
436 |
437 | if (mErrorIndicator != null) {
438 | int bw = (int) (8.5 * mErrorIndicator.getMeasuredWidth());
439 | int bh = (int) (11 * mErrorIndicator.getMeasuredHeight());
440 | mErrorIndicator.layout((w-bw)/2, (h-bh)/2, (w+bw)/2, (h+bh)/2);
441 | }
442 | }
443 |
444 | public void updateHq(boolean update) {
445 | if (mErrorIndicator != null) {
446 | if (mPatch != null) {
447 | mPatch.setImageBitmap(null);
448 | mPatch.invalidate();
449 | }
450 | return;
451 | }
452 |
453 | Rect viewArea = new Rect(getLeft(),getTop(),getRight(),getBottom());
454 | if (viewArea.width() == mSize.x || viewArea.height() == mSize.y) {
455 | // If the viewArea's size matches the unzoomed size, there is no need for an hq patch
456 | if (mPatch != null) {
457 | mPatch.setImageBitmap(null);
458 | mPatch.invalidate();
459 | }
460 | } else {
461 | final Point patchViewSize = new Point(viewArea.width(), viewArea.height());
462 | final Rect patchArea = new Rect(0, 0, mParentSize.x, mParentSize.y);
463 |
464 | // Intersect and test that there is an intersection
465 | if (!patchArea.intersect(viewArea))
466 | return;
467 |
468 | // Offset patch area to be relative to the view top left
469 | patchArea.offset(-viewArea.left, -viewArea.top);
470 |
471 | boolean area_unchanged = patchArea.equals(mPatchArea) && patchViewSize.equals(mPatchViewSize);
472 |
473 | // If being asked for the same area as last time and not because of an update then nothing to do
474 | if (area_unchanged && !update)
475 | return;
476 |
477 | boolean completeRedraw = !(area_unchanged && update);
478 |
479 | // Stop the drawing of previous patch if still going
480 | if (mDrawPatch != null) {
481 | mDrawPatch.cancel();
482 | mDrawPatch = null;
483 | }
484 |
485 | // Create and add the image view if not already done
486 | if (mPatch == null) {
487 | mPatch = new OpaqueImageView(mContext);
488 | mPatch.setScaleType(ImageView.ScaleType.MATRIX);
489 | addView(mPatch);
490 | if (mSearchView != null)
491 | mSearchView.bringToFront();
492 | }
493 |
494 | CancellableTaskDefinition task;
495 |
496 | if (completeRedraw)
497 | task = getDrawPageTask(mPatchBm, patchViewSize.x, patchViewSize.y,
498 | patchArea.left, patchArea.top,
499 | patchArea.width(), patchArea.height());
500 | else
501 | task = getUpdatePageTask(mPatchBm, patchViewSize.x, patchViewSize.y,
502 | patchArea.left, patchArea.top,
503 | patchArea.width(), patchArea.height());
504 |
505 | mDrawPatch = new CancellableAsyncTask(task) {
506 |
507 | public void onPostExecute(Boolean result) {
508 | if (result.booleanValue()) {
509 | mPatchViewSize = patchViewSize;
510 | mPatchArea = patchArea;
511 | clearRenderError();
512 | mPatch.setImageBitmap(mPatchBm);
513 | mPatch.invalidate();
514 | //requestLayout();
515 | // Calling requestLayout here doesn't lead to a later call to layout. No idea
516 | // why, but apparently others have run into the problem.
517 | mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom);
518 | } else {
519 | setRenderError("Error rendering patch");
520 | }
521 | }
522 | };
523 |
524 | mDrawPatch.execute();
525 | }
526 | }
527 |
528 | public void update() {
529 | // Cancel pending render task
530 | if (mDrawEntire != null) {
531 | mDrawEntire.cancel();
532 | mDrawEntire = null;
533 | }
534 |
535 | if (mDrawPatch != null) {
536 | mDrawPatch.cancel();
537 | mDrawPatch = null;
538 | }
539 |
540 | // Render the page in the background
541 | mDrawEntire = new CancellableAsyncTask(getUpdatePageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) {
542 |
543 | public void onPostExecute(Boolean result) {
544 | if (result.booleanValue()) {
545 | clearRenderError();
546 | mEntire.setImageBitmap(mEntireBm);
547 | mEntire.invalidate();
548 | } else {
549 | setRenderError("Error updating page");
550 | }
551 | }
552 | };
553 |
554 | mDrawEntire.execute();
555 |
556 | updateHq(true);
557 | }
558 |
559 | public void removeHq() {
560 | // Stop the drawing of the patch if still going
561 | if (mDrawPatch != null) {
562 | mDrawPatch.cancel();
563 | mDrawPatch = null;
564 | }
565 |
566 | // And get rid of it
567 | mPatchViewSize = null;
568 | mPatchArea = null;
569 | if (mPatch != null) {
570 | mPatch.setImageBitmap(null);
571 | mPatch.invalidate();
572 | }
573 | }
574 |
575 | public int getPage() {
576 | return mPageNumber;
577 | }
578 |
579 | @Override
580 | public boolean isOpaque() {
581 | return true;
582 | }
583 |
584 | public int hitLink(Link link) {
585 | if (link.isExternal()) {
586 | Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link.getURI()));
587 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); // API>=21: FLAG_ACTIVITY_NEW_DOCUMENT
588 | try {
589 | mContext.startActivity(intent);
590 | } catch (FileUriExposedException x) {
591 | Log.e(APP, x.toString());
592 | Toast.makeText(getContext(), getResources().getString(R.string.toast_file_uris_not_allowed) + link.getURI(), Toast.LENGTH_LONG).show();
593 | } catch (Throwable x) {
594 | Log.e(APP, x.toString());
595 | Toast.makeText(getContext(), x.getMessage(), Toast.LENGTH_LONG).show();
596 | }
597 | return 0;
598 | } else {
599 | return mCore.resolveLink(link);
600 | }
601 | }
602 |
603 | public int hitLink(float x, float y) {
604 | // Since link highlighting was implemented, the super class
605 | // PageView has had sufficient information to be able to
606 | // perform this method directly. Making that change would
607 | // make MuPDFCore.hitLinkPage superfluous.
608 | float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
609 | float docRelX = (x - getLeft())/scale;
610 | float docRelY = (y - getTop())/scale;
611 |
612 | if (mLinks != null)
613 | for (Link l: mLinks)
614 | if (l.getBounds().contains(docRelX, docRelY))
615 | return hitLink(l);
616 | return 0;
617 | }
618 |
619 | protected CancellableTaskDefinition getDrawPageTask(final Bitmap bm, final int sizeX, final int sizeY,
620 | final int patchX, final int patchY, final int patchWidth, final int patchHeight) {
621 | return new MuPDFCancellableTaskDefinition() {
622 | @Override
623 | public Boolean doInBackground(Cookie cookie, Void ... params) {
624 | if (bm == null)
625 | return new Boolean(false);
626 | // Workaround bug in Android Honeycomb 3.x, where the bitmap generation count
627 | // is not incremented when drawing.
628 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB &&
629 | Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
630 | bm.eraseColor(0);
631 | try {
632 | mCore.drawPage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie);
633 | return new Boolean(true);
634 | } catch (RuntimeException e) {
635 | return new Boolean(false);
636 | }
637 | }
638 | };
639 |
640 | }
641 |
642 | protected CancellableTaskDefinition getUpdatePageTask(final Bitmap bm, final int sizeX, final int sizeY,
643 | final int patchX, final int patchY, final int patchWidth, final int patchHeight)
644 | {
645 | return new MuPDFCancellableTaskDefinition() {
646 | @Override
647 | public Boolean doInBackground(Cookie cookie, Void ... params) {
648 | if (bm == null)
649 | return new Boolean(false);
650 | // Workaround bug in Android Honeycomb 3.x, where the bitmap generation count
651 | // is not incremented when drawing.
652 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB &&
653 | Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
654 | bm.eraseColor(0);
655 | try {
656 | mCore.updatePage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie);
657 | return new Boolean(true);
658 | } catch (RuntimeException e) {
659 | return new Boolean(false);
660 | }
661 | }
662 | };
663 | }
664 |
665 | protected Link[] getLinkInfo() {
666 | try {
667 | return mCore.getPageLinks(mPageNumber);
668 | } catch (RuntimeException e) {
669 | return null;
670 | }
671 | }
672 | }
673 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/DocumentActivity.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | import com.artifex.mupdf.fitz.SeekableInputStream;
4 |
5 | import android.Manifest;
6 | import android.app.Activity;
7 | import android.app.AlertDialog;
8 | import android.content.ContentResolver;
9 | import android.content.Context;
10 | import android.content.DialogInterface.OnCancelListener;
11 | import android.content.DialogInterface;
12 | import android.content.Intent;
13 | import android.content.SharedPreferences;
14 | import android.content.pm.PackageManager;
15 | import android.content.res.Resources;
16 | import android.database.Cursor;
17 | import android.graphics.Color;
18 | import android.graphics.Insets;
19 | import android.graphics.Rect;
20 | import android.graphics.drawable.ShapeDrawable;
21 | import android.graphics.drawable.shapes.RectShape;
22 | import android.net.Uri;
23 | import android.os.Build;
24 | import android.os.Bundle;
25 | import android.os.Handler;
26 | import android.provider.OpenableColumns;
27 | import android.text.Editable;
28 | import android.text.TextWatcher;
29 | import android.text.method.PasswordTransformationMethod;
30 | import android.util.DisplayMetrics;
31 | import android.util.Log;
32 | import android.view.KeyEvent;
33 | import android.view.Menu;
34 | import android.view.MenuItem.OnMenuItemClickListener;
35 | import android.view.MenuItem;
36 | import android.view.View;
37 | import android.view.Window;
38 | import android.view.WindowInsets;
39 | import android.view.WindowManager;
40 | import android.view.animation.Animation;
41 | import android.view.animation.TranslateAnimation;
42 | import android.view.inputmethod.EditorInfo;
43 | import android.view.inputmethod.InputMethodManager;
44 | import android.widget.EditText;
45 | import android.widget.ImageButton;
46 | import android.widget.LinearLayout;
47 | import android.widget.PopupMenu;
48 | import android.widget.RelativeLayout;
49 | import android.widget.SeekBar;
50 | import android.widget.TextView;
51 | import android.widget.Toast;
52 | import android.widget.ViewAnimator;
53 |
54 | import androidx.core.app.ActivityCompat;
55 | import androidx.core.content.ContextCompat;
56 |
57 | import java.io.FileNotFoundException;
58 | import java.io.IOException;
59 | import java.io.InputStream;
60 | import java.util.ArrayList;
61 | import java.util.Collections;
62 | import java.util.Locale;
63 |
64 | public class DocumentActivity extends Activity
65 | {
66 | private final String APP = "MuPDF";
67 |
68 | /* The core rendering instance */
69 | enum TopBarMode {Main, Search, More};
70 |
71 | private final float EXCLUSION_HEIGHT_FACTOR = 2.0f;
72 |
73 | private final int OUTLINE_REQUEST=0;
74 | private MuPDFCore core;
75 | private String mDocTitle;
76 | private String mDocKey;
77 | private ReaderView mDocView;
78 | private View mButtonsView;
79 | private boolean mButtonsVisible;
80 | private EditText mPasswordView;
81 | private TextView mDocNameView;
82 | private SeekBar mPageSlider;
83 | private int mPageSliderRes;
84 | private TextView mPageNumberView;
85 | private ImageButton mSearchButton;
86 | private ImageButton mOutlineButton;
87 | private ViewAnimator mTopBarSwitcher;
88 | private LinearLayout mTopBar;
89 | private LinearLayout mActionBar;
90 | private LinearLayout mSearchBar;
91 | private LinearLayout mBottomBar;
92 | private ImageButton mLinkButton;
93 | private TopBarMode mTopBarMode = TopBarMode.Main;
94 | private ImageButton mSearchBack;
95 | private ImageButton mSearchFwd;
96 | private ImageButton mSearchClose;
97 | private EditText mSearchText;
98 | private SearchTask mSearchTask;
99 | private AlertDialog.Builder mAlertBuilder;
100 | private boolean mLinkHighlight = false;
101 | private final Handler mHandler = new Handler();
102 | private boolean mAlertsActive= false;
103 | private AlertDialog mAlertDialog;
104 | private ArrayList mFlatOutline;
105 | private boolean mReturnToLibraryActivity = false;
106 |
107 | protected int mDisplayDPI;
108 | private int mLayoutEM = 10;
109 | private int mLayoutW = 312;
110 | private int mLayoutH = 504;
111 |
112 | protected Insets systemInsets = Insets.NONE;
113 |
114 | protected View mLayoutButton;
115 | protected PopupMenu mLayoutPopupMenu;
116 |
117 | private String toHex(byte[] digest) {
118 | StringBuilder builder = new StringBuilder(2 * digest.length);
119 | for (byte b : digest)
120 | builder.append(String.format("%02x", b));
121 | return builder.toString();
122 | }
123 |
124 | private MuPDFCore openBuffer(byte buffer[], String magic)
125 | {
126 | try
127 | {
128 | core = new MuPDFCore(buffer, magic);
129 | }
130 | catch (Exception e)
131 | {
132 | Log.e(APP, "Error opening document buffer: " + e);
133 | return null;
134 | }
135 | return core;
136 | }
137 |
138 | private MuPDFCore openStream(SeekableInputStream stm, String magic)
139 | {
140 | try
141 | {
142 | core = new MuPDFCore(stm, magic);
143 | }
144 | catch (Exception e)
145 | {
146 | Log.e(APP, "Error opening document stream: " + e);
147 | return null;
148 | }
149 | return core;
150 | }
151 |
152 | private MuPDFCore openCore(Uri uri, long size, String mimetype) throws IOException {
153 | ContentResolver cr = getContentResolver();
154 |
155 | Log.i(APP, "Opening document " + uri);
156 |
157 | InputStream is = cr.openInputStream(uri);
158 | byte[] buf = null;
159 | int used = -1;
160 | try {
161 | final int limit = 8 * 1024 * 1024;
162 | if (size < 0) { // size is unknown
163 | buf = new byte[limit];
164 | used = is.read(buf);
165 | boolean atEOF = is.read() == -1;
166 | if (used < 0 || (used == limit && !atEOF)) // no or partial data
167 | buf = null;
168 | } else if (size <= limit) { // size is known and below limit
169 | buf = new byte[(int) size];
170 | used = is.read(buf);
171 | if (used < 0 || used < size) // no or partial data
172 | buf = null;
173 | }
174 | if (buf != null && buf.length != used) {
175 | byte[] newbuf = new byte[used];
176 | System.arraycopy(buf, 0, newbuf, 0, used);
177 | buf = newbuf;
178 | }
179 | } catch (OutOfMemoryError e) {
180 | buf = null;
181 | } finally {
182 | is.close();
183 | }
184 |
185 | if (buf != null) {
186 | Log.i(APP, " Opening document from memory buffer of size " + buf.length);
187 | return openBuffer(buf, mimetype);
188 | } else {
189 | Log.i(APP, " Opening document from stream");
190 | return openStream(new ContentInputStream(cr, uri, size), mimetype);
191 | }
192 | }
193 |
194 | private void showCannotOpenDialog(String reason) {
195 | Resources res = getResources();
196 | AlertDialog alert = mAlertBuilder.create();
197 | setTitle(String.format(Locale.ROOT, res.getString(R.string.cannot_open_document_Reason), reason));
198 | alert.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.dismiss),
199 | new DialogInterface.OnClickListener() {
200 | public void onClick(DialogInterface dialog, int which) {
201 | finish();
202 | }
203 | });
204 | alert.show();
205 | }
206 |
207 | /** Called when the activity is first created. */
208 | @Override
209 | public void onCreate(final Bundle savedInstanceState)
210 | {
211 | super.onCreate(savedInstanceState);
212 |
213 | requestWindowFeature(Window.FEATURE_NO_TITLE);
214 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
215 |
216 | DisplayMetrics metrics = new DisplayMetrics();
217 | getWindowManager().getDefaultDisplay().getMetrics(metrics);
218 | mDisplayDPI = (int)metrics.densityDpi;
219 |
220 | mAlertBuilder = new AlertDialog.Builder(this);
221 |
222 | if (core == null) {
223 | if (savedInstanceState != null && savedInstanceState.containsKey("DocTitle")) {
224 | mDocTitle = savedInstanceState.getString("DocTitle");
225 | }
226 | }
227 | if (core == null) {
228 | Intent intent = getIntent();
229 | SeekableInputStream file;
230 |
231 | mReturnToLibraryActivity = intent.getIntExtra(getComponentName().getPackageName() + ".ReturnToLibraryActivity", 0) != 0;
232 |
233 | if (Intent.ACTION_VIEW.equals(intent.getAction())) {
234 | Uri uri = intent.getData();
235 | String mimetype = getIntent().getType();
236 |
237 | if (uri == null) {
238 | showCannotOpenDialog("No document uri to open");
239 | return;
240 | }
241 |
242 | mDocKey = uri.toString();
243 |
244 | Log.i(APP, "OPEN URI " + uri.toString());
245 | Log.i(APP, " MAGIC (Intent) " + mimetype);
246 |
247 | mDocTitle = null;
248 | long size = -1;
249 | Cursor cursor = null;
250 |
251 | try {
252 | cursor = getContentResolver().query(uri, null, null, null, null);
253 | if (cursor != null && cursor.moveToFirst()) {
254 | int idx;
255 |
256 | idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
257 | if (idx >= 0 && cursor.getType(idx) == Cursor.FIELD_TYPE_STRING)
258 | mDocTitle = cursor.getString(idx);
259 |
260 | idx = cursor.getColumnIndex(OpenableColumns.SIZE);
261 | if (idx >= 0 && cursor.getType(idx) == Cursor.FIELD_TYPE_INTEGER)
262 | size = cursor.getLong(idx);
263 |
264 | if (size == 0)
265 | size = -1;
266 | }
267 | } catch (Exception x) {
268 | // Ignore any exception and depend on default values for title
269 | // and size (unless one was decoded
270 | } finally {
271 | if (cursor != null)
272 | cursor.close();
273 | }
274 |
275 | Log.i(APP, " NAME " + mDocTitle);
276 | Log.i(APP, " SIZE " + size);
277 |
278 | if (mimetype == null || mimetype.equals("application/octet-stream")) {
279 | mimetype = getContentResolver().getType(uri);
280 | Log.i(APP, " MAGIC (Resolved) " + mimetype);
281 | }
282 | if (mimetype == null || mimetype.equals("application/octet-stream")) {
283 | mimetype = mDocTitle;
284 | Log.i(APP, " MAGIC (Filename) " + mimetype);
285 | }
286 |
287 | try {
288 | core = openCore(uri, size, mimetype);
289 | SearchTaskResult.set(null);
290 | } catch (Exception x) {
291 | showCannotOpenDialog(x.toString());
292 | return;
293 | }
294 | }
295 | if (core != null && core.needsPassword()) {
296 | requestPassword(savedInstanceState);
297 | return;
298 | }
299 | if (core != null && core.countPages() == 0)
300 | {
301 | core = null;
302 | }
303 | }
304 | if (core == null)
305 | {
306 | AlertDialog alert = mAlertBuilder.create();
307 | alert.setTitle(R.string.cannot_open_document);
308 | alert.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.dismiss),
309 | new DialogInterface.OnClickListener() {
310 | public void onClick(DialogInterface dialog, int which) {
311 | finish();
312 | }
313 | });
314 | alert.setOnCancelListener(new OnCancelListener() {
315 | @Override
316 | public void onCancel(DialogInterface dialog) {
317 | finish();
318 | }
319 | });
320 | alert.show();
321 | return;
322 | }
323 |
324 | createUI(savedInstanceState);
325 | }
326 |
327 | public void requestPassword(final Bundle savedInstanceState) {
328 | mPasswordView = new EditText(this);
329 | mPasswordView.setInputType(EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
330 | mPasswordView.setTransformationMethod(new PasswordTransformationMethod());
331 |
332 | AlertDialog alert = mAlertBuilder.create();
333 | alert.setTitle(R.string.enter_password);
334 | alert.setView(mPasswordView);
335 | alert.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.okay),
336 | new DialogInterface.OnClickListener() {
337 | public void onClick(DialogInterface dialog, int which) {
338 | if (core.authenticatePassword(mPasswordView.getText().toString())) {
339 | createUI(savedInstanceState);
340 | } else {
341 | requestPassword(savedInstanceState);
342 | }
343 | }
344 | });
345 | alert.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.cancel),
346 | new DialogInterface.OnClickListener() {
347 |
348 | public void onClick(DialogInterface dialog, int which) {
349 | finish();
350 | }
351 | });
352 | alert.show();
353 | }
354 |
355 | public void relayoutDocument() {
356 | int loc = core.layout(mDocView.mCurrent, mLayoutW, mLayoutH, mLayoutEM);
357 | mFlatOutline = null;
358 | mDocView.mHistory.clear();
359 | mDocView.refresh();
360 | mDocView.setDisplayedViewIndex(loc);
361 | }
362 |
363 | protected void applyInsets(WindowInsets windowInsets) {
364 | systemInsets = Insets.NONE;
365 | Insets systemBarInsets = windowInsets.getInsets(WindowInsets.Type.systemBars());
366 | systemInsets = Insets.max(systemInsets, systemBarInsets);
367 | Insets cutoutInsets = windowInsets.getInsets(WindowInsets.Type.displayCutout());
368 | systemInsets = Insets.max(systemInsets, cutoutInsets);
369 | mTopBar.setPadding(0, systemInsets.top, 0, 0);
370 | mBottomBar.setPadding(0, 0, 0, systemInsets.bottom);
371 | }
372 |
373 | public void createUI(Bundle savedInstanceState) {
374 | if (core == null)
375 | return;
376 |
377 | // Now create the UI.
378 | // First create the document view
379 | mDocView = new ReaderView(this) {
380 | @Override
381 | protected void onMoveToChild(int i) {
382 | if (core == null)
383 | return;
384 |
385 | mPageNumberView.setText(String.format(Locale.ROOT, "%d / %d", i + 1, core.countPages()));
386 | mPageSlider.setMax((core.countPages() - 1) * mPageSliderRes);
387 | mPageSlider.setProgress(i * mPageSliderRes);
388 | super.onMoveToChild(i);
389 | }
390 |
391 | @Override
392 | protected void onTapMainDocArea() {
393 | if (!mButtonsVisible) {
394 | showButtons();
395 | } else {
396 | if (mTopBarMode == TopBarMode.Main)
397 | hideButtons();
398 | }
399 | }
400 |
401 | @Override
402 | protected void onDocMotion() {
403 | hideButtons();
404 | }
405 |
406 | @Override
407 | public void onSizeChanged(int w, int h, int oldw, int oldh) {
408 | if (core.isReflowable()) {
409 | mLayoutW = w * 72 / mDisplayDPI;
410 | mLayoutH = h * 72 / mDisplayDPI;
411 | relayoutDocument();
412 | } else {
413 | refresh();
414 | }
415 | }
416 | };
417 | mDocView.setAdapter(new PageAdapter(this, core));
418 |
419 | mSearchTask = new SearchTask(this, core) {
420 | @Override
421 | protected void onTextFound(SearchTaskResult result) {
422 | SearchTaskResult.set(result);
423 | // Ask the ReaderView to move to the resulting page
424 | mDocView.setDisplayedViewIndex(result.pageNumber);
425 | // Make the ReaderView act on the change to SearchTaskResult
426 | // via overridden onChildSetup method.
427 | mDocView.resetupChildren();
428 | }
429 | };
430 |
431 | // Make the buttons overlay, and store all its
432 | // controls in variables
433 | makeButtonsView();
434 |
435 | // Set up the page slider
436 | int smax = Math.max(core.countPages()-1,1);
437 | mPageSliderRes = ((10 + smax - 1)/smax) * 2;
438 |
439 | // Set the file-name text
440 | String docTitle = core.getTitle();
441 | if (docTitle != null)
442 | mDocNameView.setText(docTitle);
443 | else
444 | mDocNameView.setText(mDocTitle);
445 |
446 | // Activate the seekbar
447 | mPageSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
448 | public void onStopTrackingTouch(SeekBar seekBar) {
449 | mDocView.pushHistory();
450 | mDocView.setDisplayedViewIndex((seekBar.getProgress()+mPageSliderRes/2)/mPageSliderRes);
451 | }
452 |
453 | public void onStartTrackingTouch(SeekBar seekBar) {}
454 |
455 | public void onProgressChanged(SeekBar seekBar, int progress,
456 | boolean fromUser) {
457 | updatePageNumView((progress+mPageSliderRes/2)/mPageSliderRes);
458 | }
459 | });
460 |
461 | // Activate the search-preparing button
462 | mSearchButton.setOnClickListener(new View.OnClickListener() {
463 | public void onClick(View v) {
464 | searchModeOn();
465 | }
466 | });
467 |
468 | mSearchClose.setOnClickListener(new View.OnClickListener() {
469 | public void onClick(View v) {
470 | searchModeOff();
471 | }
472 | });
473 |
474 | // Search invoking buttons are disabled while there is no text specified
475 | mSearchBack.setEnabled(false);
476 | mSearchFwd.setEnabled(false);
477 | mSearchBack.setColorFilter(Color.argb(255, 128, 128, 128));
478 | mSearchFwd.setColorFilter(Color.argb(255, 128, 128, 128));
479 |
480 | // React to interaction with the text widget
481 | mSearchText.addTextChangedListener(new TextWatcher() {
482 |
483 | public void afterTextChanged(Editable s) {
484 | boolean haveText = s.toString().length() > 0;
485 | setButtonEnabled(mSearchBack, haveText);
486 | setButtonEnabled(mSearchFwd, haveText);
487 |
488 | // Remove any previous search results
489 | if (SearchTaskResult.get() != null && !mSearchText.getText().toString().equals(SearchTaskResult.get().txt)) {
490 | SearchTaskResult.set(null);
491 | mDocView.resetupChildren();
492 | }
493 | }
494 | public void beforeTextChanged(CharSequence s, int start, int count,
495 | int after) {}
496 | public void onTextChanged(CharSequence s, int start, int before,
497 | int count) {}
498 | });
499 |
500 | //React to Done button on keyboard
501 | mSearchText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
502 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
503 | if (actionId == EditorInfo.IME_ACTION_DONE)
504 | search(1);
505 | return false;
506 | }
507 | });
508 |
509 | mSearchText.setOnKeyListener(new View.OnKeyListener() {
510 | public boolean onKey(View v, int keyCode, KeyEvent event) {
511 | if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER)
512 | search(1);
513 | return false;
514 | }
515 | });
516 |
517 | // Activate search invoking buttons
518 | mSearchBack.setOnClickListener(new View.OnClickListener() {
519 | public void onClick(View v) {
520 | search(-1);
521 | }
522 | });
523 | mSearchFwd.setOnClickListener(new View.OnClickListener() {
524 | public void onClick(View v) {
525 | search(1);
526 | }
527 | });
528 |
529 | mLinkButton.setOnClickListener(new View.OnClickListener() {
530 | public void onClick(View v) {
531 | setLinkHighlight(!mLinkHighlight);
532 | }
533 | });
534 |
535 | if (core.isReflowable()) {
536 | mLayoutButton.setVisibility(View.VISIBLE);
537 | mLayoutPopupMenu = new PopupMenu(this, mLayoutButton);
538 | mLayoutPopupMenu.getMenuInflater().inflate(R.menu.layout_menu, mLayoutPopupMenu.getMenu());
539 | mLayoutPopupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
540 | public boolean onMenuItemClick(MenuItem item) {
541 | float oldLayoutEM = mLayoutEM;
542 | int id = item.getItemId();
543 | if (id == R.id.action_layout_6pt) mLayoutEM = 6;
544 | else if (id == R.id.action_layout_7pt) mLayoutEM = 7;
545 | else if (id == R.id.action_layout_8pt) mLayoutEM = 8;
546 | else if (id == R.id.action_layout_9pt) mLayoutEM = 9;
547 | else if (id == R.id.action_layout_10pt) mLayoutEM = 10;
548 | else if (id == R.id.action_layout_11pt) mLayoutEM = 11;
549 | else if (id == R.id.action_layout_12pt) mLayoutEM = 12;
550 | else if (id == R.id.action_layout_13pt) mLayoutEM = 13;
551 | else if (id == R.id.action_layout_14pt) mLayoutEM = 14;
552 | else if (id == R.id.action_layout_15pt) mLayoutEM = 15;
553 | else if (id == R.id.action_layout_16pt) mLayoutEM = 16;
554 | if (oldLayoutEM != mLayoutEM)
555 | relayoutDocument();
556 | return true;
557 | }
558 | });
559 | mLayoutButton.setOnClickListener(new View.OnClickListener() {
560 | public void onClick(View v) {
561 | mLayoutPopupMenu.show();
562 | }
563 | });
564 | }
565 |
566 | if (core.hasOutline()) {
567 | mOutlineButton.setOnClickListener(new View.OnClickListener() {
568 | public void onClick(View v) {
569 | boolean outlineTruncated = false;
570 | if (mFlatOutline == null)
571 | {
572 | mFlatOutline = core.getOutline();
573 | outlineTruncated = core.wasOutlineTruncated();
574 | }
575 | if (mFlatOutline != null) {
576 | Intent intent = new Intent(DocumentActivity.this, OutlineActivity.class);
577 | Bundle bundle = new Bundle();
578 | bundle.putInt("POSITION", mDocView.getDisplayedViewIndex());
579 | bundle.putSerializable("OUTLINE", mFlatOutline);
580 | intent.putExtra("PALLETBUNDLE", Pallet.sendBundle(bundle));
581 | startActivityForResult(intent, OUTLINE_REQUEST);
582 | if (outlineTruncated)
583 | Toast.makeText(DocumentActivity.this, "Outline too large, truncated", Toast.LENGTH_SHORT).show();
584 | }
585 | }
586 | });
587 | } else {
588 | mOutlineButton.setVisibility(View.GONE);
589 | }
590 |
591 | // Reenstate last state if it was recorded
592 | SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
593 | mDocView.setDisplayedViewIndex(prefs.getInt("page"+mDocKey, 0));
594 |
595 | if (savedInstanceState == null || !savedInstanceState.getBoolean("ButtonsHidden", false))
596 | showButtons();
597 |
598 | if(savedInstanceState != null && savedInstanceState.getBoolean("SearchMode", false))
599 | searchModeOn();
600 |
601 | mTopBar.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
602 | public WindowInsets onApplyWindowInsets(View v, WindowInsets windowInsets)
603 | {
604 | applyInsets(windowInsets);
605 | return WindowInsets.CONSUMED;
606 | }
607 | });
608 |
609 | if (Build.VERSION.SDK_INT >= 29)
610 | mBottomBar.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
611 | public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
612 | View parent = (View) v.getParent();
613 | android.graphics.Rect exclusion;
614 |
615 | exclusion = new android.graphics.Rect(0, 0, v.getWidth(), v.getHeight());
616 | v.setSystemGestureExclusionRects(Collections.singletonList(exclusion));
617 |
618 | int extended_top = parent.getHeight() - (int) (EXCLUSION_HEIGHT_FACTOR * v.getHeight());
619 | exclusion = new android.graphics.Rect(0, extended_top, parent.getWidth(), parent.getHeight());
620 | parent.setSystemGestureExclusionRects(Collections.singletonList(exclusion));
621 | }
622 | });
623 |
624 | // Stick the document view and the buttons overlay into a parent view
625 | RelativeLayout layout = new RelativeLayout(this);
626 | layout.setBackgroundColor(Color.DKGRAY);
627 | layout.addView(mDocView);
628 | layout.addView(mButtonsView);
629 | setContentView(layout);
630 | }
631 |
632 | @Override
633 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
634 | switch (requestCode) {
635 | case OUTLINE_REQUEST:
636 | if (resultCode >= RESULT_FIRST_USER && mDocView != null) {
637 | mDocView.pushHistory();
638 | mDocView.setDisplayedViewIndex(resultCode-RESULT_FIRST_USER);
639 | }
640 | break;
641 | }
642 | super.onActivityResult(requestCode, resultCode, data);
643 | }
644 |
645 | @Override
646 | protected void onSaveInstanceState(Bundle outState) {
647 | super.onSaveInstanceState(outState);
648 |
649 | if (mDocKey != null && mDocView != null) {
650 | if (mDocTitle != null)
651 | outState.putString("DocTitle", mDocTitle);
652 |
653 | // Store current page in the prefs against the file name,
654 | // so that we can pick it up each time the file is loaded
655 | // Other info is needed only for screen-orientation change,
656 | // so it can go in the bundle
657 | SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
658 | SharedPreferences.Editor edit = prefs.edit();
659 | edit.putInt("page"+mDocKey, mDocView.getDisplayedViewIndex());
660 | edit.apply();
661 | }
662 |
663 | if (!mButtonsVisible)
664 | outState.putBoolean("ButtonsHidden", true);
665 |
666 | if (mTopBarMode == TopBarMode.Search)
667 | outState.putBoolean("SearchMode", true);
668 | }
669 |
670 | @Override
671 | protected void onPause() {
672 | super.onPause();
673 |
674 | if (mSearchTask != null)
675 | mSearchTask.stop();
676 |
677 | if (mDocKey != null && mDocView != null) {
678 | SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
679 | SharedPreferences.Editor edit = prefs.edit();
680 | edit.putInt("page"+mDocKey, mDocView.getDisplayedViewIndex());
681 | edit.apply();
682 | }
683 | }
684 |
685 | public void onDestroy()
686 | {
687 | if (mDocView != null) {
688 | mDocView.applyToChildren(new ReaderView.ViewMapper() {
689 | @Override
690 | public void applyToView(View view) {
691 | ((PageView)view).releaseBitmaps();
692 | }
693 | });
694 | }
695 | if (core != null)
696 | core.onDestroy();
697 | core = null;
698 | super.onDestroy();
699 | }
700 |
701 | private void setButtonEnabled(ImageButton button, boolean enabled) {
702 | button.setEnabled(enabled);
703 | button.setColorFilter(enabled ? Color.argb(255, 255, 255, 255) : Color.argb(255, 128, 128, 128));
704 | }
705 |
706 | private void setLinkHighlight(boolean highlight) {
707 | mLinkHighlight = highlight;
708 | // LINK_COLOR tint
709 | mLinkButton.setColorFilter(highlight ? Color.argb(0xFF, 0x00, 0x66, 0xCC) : Color.argb(0xFF, 255, 255, 255));
710 | // Inform pages of the change.
711 | mDocView.setLinksEnabled(highlight);
712 | }
713 |
714 | private void showButtons() {
715 | if (core == null)
716 | return;
717 | if (!mButtonsVisible) {
718 | mButtonsVisible = true;
719 | // Update page number text and slider
720 | int index = mDocView.getDisplayedViewIndex();
721 | updatePageNumView(index);
722 | mPageSlider.setMax((core.countPages()-1)*mPageSliderRes);
723 | mPageSlider.setProgress(index * mPageSliderRes);
724 | if (mTopBarMode == TopBarMode.Search) {
725 | mSearchText.requestFocus();
726 | showKeyboard();
727 | }
728 |
729 | Animation anim = new TranslateAnimation(0, 0, -(mTopBarSwitcher.getHeight() + systemInsets.top), 0);
730 | anim.setDuration(200);
731 | anim.setAnimationListener(new Animation.AnimationListener() {
732 | public void onAnimationStart(Animation animation) {
733 | mTopBarSwitcher.setVisibility(View.VISIBLE);
734 | }
735 | public void onAnimationRepeat(Animation animation) {}
736 | public void onAnimationEnd(Animation animation) {}
737 | });
738 | mTopBarSwitcher.startAnimation(anim);
739 |
740 | anim = new TranslateAnimation(0, 0, mBottomBar.getHeight() + systemInsets.bottom, 0);
741 | anim.setDuration(200);
742 | anim.setAnimationListener(new Animation.AnimationListener() {
743 | public void onAnimationStart(Animation animation) {
744 | mBottomBar.setVisibility(View.VISIBLE);
745 | }
746 | public void onAnimationRepeat(Animation animation) {}
747 | public void onAnimationEnd(Animation animation) {
748 | mPageNumberView.setVisibility(View.VISIBLE);
749 | }
750 | });
751 | mBottomBar.startAnimation(anim);
752 | }
753 | }
754 |
755 | private void hideButtons() {
756 | if (mButtonsVisible) {
757 | mButtonsVisible = false;
758 | hideKeyboard();
759 |
760 | Animation anim = new TranslateAnimation(0, 0, 0, -(mTopBarSwitcher.getHeight() + systemInsets.top));
761 | anim.setDuration(200);
762 | anim.setAnimationListener(new Animation.AnimationListener() {
763 | public void onAnimationStart(Animation animation) {}
764 | public void onAnimationRepeat(Animation animation) {}
765 | public void onAnimationEnd(Animation animation) {
766 | mTopBarSwitcher.setVisibility(View.INVISIBLE);
767 | }
768 | });
769 | mTopBarSwitcher.startAnimation(anim);
770 |
771 | anim = new TranslateAnimation(0, 0, 0, mBottomBar.getHeight() + systemInsets.bottom);
772 | anim.setDuration(200);
773 | anim.setAnimationListener(new Animation.AnimationListener() {
774 | public void onAnimationStart(Animation animation) {
775 | mPageNumberView.setVisibility(View.INVISIBLE);
776 | }
777 | public void onAnimationRepeat(Animation animation) {}
778 | public void onAnimationEnd(Animation animation) {
779 | mBottomBar.setVisibility(View.INVISIBLE);
780 | }
781 | });
782 | mBottomBar.startAnimation(anim);
783 | }
784 | }
785 |
786 | private void searchModeOn() {
787 | if (mTopBarMode != TopBarMode.Search) {
788 | mTopBarMode = TopBarMode.Search;
789 | //Focus on EditTextWidget
790 | mSearchText.requestFocus();
791 | showKeyboard();
792 | mActionBar.setVisibility(View.GONE);
793 | mSearchBar.setVisibility(View.VISIBLE);
794 | }
795 | }
796 |
797 | private void searchModeOff() {
798 | if (mTopBarMode == TopBarMode.Search) {
799 | mTopBarMode = TopBarMode.Main;
800 | hideKeyboard();
801 | mActionBar.setVisibility(View.VISIBLE);
802 | mSearchBar.setVisibility(View.GONE);
803 | SearchTaskResult.set(null);
804 | // Make the ReaderView act on the change to mSearchTaskResult
805 | // via overridden onChildSetup method.
806 | mDocView.resetupChildren();
807 | }
808 | }
809 |
810 | private void updatePageNumView(int index) {
811 | if (core == null)
812 | return;
813 | mPageNumberView.setText(String.format(Locale.ROOT, "%d / %d", index + 1, core.countPages()));
814 | }
815 |
816 | private void makeButtonsView() {
817 | mButtonsView = getLayoutInflater().inflate(R.layout.document_activity, null);
818 | mDocNameView = (TextView)mButtonsView.findViewById(R.id.docNameText);
819 | mPageSlider = (SeekBar)mButtonsView.findViewById(R.id.pageSlider);
820 | mPageNumberView = (TextView)mButtonsView.findViewById(R.id.pageNumber);
821 | mSearchButton = (ImageButton)mButtonsView.findViewById(R.id.searchButton);
822 | mOutlineButton = (ImageButton)mButtonsView.findViewById(R.id.outlineButton);
823 | mTopBarSwitcher = (ViewAnimator)mButtonsView.findViewById(R.id.switcher);
824 | mTopBar = (LinearLayout)mButtonsView.findViewById(R.id.topBar);
825 | mActionBar = (LinearLayout)mButtonsView.findViewById(R.id.actionBar);
826 | mSearchBar = (LinearLayout)mButtonsView.findViewById(R.id.searchBar);
827 | mBottomBar = (LinearLayout)mButtonsView.findViewById(R.id.bottomBar);
828 | mSearchBack = (ImageButton)mButtonsView.findViewById(R.id.searchBack);
829 | mSearchFwd = (ImageButton)mButtonsView.findViewById(R.id.searchForward);
830 | mSearchClose = (ImageButton)mButtonsView.findViewById(R.id.searchClose);
831 | mSearchText = (EditText)mButtonsView.findViewById(R.id.searchText);
832 | mLinkButton = (ImageButton)mButtonsView.findViewById(R.id.linkButton);
833 | mLayoutButton = mButtonsView.findViewById(R.id.layoutButton);
834 | mTopBarSwitcher.setVisibility(View.INVISIBLE);
835 | mPageNumberView.setVisibility(View.INVISIBLE);
836 | mActionBar.setVisibility(View.VISIBLE);
837 | mTopBar.setVisibility(View.VISIBLE);
838 | mSearchBar.setVisibility(View.GONE);
839 | mBottomBar.setVisibility(View.INVISIBLE);
840 | }
841 |
842 | private void showKeyboard() {
843 | InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
844 | if (imm != null)
845 | imm.showSoftInput(mSearchText, 0);
846 | }
847 |
848 | private void hideKeyboard() {
849 | InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
850 | if (imm != null)
851 | imm.hideSoftInputFromWindow(mSearchText.getWindowToken(), 0);
852 | }
853 |
854 | private void search(int direction) {
855 | hideKeyboard();
856 | int displayPage = mDocView.getDisplayedViewIndex();
857 | SearchTaskResult r = SearchTaskResult.get();
858 | int searchPage = r != null ? r.pageNumber : -1;
859 | mSearchTask.go(mSearchText.getText().toString(), direction, displayPage, searchPage);
860 | }
861 |
862 | @Override
863 | public boolean onSearchRequested() {
864 | if (mButtonsVisible && mTopBarMode == TopBarMode.Search) {
865 | hideButtons();
866 | } else {
867 | showButtons();
868 | searchModeOn();
869 | }
870 | return super.onSearchRequested();
871 | }
872 |
873 | @Override
874 | public boolean onPrepareOptionsMenu(Menu menu) {
875 | if (mButtonsVisible && mTopBarMode != TopBarMode.Search) {
876 | hideButtons();
877 | } else {
878 | showButtons();
879 | searchModeOff();
880 | }
881 | return super.onPrepareOptionsMenu(menu);
882 | }
883 |
884 | @Override
885 | protected void onStart() {
886 | super.onStart();
887 | }
888 |
889 | @Override
890 | protected void onStop() {
891 | super.onStop();
892 | }
893 |
894 | @Override
895 | public void onBackPressed() {
896 | if (mDocView == null || (mDocView != null && !mDocView.popHistory())) {
897 | super.onBackPressed();
898 | if (mReturnToLibraryActivity) {
899 | Intent intent = getPackageManager().getLaunchIntentForPackage(getComponentName().getPackageName());
900 | startActivity(intent);
901 | }
902 | }
903 | }
904 | }
905 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/artifex/mupdf/viewer/ReaderView.java:
--------------------------------------------------------------------------------
1 | package com.artifex.mupdf.viewer;
2 |
3 | import com.artifex.mupdf.fitz.Link;
4 |
5 | import java.util.LinkedList;
6 | import java.util.NoSuchElementException;
7 | import java.util.Stack;
8 |
9 | import android.content.Context;
10 | import android.graphics.Point;
11 | import android.graphics.Rect;
12 | import android.net.Uri;
13 | import android.util.AttributeSet;
14 | import android.util.DisplayMetrics;
15 | import android.util.Log;
16 | import android.util.SparseArray;
17 | import android.view.GestureDetector;
18 | import android.view.MotionEvent;
19 | import android.view.ScaleGestureDetector;
20 | import android.view.View;
21 | import android.view.WindowManager;
22 | import android.widget.Adapter;
23 | import android.widget.AdapterView;
24 | import android.widget.Scroller;
25 |
26 | public class ReaderView
27 | extends AdapterView
28 | implements GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener, Runnable {
29 | private final String APP = "MuPDF";
30 |
31 | private Context mContext;
32 | private boolean mLinksEnabled = false;
33 | private boolean tapDisabled = false;
34 | private int tapPageMargin;
35 |
36 | private static final int MOVING_DIAGONALLY = 0;
37 | private static final int MOVING_LEFT = 1;
38 | private static final int MOVING_RIGHT = 2;
39 | private static final int MOVING_UP = 3;
40 | private static final int MOVING_DOWN = 4;
41 |
42 | private static final int FLING_MARGIN = 100;
43 | private static final int GAP = 20;
44 |
45 | private static final float MIN_SCALE = 1.0f;
46 | private static final float MAX_SCALE = 64.0f;
47 |
48 | private static final boolean HORIZONTAL_SCROLLING = true;
49 |
50 | private PageAdapter mAdapter;
51 | protected int mCurrent; // Adapter's index for the current view
52 | private boolean mResetLayout;
53 | private final SparseArray
54 | mChildViews = new SparseArray(3);
55 | // Shadows the children of the adapter view
56 | // but with more sensible indexing
57 | private final LinkedList
58 | mViewCache = new LinkedList();
59 | private boolean mUserInteracting; // Whether the user is interacting
60 | private boolean mScaling; // Whether the user is currently pinch zooming
61 | private float mScale = 1.0f;
62 | private int mXScroll; // Scroll amounts recorded from events.
63 | private int mYScroll; // and then accounted for in onLayout
64 | private GestureDetector mGestureDetector;
65 | private ScaleGestureDetector mScaleGestureDetector;
66 | private Scroller mScroller;
67 | private Stepper mStepper;
68 | private int mScrollerLastX;
69 | private int mScrollerLastY;
70 | private float mLastScaleFocusX;
71 | private float mLastScaleFocusY;
72 |
73 | protected Stack mHistory;
74 |
75 | public interface ViewMapper {
76 | void applyToView(View view);
77 | }
78 |
79 | public ReaderView(Context context) {
80 | super(context);
81 | setup(context);
82 | }
83 |
84 | public ReaderView(Context context, AttributeSet attrs) {
85 | super(context, attrs);
86 | setup(context);
87 | }
88 |
89 | public ReaderView(Context context, AttributeSet attrs, int defStyle) {
90 | super(context, attrs, defStyle);
91 | setup(context);
92 | }
93 |
94 | private void setup(Context context)
95 | {
96 | mContext = context;
97 | mGestureDetector = new GestureDetector(context, this);
98 | mScaleGestureDetector = new ScaleGestureDetector(context, this);
99 | mScroller = new Scroller(context);
100 | mStepper = new Stepper(this, this);
101 | mHistory = new Stack();
102 |
103 | // Get the screen size etc to customise tap margins.
104 | // We calculate the size of 1 inch of the screen for tapping.
105 | // On some devices the dpi values returned are wrong, so we
106 | // sanity check it: we first restrict it so that we are never
107 | // less than 100 pixels (the smallest Android device screen
108 | // dimension I've seen is 480 pixels or so). Then we check
109 | // to ensure we are never more than 1/5 of the screen width.
110 | DisplayMetrics dm = new DisplayMetrics();
111 | WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
112 | wm.getDefaultDisplay().getMetrics(dm);
113 | tapPageMargin = (int)dm.xdpi;
114 | if (tapPageMargin < 100)
115 | tapPageMargin = 100;
116 | if (tapPageMargin > dm.widthPixels/5)
117 | tapPageMargin = dm.widthPixels/5;
118 | }
119 |
120 | public boolean popHistory() {
121 | if (mHistory.empty())
122 | return false;
123 | setDisplayedViewIndex(mHistory.pop());
124 | return true;
125 | }
126 |
127 | public void pushHistory() {
128 | mHistory.push(mCurrent);
129 | }
130 |
131 | public void clearHistory() {
132 | mHistory.clear();
133 | }
134 |
135 | public int getDisplayedViewIndex() {
136 | return mCurrent;
137 | }
138 |
139 | public void setDisplayedViewIndex(int i) {
140 | if (0 <= i && i < mAdapter.getCount()) {
141 | onMoveOffChild(mCurrent);
142 | mCurrent = i;
143 | onMoveToChild(i);
144 | mResetLayout = true;
145 | requestLayout();
146 | }
147 | }
148 |
149 | public void moveToNext() {
150 | View v = mChildViews.get(mCurrent+1);
151 | if (v != null)
152 | slideViewOntoScreen(v);
153 | }
154 |
155 | public void moveToPrevious() {
156 | View v = mChildViews.get(mCurrent-1);
157 | if (v != null)
158 | slideViewOntoScreen(v);
159 | }
160 |
161 | // When advancing down the page, we want to advance by about
162 | // 90% of a screenful. But we'd be happy to advance by between
163 | // 80% and 95% if it means we hit the bottom in a whole number
164 | // of steps.
165 | private int smartAdvanceAmount(int screenHeight, int max) {
166 | int advance = (int)(screenHeight * 0.9 + 0.5);
167 | int leftOver = max % advance;
168 | int steps = max / advance;
169 | if (leftOver == 0) {
170 | // We'll make it exactly. No adjustment
171 | } else if ((float)leftOver / steps <= screenHeight * 0.05) {
172 | // We can adjust up by less than 5% to make it exact.
173 | advance += (int)((float)leftOver/steps + 0.5);
174 | } else {
175 | int overshoot = advance - leftOver;
176 | if ((float)overshoot / steps <= screenHeight * 0.1) {
177 | // We can adjust down by less than 10% to make it exact.
178 | advance -= (int)((float)overshoot/steps + 0.5);
179 | }
180 | }
181 | if (advance > max)
182 | advance = max;
183 | return advance;
184 | }
185 |
186 | public void smartMoveForwards() {
187 | View v = mChildViews.get(mCurrent);
188 | if (v == null)
189 | return;
190 |
191 | // The following code works in terms of where the screen is on the views;
192 | // so for example, if the currentView is at (-100,-100), the visible
193 | // region would be at (100,100). If the previous page was (2000, 3000) in
194 | // size, the visible region of the previous page might be (2100 + GAP, 100)
195 | // (i.e. off the previous page). This is different to the way the rest of
196 | // the code in this file is written, but it's easier for me to think about.
197 | // At some point we may refactor this to fit better with the rest of the
198 | // code.
199 |
200 | // screenWidth/Height are the actual width/height of the screen. e.g. 480/800
201 | int screenWidth = getWidth();
202 | int screenHeight = getHeight();
203 | // We might be mid scroll; we want to calculate where we scroll to based on
204 | // where this scroll would end, not where we are now (to allow for people
205 | // bashing 'forwards' very fast.
206 | int remainingX = mScroller.getFinalX() - mScroller.getCurrX();
207 | int remainingY = mScroller.getFinalY() - mScroller.getCurrY();
208 | // right/bottom is in terms of pixels within the scaled document; e.g. 1000
209 | int top = -(v.getTop() + mYScroll + remainingY);
210 | int right = screenWidth -(v.getLeft() + mXScroll + remainingX);
211 | int bottom = screenHeight+top;
212 | // docWidth/Height are the width/height of the scaled document e.g. 2000x3000
213 | int docWidth = v.getMeasuredWidth();
214 | int docHeight = v.getMeasuredHeight();
215 |
216 | int xOffset, yOffset;
217 | if (bottom >= docHeight) {
218 | // We are flush with the bottom. Advance to next column.
219 | if (right + screenWidth > docWidth) {
220 | // No room for another column - go to next page
221 | View nv = mChildViews.get(mCurrent+1);
222 | if (nv == null) // No page to advance to
223 | return;
224 | int nextTop = -(nv.getTop() + mYScroll + remainingY);
225 | int nextLeft = -(nv.getLeft() + mXScroll + remainingX);
226 | int nextDocWidth = nv.getMeasuredWidth();
227 | int nextDocHeight = nv.getMeasuredHeight();
228 |
229 | // Allow for the next page maybe being shorter than the screen is high
230 | yOffset = (nextDocHeight < screenHeight ? ((nextDocHeight - screenHeight)>>1) : 0);
231 |
232 | if (nextDocWidth < screenWidth) {
233 | // Next page is too narrow to fill the screen. Scroll to the top, centred.
234 | xOffset = (nextDocWidth - screenWidth)>>1;
235 | } else {
236 | // Reset X back to the left hand column
237 | xOffset = right % screenWidth;
238 | // Adjust in case the previous page is less wide
239 | if (xOffset + screenWidth > nextDocWidth)
240 | xOffset = nextDocWidth - screenWidth;
241 | }
242 | xOffset -= nextLeft;
243 | yOffset -= nextTop;
244 | } else {
245 | // Move to top of next column
246 | xOffset = screenWidth;
247 | yOffset = screenHeight - bottom;
248 | }
249 | } else {
250 | // Advance by 90% of the screen height downwards (in case lines are partially cut off)
251 | xOffset = 0;
252 | yOffset = smartAdvanceAmount(screenHeight, docHeight - bottom);
253 | }
254 | mScrollerLastX = mScrollerLastY = 0;
255 | mScroller.startScroll(0, 0, remainingX - xOffset, remainingY - yOffset, 400);
256 | mStepper.prod();
257 | }
258 |
259 | public void smartMoveBackwards() {
260 | View v = mChildViews.get(mCurrent);
261 | if (v == null)
262 | return;
263 |
264 | // The following code works in terms of where the screen is on the views;
265 | // so for example, if the currentView is at (-100,-100), the visible
266 | // region would be at (100,100). If the previous page was (2000, 3000) in
267 | // size, the visible region of the previous page might be (2100 + GAP, 100)
268 | // (i.e. off the previous page). This is different to the way the rest of
269 | // the code in this file is written, but it's easier for me to think about.
270 | // At some point we may refactor this to fit better with the rest of the
271 | // code.
272 |
273 | // screenWidth/Height are the actual width/height of the screen. e.g. 480/800
274 | int screenWidth = getWidth();
275 | int screenHeight = getHeight();
276 | // We might be mid scroll; we want to calculate where we scroll to based on
277 | // where this scroll would end, not where we are now (to allow for people
278 | // bashing 'forwards' very fast.
279 | int remainingX = mScroller.getFinalX() - mScroller.getCurrX();
280 | int remainingY = mScroller.getFinalY() - mScroller.getCurrY();
281 | // left/top is in terms of pixels within the scaled document; e.g. 1000
282 | int left = -(v.getLeft() + mXScroll + remainingX);
283 | int top = -(v.getTop() + mYScroll + remainingY);
284 | // docWidth/Height are the width/height of the scaled document e.g. 2000x3000
285 | int docHeight = v.getMeasuredHeight();
286 |
287 | int xOffset, yOffset;
288 | if (top <= 0) {
289 | // We are flush with the top. Step back to previous column.
290 | if (left < screenWidth) {
291 | /* No room for previous column - go to previous page */
292 | View pv = mChildViews.get(mCurrent-1);
293 | if (pv == null) /* No page to advance to */
294 | return;
295 | int prevDocWidth = pv.getMeasuredWidth();
296 | int prevDocHeight = pv.getMeasuredHeight();
297 |
298 | // Allow for the next page maybe being shorter than the screen is high
299 | yOffset = (prevDocHeight < screenHeight ? ((prevDocHeight - screenHeight)>>1) : 0);
300 |
301 | int prevLeft = -(pv.getLeft() + mXScroll);
302 | int prevTop = -(pv.getTop() + mYScroll);
303 | if (prevDocWidth < screenWidth) {
304 | // Previous page is too narrow to fill the screen. Scroll to the bottom, centred.
305 | xOffset = (prevDocWidth - screenWidth)>>1;
306 | } else {
307 | // Reset X back to the right hand column
308 | xOffset = (left > 0 ? left % screenWidth : 0);
309 | if (xOffset + screenWidth > prevDocWidth)
310 | xOffset = prevDocWidth - screenWidth;
311 | while (xOffset + screenWidth*2 < prevDocWidth)
312 | xOffset += screenWidth;
313 | }
314 | xOffset -= prevLeft;
315 | yOffset -= prevTop-prevDocHeight+screenHeight;
316 | } else {
317 | // Move to bottom of previous column
318 | xOffset = -screenWidth;
319 | yOffset = docHeight - screenHeight + top;
320 | }
321 | } else {
322 | // Retreat by 90% of the screen height downwards (in case lines are partially cut off)
323 | xOffset = 0;
324 | yOffset = -smartAdvanceAmount(screenHeight, top);
325 | }
326 | mScrollerLastX = mScrollerLastY = 0;
327 | mScroller.startScroll(0, 0, remainingX - xOffset, remainingY - yOffset, 400);
328 | mStepper.prod();
329 | }
330 |
331 | public void resetupChildren() {
332 | for (int i = 0; i < mChildViews.size(); i++)
333 | onChildSetup(mChildViews.keyAt(i), mChildViews.valueAt(i));
334 | }
335 |
336 | public void applyToChildren(ViewMapper mapper) {
337 | for (int i = 0; i < mChildViews.size(); i++)
338 | mapper.applyToView(mChildViews.valueAt(i));
339 | }
340 |
341 | public void refresh() {
342 | mResetLayout = true;
343 |
344 | mScale = 1.0f;
345 | mXScroll = mYScroll = 0;
346 |
347 | /* All page views need recreating since both page and screen has changed size,
348 | * invalidating both sizes and bitmaps. */
349 | mAdapter.refresh();
350 | int numChildren = mChildViews.size();
351 | for (int i = 0; i < mChildViews.size(); i++) {
352 | View v = mChildViews.valueAt(i);
353 | onNotInUse(v);
354 | removeViewInLayout(v);
355 | }
356 | mChildViews.clear();
357 | mViewCache.clear();
358 |
359 | requestLayout();
360 | }
361 |
362 | public View getView(int i) {
363 | return mChildViews.get(i);
364 | }
365 |
366 | public View getDisplayedView() {
367 | return mChildViews.get(mCurrent);
368 | }
369 |
370 | public void run() {
371 | if (!mScroller.isFinished()) {
372 | mScroller.computeScrollOffset();
373 | int x = mScroller.getCurrX();
374 | int y = mScroller.getCurrY();
375 | mXScroll += x - mScrollerLastX;
376 | mYScroll += y - mScrollerLastY;
377 | mScrollerLastX = x;
378 | mScrollerLastY = y;
379 | requestLayout();
380 | mStepper.prod();
381 | }
382 | else if (!mUserInteracting) {
383 | // End of an inertial scroll and the user is not interacting.
384 | // The layout is stable
385 | View v = mChildViews.get(mCurrent);
386 | if (v != null)
387 | postSettle(v);
388 | }
389 | }
390 |
391 | public boolean onDown(MotionEvent arg0) {
392 | mScroller.forceFinished(true);
393 | return true;
394 | }
395 |
396 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
397 | float velocityY) {
398 | if (mScaling)
399 | return true;
400 |
401 | View v = mChildViews.get(mCurrent);
402 | if (v != null) {
403 | Rect bounds = getScrollBounds(v);
404 | switch(directionOfTravel(velocityX, velocityY)) {
405 | case MOVING_LEFT:
406 | if (HORIZONTAL_SCROLLING && bounds.left >= 0) {
407 | // Fling off to the left bring next view onto screen
408 | View vl = mChildViews.get(mCurrent+1);
409 |
410 | if (vl != null) {
411 | slideViewOntoScreen(vl);
412 | return true;
413 | }
414 | }
415 | break;
416 | case MOVING_UP:
417 | if (!HORIZONTAL_SCROLLING && bounds.top >= 0) {
418 | // Fling off to the top bring next view onto screen
419 | View vl = mChildViews.get(mCurrent+1);
420 |
421 | if (vl != null) {
422 | slideViewOntoScreen(vl);
423 | return true;
424 | }
425 | }
426 | break;
427 | case MOVING_RIGHT:
428 | if (HORIZONTAL_SCROLLING && bounds.right <= 0) {
429 | // Fling off to the right bring previous view onto screen
430 | View vr = mChildViews.get(mCurrent-1);
431 |
432 | if (vr != null) {
433 | slideViewOntoScreen(vr);
434 | return true;
435 | }
436 | }
437 | break;
438 | case MOVING_DOWN:
439 | if (!HORIZONTAL_SCROLLING && bounds.bottom <= 0) {
440 | // Fling off to the bottom bring previous view onto screen
441 | View vr = mChildViews.get(mCurrent-1);
442 |
443 | if (vr != null) {
444 | slideViewOntoScreen(vr);
445 | return true;
446 | }
447 | }
448 | break;
449 | }
450 | mScrollerLastX = mScrollerLastY = 0;
451 | // If the page has been dragged out of bounds then we want to spring back
452 | // nicely. fling jumps back into bounds instantly, so we don't want to use
453 | // fling in that case. On the other hand, we don't want to forgo a fling
454 | // just because of a slightly off-angle drag taking us out of bounds other
455 | // than in the direction of the drag, so we test for out of bounds only
456 | // in the direction of travel.
457 | //
458 | // Also don't fling if out of bounds in any direction by more than fling
459 | // margin
460 | Rect expandedBounds = new Rect(bounds);
461 | expandedBounds.inset(-FLING_MARGIN, -FLING_MARGIN);
462 |
463 | if(withinBoundsInDirectionOfTravel(bounds, velocityX, velocityY)
464 | && expandedBounds.contains(0, 0)) {
465 | mScroller.fling(0, 0, (int)velocityX, (int)velocityY, bounds.left, bounds.right, bounds.top, bounds.bottom);
466 | mStepper.prod();
467 | }
468 | }
469 |
470 | return true;
471 | }
472 |
473 | public void onLongPress(MotionEvent e) { }
474 |
475 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
476 | float distanceY) {
477 | PageView pageView = (PageView)getDisplayedView();
478 | if (!tapDisabled)
479 | onDocMotion();
480 | if (!mScaling) {
481 | mXScroll -= distanceX;
482 | mYScroll -= distanceY;
483 | requestLayout();
484 | }
485 | return true;
486 | }
487 |
488 | public void onShowPress(MotionEvent e) { }
489 |
490 | public boolean onScale(ScaleGestureDetector detector) {
491 | float previousScale = mScale;
492 | mScale = Math.min(Math.max(mScale * detector.getScaleFactor(), MIN_SCALE), MAX_SCALE);
493 |
494 | {
495 | float factor = mScale/previousScale;
496 |
497 | View v = mChildViews.get(mCurrent);
498 | if (v != null) {
499 | float currentFocusX = detector.getFocusX();
500 | float currentFocusY = detector.getFocusY();
501 | // Work out the focus point relative to the view top left
502 | int viewFocusX = (int)currentFocusX - (v.getLeft() + mXScroll);
503 | int viewFocusY = (int)currentFocusY - (v.getTop() + mYScroll);
504 | // Scroll to maintain the focus point
505 | mXScroll += viewFocusX - viewFocusX * factor;
506 | mYScroll += viewFocusY - viewFocusY * factor;
507 |
508 | if (mLastScaleFocusX>=0)
509 | mXScroll+=currentFocusX-mLastScaleFocusX;
510 | if (mLastScaleFocusY>=0)
511 | mYScroll+=currentFocusY-mLastScaleFocusY;
512 |
513 | mLastScaleFocusX=currentFocusX;
514 | mLastScaleFocusY=currentFocusY;
515 | requestLayout();
516 | }
517 | }
518 | return true;
519 | }
520 |
521 | public boolean onScaleBegin(ScaleGestureDetector detector) {
522 | tapDisabled = true;
523 | mScaling = true;
524 | // Ignore any scroll amounts yet to be accounted for: the
525 | // screen is not showing the effect of them, so they can
526 | // only confuse the user
527 | mXScroll = mYScroll = 0;
528 | mLastScaleFocusX = mLastScaleFocusY = -1;
529 | return true;
530 | }
531 |
532 | public void onScaleEnd(ScaleGestureDetector detector) {
533 | mScaling = false;
534 | }
535 |
536 | @Override
537 | public boolean onTouchEvent(MotionEvent event) {
538 | if ((event.getAction() & event.getActionMasked()) == MotionEvent.ACTION_DOWN)
539 | {
540 | tapDisabled = false;
541 | }
542 |
543 | mScaleGestureDetector.onTouchEvent(event);
544 | mGestureDetector.onTouchEvent(event);
545 |
546 | if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
547 | mUserInteracting = true;
548 | }
549 | if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
550 | mUserInteracting = false;
551 |
552 | View v = mChildViews.get(mCurrent);
553 | if (v != null) {
554 | if (mScroller.isFinished()) {
555 | // If, at the end of user interaction, there is no
556 | // current inertial scroll in operation then animate
557 | // the view onto screen if necessary
558 | slideViewOntoScreen(v);
559 | }
560 |
561 | if (mScroller.isFinished()) {
562 | // If still there is no inertial scroll in operation
563 | // then the layout is stable
564 | postSettle(v);
565 | }
566 | }
567 | }
568 |
569 | requestLayout();
570 | return true;
571 | }
572 |
573 | @Override
574 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
575 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
576 |
577 | int n = getChildCount();
578 | for (int i = 0; i < n; i++)
579 | measureView(getChildAt(i));
580 | }
581 |
582 | @Override
583 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
584 | super.onLayout(changed, left, top, right, bottom);
585 |
586 | try {
587 | onLayout2(changed, left, top, right, bottom);
588 | }
589 | catch (java.lang.OutOfMemoryError e) {
590 | System.out.println("Out of memory during layout");
591 | }
592 | }
593 |
594 | private void onLayout2(boolean changed, int left, int top, int right,
595 | int bottom) {
596 |
597 | // "Edit mode" means when the View is being displayed in the Android GUI editor. (this class
598 | // is instantiated in the IDE, so we need to be a bit careful what we do).
599 | if (isInEditMode())
600 | return;
601 |
602 | View cv = mChildViews.get(mCurrent);
603 | Point cvOffset;
604 |
605 | if (!mResetLayout) {
606 | // Move to next or previous if current is sufficiently off center
607 | if (cv != null) {
608 | boolean move;
609 | cvOffset = subScreenSizeOffset(cv);
610 | // cv.getRight() may be out of date with the current scale
611 | // so add left to the measured width for the correct position
612 | if (HORIZONTAL_SCROLLING)
613 | move = cv.getLeft() + cv.getMeasuredWidth() + cvOffset.x + GAP/2 + mXScroll < getWidth()/2;
614 | else
615 | move = cv.getTop() + cv.getMeasuredHeight() + cvOffset.y + GAP/2 + mYScroll < getHeight()/2;
616 | if (move && mCurrent + 1 < mAdapter.getCount()) {
617 | postUnsettle(cv);
618 | // post to invoke test for end of animation
619 | // where we must set hq area for the new current view
620 | mStepper.prod();
621 |
622 | onMoveOffChild(mCurrent);
623 | mCurrent++;
624 | onMoveToChild(mCurrent);
625 | }
626 |
627 | if (HORIZONTAL_SCROLLING)
628 | move = cv.getLeft() - cvOffset.x - GAP/2 + mXScroll >= getWidth()/2;
629 | else
630 | move = cv.getTop() - cvOffset.y - GAP/2 + mYScroll >= getHeight()/2;
631 | if (move && mCurrent > 0) {
632 | postUnsettle(cv);
633 | // post to invoke test for end of animation
634 | // where we must set hq area for the new current view
635 | mStepper.prod();
636 |
637 | onMoveOffChild(mCurrent);
638 | mCurrent--;
639 | onMoveToChild(mCurrent);
640 | }
641 | }
642 |
643 | // Remove not needed children and hold them for reuse
644 | int numChildren = mChildViews.size();
645 | int childIndices[] = new int[numChildren];
646 | for (int i = 0; i < numChildren; i++)
647 | childIndices[i] = mChildViews.keyAt(i);
648 |
649 | for (int i = 0; i < numChildren; i++) {
650 | int ai = childIndices[i];
651 | if (ai < mCurrent - 1 || ai > mCurrent + 1) {
652 | View v = mChildViews.get(ai);
653 | onNotInUse(v);
654 | mViewCache.add(v);
655 | removeViewInLayout(v);
656 | mChildViews.remove(ai);
657 | }
658 | }
659 | } else {
660 | mResetLayout = false;
661 | mXScroll = mYScroll = 0;
662 |
663 | // Remove all children and hold them for reuse
664 | int numChildren = mChildViews.size();
665 | for (int i = 0; i < numChildren; i++) {
666 | View v = mChildViews.valueAt(i);
667 | onNotInUse(v);
668 | mViewCache.add(v);
669 | removeViewInLayout(v);
670 | }
671 | mChildViews.clear();
672 |
673 | // post to ensure generation of hq area
674 | mStepper.prod();
675 | }
676 |
677 | // Ensure current view is present
678 | int cvLeft, cvRight, cvTop, cvBottom;
679 | boolean notPresent = (mChildViews.get(mCurrent) == null);
680 | cv = getOrCreateChild(mCurrent);
681 | // When the view is sub-screen-size in either dimension we
682 | // offset it to center within the screen area, and to keep
683 | // the views spaced out
684 | cvOffset = subScreenSizeOffset(cv);
685 | if (notPresent) {
686 | // Main item not already present. Just place it top left
687 | cvLeft = cvOffset.x;
688 | cvTop = cvOffset.y;
689 | } else {
690 | // Main item already present. Adjust by scroll offsets
691 | cvLeft = cv.getLeft() + mXScroll;
692 | cvTop = cv.getTop() + mYScroll;
693 | }
694 | // Scroll values have been accounted for
695 | mXScroll = mYScroll = 0;
696 | cvRight = cvLeft + cv.getMeasuredWidth();
697 | cvBottom = cvTop + cv.getMeasuredHeight();
698 |
699 | if (!mUserInteracting && mScroller.isFinished()) {
700 | Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
701 | cvRight += corr.x;
702 | cvLeft += corr.x;
703 | cvTop += corr.y;
704 | cvBottom += corr.y;
705 | } else if (HORIZONTAL_SCROLLING && cv.getMeasuredHeight() <= getHeight()) {
706 | // When the current view is as small as the screen in height, clamp
707 | // it vertically
708 | Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
709 | cvTop += corr.y;
710 | cvBottom += corr.y;
711 | } else if (!HORIZONTAL_SCROLLING && cv.getMeasuredWidth() <= getWidth()) {
712 | // When the current view is as small as the screen in width, clamp
713 | // it horizontally
714 | Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
715 | cvRight += corr.x;
716 | cvLeft += corr.x;
717 | }
718 |
719 | cv.layout(cvLeft, cvTop, cvRight, cvBottom);
720 |
721 | if (mCurrent > 0) {
722 | View lv = getOrCreateChild(mCurrent - 1);
723 | Point leftOffset = subScreenSizeOffset(lv);
724 | if (HORIZONTAL_SCROLLING)
725 | {
726 | int gap = leftOffset.x + GAP + cvOffset.x;
727 | lv.layout(cvLeft - lv.getMeasuredWidth() - gap,
728 | (cvBottom + cvTop - lv.getMeasuredHeight())/2,
729 | cvLeft - gap,
730 | (cvBottom + cvTop + lv.getMeasuredHeight())/2);
731 | } else {
732 | int gap = leftOffset.y + GAP + cvOffset.y;
733 | lv.layout((cvLeft + cvRight - lv.getMeasuredWidth())/2,
734 | cvTop - lv.getMeasuredHeight() - gap,
735 | (cvLeft + cvRight + lv.getMeasuredWidth())/2,
736 | cvTop - gap);
737 | }
738 | }
739 |
740 | if (mCurrent + 1 < mAdapter.getCount()) {
741 | View rv = getOrCreateChild(mCurrent + 1);
742 | Point rightOffset = subScreenSizeOffset(rv);
743 | if (HORIZONTAL_SCROLLING)
744 | {
745 | int gap = cvOffset.x + GAP + rightOffset.x;
746 | rv.layout(cvRight + gap,
747 | (cvBottom + cvTop - rv.getMeasuredHeight())/2,
748 | cvRight + rv.getMeasuredWidth() + gap,
749 | (cvBottom + cvTop + rv.getMeasuredHeight())/2);
750 | } else {
751 | int gap = cvOffset.y + GAP + rightOffset.y;
752 | rv.layout((cvLeft + cvRight - rv.getMeasuredWidth())/2,
753 | cvBottom + gap,
754 | (cvLeft + cvRight + rv.getMeasuredWidth())/2,
755 | cvBottom + gap + rv.getMeasuredHeight());
756 | }
757 | }
758 |
759 | invalidate();
760 | }
761 |
762 | @Override
763 | public Adapter getAdapter() {
764 | return mAdapter;
765 | }
766 |
767 | @Override
768 | public View getSelectedView() {
769 | return null;
770 | }
771 |
772 | @Override
773 | public void setAdapter(Adapter adapter) {
774 | if (mAdapter != null && mAdapter != adapter)
775 | mAdapter.releaseBitmaps();
776 | mAdapter = (PageAdapter) adapter;
777 |
778 | requestLayout();
779 | }
780 |
781 | @Override
782 | public void setSelection(int arg0) {
783 | throw new UnsupportedOperationException(getContext().getString(R.string.not_supported));
784 | }
785 |
786 | private View getCached() {
787 | if (mViewCache.size() == 0)
788 | return null;
789 | else
790 | return mViewCache.removeFirst();
791 | }
792 |
793 | private View getOrCreateChild(int i) {
794 | View v = mChildViews.get(i);
795 | if (v == null) {
796 | v = mAdapter.getView(i, getCached(), this);
797 | addAndMeasureChild(i, v);
798 | onChildSetup(i, v);
799 | }
800 |
801 | return v;
802 | }
803 |
804 | private void addAndMeasureChild(int i, View v) {
805 | LayoutParams params = v.getLayoutParams();
806 | if (params == null) {
807 | params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
808 | }
809 | addViewInLayout(v, 0, params, true);
810 | mChildViews.append(i, v); // Record the view against its adapter index
811 | measureView(v);
812 | }
813 |
814 | private void measureView(View v) {
815 | // See what size the view wants to be
816 | v.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
817 |
818 | // Work out a scale that will fit it to this view
819 | float scale = Math.min((float)getWidth()/(float)v.getMeasuredWidth(),
820 | (float)getHeight()/(float)v.getMeasuredHeight());
821 | // Use the fitting values scaled by our current scale factor
822 | v.measure(View.MeasureSpec.EXACTLY | (int)(v.getMeasuredWidth()*scale*mScale),
823 | View.MeasureSpec.EXACTLY | (int)(v.getMeasuredHeight()*scale*mScale));
824 | }
825 |
826 | private Rect getScrollBounds(int left, int top, int right, int bottom) {
827 | int xmin = getWidth() - right;
828 | int xmax = -left;
829 | int ymin = getHeight() - bottom;
830 | int ymax = -top;
831 |
832 | // In either dimension, if view smaller than screen then
833 | // constrain it to be central
834 | if (xmin > xmax) xmin = xmax = (xmin + xmax)/2;
835 | if (ymin > ymax) ymin = ymax = (ymin + ymax)/2;
836 |
837 | return new Rect(xmin, ymin, xmax, ymax);
838 | }
839 |
840 | private Rect getScrollBounds(View v) {
841 | // There can be scroll amounts not yet accounted for in
842 | // onLayout, so add mXScroll and mYScroll to the current
843 | // positions when calculating the bounds.
844 | return getScrollBounds(v.getLeft() + mXScroll,
845 | v.getTop() + mYScroll,
846 | v.getLeft() + v.getMeasuredWidth() + mXScroll,
847 | v.getTop() + v.getMeasuredHeight() + mYScroll);
848 | }
849 |
850 | private Point getCorrection(Rect bounds) {
851 | return new Point(Math.min(Math.max(0,bounds.left),bounds.right),
852 | Math.min(Math.max(0,bounds.top),bounds.bottom));
853 | }
854 |
855 | private void postSettle(final View v) {
856 | // onSettle and onUnsettle are posted so that the calls
857 | // won't be executed until after the system has performed
858 | // layout.
859 | post (new Runnable() {
860 | public void run () {
861 | onSettle(v);
862 | }
863 | });
864 | }
865 |
866 | private void postUnsettle(final View v) {
867 | post (new Runnable() {
868 | public void run () {
869 | onUnsettle(v);
870 | }
871 | });
872 | }
873 |
874 | private void slideViewOntoScreen(View v) {
875 | Point corr = getCorrection(getScrollBounds(v));
876 | if (corr.x != 0 || corr.y != 0) {
877 | mScrollerLastX = mScrollerLastY = 0;
878 | mScroller.startScroll(0, 0, corr.x, corr.y, 400);
879 | mStepper.prod();
880 | }
881 | }
882 |
883 | private Point subScreenSizeOffset(View v) {
884 | return new Point(Math.max((getWidth() - v.getMeasuredWidth())/2, 0),
885 | Math.max((getHeight() - v.getMeasuredHeight())/2, 0));
886 | }
887 |
888 | private static int directionOfTravel(float vx, float vy) {
889 | if (Math.abs(vx) > 2 * Math.abs(vy))
890 | return (vx > 0) ? MOVING_RIGHT : MOVING_LEFT;
891 | else if (Math.abs(vy) > 2 * Math.abs(vx))
892 | return (vy > 0) ? MOVING_DOWN : MOVING_UP;
893 | else
894 | return MOVING_DIAGONALLY;
895 | }
896 |
897 | private static boolean withinBoundsInDirectionOfTravel(Rect bounds, float vx, float vy) {
898 | switch (directionOfTravel(vx, vy)) {
899 | case MOVING_DIAGONALLY: return bounds.contains(0, 0);
900 | case MOVING_LEFT: return bounds.left <= 0;
901 | case MOVING_RIGHT: return bounds.right >= 0;
902 | case MOVING_UP: return bounds.top <= 0;
903 | case MOVING_DOWN: return bounds.bottom >= 0;
904 | default: throw new NoSuchElementException();
905 | }
906 | }
907 |
908 | protected void onTapMainDocArea() {}
909 | protected void onDocMotion() {}
910 |
911 | public void setLinksEnabled(boolean b) {
912 | mLinksEnabled = b;
913 | resetupChildren();
914 | invalidate();
915 | }
916 |
917 | public boolean onSingleTapUp(MotionEvent e) {
918 | Link link = null;
919 | if (!tapDisabled) {
920 | PageView pageView = (PageView) getDisplayedView();
921 | if (mLinksEnabled && pageView != null) {
922 | int page = pageView.hitLink(e.getX(), e.getY());
923 | if (page > 0) {
924 | pushHistory();
925 | setDisplayedViewIndex(page);
926 | } else {
927 | onTapMainDocArea();
928 | }
929 | } else if (e.getX() < tapPageMargin) {
930 | smartMoveBackwards();
931 | } else if (e.getX() > super.getWidth() - tapPageMargin) {
932 | smartMoveForwards();
933 | } else if (e.getY() < tapPageMargin) {
934 | smartMoveBackwards();
935 | } else if (e.getY() > super.getHeight() - tapPageMargin) {
936 | smartMoveForwards();
937 | } else {
938 | onTapMainDocArea();
939 | }
940 | }
941 | return true;
942 | }
943 |
944 | protected void onChildSetup(int i, View v) {
945 | if (SearchTaskResult.get() != null
946 | && SearchTaskResult.get().pageNumber == i)
947 | ((PageView) v).setSearchBoxes(SearchTaskResult.get().searchBoxes);
948 | else
949 | ((PageView) v).setSearchBoxes(null);
950 |
951 | ((PageView) v).setLinkHighlighting(mLinksEnabled);
952 | }
953 |
954 | protected void onMoveToChild(int i) {
955 | if (SearchTaskResult.get() != null
956 | && SearchTaskResult.get().pageNumber != i) {
957 | SearchTaskResult.set(null);
958 | resetupChildren();
959 | }
960 | }
961 |
962 | protected void onMoveOffChild(int i) {
963 | }
964 |
965 | protected void onSettle(View v) {
966 | // When the layout has settled ask the page to render
967 | // in HQ
968 | ((PageView) v).updateHq(false);
969 | }
970 |
971 | protected void onUnsettle(View v) {
972 | // When something changes making the previous settled view
973 | // no longer appropriate, tell the page to remove HQ
974 | ((PageView) v).removeHq();
975 | }
976 |
977 | protected void onNotInUse(View v) {
978 | ((PageView) v).releaseResources();
979 | }
980 | }
981 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published by
637 | the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------