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