├── 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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 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 | --------------------------------------------------------------------------------