initial commit with overhauled / rebranded project
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
custom: https://grapheneos.org/donate
|
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
*.iml
|
||||
.gradle
|
||||
local.properties
|
||||
build/
|
||||
signing.properties
|
||||
*.jks
|
||||
/.idea
|
19
LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2019 Daniel Micay
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
177
PDFJS_LICENSE
Normal file
@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
62
app/build.gradle
Normal file
@ -0,0 +1,62 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion "29.0.0"
|
||||
defaultConfig {
|
||||
applicationId "org.grapheneos.pdfviewer"
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 28
|
||||
versionCode 1
|
||||
versionName versionCode.toString()
|
||||
resConfigs "en"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
}
|
||||
|
||||
def props = new Properties()
|
||||
def propFile = new File('signing.properties')
|
||||
|
||||
if (propFile.canRead()) {
|
||||
props.load(new FileInputStream(propFile))
|
||||
|
||||
if (props != null &&
|
||||
props.containsKey('STORE_FILE') &&
|
||||
props.containsKey('STORE_PASSWORD') &&
|
||||
props.containsKey('KEY_ALIAS') &&
|
||||
props.containsKey('KEY_PASSWORD')) {
|
||||
android.signingConfigs.release.storeFile = rootProject.file(props['STORE_FILE'])
|
||||
android.signingConfigs.release.storePassword = props['STORE_PASSWORD']
|
||||
android.signingConfigs.release.keyAlias = props['KEY_ALIAS']
|
||||
android.signingConfigs.release.keyPassword = props['KEY_PASSWORD']
|
||||
} else {
|
||||
println 'signing.properties found but some entries are missing'
|
||||
android.buildTypes.release.signingConfig = null
|
||||
}
|
||||
} else {
|
||||
println 'signing.properties not found'
|
||||
android.buildTypes.release.signingConfig = null
|
||||
}
|
16
app/lint.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<lint>
|
||||
<!-- full backups are desired -->
|
||||
<issue id="AllowBackup">
|
||||
<ignore path="src/main/AndroidManifest.xml"/>
|
||||
</issue>
|
||||
|
||||
<!-- Google app indexing doesn't make any sense for this app -->
|
||||
<issue id="AppLinkUrlError">
|
||||
<ignore path="src/main/AndroidManifest.xml"/>
|
||||
</issue>
|
||||
|
||||
<!-- targetSandboxVersion unused with API level < 26 -->
|
||||
<issue id="UnusedAttribute">
|
||||
<ignore path="src/main/AndroidManifest.xml"/>
|
||||
</issue>
|
||||
</lint>
|
3
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
-keepclassmembers class * {
|
||||
@android.webkit.JavascriptInterface <methods>;
|
||||
}
|
29
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.grapheneos.pdfviewer"
|
||||
android:targetSandboxVersion="2">
|
||||
<application android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
android:allowBackup="true">
|
||||
<activity android:name=".PdfViewer"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:mimeType="application/pdf" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="false" />
|
||||
</application>
|
||||
</manifest>
|
1
app/src/main/assets/pdf.js
Normal file
1
app/src/main/assets/pdf.worker.js
vendored
Normal file
66
app/src/main/assets/viewer.css
Normal file
@ -0,0 +1,66 @@
|
||||
body, canvas {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.textLayer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0.2;
|
||||
line-height: 1.0;
|
||||
}
|
||||
|
||||
.textLayer > div {
|
||||
color: transparent;
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
cursor: text;
|
||||
transform-origin: 0% 0%;
|
||||
}
|
||||
|
||||
.textLayer .highlight {
|
||||
margin: -1px;
|
||||
padding: 1px;
|
||||
|
||||
background-color: rgb(180, 0, 170);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.textLayer .highlight.begin {
|
||||
border-radius: 4px 0px 0px 4px;
|
||||
}
|
||||
|
||||
.textLayer .highlight.end {
|
||||
border-radius: 0px 4px 4px 0px;
|
||||
}
|
||||
|
||||
.textLayer .highlight.middle {
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.textLayer .highlight.selected {
|
||||
background-color: rgb(0, 100, 0);
|
||||
}
|
||||
|
||||
.textLayer ::selection { background: rgb(0,0,255); }
|
||||
.textLayer ::-moz-selection { background: rgb(0,0,255); }
|
||||
|
||||
.textLayer .endOfContent {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 100%;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
z-index: -1;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.textLayer .endOfContent.active {
|
||||
top: 0px;
|
||||
}
|
14
app/src/main/assets/viewer.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no" />
|
||||
<title>PDF</title>
|
||||
<link rel="stylesheet" href="viewer.css" />
|
||||
<script src="pdf.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="content"></canvas>
|
||||
<div id="text" class="textLayer"></div>
|
||||
<script src="viewer.js"></script>
|
||||
</body>
|
||||
</html>
|
193
app/src/main/assets/viewer.js
Normal file
@ -0,0 +1,193 @@
|
||||
"use strict";
|
||||
|
||||
let pdfDoc = null;
|
||||
let pageRendering = false;
|
||||
let renderPending = false;
|
||||
let renderPendingLazy = false;
|
||||
const canvas = document.getElementById('content');
|
||||
let zoomLevel = 100;
|
||||
let textLayerDiv = document.getElementById("text");
|
||||
const zoomLevels = [50, 75, 100, 125, 150];
|
||||
let task = null;
|
||||
|
||||
let newPageNumber = 0;
|
||||
let newZoomLevel = 0;
|
||||
let useRender;
|
||||
|
||||
const cache = [];
|
||||
const maxCached = 6;
|
||||
|
||||
function maybeRenderNextPage() {
|
||||
if (renderPending) {
|
||||
pageRendering = false;
|
||||
renderPending = false;
|
||||
renderPage(channel.getPage(), renderPendingLazy, false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleRenderingError(error) {
|
||||
console.log("rendering error: " + error);
|
||||
|
||||
pageRendering = false;
|
||||
maybeRenderNextPage();
|
||||
}
|
||||
|
||||
function doPrerender(pageNumber, prerenderTrigger) {
|
||||
if (useRender) {
|
||||
if (pageNumber + 1 <= pdfDoc.numPages) {
|
||||
renderPage(pageNumber + 1, false, true, pageNumber);
|
||||
} else if (pageNumber - 1 > 0) {
|
||||
renderPage(pageNumber - 1, false, true, pageNumber);
|
||||
}
|
||||
} else if (pageNumber == prerenderTrigger + 1) {
|
||||
if (prerenderTrigger - 1 > 0) {
|
||||
renderPage(prerenderTrigger - 1, false, true, prerenderTrigger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function display(newCanvas) {
|
||||
canvas.height = newCanvas.height;
|
||||
canvas.width = newCanvas.width;
|
||||
canvas.style.height = newCanvas.style.height;
|
||||
canvas.style.width = newCanvas.style.width;
|
||||
canvas.getContext("2d", { alpha: false }).drawImage(newCanvas, 0, 0);
|
||||
scrollTo(0, 0);
|
||||
}
|
||||
|
||||
function renderPage(pageNumber, lazy, prerender, prerenderTrigger=0) {
|
||||
pageRendering = true;
|
||||
useRender = !prerender;
|
||||
|
||||
newPageNumber = pageNumber;
|
||||
newZoomLevel = zoomLevels[channel.getZoomLevel()];
|
||||
console.log("page: " + pageNumber + ", zoom: " + newZoomLevel + ", prerender: " + prerender);
|
||||
for (let i = 0; i < cache.length; i++) {
|
||||
const cached = cache[i];
|
||||
if (cached.pageNumber === pageNumber && cached.zoomLevel === newZoomLevel) {
|
||||
if (useRender) {
|
||||
cache.splice(i, 1);
|
||||
cache.push(cached);
|
||||
|
||||
display(cached.canvas);
|
||||
|
||||
textLayerDiv.replaceWith(cached.textLayerDiv);
|
||||
textLayerDiv = cached.textLayerDiv;
|
||||
}
|
||||
|
||||
pageRendering = false;
|
||||
doPrerender(pageNumber, prerenderTrigger);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pdfDoc.getPage(pageNumber).then(function(page) {
|
||||
if (maybeRenderNextPage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCanvas = document.createElement("canvas");
|
||||
const viewport = page.getViewport(newZoomLevel / 100)
|
||||
const ratio = window.devicePixelRatio;
|
||||
newCanvas.height = viewport.height * ratio;
|
||||
newCanvas.width = viewport.width * ratio;
|
||||
newCanvas.style.height = viewport.height + "px";
|
||||
newCanvas.style.width = viewport.width + "px";
|
||||
const newContext = newCanvas.getContext("2d", { alpha: false });
|
||||
newContext.scale(ratio, ratio);
|
||||
|
||||
if (useRender) {
|
||||
if (newZoomLevel != zoomLevel) {
|
||||
canvas.style.height = viewport.height + "px";
|
||||
canvas.style.width = viewport.width + "px";
|
||||
}
|
||||
zoomLevel = newZoomLevel;
|
||||
}
|
||||
|
||||
task = page.render({
|
||||
canvasContext: newContext,
|
||||
viewport: viewport
|
||||
});
|
||||
|
||||
task.then(function() {
|
||||
task = null;
|
||||
|
||||
let rendered = false;
|
||||
function render() {
|
||||
if (!useRender || rendered) {
|
||||
return;
|
||||
}
|
||||
display(newCanvas);
|
||||
rendered = true;
|
||||
}
|
||||
render();
|
||||
|
||||
const textLayerFrag = document.createDocumentFragment();
|
||||
task = PDFJS.renderTextLayer({
|
||||
textContentStream: page.streamTextContent(),
|
||||
container: textLayerFrag,
|
||||
viewport: viewport
|
||||
});
|
||||
task.promise.then(function() {
|
||||
task = null;
|
||||
|
||||
render();
|
||||
|
||||
const newTextLayerDiv = textLayerDiv.cloneNode();
|
||||
newTextLayerDiv.style.height = newCanvas.style.height;
|
||||
newTextLayerDiv.style.width = newCanvas.style.width;
|
||||
if (useRender) {
|
||||
textLayerDiv.replaceWith(newTextLayerDiv);
|
||||
textLayerDiv = newTextLayerDiv;
|
||||
}
|
||||
newTextLayerDiv.appendChild(textLayerFrag);
|
||||
|
||||
if (cache.length === maxCached) {
|
||||
cache.shift()
|
||||
}
|
||||
cache.push({
|
||||
pageNumber: pageNumber,
|
||||
zoomLevel: newZoomLevel,
|
||||
canvas: newCanvas,
|
||||
textLayerDiv: newTextLayerDiv
|
||||
});
|
||||
|
||||
pageRendering = false;
|
||||
doPrerender(pageNumber, prerenderTrigger);
|
||||
}).catch(handleRenderingError);
|
||||
}).catch(handleRenderingError);
|
||||
});
|
||||
}
|
||||
|
||||
function onRenderPage(lazy) {
|
||||
if (pageRendering) {
|
||||
if (newPageNumber === channel.getPage() && newZoomLevel === zoomLevels[channel.getZoomLevel()]) {
|
||||
useRender = true;
|
||||
return;
|
||||
}
|
||||
|
||||
renderPending = true;
|
||||
renderPendingLazy = lazy;
|
||||
if (task !== null) {
|
||||
task.cancel();
|
||||
task = null;
|
||||
}
|
||||
} else {
|
||||
renderPage(channel.getPage(), lazy, false);
|
||||
}
|
||||
}
|
||||
|
||||
PDFJS.getDocument("https://localhost/placeholder.pdf").then(function(newDoc) {
|
||||
pdfDoc = newDoc;
|
||||
channel.setNumPages(pdfDoc.numPages);
|
||||
pdfDoc.getMetadata().then(function(data) {
|
||||
channel.setDocumentProperties(JSON.stringify(data.info));
|
||||
}).catch(function(error) {
|
||||
console.log("getMetadata error: " + error);
|
||||
});
|
||||
renderPage(channel.getPage(), false, false);
|
||||
}).catch(function(error) {
|
||||
console.log("getDocument error: " + error);
|
||||
});
|
BIN
app/src/main/ic_launcher-web.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
442
app/src/main/java/org/grapheneos/pdfviewer/PdfViewer.java
Normal file
@ -0,0 +1,442 @@
|
||||
package org.grapheneos.pdfviewer;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import org.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment;
|
||||
import org.grapheneos.pdfviewer.fragment.JumpToPageFragment;
|
||||
import org.grapheneos.pdfviewer.loader.DocumentPropertiesLoader;
|
||||
|
||||
public class PdfViewer extends AppCompatActivity implements LoaderManager.LoaderCallbacks<List<CharSequence>> {
|
||||
public static final String TAG = "PdfViewer";
|
||||
|
||||
private static final String STATE_URI = "uri";
|
||||
private static final String STATE_PAGE = "page";
|
||||
private static final String STATE_ZOOM_LEVEL = "zoomLevel";
|
||||
private static final String KEY_PROPERTIES = "properties";
|
||||
|
||||
private static final String CONTENT_SECURITY_POLICY =
|
||||
"default-src 'none'; " +
|
||||
"form-action 'none'; " +
|
||||
"connect-src https://localhost/placeholder.pdf; " +
|
||||
"img-src blob: 'self'; " +
|
||||
"script-src 'self'; " +
|
||||
"style-src 'self'; " +
|
||||
"frame-ancestors 'none'; " +
|
||||
"base-uri 'none'";
|
||||
|
||||
private static final String FEATURE_POLICY =
|
||||
"accelerometer 'none'; " +
|
||||
"ambient-light-sensor 'none'; " +
|
||||
"autoplay 'none'; " +
|
||||
"camera 'none'; " +
|
||||
"encrypted-media 'none'; " +
|
||||
"fullscreen 'none'; " +
|
||||
"geolocation 'none'; " +
|
||||
"gyroscope 'none'; " +
|
||||
"magnetometer 'none'; " +
|
||||
"microphone 'none'; " +
|
||||
"midi 'none'; " +
|
||||
"payment 'none'; " +
|
||||
"picture-in-picture 'none'; " +
|
||||
"speaker 'none'; " +
|
||||
"sync-xhr 'none'; " +
|
||||
"usb 'none'; " +
|
||||
"vr 'none'";
|
||||
|
||||
private static final int MIN_ZOOM_LEVEL = 0;
|
||||
private static final int MAX_ZOOM_LEVEL = 4;
|
||||
private static final int ALPHA_LOW = 130;
|
||||
private static final int ALPHA_HIGH = 255;
|
||||
private static final int ACTION_OPEN_DOCUMENT_REQUEST_CODE = 1;
|
||||
private static final int STATE_LOADED = 1;
|
||||
private static final int STATE_END = 2;
|
||||
private static final int PADDING = 10;
|
||||
|
||||
private Uri mUri;
|
||||
public int mPage;
|
||||
public int mNumPages;
|
||||
private int mZoomLevel = 2;
|
||||
private int mDocumentState;
|
||||
private List<CharSequence> mDocumentProperties;
|
||||
private InputStream mInputStream;
|
||||
|
||||
private WebView mWebView;
|
||||
private TextView mTextView;
|
||||
private Toast mToast;
|
||||
|
||||
private class Channel {
|
||||
@JavascriptInterface
|
||||
public int getPage() {
|
||||
return mPage;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public int getZoomLevel() {
|
||||
return mZoomLevel;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void setNumPages(int numPages) {
|
||||
mNumPages = numPages;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void setDocumentProperties(final String properties) {
|
||||
if (mDocumentProperties != null) {
|
||||
throw new SecurityException("mDocumentProperties not null");
|
||||
}
|
||||
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(KEY_PROPERTIES, properties);
|
||||
runOnUiThread(() -> {
|
||||
LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesLoader.ID, args, PdfViewer.this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Can be removed once minSdkVersion >= 26
|
||||
@SuppressWarnings("deprecation")
|
||||
private void disableSaveFormData(final WebSettings settings) {
|
||||
settings.setSaveFormData(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.webview);
|
||||
|
||||
mWebView = findViewById(R.id.webview);
|
||||
final WebSettings settings = mWebView.getSettings();
|
||||
settings.setAllowContentAccess(false);
|
||||
settings.setAllowFileAccess(false);
|
||||
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
|
||||
settings.setJavaScriptEnabled(true);
|
||||
disableSaveFormData(settings);
|
||||
|
||||
CookieManager.getInstance().setAcceptCookie(false);
|
||||
|
||||
mWebView.addJavascriptInterface(new Channel(), "channel");
|
||||
|
||||
mWebView.setWebViewClient(new WebViewClient() {
|
||||
private WebResourceResponse fromAsset(final String mime, final String path) {
|
||||
try {
|
||||
InputStream inputStream = getAssets().open(path.substring(1));
|
||||
return new WebResourceResponse(mime, null, inputStream);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
||||
if (!"GET".equals(request.getMethod())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Uri url = request.getUrl();
|
||||
if (!"localhost".equals(url.getHost())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String path = url.getPath();
|
||||
Log.d(TAG, "path " + path);
|
||||
|
||||
if ("/placeholder.pdf".equals(path)) {
|
||||
return new WebResourceResponse("application/pdf", null, mInputStream);
|
||||
}
|
||||
|
||||
if ("/viewer.html".equals(path)) {
|
||||
final WebResourceResponse response = fromAsset("text/html", path);
|
||||
HashMap<String, String> headers = new HashMap<String, String>();
|
||||
headers.put("Content-Security-Policy", CONTENT_SECURITY_POLICY);
|
||||
headers.put("Feature-Policy", FEATURE_POLICY);
|
||||
response.setResponseHeaders(headers);
|
||||
return response;
|
||||
}
|
||||
|
||||
if ("/viewer.css".equals(path)) {
|
||||
return fromAsset("text/css", path);
|
||||
}
|
||||
|
||||
if ("/viewer.js".equals(path)) {
|
||||
return fromAsset("application/javascript", path);
|
||||
}
|
||||
|
||||
if ("/pdf.js".equals(path)) {
|
||||
return fromAsset("application/javascript", path);
|
||||
}
|
||||
|
||||
if ("/pdf.worker.js".equals(path)) {
|
||||
return fromAsset("application/javascript", path);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
mDocumentState = STATE_LOADED;
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
});
|
||||
|
||||
mTextView = new TextView(this);
|
||||
mTextView.setBackgroundColor(Color.DKGRAY);
|
||||
mTextView.setTextColor(ColorStateList.valueOf(Color.WHITE));
|
||||
mTextView.setTextSize(18);
|
||||
mTextView.setPadding(PADDING, 0, PADDING, 0);
|
||||
|
||||
// If loaders are not being initialized in onCreate(), the result will not be delivered
|
||||
// after orientation change (See FragmentHostCallback), thus initialize the
|
||||
// loader manager impl so that the result will be delivered.
|
||||
LoaderManager.getInstance(this);
|
||||
|
||||
final Intent intent = getIntent();
|
||||
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
|
||||
if (!"application/pdf".equals(intent.getType())) {
|
||||
Log.e(TAG, "invalid mime type");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
mUri = intent.getData();
|
||||
mPage = 1;
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
mUri = savedInstanceState.getParcelable(STATE_URI);
|
||||
mPage = savedInstanceState.getInt(STATE_PAGE);
|
||||
mZoomLevel = savedInstanceState.getInt(STATE_ZOOM_LEVEL);
|
||||
}
|
||||
|
||||
if (mUri != null) {
|
||||
loadPdf();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<List<CharSequence>> onCreateLoader(int id, Bundle args) {
|
||||
return new DocumentPropertiesLoader(this, args.getString(KEY_PROPERTIES), mNumPages, mUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<List<CharSequence>> loader, List<CharSequence> data) {
|
||||
mDocumentProperties = data;
|
||||
LoaderManager.getInstance(this).destroyLoader(DocumentPropertiesLoader.ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<List<CharSequence>> loader) {
|
||||
mDocumentProperties = null;
|
||||
}
|
||||
|
||||
private void loadPdf() {
|
||||
try {
|
||||
if (mInputStream != null) {
|
||||
mInputStream.close();
|
||||
}
|
||||
mInputStream = getContentResolver().openInputStream(mUri);
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
}
|
||||
mWebView.loadUrl("https://localhost/viewer.html");
|
||||
}
|
||||
|
||||
private void renderPage(final boolean lazy) {
|
||||
mWebView.evaluateJavascript(lazy ? "onRenderPage(true)" : "onRenderPage(false)", null);
|
||||
}
|
||||
|
||||
private void openDocument() {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("application/pdf");
|
||||
startActivityForResult(intent, ACTION_OPEN_DOCUMENT_REQUEST_CODE);
|
||||
}
|
||||
|
||||
private static void enableDisableMenuItem(MenuItem item, boolean enable) {
|
||||
if (enable) {
|
||||
if (!item.isEnabled()) {
|
||||
item.setEnabled(true);
|
||||
item.getIcon().setAlpha(ALPHA_HIGH);
|
||||
}
|
||||
} else if (item.isEnabled()) {
|
||||
item.setEnabled(false);
|
||||
item.getIcon().setAlpha(ALPHA_LOW);
|
||||
}
|
||||
}
|
||||
|
||||
public void onJumpToPageInDocument(int selected_page) {
|
||||
if (selected_page >= 1 && selected_page <= mNumPages) {
|
||||
mPage = selected_page;
|
||||
renderPage(false);
|
||||
showPageNumber();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle savedInstanceState) {
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
savedInstanceState.putParcelable(STATE_URI, mUri);
|
||||
savedInstanceState.putInt(STATE_PAGE, mPage);
|
||||
savedInstanceState.putInt(STATE_ZOOM_LEVEL, mZoomLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent resultData) {
|
||||
if (requestCode == ACTION_OPEN_DOCUMENT_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
||||
if (resultData != null) {
|
||||
mUri = resultData.getData();
|
||||
mPage = 1;
|
||||
mDocumentProperties = null;
|
||||
loadPdf();
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showPageNumber() {
|
||||
if (mToast != null) {
|
||||
mToast.cancel();
|
||||
}
|
||||
mTextView.setText(String.format("%s/%s", mPage, mNumPages));
|
||||
mToast = new Toast(getApplicationContext());
|
||||
mToast.setGravity(Gravity.BOTTOM | Gravity.END, PADDING, PADDING);
|
||||
mToast.setDuration(Toast.LENGTH_SHORT);
|
||||
mToast.setView(mTextView);
|
||||
mToast.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.pdf_viewer, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
final int ids[] = { R.id.action_zoom_in, R.id.action_zoom_out, R.id.action_jump_to_page,
|
||||
R.id.action_next, R.id.action_previous, R.id.action_view_document_properties };
|
||||
if (mDocumentState < STATE_LOADED) {
|
||||
for (final int id : ids) {
|
||||
final MenuItem item = menu.findItem(id);
|
||||
if (item.isVisible()) {
|
||||
item.setVisible(false);
|
||||
}
|
||||
}
|
||||
} else if (mDocumentState == STATE_LOADED) {
|
||||
for (final int id : ids) {
|
||||
final MenuItem item = menu.findItem(id);
|
||||
if (!item.isVisible()) {
|
||||
item.setVisible(true);
|
||||
}
|
||||
}
|
||||
mDocumentState = STATE_END;
|
||||
}
|
||||
|
||||
switch (mZoomLevel) {
|
||||
case MAX_ZOOM_LEVEL:
|
||||
enableDisableMenuItem(menu.findItem(R.id.action_zoom_in), false);
|
||||
return true;
|
||||
case MIN_ZOOM_LEVEL:
|
||||
enableDisableMenuItem(menu.findItem(R.id.action_zoom_out), false);
|
||||
return true;
|
||||
default:
|
||||
enableDisableMenuItem(menu.findItem(R.id.action_zoom_in), true);
|
||||
enableDisableMenuItem(menu.findItem(R.id.action_zoom_out), true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_previous:
|
||||
if (mPage > 1) {
|
||||
mPage--;
|
||||
renderPage(false);
|
||||
showPageNumber();
|
||||
}
|
||||
return true;
|
||||
|
||||
case R.id.action_next:
|
||||
if (mPage < mNumPages) {
|
||||
mPage++;
|
||||
renderPage(false);
|
||||
showPageNumber();
|
||||
}
|
||||
return true;
|
||||
|
||||
case R.id.action_open:
|
||||
openDocument();
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
||||
case R.id.action_zoom_out:
|
||||
if (mZoomLevel > 0) {
|
||||
mZoomLevel--;
|
||||
renderPage(true);
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
return true;
|
||||
|
||||
case R.id.action_zoom_in:
|
||||
if (mZoomLevel < MAX_ZOOM_LEVEL) {
|
||||
mZoomLevel++;
|
||||
renderPage(true);
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
return true;
|
||||
|
||||
case R.id.action_view_document_properties:
|
||||
DocumentPropertiesFragment
|
||||
.getInstance((ArrayList<CharSequence>) mDocumentProperties)
|
||||
.show(getSupportFragmentManager(), DocumentPropertiesFragment.TAG);
|
||||
return true;
|
||||
|
||||
case R.id.action_jump_to_page:
|
||||
new JumpToPageFragment()
|
||||
.show(getSupportFragmentManager(), JumpToPageFragment.TAG);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
}
|
196
app/src/main/java/org/grapheneos/pdfviewer/Utils.java
Normal file
@ -0,0 +1,196 @@
|
||||
package org.grapheneos.pdfviewer;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.math.RoundingMode;
|
||||
import java.text.DateFormat;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.Calendar;
|
||||
|
||||
public class Utils {
|
||||
public static String parseFileSize(long fileSize) {
|
||||
final double kb = fileSize / 1000;
|
||||
|
||||
if (kb == 0) {
|
||||
return fileSize + " Bytes";
|
||||
}
|
||||
|
||||
final DecimalFormat format = new DecimalFormat("#.##");
|
||||
format.setRoundingMode(RoundingMode.CEILING);
|
||||
|
||||
if (kb < 1000) {
|
||||
return format.format(kb) + " kB (" + fileSize + " Bytes)";
|
||||
}
|
||||
return format.format(kb / 1000) + " MB (" + fileSize + " Bytes)";
|
||||
}
|
||||
|
||||
// Parse date as per PDF spec (complies with PDF v1.4 to v1.7)
|
||||
public static String parseDate(String date) throws ParseException {
|
||||
int position = 0;
|
||||
|
||||
// D: prefix is optional for PDF < v1.7; required for PDF v1.7
|
||||
if (!date.startsWith("D:")) {
|
||||
date = "D:" + date;
|
||||
}
|
||||
if (date.length() < 6 || date.length() > 23) {
|
||||
throw new ParseException("Invalid datetime length", position);
|
||||
}
|
||||
|
||||
final Calendar calendar = Calendar.getInstance();
|
||||
final int currentYear = calendar.get(Calendar.YEAR);
|
||||
int year;
|
||||
|
||||
// Year is required
|
||||
String field = date.substring(position += 2, 6);
|
||||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid year", position);
|
||||
}
|
||||
year = Integer.valueOf(field);
|
||||
if (year > currentYear) {
|
||||
year = currentYear;
|
||||
}
|
||||
|
||||
position += 4;
|
||||
|
||||
// Default value for month and day shall be 1 (calendar month starts at 0 in Java 7),
|
||||
// all others default to 0
|
||||
int month = 0;
|
||||
int day = 1;
|
||||
int hours = 0;
|
||||
int minutes = 0;
|
||||
int seconds = 0;
|
||||
|
||||
// All succeeding fields are optional, but each preceding field must be present
|
||||
if (date.length() > 8) {
|
||||
field = date.substring(position, 8);
|
||||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid month", position);
|
||||
}
|
||||
month = Integer.valueOf(field) - 1;
|
||||
if (month > 11) {
|
||||
throw new ParseException("Invalid month", position);
|
||||
}
|
||||
position += 2;
|
||||
}
|
||||
if (date.length() > 10) {
|
||||
field = date.substring(8, 10);
|
||||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid day", position);
|
||||
}
|
||||
day = Integer.valueOf(field);
|
||||
if (day > 31) {
|
||||
throw new ParseException("Invalid day", position);
|
||||
}
|
||||
position += 2;
|
||||
}
|
||||
if (date.length() > 12) {
|
||||
field = date.substring(10, 12);
|
||||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid hours", position);
|
||||
}
|
||||
hours = Integer.valueOf(field);
|
||||
if (hours > 23) {
|
||||
throw new ParseException("Invalid hours", position);
|
||||
}
|
||||
position += 2;
|
||||
}
|
||||
if (date.length() > 14) {
|
||||
field = date.substring(12, 14);
|
||||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid minutes", position);
|
||||
}
|
||||
minutes = Integer.valueOf(field);
|
||||
if (minutes > 59) {
|
||||
throw new ParseException("Invalid minutes", position);
|
||||
}
|
||||
position += 2;
|
||||
}
|
||||
if (date.length() > 16) {
|
||||
field = date.substring(14, 16);
|
||||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid seconds", position);
|
||||
}
|
||||
seconds = Integer.valueOf(field);
|
||||
if (seconds > 59) {
|
||||
throw new ParseException("Invalid seconds", position);
|
||||
}
|
||||
position += 2;
|
||||
}
|
||||
|
||||
|
||||
if (date.length() > position) {
|
||||
int offsetHours = 0;
|
||||
int offsetMinutes = 0;
|
||||
|
||||
final char utRel = date.charAt(position);
|
||||
if (utRel != '\u002D' && utRel != '\u002B' && utRel != '\u005A') {
|
||||
throw new ParseException("Invalid UT relation", position);
|
||||
}
|
||||
|
||||
position++;
|
||||
|
||||
if (date.length() > position + 2) {
|
||||
field = date.substring(position, position + 2);
|
||||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid UTC offset hours", position);
|
||||
}
|
||||
offsetHours = Integer.parseInt(field);
|
||||
final int offsetHoursMinutes = offsetHours * 100 + offsetMinutes;
|
||||
|
||||
// Validate UTC offset (UTC-12:00 to UTC+14:00)
|
||||
if ((utRel == '\u002D' && offsetHoursMinutes > 1200) ||
|
||||
(utRel == '\u002B' && offsetHoursMinutes > 1400)) {
|
||||
throw new ParseException("Invalid UTC offset hours", position);
|
||||
}
|
||||
|
||||
position += 2;
|
||||
|
||||
// Apostrophe shall succeed HH and precede mm
|
||||
if (date.charAt(position) != '\'') {
|
||||
throw new ParseException("Expected apostrophe", position);
|
||||
}
|
||||
|
||||
position++;
|
||||
|
||||
if (date.length() > position + 2) {
|
||||
field = date.substring(position, position + 2);
|
||||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid UTC offset minutes", position);
|
||||
}
|
||||
offsetMinutes = Integer.parseInt(field);
|
||||
if (offsetMinutes > 59) {
|
||||
throw new ParseException("Invalid UTC offset minutes", position);
|
||||
}
|
||||
position += 2;
|
||||
}
|
||||
|
||||
// Apostrophe shall succeed mm
|
||||
if (date.charAt(position) != '\'') {
|
||||
throw new ParseException("Expected apostrophe", position);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
switch (utRel) {
|
||||
case '\u002D':
|
||||
hours -= offsetHours;
|
||||
minutes -= offsetMinutes;
|
||||
break;
|
||||
case '\u002B':
|
||||
hours += offsetHours;
|
||||
minutes += offsetMinutes;
|
||||
break;
|
||||
default:
|
||||
// "Z" means equal to UTC
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
calendar.set(year, month, day, hours, minutes, seconds);
|
||||
|
||||
return DateFormat
|
||||
.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.LONG)
|
||||
.format(calendar.getTime());
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package org.grapheneos.pdfviewer.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.grapheneos.pdfviewer.R;
|
||||
|
||||
public class DocumentPropertiesFragment extends DialogFragment {
|
||||
public static final String TAG = "DocumentPropertiesFragment";
|
||||
|
||||
private static final String KEY_DOCUMENT_PROPERTIES = "key_document_properties";
|
||||
|
||||
private static DocumentPropertiesFragment sDocumentPropertiesFragment;
|
||||
|
||||
private List<String> mDocumentProperties;
|
||||
|
||||
public static DocumentPropertiesFragment getInstance(final ArrayList<CharSequence> metaData) {
|
||||
if (sDocumentPropertiesFragment == null) {
|
||||
sDocumentPropertiesFragment = new DocumentPropertiesFragment();
|
||||
final Bundle args = new Bundle();
|
||||
args.putCharSequenceArrayList(KEY_DOCUMENT_PROPERTIES, metaData);
|
||||
sDocumentPropertiesFragment.setArguments(args);
|
||||
} else {
|
||||
final Bundle args = sDocumentPropertiesFragment.getArguments();
|
||||
args.clear();
|
||||
args.putCharSequenceArrayList(KEY_DOCUMENT_PROPERTIES, metaData);
|
||||
}
|
||||
return sDocumentPropertiesFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mDocumentProperties = getArguments().getStringArrayList(KEY_DOCUMENT_PROPERTIES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity activity = getActivity();
|
||||
final AlertDialog.Builder dialog = new AlertDialog.Builder(activity)
|
||||
.setPositiveButton(android.R.string.ok, null);
|
||||
|
||||
if (mDocumentProperties != null) {
|
||||
dialog.setTitle(getString(R.string.action_view_document_properties));
|
||||
dialog.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1,
|
||||
mDocumentProperties), null);
|
||||
} else {
|
||||
dialog.setTitle(R.string.document_properties_retrieval_failed);
|
||||
}
|
||||
return dialog.create();
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package org.grapheneos.pdfviewer.fragment;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.NumberPicker;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import org.grapheneos.pdfviewer.PdfViewer;
|
||||
|
||||
public class JumpToPageFragment extends DialogFragment {
|
||||
public static final String TAG = "JumpToPageFragment";
|
||||
|
||||
private final static String STATE_PICKER_CUR = "picker_cur";
|
||||
private final static String STATE_PICKER_MIN = "picker_min";
|
||||
private final static String STATE_PICKER_MAX = "picker_max";
|
||||
|
||||
private NumberPicker mPicker;
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
mPicker.setMinValue(savedInstanceState.getInt(STATE_PICKER_MIN));
|
||||
mPicker.setMaxValue(savedInstanceState.getInt(STATE_PICKER_MAX));
|
||||
mPicker.setValue(savedInstanceState.getInt(STATE_PICKER_CUR));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
mPicker = new NumberPicker(getActivity());
|
||||
mPicker.setMinValue(1);
|
||||
mPicker.setMaxValue(((PdfViewer)getActivity()).mNumPages);
|
||||
mPicker.setValue(((PdfViewer)getActivity()).mPage);
|
||||
|
||||
final FrameLayout layout = new FrameLayout(getActivity());
|
||||
layout.addView(mPicker, new FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
Gravity.CENTER));
|
||||
|
||||
return new AlertDialog.Builder(getActivity())
|
||||
.setView(layout)
|
||||
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
mPicker.clearFocus();
|
||||
((PdfViewer)getActivity()).onJumpToPageInDocument(mPicker.getValue());
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
outState.putInt(STATE_PICKER_MIN, mPicker.getMinValue());
|
||||
outState.putInt(STATE_PICKER_MAX, mPicker.getMaxValue());
|
||||
outState.putInt(STATE_PICKER_CUR, mPicker.getValue());
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
package org.grapheneos.pdfviewer.loader;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.loader.content.AsyncTaskLoader;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.grapheneos.pdfviewer.R;
|
||||
import org.grapheneos.pdfviewer.Utils;
|
||||
|
||||
public class DocumentPropertiesLoader extends AsyncTaskLoader<List<CharSequence>> {
|
||||
public static final String TAG = "DocumentPropertiesLoader";
|
||||
|
||||
public static final int ID = 1;
|
||||
|
||||
private final String mProperties;
|
||||
private final int mNumPages;
|
||||
private final Uri mUri;
|
||||
|
||||
private Cursor mCursor;
|
||||
|
||||
public DocumentPropertiesLoader(Context context, String properties, int numPages, Uri uri) {
|
||||
super(context);
|
||||
|
||||
mProperties = properties;
|
||||
mNumPages = numPages;
|
||||
mUri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CharSequence> loadInBackground() {
|
||||
final Context context = getContext();
|
||||
|
||||
final String[] names = context.getResources().getStringArray(R.array.property_names);
|
||||
final List<CharSequence> properties = new ArrayList<>(names.length);
|
||||
|
||||
mCursor = context.getContentResolver().query(mUri, null, null, null, null);
|
||||
if (mCursor != null) {
|
||||
mCursor.moveToFirst();
|
||||
|
||||
final int indexName = mCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (indexName >= 0) {
|
||||
properties.add(getProperty(null, names[0], mCursor.getString(indexName)));
|
||||
}
|
||||
|
||||
final int indexSize = mCursor.getColumnIndex(OpenableColumns.SIZE);
|
||||
if (indexSize >= 0) {
|
||||
final long fileSize = Long.valueOf(mCursor.getString(indexSize));
|
||||
properties.add(getProperty(null, names[1], Utils.parseFileSize(fileSize)));
|
||||
}
|
||||
|
||||
mCursor.close();
|
||||
}
|
||||
|
||||
try {
|
||||
final JSONObject json = new JSONObject(mProperties);
|
||||
|
||||
properties.add(getProperty(json, names[2], "Title"));
|
||||
properties.add(getProperty(json, names[3], "Author"));
|
||||
properties.add(getProperty(json, names[4], "Subject"));
|
||||
properties.add(getProperty(json, names[5], "Keywords"));
|
||||
properties.add(getProperty(json, names[6], "CreationDate"));
|
||||
properties.add(getProperty(json, names[7], "ModDate"));
|
||||
properties.add(getProperty(json, names[8], "Producer"));
|
||||
properties.add(getProperty(json, names[9], "Creator"));
|
||||
properties.add(getProperty(json, names[10], "PDFFormatVersion"));
|
||||
properties.add(getProperty(null, names[11], String.valueOf(mNumPages)));
|
||||
|
||||
return properties;
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliverResult(List<CharSequence> properties) {
|
||||
if (isReset()) {
|
||||
onReleaseResources();
|
||||
} else if (isStarted()) {
|
||||
super.deliverResult(properties);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStartLoading() {
|
||||
forceLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStopLoading() {
|
||||
cancelLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled(List<CharSequence> properties) {
|
||||
super.onCanceled(properties);
|
||||
|
||||
onReleaseResources();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReset() {
|
||||
super.onReset();
|
||||
|
||||
onStopLoading();
|
||||
onReleaseResources();
|
||||
}
|
||||
|
||||
private void onReleaseResources() {
|
||||
if (mCursor != null) {
|
||||
mCursor.close();
|
||||
mCursor = null;
|
||||
}
|
||||
}
|
||||
|
||||
private CharSequence getProperty(final JSONObject json, String name, String specName) {
|
||||
final SpannableStringBuilder property = new SpannableStringBuilder(name).append(":\n");
|
||||
final String value = json != null ? json.optString(specName, "-") : specName;
|
||||
|
||||
if (specName.endsWith("Date")) {
|
||||
final Context context = getContext();
|
||||
try {
|
||||
property.append(value.equals("-") ? value : Utils.parseDate(value));
|
||||
} catch (ParseException e) {
|
||||
Log.w(TAG, e.getMessage() + " for " + value + " at offset: " + e.getErrorOffset());
|
||||
property.append(context.getString(R.string.document_properties_invalid_date));
|
||||
}
|
||||
} else {
|
||||
property.append(value);
|
||||
}
|
||||
property.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return property;
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 153 B |
BIN
app/src/main/res/drawable-hdpi/ic_navigate_before_white_24dp.png
Normal file
After Width: | Height: | Size: 132 B |
BIN
app/src/main/res/drawable-hdpi/ic_navigate_next_white_24dp.png
Normal file
After Width: | Height: | Size: 133 B |
BIN
app/src/main/res/drawable-hdpi/ic_pageview_white_24dp.png
Normal file
After Width: | Height: | Size: 360 B |
BIN
app/src/main/res/drawable-hdpi/ic_zoom_in_white_24dp.png
Normal file
After Width: | Height: | Size: 422 B |
BIN
app/src/main/res/drawable-hdpi/ic_zoom_out_white_24dp.png
Normal file
After Width: | Height: | Size: 412 B |
After Width: | Height: | Size: 133 B |
BIN
app/src/main/res/drawable-mdpi/ic_navigate_before_white_24dp.png
Normal file
After Width: | Height: | Size: 116 B |
BIN
app/src/main/res/drawable-mdpi/ic_navigate_next_white_24dp.png
Normal file
After Width: | Height: | Size: 113 B |
BIN
app/src/main/res/drawable-mdpi/ic_pageview_white_24dp.png
Normal file
After Width: | Height: | Size: 226 B |
BIN
app/src/main/res/drawable-mdpi/ic_zoom_in_white_24dp.png
Normal file
After Width: | Height: | Size: 257 B |
BIN
app/src/main/res/drawable-mdpi/ic_zoom_out_white_24dp.png
Normal file
After Width: | Height: | Size: 249 B |
After Width: | Height: | Size: 206 B |
After Width: | Height: | Size: 136 B |
BIN
app/src/main/res/drawable-xhdpi/ic_navigate_next_white_24dp.png
Normal file
After Width: | Height: | Size: 144 B |
BIN
app/src/main/res/drawable-xhdpi/ic_pageview_white_24dp.png
Normal file
After Width: | Height: | Size: 392 B |
BIN
app/src/main/res/drawable-xhdpi/ic_zoom_in_white_24dp.png
Normal file
After Width: | Height: | Size: 486 B |
BIN
app/src/main/res/drawable-xhdpi/ic_zoom_out_white_24dp.png
Normal file
After Width: | Height: | Size: 470 B |
After Width: | Height: | Size: 283 B |
After Width: | Height: | Size: 202 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_navigate_next_white_24dp.png
Normal file
After Width: | Height: | Size: 213 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_pageview_white_24dp.png
Normal file
After Width: | Height: | Size: 601 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_zoom_in_white_24dp.png
Normal file
After Width: | Height: | Size: 737 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_zoom_out_white_24dp.png
Normal file
After Width: | Height: | Size: 731 B |
After Width: | Height: | Size: 372 B |
After Width: | Height: | Size: 197 B |
After Width: | Height: | Size: 206 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_pageview_white_24dp.png
Normal file
After Width: | Height: | Size: 770 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_zoom_in_white_24dp.png
Normal file
After Width: | Height: | Size: 954 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_zoom_out_white_24dp.png
Normal file
After Width: | Height: | Size: 925 B |
16
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="31.16883"
|
||||
android:viewportHeight="31.16883">
|
||||
<group android:translateX="3.5844157"
|
||||
android:translateY="3.5844157">
|
||||
<path
|
||||
android:pathData="M4.734,7.125h15.594v10.625h-15.594z"
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M7,11.5h1v-1L7,10.5v1zM19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM9.5,11.5c0,0.83 -0.67,1.5 -1.5,1.5L7,13v2L5.5,15L5.5,9L8,9c0.83,0 1.5,0.67 1.5,1.5v1zM19.5,10.5L17,10.5v1h1.5L18.5,13L17,13v2h-1.5L15.5,9h4v1.5zM14.5,13.5c0,0.83 -0.67,1.5 -1.5,1.5h-2.5L10.5,9L13,9c0.83,0 1.5,0.67 1.5,1.5v3zM12,13.5h1v-3h-1v3z"
|
||||
android:fillColor="#000000"/>
|
||||
</group>
|
||||
</vector>
|
5
app/src/main/res/layout/webview.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
51
app/src/main/res/menu/pdf_viewer.xml
Normal file
@ -0,0 +1,51 @@
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
|
||||
<!--<item-->
|
||||
<!--android:id="@+id/action_settings"-->
|
||||
<!--android:orderInCategory="100"-->
|
||||
<!--android:showAsAction="never"-->
|
||||
<!--android:title="@string/action_settings"/>-->
|
||||
|
||||
<item
|
||||
android:id="@+id/action_previous"
|
||||
android:icon="@drawable/ic_navigate_before_white_24dp"
|
||||
android:title="@string/action_previous"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_next"
|
||||
android:icon="@drawable/ic_navigate_next_white_24dp"
|
||||
android:title="@string/action_next"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_open"
|
||||
android:icon="@drawable/ic_insert_drive_file_white_24dp"
|
||||
android:title="@string/action_open"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_zoom_out"
|
||||
android:icon="@drawable/ic_zoom_out_white_24dp"
|
||||
android:title="@string/action_zoom_out"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_zoom_in"
|
||||
android:icon="@drawable/ic_zoom_in_white_24dp"
|
||||
android:title="@string/action_zoom_in"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_jump_to_page"
|
||||
android:icon="@drawable/ic_pageview_white_24dp"
|
||||
android:title="@string/action_jump_to_page"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_view_document_properties"
|
||||
android:title="@string/action_view_document_properties"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 973 B |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 705 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
17
app/src/main/res/values/arrays.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="property_names">
|
||||
<item>File name</item>
|
||||
<item>File size</item>
|
||||
<item>Title</item>
|
||||
<item>Author</item>
|
||||
<item>Subject</item>
|
||||
<item>Keywords</item>
|
||||
<item>Creation date</item>
|
||||
<item>Modify date</item>
|
||||
<item>Producer</item>
|
||||
<item>Creator</item>
|
||||
<item>PDF version</item>
|
||||
<item>Pages</item>
|
||||
</string-array>
|
||||
</resources>
|
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#000000</color>
|
||||
</resources>
|
15
app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">PDF Viewer</string>
|
||||
<!--<string name="action_settings">Settings</string>-->
|
||||
<string name="action_previous">Previous page</string>
|
||||
<string name="action_next">Next page</string>
|
||||
<string name="action_open">Open document</string>
|
||||
<string name="action_zoom_out">Zoom out</string>
|
||||
<string name="action_zoom_in">Zoom in</string>
|
||||
<string name="action_jump_to_page">Jump to page</string>
|
||||
<string name="action_view_document_properties">Properties</string>
|
||||
|
||||
<string name="document_properties_invalid_date">Invalid date</string>
|
||||
<string name="document_properties_retrieval_failed">Failed to obtain document metadata</string>
|
||||
</resources>
|
8
app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
|
||||
</resources>
|
31
build.gradle
Normal file
@ -0,0 +1,31 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.1'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
gradle.projectsEvaluated {
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
2
gradle.properties
Normal file
@ -0,0 +1,2 @@
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
188
gradlew
vendored
Executable file
@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
100
gradlew.bat
vendored
Normal file
@ -0,0 +1,100 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem http://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
1
settings.gradle
Normal file
@ -0,0 +1 @@
|
||||
include ':app'
|