Compare commits
158 Commits
b9da0073b7
...
libpdfview
Author | SHA1 | Date |
---|---|---|
Matéo Duparc | 2296593275 | |
Matéo Duparc | 59973a6b42 | |
Matéo Duparc | 476f4aa685 | |
Daniel Micay | c7241646fe | |
Daniel Micay | 516d99bb13 | |
Daniel Micay | 5afd670354 | |
Hanouta | 20d6c4ea9f | |
Hanouta | 0060e2cb73 | |
Daniel Micay | a820314362 | |
Daniel Micay | e064995a1b | |
Daniel Micay | 0f520dd517 | |
dependabot[bot] | e9c5deb95d | |
octocorvus | 457cbc9e98 | |
dependabot[bot] | efc1a27203 | |
octocorvus | 123bcf30a8 | |
octocorvus | bfb5e4a538 | |
dependabot[bot] | 145fd9896c | |
octocorvus | 6f40b25b31 | |
dependabot[bot] | f6dd0b2a87 | |
Daniel Micay | 4de602be3a | |
Patryk Mis | 5d6c6aa4e5 | |
Patryk Mis | 3119c94d71 | |
Patryk Mis | 115ddcb164 | |
Daniel Micay | 29a004091e | |
Pratyush | 195bba7891 | |
Pratyush | 17c7c84296 | |
Pratyush | 61607858ef | |
Matéo Duparc | c74b374ec4 | |
octocorvus | fb59568765 | |
Patryk Mis | 4d1807718e | |
Daniel Micay | a6b4144a08 | |
Daniel Micay | 5c8c4d7d83 | |
dependabot[bot] | e161b71d22 | |
Daniel Micay | 759417f4da | |
octocorvus | bb14ba1a25 | |
Daniel Micay | a59e72d9e0 | |
Daniel Micay | f2b0162630 | |
octocorvus | ae1c0874ce | |
octocorvus | d445c48f3c | |
octocorvus | 69696ae2a9 | |
octocorvus | 2dea11799c | |
octocorvus | f5a1452a2d | |
octocorvus | f87941ea22 | |
octocorvus | 371c9509f6 | |
octocorvus | d01131d4c6 | |
octocorvus | 2935bd4b27 | |
Daniel Micay | 30b8769ed2 | |
Daniel Micay | f6127ca0e0 | |
octocorvus | 0c6dcd35a7 | |
Daniel Micay | 1e5988346c | |
amalgame21 | 8d44658c80 | |
Daniel Micay | 6db24ef23d | |
Daniel Micay | deba479a69 | |
Daniel Micay | 7703b1a175 | |
Daniel Micay | d0c95f5478 | |
Daniel Micay | f60cd27e65 | |
Daniel Micay | a25d0a4405 | |
Daniel Micay | 424010af1c | |
Daniel Micay | 284fc306fc | |
Daniel Micay | d78e15b1b1 | |
Matéo Duparc | 6e8fdb56a5 | |
Xsims | c6fb2aceda | |
Daniel Micay | 7f59118453 | |
Daniel Micay | 5b46b4c80e | |
r3g_5z | e34c727ff7 | |
Daniel Micay | 3e60cca098 | |
Daniel Micay | 48910db7c6 | |
Daniel Micay | fd510a3c19 | |
Daniel Micay | b8dcc33696 | |
Daniel Micay | 45a0952296 | |
Patryk Mis | aaa93a6f88 | |
Daniel Micay | 414d7fe3d1 | |
randomhydrosol | 99c8c13fec | |
loryeam | 1cf1c7ec28 | |
octocorvus | ac4aa0bbb9 | |
dependabot[bot] | abfb212011 | |
dependabot[bot] | a3ba77534c | |
Daniel Micay | ed3cf2c08f | |
smdyv | e1dd3db05a | |
Daniel Micay | 22bd993a75 | |
dependabot[bot] | a73caa5914 | |
Matéo Duparc | 2766000ebc | |
Daniel Micay | eead76ad86 | |
loryeam | fbd6c45c3f | |
Pratyush | 8d0caf65a2 | |
Pratyush | 221e45cd52 | |
Pratyush | ee87e43bcb | |
Pratyush | 5fadf7f47d | |
Pratyush | 7ff831769e | |
Daniel Micay | 9e40a05be3 | |
Pratyush | 18761b3e4a | |
Pratyush | 43f971c028 | |
Pratyush | 2b81d4dcc3 | |
Pratyush | 5a55c8045b | |
Pratyush | 718b448cc5 | |
Pratyush | afaf71a4a2 | |
Pratyush | 8a308b69e5 | |
dependabot[bot] | ea5c34ed52 | |
Matéo Duparc | 06e54dbb03 | |
Daniel Micay | fee1b1516f | |
dependabot[bot] | bc77ecbcc6 | |
Daniel Micay | 955bed3614 | |
Daniel Micay | ee3605428e | |
Daniel Micay | e967af2397 | |
Daniel Micay | 9ea5cb1802 | |
Daniel Micay | a307f2892c | |
Pratyush | 8c795d4552 | |
Daniel Micay | d7ca71770f | |
Daniel Micay | a29ec202a7 | |
Pratyush | ea9a57fabe | |
Pratyush | 68fc48d3a9 | |
quh4gko8 | 3ebdfd0a11 | |
quh4gko8 | ff11df007f | |
Daniel Micay | fa2db54804 | |
Patryk Mis | 7634c00f59 | |
Daniel Micay | f67021e975 | |
dependabot[bot] | adb87cbaef | |
Daniel Micay | 09ccfb069d | |
Daniel Micay | 2a91d95eef | |
dependabot[bot] | a48213c450 | |
dependabot[bot] | 2f4c1ebfed | |
Daniel Micay | 22b400c68f | |
Daniel Micay | 6a6a263b0b | |
Daniel Micay | 8cd685c8c7 | |
Daniel Micay | be3f43e7a4 | |
Daniel Micay | 5eb98841d6 | |
Daniel Micay | e297fdf7e4 | |
Daniel Micay | 9355f54956 | |
dependabot[bot] | 5dfdaa2c17 | |
June | 3411fb0426 | |
dependabot[bot] | af567a5c0c | |
Daniel Micay | 6a7982ce80 | |
Daniel Micay | 1a3e816cfc | |
Daniel Micay | c6113df31d | |
Daniel Micay | db98371c9d | |
Pratyush | 87c71ddac2 | |
Daniel Micay | 7dab1cdc91 | |
Daniel Micay | 61d204d188 | |
Daniel Micay | 96651d02af | |
Daniel Micay | 21d7b9d76e | |
Pratyush | eb6eb8046c | |
dependabot[bot] | c1a1263bd5 | |
smdyv | d71f804b5a | |
Pratyush | ba9a6c8206 | |
Daniel Micay | c0eea02bb1 | |
Patryk Mis | eb6c14740f | |
dependabot[bot] | 6847883605 | |
dependabot[bot] | 975dfb966c | |
dependabot[bot] | ec965ddaaa | |
June | 087a01cf72 | |
Matéo Duparc | 8fe8b2f4b3 | |
Daniel Micay | 94c947305b | |
emschu | 0c21e13459 | |
June | cb4a0e8684 | |
June | ea519819c6 | |
June | 26e6d77574 | |
Matéo Duparc | 0f312347dd | |
Matéo Duparc | 4e4b7c5da4 |
|
@ -1,12 +0,0 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
target-branch: main
|
||||
- package-ecosystem: gradle
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
target-branch: main
|
|
@ -1,20 +0,0 @@
|
|||
name: Build application
|
||||
|
||||
on: [pull_request, push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build --no-daemon
|
|
@ -1,11 +0,0 @@
|
|||
name: Validate Gradle Wrapper
|
||||
|
||||
on: [pull_request, push]
|
||||
|
||||
jobs:
|
||||
validation:
|
||||
name: Validation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: gradle/wrapper-validation-action@v1
|
|
@ -1,3 +0,0 @@
|
|||
[submodule "pdfjs-dist"]
|
||||
path = third_party/pdfjs-dist
|
||||
url = https://github.com/mozilla/pdfjs-dist.git
|
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
|||
Copyright © 2017-2022 GrapheneOS
|
||||
Copyright © 2017-2023 GrapheneOS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
17
README.md
17
README.md
|
@ -1,11 +1,6 @@
|
|||
Simple Android PDF viewer based on pdf.js and content providers. The app
|
||||
doesn't require any permissions. The PDF stream is fed into the sandboxed
|
||||
WebView without giving it access to content or files. Content-Security-Policy
|
||||
is used to enforce that the JavaScript and styling properties within the
|
||||
WebView are entirely static content from the apk assets. It reuses the hardened
|
||||
Chromium rendering stack while only exposing a tiny subset of the attack
|
||||
surface compared to actual web content. The PDF rendering code itself is memory
|
||||
safe with dynamic code evaluation disabled, and even if an attacker did gain
|
||||
code execution by exploiting the underlying web rendering engine, they're
|
||||
within the Chromium renderer sandbox with no access to the network (unlike a
|
||||
browser), files, or other content.
|
||||
Fork of GrapheneOS' [PdfViewer](https://github.com/GrapheneOS/PdfViewer) to work as a library.
|
||||
|
||||
## Warning !
|
||||
The only goal of this library is to be integrated in [DroidFS](https://forge.chapril.org/hardcoresushi/DroidFS). Use it at your own risk !
|
||||
|
||||
The npm dependency has been removed. Instead, `pdf.min.js` and `pdf.worker.min.js` must be manually placed in `app/pdfjs-dist/build/`.
|
||||
|
|
|
@ -9,10 +9,16 @@ if (useKeystoreProperties) {
|
|||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
if (useKeystoreProperties) {
|
||||
signingConfigs {
|
||||
|
@ -32,25 +38,19 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
compileSdk = 32
|
||||
buildToolsVersion = "32.0.0"
|
||||
compileSdk = 33
|
||||
buildToolsVersion = "33.0.2"
|
||||
|
||||
namespace = "app.grapheneos.pdfviewer"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.grapheneos.pdfviewer"
|
||||
minSdk = 26
|
||||
targetSdk = 31
|
||||
versionCode = 12
|
||||
versionName = versionCode.toString()
|
||||
minSdk = 21
|
||||
targetSdk = 33
|
||||
resourceConfigurations.add("en")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
|
||||
getByName("release") {
|
||||
isShrinkResources = true
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
if (useKeystoreProperties) {
|
||||
|
@ -60,7 +60,6 @@ android {
|
|||
|
||||
create("play") {
|
||||
initWith(getByName("release"))
|
||||
applicationIdSuffix = ".play"
|
||||
if (useKeystoreProperties) {
|
||||
signingConfig = signingConfigs.getByName("play")
|
||||
}
|
||||
|
@ -68,20 +67,21 @@ android {
|
|||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility(JavaVersion.VERSION_11)
|
||||
targetCompatibility(JavaVersion.VERSION_11)
|
||||
sourceCompatibility(JavaVersion.VERSION_17)
|
||||
targetCompatibility(JavaVersion.VERSION_17)
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||
implementation("com.google.android.material:material:1.5.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("com.google.android.material:material:1.9.0")
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<lint>
|
||||
<!-- full backups are desired -->
|
||||
<issue id="AllowBackup">
|
||||
<ignore path="src/main/AndroidManifest.xml"/>
|
||||
<!-- overly aggressive check -->
|
||||
<issue id="VectorPath">
|
||||
<ignore path="src/main/res/drawable/ic_rotate_left_24dp.xml"/>
|
||||
<ignore path="src/main/res/drawable/ic_rotate_right_24dp.xml"/>
|
||||
</issue>
|
||||
|
||||
<!-- Google app indexing doesn't make any sense for this app -->
|
||||
|
|
|
@ -1,28 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="app.grapheneos.pdfviewer"
|
||||
android:targetSandboxVersion="2">
|
||||
<original-package android:name="org.grapheneos.pdfviewer" />
|
||||
|
||||
<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:exported="true">
|
||||
<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>
|
||||
|
||||
<application>
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
|
|
|
@ -1 +1 @@
|
|||
../../../../third_party/pdfjs-dist/build/pdf.min.js
|
||||
../../../pdfjs-dist/build/pdf.min.js
|
|
@ -1 +1 @@
|
|||
../../../../third_party/pdfjs-dist/build/pdf.worker.min.js
|
||||
../../../pdfjs-dist/build/pdf.worker.min.js
|
|
@ -1,36 +1,56 @@
|
|||
:root {
|
||||
--text-layer-opacity: 0.2;
|
||||
--text-layer-foreground: transparent;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body, canvas {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
canvas {
|
||||
margin: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #c0c0c0;
|
||||
}
|
||||
|
||||
#container {
|
||||
--scale-factor: 1;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#container canvas, #container .textLayer {
|
||||
/* overlay child elements on top of each other */
|
||||
grid-row-start: 1;
|
||||
grid-column-start: 1;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.textLayer {
|
||||
position: absolute;
|
||||
text-align: initial;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
opacity: 0.2;
|
||||
opacity: var(--text-layer-opacity);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.textLayer span,
|
||||
.textLayer br {
|
||||
color: transparent;
|
||||
color: var(--text-layer-foreground);
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
cursor: text;
|
||||
transform-origin: 0% 0%;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.textLayer .highlight {
|
||||
|
@ -79,3 +99,13 @@ body {
|
|||
.textLayer .endOfContent.active {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
[data-main-rotation="90"] {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
[data-main-rotation="180"] {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
[data-main-rotation="270"] {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
<script src="pdf.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="content"></canvas>
|
||||
<div id="text" class="textLayer"></div>
|
||||
<div id="container">
|
||||
<canvas id="content"></canvas>
|
||||
<div id="text" class="textLayer"></div>
|
||||
</div>
|
||||
<script src="viewer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -6,7 +6,8 @@ let pdfDoc = null;
|
|||
let pageRendering = false;
|
||||
let renderPending = false;
|
||||
let renderPendingZoom = 0;
|
||||
const canvas = document.getElementById('content');
|
||||
const canvas = document.getElementById("content");
|
||||
const container = document.getElementById("container");
|
||||
let orientationDegrees = 0;
|
||||
let zoomRatio = 1;
|
||||
let textLayerDiv = document.getElementById("text");
|
||||
|
@ -19,6 +20,8 @@ let useRender;
|
|||
const cache = [];
|
||||
const maxCached = 6;
|
||||
|
||||
let isTextLayerVisible = false;
|
||||
|
||||
function maybeRenderNextPage() {
|
||||
if (renderPending) {
|
||||
pageRendering = false;
|
||||
|
@ -61,6 +64,21 @@ function display(newCanvas, zoom) {
|
|||
}
|
||||
}
|
||||
|
||||
function setLayerTransform(pageWidth, pageHeight, layerDiv) {
|
||||
const translate = {
|
||||
X: Math.max(0, pageWidth - document.body.clientWidth) / 2,
|
||||
Y: Math.max(0, pageHeight - document.body.clientHeight) / 2
|
||||
};
|
||||
layerDiv.style.translate = `${translate.X}px ${translate.Y}px`;
|
||||
}
|
||||
|
||||
function getDefaultZoomRatio(page, orientationDegrees) {
|
||||
const viewport = page.getViewport({scale: 1, rotation: orientationDegrees});
|
||||
const widthZoomRatio = document.body.clientWidth / viewport.width;
|
||||
const heightZoomRatio = document.body.clientHeight / viewport.height;
|
||||
return Math.max(Math.min(widthZoomRatio, heightZoomRatio, channel.getMaxZoomRatio()), channel.getMinZoomRatio());
|
||||
}
|
||||
|
||||
function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
||||
pageRendering = true;
|
||||
useRender = !prerender;
|
||||
|
@ -73,7 +91,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
|||
for (let i = 0; i < cache.length; i++) {
|
||||
const cached = cache[i];
|
||||
if (cached.pageNumber === pageNumber && cached.zoomRatio === newZoomRatio &&
|
||||
cache.orientationDegrees === orientationDegrees) {
|
||||
cached.orientationDegrees === orientationDegrees) {
|
||||
if (useRender) {
|
||||
cache.splice(i, 1);
|
||||
cache.push(cached);
|
||||
|
@ -82,6 +100,8 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
|||
|
||||
textLayerDiv.replaceWith(cached.textLayerDiv);
|
||||
textLayerDiv = cached.textLayerDiv;
|
||||
setLayerTransform(cached.pageWidth, cached.pageHeight, textLayerDiv);
|
||||
container.style.setProperty("--scale-factor", newZoomRatio.toString());
|
||||
}
|
||||
|
||||
pageRendering = false;
|
||||
|
@ -95,7 +115,15 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
|||
return;
|
||||
}
|
||||
|
||||
const viewport = page.getViewport({scale: newZoomRatio, rotation: orientationDegrees})
|
||||
const defaultZoomRatio = getDefaultZoomRatio(page, orientationDegrees);
|
||||
|
||||
if (cache.length === 0) {
|
||||
zoomRatio = defaultZoomRatio;
|
||||
newZoomRatio = defaultZoomRatio;
|
||||
channel.setZoomRatio(defaultZoomRatio);
|
||||
}
|
||||
|
||||
const viewport = page.getViewport({scale: newZoomRatio, rotation: orientationDegrees});
|
||||
|
||||
if (useRender) {
|
||||
if (newZoomRatio !== zoomRatio) {
|
||||
|
@ -105,7 +133,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
|||
zoomRatio = newZoomRatio;
|
||||
}
|
||||
|
||||
if (zoom == 2) {
|
||||
if (zoom === 2) {
|
||||
pageRendering = false;
|
||||
return;
|
||||
}
|
||||
|
@ -137,10 +165,10 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
|||
}
|
||||
render();
|
||||
|
||||
const textLayerFrag = document.createDocumentFragment();
|
||||
const newTextLayerDiv = textLayerDiv.cloneNode();
|
||||
task = pdfjsLib.renderTextLayer({
|
||||
textContentStream: page.streamTextContent(),
|
||||
container: textLayerFrag,
|
||||
textContentSource: page.streamTextContent(),
|
||||
container: newTextLayerDiv,
|
||||
viewport: viewport
|
||||
});
|
||||
task.promise.then(function() {
|
||||
|
@ -148,24 +176,36 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
|||
|
||||
render();
|
||||
|
||||
const newTextLayerDiv = textLayerDiv.cloneNode();
|
||||
newTextLayerDiv.style.height = newCanvas.style.height;
|
||||
newTextLayerDiv.style.width = newCanvas.style.width;
|
||||
// We use CSS transform to rotate a text layer div of zero
|
||||
// degrees rotation. So, when the rotation is 90 or 270
|
||||
// degrees, set width and height of the text layer div to the
|
||||
// height and width of the canvas, respectively, to prevent
|
||||
// text layer misalignment.
|
||||
if (orientationDegrees % 180 === 0) {
|
||||
newTextLayerDiv.style.height = newCanvas.style.height;
|
||||
newTextLayerDiv.style.width = newCanvas.style.width;
|
||||
} else {
|
||||
newTextLayerDiv.style.height = newCanvas.style.width;
|
||||
newTextLayerDiv.style.width = newCanvas.style.height;
|
||||
}
|
||||
setLayerTransform(viewport.width, viewport.height, newTextLayerDiv);
|
||||
if (useRender) {
|
||||
textLayerDiv.replaceWith(newTextLayerDiv);
|
||||
textLayerDiv = newTextLayerDiv;
|
||||
container.style.setProperty("--scale-factor", newZoomRatio.toString());
|
||||
}
|
||||
newTextLayerDiv.appendChild(textLayerFrag);
|
||||
|
||||
if (cache.length === maxCached) {
|
||||
cache.shift()
|
||||
cache.shift();
|
||||
}
|
||||
cache.push({
|
||||
pageNumber: pageNumber,
|
||||
zoomRatio: newZoomRatio,
|
||||
orientationDegrees: orientationDegrees,
|
||||
canvas: newCanvas,
|
||||
textLayerDiv: newTextLayerDiv
|
||||
textLayerDiv: newTextLayerDiv,
|
||||
pageWidth: viewport.width,
|
||||
pageHeight: viewport.height
|
||||
});
|
||||
|
||||
pageRendering = false;
|
||||
|
@ -198,15 +238,44 @@ function isTextSelected() {
|
|||
return window.getSelection().toString() !== "";
|
||||
}
|
||||
|
||||
pdfjsLib.getDocument("https://localhost/placeholder.pdf").promise.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);
|
||||
function toggleTextLayerVisibility() {
|
||||
let textLayerForeground = "red";
|
||||
let textLayerOpacity = 1;
|
||||
if (isTextLayerVisible) {
|
||||
textLayerForeground = "transparent";
|
||||
textLayerOpacity = 0.2;
|
||||
}
|
||||
document.documentElement.style.setProperty("--text-layer-foreground", textLayerForeground);
|
||||
document.documentElement.style.setProperty("--text-layer-opacity", textLayerOpacity.toString());
|
||||
isTextLayerVisible = !isTextLayerVisible;
|
||||
}
|
||||
|
||||
function loadDocument() {
|
||||
const pdfPassword = channel.getPassword();
|
||||
const loadingTask = pdfjsLib.getDocument({ url: "https://localhost/placeholder.pdf", password: pdfPassword });
|
||||
loadingTask.onPassword = (_, error) => {
|
||||
if (error === pdfjsLib.PasswordResponses.NEED_PASSWORD) {
|
||||
channel.showPasswordPrompt();
|
||||
} else if (error === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {
|
||||
channel.invalidPassword();
|
||||
}
|
||||
};
|
||||
|
||||
loadingTask.promise.then(function (newDoc) {
|
||||
channel.onLoaded();
|
||||
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);
|
||||
}, function (reason) {
|
||||
console.error(reason.name + ": " + reason.message);
|
||||
});
|
||||
renderPage(channel.getPage(), false, false);
|
||||
}).catch(function(error) {
|
||||
console.log("getDocument error: " + error);
|
||||
});
|
||||
}
|
||||
|
||||
window.onresize = () => {
|
||||
setLayerTransform(canvas.clientWidth, canvas.clientHeight, textLayerDiv);
|
||||
};
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
|
@ -1,12 +1,10 @@
|
|||
package app.grapheneos.pdfviewer;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
|
@ -26,11 +24,14 @@ import android.widget.TextView;
|
|||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
|
@ -38,23 +39,25 @@ import com.google.android.material.snackbar.Snackbar;
|
|||
|
||||
import app.grapheneos.pdfviewer.databinding.PdfviewerBinding;
|
||||
import app.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment;
|
||||
import app.grapheneos.pdfviewer.fragment.PasswordPromptFragment;
|
||||
import app.grapheneos.pdfviewer.fragment.JumpToPageFragment;
|
||||
import app.grapheneos.pdfviewer.loader.DocumentPropertiesLoader;
|
||||
import app.grapheneos.pdfviewer.ktx.ViewKt;
|
||||
import app.grapheneos.pdfviewer.loader.DocumentPropertiesAsyncTaskLoader;
|
||||
import app.grapheneos.pdfviewer.viewModel.PasswordStatus;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class PdfViewer extends AppCompatActivity implements LoaderManager.LoaderCallbacks<List<CharSequence>> {
|
||||
public class PdfViewer 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_RATIO = "zoomRatio";
|
||||
private static final String STATE_DOCUMENT_ORIENTATION_DEGREES = "documentOrientationDegrees";
|
||||
private static final String KEY_PROPERTIES = "properties";
|
||||
private static final int MIN_WEBVIEW_RELEASE = 89;
|
||||
private static final int MIN_WEBVIEW_RELEASE = 92;
|
||||
|
||||
private static final String CONTENT_SECURITY_POLICY =
|
||||
"default-src 'none'; " +
|
||||
|
@ -78,6 +81,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
"document-domain=(), " +
|
||||
"encrypted-media=(), " +
|
||||
"fullscreen=(), " +
|
||||
"gamepad=(), " +
|
||||
"geolocation=(), " +
|
||||
"gyroscope=(), " +
|
||||
"hid=(), " +
|
||||
|
@ -91,32 +95,37 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
"publickey-credentials-get=(), " +
|
||||
"screen-wake-lock=(), " +
|
||||
"serial=(), " +
|
||||
"speaker-selection=(), " +
|
||||
"sync-xhr=(), " +
|
||||
"usb=(), " +
|
||||
"xr-spatial-tracking=()";
|
||||
|
||||
private static final float MIN_ZOOM_RATIO = 0.5f;
|
||||
private static final float MIN_ZOOM_RATIO = 0.2f;
|
||||
private static final float MAX_ZOOM_RATIO = 1.5f;
|
||||
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 float mZoomRatio = 1f;
|
||||
private int mDocumentOrientationDegrees;
|
||||
private int mDocumentState;
|
||||
private String mEncryptedDocumentPassword;
|
||||
private List<CharSequence> mDocumentProperties;
|
||||
private InputStream mInputStream;
|
||||
private ByteArrayInputStream mInputStream;
|
||||
|
||||
private PdfviewerBinding binding;
|
||||
private TextView mTextView;
|
||||
private Toast mToast;
|
||||
private Snackbar snackbar;
|
||||
|
||||
AppCompatActivity activity;
|
||||
String fileName;
|
||||
Long fileSize;
|
||||
private PasswordPromptFragment mPasswordPromptFragment;
|
||||
public PasswordStatus passwordValidationViewModel;
|
||||
|
||||
private class Channel {
|
||||
@JavascriptInterface
|
||||
|
@ -129,6 +138,21 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
return mZoomRatio;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void setZoomRatio(final float ratio) {
|
||||
mZoomRatio = Math.max(Math.min(ratio, MAX_ZOOM_RATIO), MIN_ZOOM_RATIO);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public float getMinZoomRatio() {
|
||||
return MIN_ZOOM_RATIO;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public float getMaxZoomRatio() {
|
||||
return MAX_ZOOM_RATIO;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public int getDocumentOrientationDegrees() {
|
||||
return mDocumentOrientationDegrees;
|
||||
|
@ -137,7 +161,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
@JavascriptInterface
|
||||
public void setNumPages(int numPages) {
|
||||
mNumPages = numPages;
|
||||
runOnUiThread(PdfViewer.this::invalidateOptionsMenu);
|
||||
activity.runOnUiThread(activity::invalidateOptionsMenu);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
|
@ -148,19 +172,44 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(KEY_PROPERTIES, properties);
|
||||
runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesLoader.ID, args, PdfViewer.this));
|
||||
activity.runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this.activity).restartLoader(DocumentPropertiesAsyncTaskLoader.ID, args, PdfViewer.this));
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void showPasswordPrompt() {
|
||||
if (!getPasswordPromptFragment().isAdded()) {
|
||||
getPasswordPromptFragment().show(activity.getSupportFragmentManager(), PasswordPromptFragment.class.getName());
|
||||
}
|
||||
passwordValidationViewModel.passwordMissing();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void invalidPassword() {
|
||||
activity.runOnUiThread(() -> passwordValidationViewModel.invalid());
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void onLoaded() {
|
||||
passwordValidationViewModel.validated();
|
||||
if (getPasswordPromptFragment().isAdded()) {
|
||||
getPasswordPromptFragment().dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getPassword() {
|
||||
return mEncryptedDocumentPassword != null ? mEncryptedDocumentPassword : "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint({"SetJavaScriptEnabled", "ClickableViewAccessibility"})
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = PdfviewerBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
setSupportActionBar(binding.toolbar);
|
||||
public PdfViewer(@NonNull AppCompatActivity activity) {
|
||||
this.activity = activity;
|
||||
binding = PdfviewerBinding.inflate(activity.getLayoutInflater());
|
||||
activity.setContentView(binding.getRoot());
|
||||
activity.setSupportActionBar(binding.toolbar);
|
||||
passwordValidationViewModel = new ViewModelProvider(activity, ViewModelProvider.AndroidViewModelFactory.getInstance(activity.getApplication())).get(PasswordStatus.class);
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
WindowCompat.setDecorFitsSystemWindows(activity.getWindow(), false);
|
||||
|
||||
// Margins for the toolbar are needed, so that content of the toolbar
|
||||
// is not covered by a system button navigation bar when in landscape.
|
||||
|
@ -184,6 +233,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
settings.setAllowFileAccess(false);
|
||||
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
|
||||
settings.setJavaScriptEnabled(true);
|
||||
settings.setMinimumFontSize(1);
|
||||
|
||||
CookieManager.getInstance().setAcceptCookie(false);
|
||||
|
||||
|
@ -192,7 +242,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
binding.webview.setWebViewClient(new WebViewClient() {
|
||||
private WebResourceResponse fromAsset(final String mime, final String path) {
|
||||
try {
|
||||
InputStream inputStream = getAssets().open(path.substring(1));
|
||||
InputStream inputStream = activity.getAssets().open(path.substring(1));
|
||||
return new WebResourceResponse(mime, null, inputStream);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
|
@ -214,6 +264,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
Log.d(TAG, "path " + path);
|
||||
|
||||
if ("/placeholder.pdf".equals(path)) {
|
||||
mInputStream.reset();
|
||||
return new WebResourceResponse("application/pdf", null, mInputStream);
|
||||
}
|
||||
|
||||
|
@ -246,28 +297,25 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
mDocumentState = STATE_LOADED;
|
||||
invalidateOptionsMenu();
|
||||
activity.invalidateOptionsMenu();
|
||||
loadPdfWithPassword(mEncryptedDocumentPassword);
|
||||
}
|
||||
});
|
||||
|
||||
GestureHelper.attach(PdfViewer.this, binding.webview,
|
||||
GestureHelper.attach(activity, binding.webview,
|
||||
new GestureHelper.GestureListener() {
|
||||
@Override
|
||||
public boolean onTapUp() {
|
||||
if (mUri != null) {
|
||||
binding.webview.evaluateJavascript("isTextSelected()", selection -> {
|
||||
if (!Boolean.parseBoolean(selection)) {
|
||||
if ((getWindow().getDecorView().getSystemUiVisibility() &
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
|
||||
hideSystemUi();
|
||||
} else {
|
||||
showSystemUi();
|
||||
}
|
||||
binding.webview.evaluateJavascript("isTextSelected()", selection -> {
|
||||
if (!Boolean.parseBoolean(selection)) {
|
||||
if (activity.getSupportActionBar().isShowing()) {
|
||||
hideSystemUi();
|
||||
} else {
|
||||
showSystemUi();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -286,62 +334,68 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
}
|
||||
});
|
||||
|
||||
mTextView = new TextView(this);
|
||||
mTextView = new TextView(activity);
|
||||
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);
|
||||
|
||||
snackbar = Snackbar.make(binding.webview, "", Snackbar.LENGTH_LONG);
|
||||
|
||||
final Intent intent = getIntent();
|
||||
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
|
||||
if (!"application/pdf".equals(intent.getType())) {
|
||||
snackbar.setText(R.string.invalid_mime_type).show();
|
||||
return;
|
||||
}
|
||||
mUri = intent.getData();
|
||||
mPage = 1;
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
mUri = savedInstanceState.getParcelable(STATE_URI);
|
||||
mPage = savedInstanceState.getInt(STATE_PAGE);
|
||||
mZoomRatio = savedInstanceState.getFloat(STATE_ZOOM_RATIO);
|
||||
mDocumentOrientationDegrees = savedInstanceState.getInt(STATE_DOCUMENT_ORIENTATION_DEGREES);
|
||||
}
|
||||
|
||||
if (mUri != null) {
|
||||
if ("file".equals(mUri.getScheme())) {
|
||||
snackbar.setText(R.string.legacy_file_uri).show();
|
||||
return;
|
||||
}
|
||||
|
||||
loadPdf();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
public void onDestroy() {
|
||||
binding.webview.removeJavascriptInterface("channel");
|
||||
binding.getRoot().removeView(binding.webview);
|
||||
binding.webview.destroy();
|
||||
maybeCloseInputStream();
|
||||
}
|
||||
|
||||
// The user could have left the activity to update the WebView
|
||||
invalidateOptionsMenu();
|
||||
if (getWebViewRelease() >= MIN_WEBVIEW_RELEASE) {
|
||||
binding.webviewOutOfDateLayout.setVisibility(View.GONE);
|
||||
binding.webview.setVisibility(View.VISIBLE);
|
||||
void maybeCloseInputStream() {
|
||||
InputStream stream = mInputStream;
|
||||
if (stream == null) {
|
||||
return;
|
||||
}
|
||||
mInputStream = null;
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
private PasswordPromptFragment getPasswordPromptFragment() {
|
||||
if (mPasswordPromptFragment == null) {
|
||||
final Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag(PasswordPromptFragment.class.getName());
|
||||
if (fragment != null) {
|
||||
mPasswordPromptFragment = (PasswordPromptFragment) fragment;
|
||||
} else {
|
||||
mPasswordPromptFragment = new PasswordPromptFragment(this);
|
||||
}
|
||||
}
|
||||
return mPasswordPromptFragment;
|
||||
}
|
||||
|
||||
private void setToolbarTitleWithDocumentName() {
|
||||
String documentName = getCurrentDocumentName();
|
||||
if (documentName != null && !documentName.isEmpty()) {
|
||||
activity.getSupportActionBar().setTitle(documentName);
|
||||
} else {
|
||||
binding.webview.setVisibility(View.GONE);
|
||||
binding.webviewOutOfDateMessage.setText(getString(R.string.webview_out_of_date_message, getWebViewRelease(), MIN_WEBVIEW_RELEASE));
|
||||
binding.webviewOutOfDateLayout.setVisibility(View.VISIBLE);
|
||||
activity.getSupportActionBar().setTitle(R.string.app_name);
|
||||
}
|
||||
}
|
||||
|
||||
public void onResume() {
|
||||
// The user could have left the activity to update the WebView
|
||||
activity.invalidateOptionsMenu();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (getWebViewRelease() >= MIN_WEBVIEW_RELEASE) {
|
||||
binding.webviewOutOfDateLayout.setVisibility(View.GONE);
|
||||
binding.webview.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.webview.setVisibility(View.GONE);
|
||||
binding.webviewOutOfDateMessage.setText(activity.getString(R.string.webview_out_of_date_message, getWebViewRelease(), MIN_WEBVIEW_RELEASE));
|
||||
binding.webviewOutOfDateLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private int getWebViewRelease() {
|
||||
PackageInfo webViewPackage = WebView.getCurrentWebViewPackage();
|
||||
String webViewVersionName = webViewPackage.versionName;
|
||||
|
@ -351,13 +405,14 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
@NonNull
|
||||
@Override
|
||||
public Loader<List<CharSequence>> onCreateLoader(int id, Bundle args) {
|
||||
return new DocumentPropertiesLoader(this, args.getString(KEY_PROPERTIES), mNumPages, mUri);
|
||||
return new DocumentPropertiesAsyncTaskLoader(activity, args.getString(KEY_PROPERTIES), mNumPages, fileName, fileSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<List<CharSequence>> loader, List<CharSequence> data) {
|
||||
mDocumentProperties = data;
|
||||
LoaderManager.getInstance(this).destroyLoader(DocumentPropertiesLoader.ID);
|
||||
setToolbarTitleWithDocumentName();
|
||||
LoaderManager.getInstance(activity).destroyLoader(DocumentPropertiesAsyncTaskLoader.ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -365,21 +420,22 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
mDocumentProperties = null;
|
||||
}
|
||||
|
||||
private void loadPdf() {
|
||||
try {
|
||||
if (mInputStream != null) {
|
||||
mInputStream.close();
|
||||
}
|
||||
mInputStream = getContentResolver().openInputStream(mUri);
|
||||
} catch (IOException e) {
|
||||
snackbar.setText(R.string.io_error).show();
|
||||
return;
|
||||
}
|
||||
|
||||
public void loadPdf(ByteArrayInputStream inputStream, String fileName, Long fileSize) {
|
||||
mPage = 1;
|
||||
mDocumentProperties = null;
|
||||
mInputStream = inputStream;
|
||||
this.fileName = fileName;
|
||||
this.fileSize = fileSize;
|
||||
showSystemUi();
|
||||
activity.invalidateOptionsMenu();
|
||||
binding.webview.loadUrl("https://localhost/viewer.html");
|
||||
}
|
||||
|
||||
public void loadPdfWithPassword(final String password) {
|
||||
mEncryptedDocumentPassword = password;
|
||||
binding.webview.evaluateJavascript("loadDocument()", null);
|
||||
}
|
||||
|
||||
private void renderPage(final int zoom) {
|
||||
binding.webview.evaluateJavascript("onRenderPage(" + zoom + ")", null);
|
||||
}
|
||||
|
@ -392,18 +448,11 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
renderPage(0);
|
||||
}
|
||||
|
||||
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 void zoomIn(float value, boolean end) {
|
||||
if (mZoomRatio < MAX_ZOOM_RATIO) {
|
||||
mZoomRatio = Math.min(mZoomRatio + value, MAX_ZOOM_RATIO);
|
||||
renderPage(end ? 1 : 2);
|
||||
invalidateOptionsMenu();
|
||||
activity.invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -411,7 +460,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
if (mZoomRatio > MIN_ZOOM_RATIO) {
|
||||
mZoomRatio = Math.max(mZoomRatio - value, MIN_ZOOM_RATIO);
|
||||
renderPage(end ? 1 : 2);
|
||||
invalidateOptionsMenu();
|
||||
activity.invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -434,51 +483,18 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
mPage = selected_page;
|
||||
renderPage(0);
|
||||
showPageNumber();
|
||||
invalidateOptionsMenu();
|
||||
activity.invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
private void showSystemUi() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||
getSupportActionBar().show();
|
||||
ViewKt.showSystemUi(binding.getRoot(), activity.getWindow());
|
||||
activity.getSupportActionBar().show();
|
||||
}
|
||||
|
||||
private void hideSystemUi() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE);
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
savedInstanceState.putParcelable(STATE_URI, mUri);
|
||||
savedInstanceState.putInt(STATE_PAGE, mPage);
|
||||
savedInstanceState.putFloat(STATE_ZOOM_RATIO, mZoomRatio);
|
||||
savedInstanceState.putInt(STATE_DOCUMENT_ORIENTATION_DEGREES, mDocumentOrientationDegrees);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent resultData) {
|
||||
super.onActivityResult(requestCode, resultCode, resultData);
|
||||
|
||||
if (requestCode == ACTION_OPEN_DOCUMENT_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
||||
if (resultData != null) {
|
||||
mUri = resultData.getData();
|
||||
mPage = 1;
|
||||
mDocumentProperties = null;
|
||||
loadPdf();
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
ViewKt.hideSystemUi(binding.getRoot(), activity.getWindow());
|
||||
activity.getSupportActionBar().hide();
|
||||
}
|
||||
|
||||
private void showPageNumber() {
|
||||
|
@ -486,27 +502,29 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
mToast.cancel();
|
||||
}
|
||||
mTextView.setText(String.format("%s/%s", mPage, mNumPages));
|
||||
mToast = new Toast(getApplicationContext());
|
||||
mToast = new Toast(activity);
|
||||
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();
|
||||
public void onCreateOptionMenu(@NonNull Menu menu) {
|
||||
MenuInflater inflater = activity.getMenuInflater();
|
||||
inflater.inflate(R.menu.pdf_viewer, menu);
|
||||
return true;
|
||||
if (BuildConfig.DEBUG) {
|
||||
inflater.inflate(R.menu.pdf_viewer_debug, menu);
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
public boolean onPrepareOptionsMenu(@NonNull Menu menu) {
|
||||
final ArrayList<Integer> ids = new ArrayList<>(Arrays.asList(R.id.action_jump_to_page,
|
||||
R.id.action_next, R.id.action_previous, R.id.action_first, R.id.action_last,
|
||||
R.id.action_rotate_clockwise, R.id.action_rotate_counterclockwise,
|
||||
R.id.action_view_document_properties };
|
||||
R.id.action_view_document_properties));
|
||||
if (BuildConfig.DEBUG) {
|
||||
ids.add(R.id.debug_action_toggle_text_layer_visibility);
|
||||
}
|
||||
if (mDocumentState < STATE_LOADED) {
|
||||
for (final int id : ids) {
|
||||
final MenuItem item = menu.findItem(id);
|
||||
|
@ -524,16 +542,12 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
mDocumentState = STATE_END;
|
||||
}
|
||||
|
||||
enableDisableMenuItem(menu.findItem(R.id.action_open), getWebViewRelease() >= MIN_WEBVIEW_RELEASE);
|
||||
enableDisableMenuItem(menu.findItem(R.id.action_zoom_in), mZoomRatio != MAX_ZOOM_RATIO);
|
||||
enableDisableMenuItem(menu.findItem(R.id.action_zoom_out), mZoomRatio != MIN_ZOOM_RATIO);
|
||||
enableDisableMenuItem(menu.findItem(R.id.action_next), mPage < mNumPages);
|
||||
enableDisableMenuItem(menu.findItem(R.id.action_previous), mPage > 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == R.id.action_previous) {
|
||||
|
@ -548,15 +562,6 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
} else if (itemId == R.id.action_last) {
|
||||
onJumpToPageInDocument(mNumPages);
|
||||
return true;
|
||||
} else if (itemId == R.id.action_open) {
|
||||
openDocument();
|
||||
return true;
|
||||
} else if (itemId == R.id.action_zoom_out) {
|
||||
zoomOut(0.25f, true);
|
||||
return true;
|
||||
} else if (itemId == R.id.action_zoom_in) {
|
||||
zoomIn(0.25f, true);
|
||||
return true;
|
||||
} else if (itemId == R.id.action_rotate_clockwise) {
|
||||
documentOrientationChanged(90);
|
||||
return true;
|
||||
|
@ -566,14 +571,32 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||
} else if (itemId == R.id.action_view_document_properties) {
|
||||
DocumentPropertiesFragment
|
||||
.newInstance(mDocumentProperties)
|
||||
.show(getSupportFragmentManager(), DocumentPropertiesFragment.TAG);
|
||||
.show(activity.getSupportFragmentManager(), DocumentPropertiesFragment.TAG);
|
||||
return true;
|
||||
} else if (itemId == R.id.action_jump_to_page) {
|
||||
new JumpToPageFragment()
|
||||
.show(getSupportFragmentManager(), JumpToPageFragment.TAG);
|
||||
new JumpToPageFragment(this)
|
||||
.show(activity.getSupportFragmentManager(), JumpToPageFragment.TAG);
|
||||
return true;
|
||||
} else if (itemId == R.id.debug_action_toggle_text_layer_visibility) {
|
||||
binding.webview.evaluateJavascript("toggleTextLayerVisibility()", null);
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getCurrentDocumentName() {
|
||||
if (mDocumentProperties == null || mDocumentProperties.isEmpty()) return "";
|
||||
String fileName = "";
|
||||
String title = "";
|
||||
for (CharSequence property : mDocumentProperties) {
|
||||
if (property.toString().startsWith("File name:")) {
|
||||
fileName = property.toString().replace("File name:", "");
|
||||
}
|
||||
if (property.toString().startsWith("Title:-")) {
|
||||
title = property.toString().replace("Title:-", "");
|
||||
}
|
||||
}
|
||||
return fileName.length() > 2 ? fileName : title;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,20 +9,13 @@ import java.text.ParseException;
|
|||
import java.util.Calendar;
|
||||
|
||||
public class Utils {
|
||||
public static String parseFileSize(long fileSize) {
|
||||
final double kb = fileSize / 1000d;
|
||||
|
||||
if (kb == 0d) {
|
||||
return fileSize + " Bytes";
|
||||
private static int parseIntSafely(String field) throws ParseException {
|
||||
try {
|
||||
return Integer.parseInt(field);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParseException("Error while parsing int", -1);
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -39,14 +32,13 @@ public class Utils {
|
|||
|
||||
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.parseInt(field);
|
||||
int year = parseIntSafely(field);
|
||||
if (year > currentYear) {
|
||||
year = currentYear;
|
||||
}
|
||||
|
@ -67,7 +59,7 @@ public class Utils {
|
|||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid month", position);
|
||||
}
|
||||
month = Integer.parseInt(field) - 1;
|
||||
month = parseIntSafely(field) - 1;
|
||||
if (month > 11) {
|
||||
throw new ParseException("Invalid month", position);
|
||||
}
|
||||
|
@ -78,7 +70,7 @@ public class Utils {
|
|||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid day", position);
|
||||
}
|
||||
day = Integer.parseInt(field);
|
||||
day = parseIntSafely(field);
|
||||
if (day > 31) {
|
||||
throw new ParseException("Invalid day", position);
|
||||
}
|
||||
|
@ -89,7 +81,7 @@ public class Utils {
|
|||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid hours", position);
|
||||
}
|
||||
hours = Integer.parseInt(field);
|
||||
hours = parseIntSafely(field);
|
||||
if (hours > 23) {
|
||||
throw new ParseException("Invalid hours", position);
|
||||
}
|
||||
|
@ -100,7 +92,7 @@ public class Utils {
|
|||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid minutes", position);
|
||||
}
|
||||
minutes = Integer.parseInt(field);
|
||||
minutes = parseIntSafely(field);
|
||||
if (minutes > 59) {
|
||||
throw new ParseException("Invalid minutes", position);
|
||||
}
|
||||
|
@ -111,7 +103,7 @@ public class Utils {
|
|||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid seconds", position);
|
||||
}
|
||||
seconds = Integer.parseInt(field);
|
||||
seconds = parseIntSafely(field);
|
||||
if (seconds > 59) {
|
||||
throw new ParseException("Invalid seconds", position);
|
||||
}
|
||||
|
@ -135,7 +127,7 @@ public class Utils {
|
|||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid UTC offset hours", position);
|
||||
}
|
||||
offsetHours = Integer.parseInt(field);
|
||||
offsetHours = parseIntSafely(field);
|
||||
final int offsetHoursMinutes = offsetHours * 100 + offsetMinutes;
|
||||
|
||||
// Validate UTC offset (UTC-12:00 to UTC+14:00)
|
||||
|
@ -158,7 +150,7 @@ public class Utils {
|
|||
if (!TextUtils.isDigitsOnly(field)) {
|
||||
throw new ParseException("Invalid UTC offset minutes", position);
|
||||
}
|
||||
offsetMinutes = Integer.parseInt(field);
|
||||
offsetMinutes = parseIntSafely(field);
|
||||
if (offsetMinutes > 59) {
|
||||
throw new ParseException("Invalid UTC offset minutes", position);
|
||||
}
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
package app.grapheneos.pdfviewer.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import app.grapheneos.pdfviewer.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DocumentPropertiesFragment extends DialogFragment {
|
||||
public static final String TAG = "DocumentPropertiesFragment";
|
||||
|
||||
private static final String KEY_DOCUMENT_PROPERTIES = "document_properties";
|
||||
|
||||
private List<String> mDocumentProperties;
|
||||
|
||||
public static DocumentPropertiesFragment newInstance(final List<CharSequence> metaData) {
|
||||
final DocumentPropertiesFragment fragment = new DocumentPropertiesFragment();
|
||||
final Bundle args = new Bundle();
|
||||
|
||||
args.putCharSequenceArrayList(KEY_DOCUMENT_PROPERTIES, (ArrayList<CharSequence>) metaData);
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (getArguments() != null) {
|
||||
mDocumentProperties = getArguments().getStringArrayList(KEY_DOCUMENT_PROPERTIES);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity activity = requireActivity();
|
||||
final MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(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,53 @@
|
|||
package app.grapheneos.pdfviewer.fragment
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import app.grapheneos.pdfviewer.R
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
class DocumentPropertiesFragment : DialogFragment() {
|
||||
|
||||
// TODO replace with nav args once the `PdfViewer` activity is converted to kotlin
|
||||
private val mDocumentProperties: List<String> by lazy {
|
||||
requireArguments().getStringArrayList(KEY_DOCUMENT_PROPERTIES)?.toList() ?: emptyList()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(requireActivity())
|
||||
.setPositiveButton(android.R.string.ok, null).apply {
|
||||
if (mDocumentProperties.isNotEmpty()) {
|
||||
setTitle(getString(R.string.action_view_document_properties))
|
||||
setAdapter(
|
||||
ArrayAdapter(
|
||||
requireActivity(),
|
||||
android.R.layout.simple_list_item_1,
|
||||
mDocumentProperties
|
||||
), null
|
||||
)
|
||||
} else {
|
||||
setTitle(R.string.document_properties_retrieval_failed)
|
||||
}
|
||||
}
|
||||
.create()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG = "DocumentPropertiesFragment"
|
||||
private const val KEY_DOCUMENT_PROPERTIES = "document_properties"
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(metaData: List<CharSequence>): DocumentPropertiesFragment {
|
||||
val fragment = DocumentPropertiesFragment()
|
||||
val args = Bundle()
|
||||
args.putCharSequenceArrayList(
|
||||
KEY_DOCUMENT_PROPERTIES,
|
||||
metaData as ArrayList<CharSequence>
|
||||
)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package app.grapheneos.pdfviewer.fragment;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.NumberPicker;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import app.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));
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
mPicker = new NumberPicker(getActivity());
|
||||
mPicker.setMinValue(1);
|
||||
mPicker.setMaxValue(((PdfViewer)requireActivity()).mNumPages);
|
||||
mPicker.setValue(((PdfViewer)requireActivity()).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 MaterialAlertDialogBuilder(requireActivity())
|
||||
.setView(layout)
|
||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
|
||||
mPicker.clearFocus();
|
||||
((PdfViewer)requireActivity()).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,58 @@
|
|||
package app.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.fragment.app.DialogFragment
|
||||
import app.grapheneos.pdfviewer.PdfViewer
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
class JumpToPageFragment(private val pdfViewer: PdfViewer) : DialogFragment() {
|
||||
|
||||
companion object {
|
||||
const val TAG = "JumpToPageFragment"
|
||||
private const val STATE_PICKER_CUR = "picker_cur"
|
||||
private const val STATE_PICKER_MIN = "picker_min"
|
||||
private const val STATE_PICKER_MAX = "picker_max"
|
||||
}
|
||||
|
||||
private val mPicker: NumberPicker by lazy { NumberPicker(requireActivity()) }
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
mPicker.minValue = savedInstanceState.getInt(STATE_PICKER_MIN)
|
||||
mPicker.maxValue = savedInstanceState.getInt(STATE_PICKER_MAX)
|
||||
mPicker.value = savedInstanceState.getInt(STATE_PICKER_CUR)
|
||||
} else {
|
||||
mPicker.minValue = 1
|
||||
mPicker.maxValue = pdfViewer.mNumPages
|
||||
mPicker.value = pdfViewer.mPage
|
||||
}
|
||||
val layout = FrameLayout(requireActivity())
|
||||
layout.addView(
|
||||
mPicker, FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
Gravity.CENTER
|
||||
)
|
||||
)
|
||||
return MaterialAlertDialogBuilder(requireActivity())
|
||||
.setView(layout)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
mPicker.clearFocus()
|
||||
pdfViewer.onJumpToPageInDocument(mPicker.value)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putInt(STATE_PICKER_MIN, mPicker.minValue)
|
||||
outState.putInt(STATE_PICKER_MAX, mPicker.maxValue)
|
||||
outState.putInt(STATE_PICKER_CUR, mPicker.value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package app.grapheneos.pdfviewer.fragment
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextUtils
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import app.grapheneos.pdfviewer.PdfViewer
|
||||
import app.grapheneos.pdfviewer.R
|
||||
import app.grapheneos.pdfviewer.databinding.PasswordDialogFragmentBinding
|
||||
import app.grapheneos.pdfviewer.viewModel.PasswordStatus
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
||||
class PasswordPromptFragment(private val pdfViewer: PdfViewer) : DialogFragment() {
|
||||
|
||||
private lateinit var passwordLayout : TextInputLayout
|
||||
private lateinit var passwordEditText : TextInputEditText
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val passwordPrompt = MaterialAlertDialogBuilder(requireContext())
|
||||
val passwordDialogFragmentBinding =
|
||||
PasswordDialogFragmentBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
passwordLayout = passwordDialogFragmentBinding.pdfPasswordTextInputLayout
|
||||
passwordEditText = passwordDialogFragmentBinding.pdfPasswordEditText
|
||||
passwordPrompt.setView(passwordDialogFragmentBinding.root)
|
||||
passwordEditText.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
override fun onTextChanged(input: CharSequence, i: Int, i1: Int, i2: Int) {
|
||||
updatePositiveButton()
|
||||
}
|
||||
override fun afterTextChanged(editable: Editable) {}
|
||||
})
|
||||
passwordEditText.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId != IME_ACTION_DONE) return@setOnEditorActionListener false
|
||||
sendPassword()
|
||||
true
|
||||
}
|
||||
passwordPrompt.setPositiveButton(R.string.open, null)
|
||||
passwordPrompt.setNegativeButton(R.string.cancel, null)
|
||||
val dialog = passwordPrompt.create()
|
||||
passwordPrompt.setCancelable(false)
|
||||
isCancelable = false
|
||||
dialog.setCanceledOnTouchOutside(false)
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
|
||||
pdfViewer.passwordValidationViewModel.status.observe(
|
||||
this
|
||||
) {
|
||||
when (it) {
|
||||
PasswordStatus.Status.MissingPassword -> {
|
||||
passwordEditText.editableText.clear()
|
||||
passwordDialogFragmentBinding.title.setText(R.string.password_prompt_description)
|
||||
}
|
||||
PasswordStatus.Status.InvalidPassword -> {
|
||||
passwordEditText.editableText.clear()
|
||||
passwordDialogFragmentBinding.pdfPasswordTextInputLayout.error =
|
||||
"invalid password"
|
||||
}
|
||||
PasswordStatus.Status.Validated -> {
|
||||
//Activity will dismiss the dialog
|
||||
}
|
||||
else -> {
|
||||
throw NullPointerException("status shouldn't be null")
|
||||
}
|
||||
}
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun updatePositiveButton() {
|
||||
passwordLayout.error = ""
|
||||
val btn = (dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
btn.isEnabled = passwordEditText.text?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
private fun sendPassword() {
|
||||
val password = passwordEditText.text.toString()
|
||||
if (!TextUtils.isEmpty(password)) {
|
||||
pdfViewer.loadPdfWithPassword(password)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
updatePositiveButton()
|
||||
passwordEditText.requestFocus()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
(dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
|
||||
sendPassword()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package app.grapheneos.pdfviewer.ktx
|
||||
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
|
||||
private val systemBars = WindowInsetsCompat.Type.statusBars()
|
||||
|
||||
fun View.hideSystemUi(window: Window) {
|
||||
val controller = WindowCompat.getInsetsController(window, this)
|
||||
controller.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
controller.hide(systemBars)
|
||||
}
|
||||
|
||||
fun View.showSystemUi(window: Window) {
|
||||
WindowCompat.getInsetsController(window, this).show(systemBars)
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package app.grapheneos.pdfviewer.loader;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.loader.content.AsyncTaskLoader;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DocumentPropertiesAsyncTaskLoader extends AsyncTaskLoader<List<CharSequence>> {
|
||||
|
||||
public static final String TAG = "DocumentPropertiesLoader";
|
||||
|
||||
public static final int ID = 1;
|
||||
|
||||
private final String mProperties;
|
||||
private final int mNumPages;
|
||||
|
||||
String fileName;
|
||||
Long fileSize;
|
||||
|
||||
public DocumentPropertiesAsyncTaskLoader(Context context, String properties, int numPages, String fileName, Long fileSize) {
|
||||
super(context);
|
||||
|
||||
mProperties = properties;
|
||||
mNumPages = numPages;
|
||||
this.fileName = fileName;
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onStartLoading() {
|
||||
forceLoad();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<CharSequence> loadInBackground() {
|
||||
|
||||
DocumentPropertiesLoader loader = new DocumentPropertiesLoader(
|
||||
getContext(),
|
||||
mProperties,
|
||||
mNumPages,
|
||||
fileName,
|
||||
fileSize
|
||||
);
|
||||
|
||||
return loader.loadAsList();
|
||||
}
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
package app.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 app.grapheneos.pdfviewer.R;
|
||||
import app.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.parseLong(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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package app.grapheneos.pdfviewer.loader
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.format.Formatter
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.Log
|
||||
import app.grapheneos.pdfviewer.R
|
||||
import org.json.JSONException
|
||||
|
||||
class DocumentPropertiesLoader(
|
||||
private val context: Context,
|
||||
private val properties: String,
|
||||
private val numPages: Int,
|
||||
private val fileName: String,
|
||||
private val fileSize: Long,
|
||||
) {
|
||||
|
||||
fun loadAsList(): List<CharSequence> {
|
||||
return load().map { item ->
|
||||
val name = context.getString(item.key.nameResource)
|
||||
val value = item.value
|
||||
|
||||
SpannableStringBuilder()
|
||||
.append(name)
|
||||
.append(":\n")
|
||||
.append(value)
|
||||
.apply {
|
||||
setSpan(
|
||||
StyleSpan(Typeface.BOLD),
|
||||
0,
|
||||
name.length,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun load(): Map<DocumentProperty, String> {
|
||||
val result = mutableMapOf<DocumentProperty, String>()
|
||||
result.addFileProperties()
|
||||
result.addPageSizeProperty()
|
||||
result.addPDFJsProperties()
|
||||
return result
|
||||
}
|
||||
|
||||
private fun MutableMap<DocumentProperty, String>.addPageSizeProperty() {
|
||||
this[DocumentProperty.Pages] = java.lang.String.valueOf(numPages)
|
||||
}
|
||||
|
||||
private fun MutableMap<DocumentProperty, String>.addFileProperties() {
|
||||
putAll(getFileProperties())
|
||||
}
|
||||
|
||||
private fun MutableMap<DocumentProperty, String>.addPDFJsProperties() {
|
||||
putAll(getPDFJsProperties())
|
||||
}
|
||||
|
||||
private fun getPDFJsProperties(): Map<DocumentProperty, String> {
|
||||
return try {
|
||||
PDFJsPropertiesToDocumentPropertyConverter(
|
||||
properties,
|
||||
context.getString(R.string.document_properties_invalid_date),
|
||||
parseExceptionListener = { parseException, value ->
|
||||
Log.w(
|
||||
DocumentPropertiesAsyncTaskLoader.TAG,
|
||||
"${parseException.message} for $value at offset: ${parseException.errorOffset}"
|
||||
)
|
||||
}
|
||||
).convert()
|
||||
} catch (e: JSONException) {
|
||||
Log.w(
|
||||
DocumentPropertiesAsyncTaskLoader.TAG,
|
||||
"invalid properties"
|
||||
)
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileProperties(): Map<DocumentProperty, String> {
|
||||
val collections = mutableMapOf<DocumentProperty, String>()
|
||||
collections[DocumentProperty.FileName] = fileName
|
||||
collections[DocumentProperty.FileSize] = Formatter.formatFileSize(context, fileSize)
|
||||
return collections
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package app.grapheneos.pdfviewer.loader
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import app.grapheneos.pdfviewer.R
|
||||
|
||||
private const val TITLE_KEY = "Title"
|
||||
private const val AUTHOR_KEY = "Author"
|
||||
private const val SUBJECT_KEY = "Subject"
|
||||
private const val KEYWORDS_KEY = "Keywords"
|
||||
private const val CREATION_DATE_KEY = "CreationDate"
|
||||
private const val MODIFY_DATE_KEY = "ModDate"
|
||||
private const val PRODUCER_KEY = "Producer"
|
||||
private const val CREATOR_KEY = "Creator"
|
||||
private const val PDF_VERSION_KEY = "PDFFormatVersion"
|
||||
|
||||
const val DEFAULT_VALUE = "-"
|
||||
|
||||
enum class DocumentProperty(
|
||||
val key: String = "",
|
||||
@StringRes val nameResource: Int,
|
||||
val isDate: Boolean = false
|
||||
) {
|
||||
FileName(key = "", nameResource = R.string.file_name),
|
||||
FileSize(key = "", nameResource = R.string.file_size),
|
||||
Pages(key = "", nameResource = R.string.pages),
|
||||
Title(key = TITLE_KEY, nameResource = R.string.title),
|
||||
Author(key = AUTHOR_KEY, nameResource = R.string.author),
|
||||
Subject(key = SUBJECT_KEY, nameResource = R.string.subject),
|
||||
Keywords(key = KEYWORDS_KEY, nameResource = R.string.keywords),
|
||||
CreationDate(key = CREATION_DATE_KEY, nameResource = R.string.creation_date, isDate = true),
|
||||
ModifyDate(key = MODIFY_DATE_KEY, nameResource = R.string.modify_date, isDate = true),
|
||||
Producer(key = PRODUCER_KEY, nameResource = R.string.producer),
|
||||
Creator(key = CREATOR_KEY, nameResource = R.string.creator),
|
||||
PDFVersion(key = PDF_VERSION_KEY, nameResource = R.string.pdf_version);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package app.grapheneos.pdfviewer.loader
|
||||
|
||||
import app.grapheneos.pdfviewer.Utils
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.text.ParseException
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
class PDFJsPropertiesToDocumentPropertyConverter(
|
||||
private val properties: String,
|
||||
private val propertyInvalidDate: String,
|
||||
private val parseExceptionListener: (e: ParseException, value: String) -> Unit
|
||||
) {
|
||||
|
||||
@Throws(JSONException::class)
|
||||
fun convert(): Map<DocumentProperty, String> {
|
||||
val result = mutableMapOf<DocumentProperty, String>()
|
||||
|
||||
val json = JSONObject(properties)
|
||||
addJsonProperties(json, result)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun addJsonProperties(
|
||||
json: JSONObject,
|
||||
collections: MutableMap<DocumentProperty, String>
|
||||
) {
|
||||
for (documentProperty in DocumentProperty.values()) {
|
||||
val key = documentProperty.key
|
||||
if (key.isEmpty()) continue
|
||||
val value = json.optString(key, DEFAULT_VALUE)
|
||||
collections[documentProperty] = prettify(documentProperty, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun prettify(property: DocumentProperty, value: String): String {
|
||||
if (value != DEFAULT_VALUE && property.isDate) {
|
||||
return try {
|
||||
Utils.parseDate(value)
|
||||
} catch (parseException: ParseException) {
|
||||
parseExceptionListener.invoke(parseException, value)
|
||||
propertyInvalidDate
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package app.grapheneos.pdfviewer.viewModel
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class PasswordStatus : ViewModel() {
|
||||
|
||||
enum class Status {
|
||||
MissingPassword,
|
||||
InvalidPassword,
|
||||
Validated
|
||||
}
|
||||
|
||||
val status: MutableLiveData<Status> = MutableLiveData(Status.MissingPassword)
|
||||
|
||||
fun passwordMissing() {
|
||||
status.postValue(Status.MissingPassword)
|
||||
}
|
||||
|
||||
fun invalid() {
|
||||
status.postValue(Status.InvalidPassword)
|
||||
}
|
||||
|
||||
fun validated() {
|
||||
status.postValue(Status.Validated)
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8.83c0,-0.53 -0.21,-1.04 -0.59,-1.41l-4.83,-4.83c-0.37,-0.38 -0.88,-0.59 -1.41,-0.59L6,2zM13,8L13,3.5L18.5,9L14,9c-0.55,0 -1,-0.45 -1,-1z" />
|
||||
</vector>
|
|
@ -1,30 +0,0 @@
|
|||
<!--
|
||||
Copyright (C) 2021 Google Inc.
|
||||
|
||||
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.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group
|
||||
android:scaleX="2.43375"
|
||||
android:scaleY="2.43375"
|
||||
android:translateX="24.795"
|
||||
android:translateY="24.795">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M18,4L6,4C4.9,4 4,4.9 4,6v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,6C20,4.9 19.1,4 18,4ZM9.5,11.5C9.5,12.33 8.83,13 8,13L7,13v1.25C7,14.66 6.66,15 6.25,15 5.84,15 5.5,14.66 5.5,14.25L5.5,10c0,-0.55 0.45,-1 1,-1L8,9c0.83,0 1.5,0.67 1.5,1.5zM14.5,13.5c0,0.83 -0.67,1.5 -1.5,1.5h-2c-0.28,0 -0.5,-0.22 -0.5,-0.5v-5C10.5,9.22 10.72,9 11,9h2c0.83,0 1.5,0.67 1.5,1.5zM18.5,9.75c0,0.41 -0.34,0.75 -0.75,0.75L17,10.5v1h0.75c0.41,0 0.75,0.34 0.75,0.75 0,0.41 -0.34,0.75 -0.75,0.75L17,13v1.25C17,14.66 16.66,15 16.25,15 15.84,15 15.5,14.66 15.5,14.25L15.5,10c0,-0.55 0.45,-1 1,-1h1.25c0.41,0 0.75,0.34 0.75,0.75zM7,11.5h1v-1L7,10.5ZM12,13.5h1v-3h-1z" />
|
||||
</group>
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27c1.2,-1.4 1.82,-3.31 1.48,-5.34 -0.47,-2.78 -2.79,-5 -5.59,-5.34 -4.23,-0.52 -7.78,3.04 -7.27,7.27 0.34,2.8 2.56,5.12 5.34,5.59 2.03,0.34 3.94,-0.28 5.34,-1.48l0.27,0.28v0.79l4.26,4.25c0.41,0.41 1.07,0.41 1.48,0l0.01,-0.01c0.41,-0.41 0.41,-1.07 0,-1.48L15.5,14zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14zM9.5,7c-0.28,0 -0.5,0.22 -0.5,0.5L9,9L7.5,9c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5L9,10v1.5c0,0.28 0.22,0.5 0.5,0.5s0.5,-0.22 0.5,-0.5L10,10h1.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5L10,9L10,7.5c0,-0.28 -0.22,-0.5 -0.5,-0.5z" />
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27c1.2,-1.4 1.82,-3.31 1.48,-5.34 -0.47,-2.78 -2.79,-5 -5.59,-5.34 -4.23,-0.52 -7.79,3.04 -7.27,7.27 0.34,2.8 2.56,5.12 5.34,5.59 2.03,0.34 3.94,-0.28 5.34,-1.48l0.27,0.28v0.79l4.26,4.25c0.41,0.41 1.07,0.41 1.48,0l0.01,-0.01c0.41,-0.41 0.41,-1.07 0,-1.48L15.5,14zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14zM7.5,9h4c0.28,0 0.5,0.22 0.5,0.5s-0.22,0.5 -0.5,0.5h-4c-0.28,0 -0.5,-0.22 -0.5,-0.5s0.22,-0.5 0.5,-0.5z" />
|
||||
</vector>
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:text="@string/password_prompt_description"
|
||||
android:textColor="?android:attr/colorAccent"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/pdf_password_text_input_layout"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:hintEnabled="false"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:passwordToggleTint="?android:attr/colorAccent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/pdf_password_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:hint="@string/password_prompt_hint"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
|
@ -14,7 +14,6 @@
|
|||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="#212121"
|
||||
app:popupTheme="@style/AppTheme.PopupOverlay" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
|
|
@ -11,31 +11,13 @@
|
|||
android:id="@+id/action_previous"
|
||||
android:icon="@drawable/ic_navigate_before_24dp"
|
||||
android:title="@string/action_previous"
|
||||
app:showAsAction="ifRoom" />
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_next"
|
||||
android:icon="@drawable/ic_navigate_next_24dp"
|
||||
android:title="@string/action_next"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_open"
|
||||
android:icon="@drawable/ic_insert_drive_file_24dp"
|
||||
android:title="@string/action_open"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_zoom_out"
|
||||
android:icon="@drawable/ic_zoom_out_24dp"
|
||||
android:title="@string/action_zoom_out"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_zoom_in"
|
||||
android:icon="@drawable/ic_zoom_in_24dp"
|
||||
android:title="@string/action_zoom_in"
|
||||
app:showAsAction="ifRoom" />
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_first"
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/debug_action_toggle_text_layer_visibility"
|
||||
android:title="@string/debug_action_toggle_text_layer_visibility"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
|
@ -1,5 +0,0 @@
|
|||
<?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>
|
|
@ -1,5 +0,0 @@
|
|||
<?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>
|
|
@ -1,3 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:shrinkMode="strict" />
|
|
@ -1,13 +1,14 @@
|
|||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">#DEFFFFFF</item>
|
||||
<item name="android:statusBarColor">#212121</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightNavigationBar" tools:ignore="NewApi">false</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.Material3.Dark" />
|
||||
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.Material3.DynamicColors.Dark" />
|
||||
|
||||
</resources>
|
|
@ -1,17 +0,0 @@
|
|||
<?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>
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
|
@ -1,26 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<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_first">First page</string>
|
||||
<string name="action_last">Last page</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_rotate_clockwise">Rotate clockwise</string>
|
||||
<string name="action_rotate_counterclockwise">Rotate counterclockwise</string>
|
||||
<string name="action_jump_to_page">Jump to page</string>
|
||||
<string name="action_view_document_properties">Properties</string>
|
||||
|
||||
<string name="debug_action_toggle_text_layer_visibility">Toggle text layer visibility</string>
|
||||
|
||||
<string name="document_properties_invalid_date">Invalid date</string>
|
||||
<string name="document_properties_retrieval_failed">Failed to obtain document metadata</string>
|
||||
|
||||
<string name="invalid_mime_type">Cannot open file with invalid MIME type</string>
|
||||
<string name="legacy_file_uri">Cannot open legacy file paths from insecure apps</string>
|
||||
<string name="io_error">Received I/O error trying to open content</string>
|
||||
|
||||
<string name="webview_out_of_date_title">WebView out-of-date</string>
|
||||
<string name="webview_out_of_date_message">Your current WebView version is %d. The WebView should be at least version %d for the PDF Viewer to work.</string>
|
||||
<string name="webview_out_of_date_message" tools:ignore="PluralsCandidate">Your current WebView version is %1$d. The WebView should be at least version %2$d for the PDF Viewer to work.</string>
|
||||
|
||||
<string name="password_prompt_hint">Password</string>
|
||||
<string name="password_prompt_description">Enter the password to decrypt this PDF file</string>
|
||||
<string name="open">Open</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
|
||||
<string name="file_name">File name</string>
|
||||
<string name="file_size">File size</string>
|
||||
<string name="title">Title</string>
|
||||
<string name="author">Author</string>
|
||||
<string name="subject">Subject</string>
|
||||
<string name="keywords">Keywords</string>
|
||||
<string name="creation_date">Creation date</string>
|
||||
<string name="modify_date">Modify date</string>
|
||||
<string name="producer">Producer</string>
|
||||
<string name="creator">Creator</string>
|
||||
<string name="pdf_version">PDF version</string>
|
||||
<string name="pages">Pages</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">#000000</item>
|
||||
<item name="android:statusBarColor">#212121</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightNavigationBar" tools:ignore="NewApi">true</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.Material3.Dark" />
|
||||
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.Material3.DynamicColors.Dark" />
|
||||
|
||||
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.Material3.Light" />
|
||||
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.Material3.DynamicColors.Light" />
|
||||
|
||||
</resources>
|
|
@ -1,12 +1,6 @@
|
|||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.1.2")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
|
||||
}
|
||||
plugins {
|
||||
id("com.android.application") apply false
|
||||
id("org.jetbrains.kotlin.android") apply false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
android.enableR8.fullMode=true
|
||||
kotlin.code.style=official
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -1,6 +1,8 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=8cc27038d5dbd815759851ba53e70cf62e481b87494cc97cfd97982ada5ba634
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||
distributionSha256Sum=38f66cd6eef217b4c35855bb11ea4e9fbc53594ccccb5fb82dfd317ef8c2c5a3
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
@ -80,13 +80,10 @@ do
|
|||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${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"'
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
@ -133,22 +130,29 @@ 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.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
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
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
|
@ -193,6 +197,10 @@ if "$cygwin" || "$msys" ; then
|
|||
done
|
||||
fi
|
||||
|
||||
|
||||
# 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"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
|
@ -205,6 +213,12 @@ set -- \
|
|||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
|
@ -25,7 +25,8 @@
|
|||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
|
@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 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
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit a7fc0d33a11d032741f44d0b623780253747da10
|
Loading…
Reference in New Issue