Compare commits
22 Commits
30b8769ed2
...
29a004091e
Author | SHA1 | Date |
---|---|---|
Daniel Micay | 29a004091e | |
Pratyush | 195bba7891 | |
Pratyush | 17c7c84296 | |
Pratyush | 61607858ef | |
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 |
|
@ -10,3 +10,8 @@ updates:
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
target-branch: main
|
target-branch: main
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
target-branch: main
|
||||||
|
|
|
@ -8,13 +8,16 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
cache: npm
|
||||||
- name: Set up JDK 19
|
- name: Set up JDK 20
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: 19
|
java-version: 20
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
|
- run: npm ci --ignore-scripts
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew build --no-daemon
|
run: ./gradlew build --no-daemon
|
||||||
|
|
|
@ -6,3 +6,4 @@ keystore.properties
|
||||||
*.keystore
|
*.keystore
|
||||||
/.idea
|
/.idea
|
||||||
/releases
|
/releases
|
||||||
|
/node_modules
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
[submodule "pdfjs-dist"]
|
|
||||||
path = third_party/pdfjs-dist
|
|
||||||
url = https://github.com/mozilla/pdfjs-dist.git
|
|
|
@ -15,7 +15,7 @@ plugins {
|
||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion.set(JavaLanguageVersion.of(11))
|
languageVersion.set(JavaLanguageVersion.of(17))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,16 +76,17 @@ android {
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility(JavaVersion.VERSION_11)
|
sourceCompatibility(JavaVersion.VERSION_17)
|
||||||
targetCompatibility(JavaVersion.VERSION_11)
|
targetCompatibility(JavaVersion.VERSION_17)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
../../../../third_party/pdfjs-dist/build/pdf.min.js
|
../../../../node_modules/pdfjs-dist/build/pdf.min.js
|
|
@ -1 +1 @@
|
||||||
../../../../third_party/pdfjs-dist/build/pdf.worker.min.js
|
../../../../node_modules/pdfjs-dist/build/pdf.worker.min.js
|
|
@ -1,3 +1,8 @@
|
||||||
|
:root {
|
||||||
|
--text-layer-opacity: 0.2;
|
||||||
|
--text-layer-foreground: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -12,6 +17,8 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
#container {
|
#container {
|
||||||
|
--scale-factor: 1;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -24,25 +31,26 @@ body {
|
||||||
grid-column-start: 1;
|
grid-column-start: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas, .textLayer {
|
canvas {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textLayer {
|
.textLayer {
|
||||||
text-align: initial;
|
text-align: initial;
|
||||||
|
position: absolute;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
opacity: 0.2;
|
opacity: var(--text-layer-opacity);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textLayer span,
|
.textLayer span,
|
||||||
.textLayer br {
|
.textLayer br {
|
||||||
color: transparent;
|
color: var(--text-layer-foreground);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
transform-origin: 0% 0%;
|
transform-origin: 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textLayer .highlight {
|
.textLayer .highlight {
|
||||||
|
@ -91,3 +99,13 @@ canvas, .textLayer {
|
||||||
.textLayer .endOfContent.active {
|
.textLayer .endOfContent.active {
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-main-rotation="90"] {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
[data-main-rotation="180"] {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
[data-main-rotation="270"] {
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ let pdfDoc = null;
|
||||||
let pageRendering = false;
|
let pageRendering = false;
|
||||||
let renderPending = false;
|
let renderPending = false;
|
||||||
let renderPendingZoom = 0;
|
let renderPendingZoom = 0;
|
||||||
const canvas = document.getElementById('content');
|
const canvas = document.getElementById("content");
|
||||||
|
const container = document.getElementById("container");
|
||||||
let orientationDegrees = 0;
|
let orientationDegrees = 0;
|
||||||
let zoomRatio = 1;
|
let zoomRatio = 1;
|
||||||
let textLayerDiv = document.getElementById("text");
|
let textLayerDiv = document.getElementById("text");
|
||||||
|
@ -19,6 +20,8 @@ let useRender;
|
||||||
const cache = [];
|
const cache = [];
|
||||||
const maxCached = 6;
|
const maxCached = 6;
|
||||||
|
|
||||||
|
let isTextLayerVisible = false;
|
||||||
|
|
||||||
function maybeRenderNextPage() {
|
function maybeRenderNextPage() {
|
||||||
if (renderPending) {
|
if (renderPending) {
|
||||||
pageRendering = false;
|
pageRendering = false;
|
||||||
|
@ -61,6 +64,14 @@ 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 renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
||||||
pageRendering = true;
|
pageRendering = true;
|
||||||
useRender = !prerender;
|
useRender = !prerender;
|
||||||
|
@ -82,6 +93,8 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
||||||
|
|
||||||
textLayerDiv.replaceWith(cached.textLayerDiv);
|
textLayerDiv.replaceWith(cached.textLayerDiv);
|
||||||
textLayerDiv = cached.textLayerDiv;
|
textLayerDiv = cached.textLayerDiv;
|
||||||
|
setLayerTransform(cached.pageWidth, cached.pageHeight, textLayerDiv);
|
||||||
|
container.style.setProperty("--scale-factor", newZoomRatio.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
pageRendering = false;
|
pageRendering = false;
|
||||||
|
@ -95,7 +108,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewport = page.getViewport({scale: newZoomRatio, rotation: orientationDegrees})
|
const viewport = page.getViewport({scale: newZoomRatio, rotation: orientationDegrees});
|
||||||
|
|
||||||
if (useRender) {
|
if (useRender) {
|
||||||
if (newZoomRatio !== zoomRatio) {
|
if (newZoomRatio !== zoomRatio) {
|
||||||
|
@ -105,7 +118,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
||||||
zoomRatio = newZoomRatio;
|
zoomRatio = newZoomRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (zoom == 2) {
|
if (zoom === 2) {
|
||||||
pageRendering = false;
|
pageRendering = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -137,10 +150,10 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
|
|
||||||
const textLayerFrag = document.createDocumentFragment();
|
const newTextLayerDiv = textLayerDiv.cloneNode();
|
||||||
task = pdfjsLib.renderTextLayer({
|
task = pdfjsLib.renderTextLayer({
|
||||||
textContentStream: page.streamTextContent(),
|
textContentSource: page.streamTextContent(),
|
||||||
container: textLayerFrag,
|
container: newTextLayerDiv,
|
||||||
viewport: viewport
|
viewport: viewport
|
||||||
});
|
});
|
||||||
task.promise.then(function() {
|
task.promise.then(function() {
|
||||||
|
@ -148,14 +161,24 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
||||||
|
|
||||||
render();
|
render();
|
||||||
|
|
||||||
const newTextLayerDiv = textLayerDiv.cloneNode();
|
// 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.height = newCanvas.style.height;
|
||||||
newTextLayerDiv.style.width = newCanvas.style.width;
|
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) {
|
if (useRender) {
|
||||||
textLayerDiv.replaceWith(newTextLayerDiv);
|
textLayerDiv.replaceWith(newTextLayerDiv);
|
||||||
textLayerDiv = newTextLayerDiv;
|
textLayerDiv = newTextLayerDiv;
|
||||||
|
container.style.setProperty("--scale-factor", newZoomRatio.toString());
|
||||||
}
|
}
|
||||||
newTextLayerDiv.appendChild(textLayerFrag);
|
|
||||||
|
|
||||||
if (cache.length === maxCached) {
|
if (cache.length === maxCached) {
|
||||||
cache.shift()
|
cache.shift()
|
||||||
|
@ -165,7 +188,9 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
|
||||||
zoomRatio: newZoomRatio,
|
zoomRatio: newZoomRatio,
|
||||||
orientationDegrees: orientationDegrees,
|
orientationDegrees: orientationDegrees,
|
||||||
canvas: newCanvas,
|
canvas: newCanvas,
|
||||||
textLayerDiv: newTextLayerDiv
|
textLayerDiv: newTextLayerDiv,
|
||||||
|
pageWidth: viewport.width,
|
||||||
|
pageHeight: viewport.height
|
||||||
});
|
});
|
||||||
|
|
||||||
pageRendering = false;
|
pageRendering = false;
|
||||||
|
@ -198,6 +223,18 @@ function isTextSelected() {
|
||||||
return window.getSelection().toString() !== "";
|
return window.getSelection().toString() !== "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
function loadDocument() {
|
||||||
const pdfPassword = channel.getPassword();
|
const pdfPassword = channel.getPassword();
|
||||||
const loadingTask = pdfjsLib.getDocument({ url: "https://localhost/placeholder.pdf", password: pdfPassword });
|
const loadingTask = pdfjsLib.getDocument({ url: "https://localhost/placeholder.pdf", password: pdfPassword });
|
||||||
|
@ -223,3 +260,7 @@ function loadDocument() {
|
||||||
console.error(reason.name + ": " + reason.message);
|
console.error(reason.name + ": " + reason.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.onresize = () => {
|
||||||
|
setLayerTransform(canvas.clientWidth, canvas.clientHeight, textLayerDiv);
|
||||||
|
}
|
||||||
|
|
|
@ -45,12 +45,14 @@ import app.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment;
|
||||||
import app.grapheneos.pdfviewer.fragment.PasswordPromptFragment;
|
import app.grapheneos.pdfviewer.fragment.PasswordPromptFragment;
|
||||||
import app.grapheneos.pdfviewer.fragment.JumpToPageFragment;
|
import app.grapheneos.pdfviewer.fragment.JumpToPageFragment;
|
||||||
import app.grapheneos.pdfviewer.ktx.ViewKt;
|
import app.grapheneos.pdfviewer.ktx.ViewKt;
|
||||||
import app.grapheneos.pdfviewer.loader.DocumentPropertiesLoader;
|
import app.grapheneos.pdfviewer.loader.DocumentPropertiesAsyncTaskLoader;
|
||||||
import app.grapheneos.pdfviewer.viewModel.PasswordStatus;
|
import app.grapheneos.pdfviewer.viewModel.PasswordStatus;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -189,7 +191,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
||||||
|
|
||||||
final Bundle args = new Bundle();
|
final Bundle args = new Bundle();
|
||||||
args.putString(KEY_PROPERTIES, properties);
|
args.putString(KEY_PROPERTIES, properties);
|
||||||
runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesLoader.ID, args, PdfViewer.this));
|
runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesAsyncTaskLoader.ID, args, PdfViewer.this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
|
@ -474,14 +476,14 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Loader<List<CharSequence>> onCreateLoader(int id, Bundle args) {
|
public Loader<List<CharSequence>> onCreateLoader(int id, Bundle args) {
|
||||||
return new DocumentPropertiesLoader(this, args.getString(KEY_PROPERTIES), mNumPages, mUri);
|
return new DocumentPropertiesAsyncTaskLoader(this, args.getString(KEY_PROPERTIES), mNumPages, mUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(@NonNull Loader<List<CharSequence>> loader, List<CharSequence> data) {
|
public void onLoadFinished(@NonNull Loader<List<CharSequence>> loader, List<CharSequence> data) {
|
||||||
mDocumentProperties = data;
|
mDocumentProperties = data;
|
||||||
setToolbarTitleWithDocumentName();
|
setToolbarTitleWithDocumentName();
|
||||||
LoaderManager.getInstance(this).destroyLoader(DocumentPropertiesLoader.ID);
|
LoaderManager.getInstance(this).destroyLoader(DocumentPropertiesAsyncTaskLoader.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -618,15 +620,21 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
||||||
super.onCreateOptionsMenu(menu);
|
super.onCreateOptionsMenu(menu);
|
||||||
MenuInflater inflater = getMenuInflater();
|
MenuInflater inflater = getMenuInflater();
|
||||||
inflater.inflate(R.menu.pdf_viewer, menu);
|
inflater.inflate(R.menu.pdf_viewer, menu);
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
inflater.inflate(R.menu.pdf_viewer_debug, menu);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPrepareOptionsMenu(@NonNull Menu menu) {
|
public boolean onPrepareOptionsMenu(@NonNull Menu menu) {
|
||||||
final int[] ids = {R.id.action_jump_to_page, R.id.action_next, R.id.action_previous,
|
final ArrayList<Integer> ids = new ArrayList<>(Arrays.asList(R.id.action_jump_to_page,
|
||||||
R.id.action_first, R.id.action_last, R.id.action_rotate_clockwise,
|
R.id.action_next, R.id.action_previous, R.id.action_first, R.id.action_last,
|
||||||
R.id.action_rotate_counterclockwise, R.id.action_view_document_properties,
|
R.id.action_rotate_clockwise, R.id.action_rotate_counterclockwise,
|
||||||
R.id.action_share, R.id.action_save_as};
|
R.id.action_view_document_properties, R.id.action_share, R.id.action_save_as));
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
ids.add(R.id.debug_action_toggle_text_layer_visibility);
|
||||||
|
}
|
||||||
if (mDocumentState < STATE_LOADED) {
|
if (mDocumentState < STATE_LOADED) {
|
||||||
for (final int id : ids) {
|
for (final int id : ids) {
|
||||||
final MenuItem item = menu.findItem(id);
|
final MenuItem item = menu.findItem(id);
|
||||||
|
@ -691,6 +699,9 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
||||||
return true;
|
return true;
|
||||||
} else if (itemId == R.id.action_save_as) {
|
} else if (itemId == R.id.action_save_as) {
|
||||||
saveDocument();
|
saveDocument();
|
||||||
|
} else if (itemId == R.id.debug_action_toggle_text_layer_visibility) {
|
||||||
|
binding.webview.evaluateJavascript("toggleTextLayerVisibility()", null);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
|
|
|
@ -9,21 +9,6 @@ import java.text.ParseException;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
public static String parseFileSize(long fileSize) {
|
|
||||||
final double kb = fileSize / 1000d;
|
|
||||||
|
|
||||||
if (kb == 0d) {
|
|
||||||
return fileSize + " Bytes";
|
|
||||||
}
|
|
||||||
|
|
||||||
final DecimalFormat format = new DecimalFormat("#.##");
|
|
||||||
format.setRoundingMode(RoundingMode.CEILING);
|
|
||||||
|
|
||||||
if (kb < 1000) {
|
|
||||||
return format.format(kb) + " kB (" + fileSize + " Bytes)";
|
|
||||||
}
|
|
||||||
return format.format(kb / 1000) + " MB (" + fileSize + " Bytes)";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int parseIntSafely(String field) throws ParseException {
|
private static int parseIntSafely(String field) throws ParseException {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -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,61 +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;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
mPicker = new NumberPicker(getActivity());
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
mPicker.setMinValue(savedInstanceState.getInt(STATE_PICKER_MIN));
|
|
||||||
mPicker.setMaxValue(savedInstanceState.getInt(STATE_PICKER_MAX));
|
|
||||||
mPicker.setValue(savedInstanceState.getInt(STATE_PICKER_CUR));
|
|
||||||
} else {
|
|
||||||
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,60 @@
|
||||||
|
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 : 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 {
|
||||||
|
|
||||||
|
val viewerActivity: PdfViewer = (requireActivity() as PdfViewer)
|
||||||
|
|
||||||
|
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 = viewerActivity.mNumPages
|
||||||
|
mPicker.value = viewerActivity.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()
|
||||||
|
viewerActivity.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,48 @@
|
||||||
|
package app.grapheneos.pdfviewer.loader;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
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;
|
||||||
|
private final Uri mUri;
|
||||||
|
|
||||||
|
public DocumentPropertiesAsyncTaskLoader(Context context, String properties, int numPages, Uri uri) {
|
||||||
|
super(context);
|
||||||
|
|
||||||
|
mProperties = properties;
|
||||||
|
mNumPages = numPages;
|
||||||
|
mUri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStartLoading() {
|
||||||
|
forceLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public List<CharSequence> loadInBackground() {
|
||||||
|
|
||||||
|
DocumentPropertiesLoader loader = new DocumentPropertiesLoader(
|
||||||
|
getContext(),
|
||||||
|
mProperties,
|
||||||
|
mNumPages,
|
||||||
|
mUri
|
||||||
|
);
|
||||||
|
|
||||||
|
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 != null && 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,111 @@
|
||||||
|
package app.grapheneos.pdfviewer.loader
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
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 mUri: Uri
|
||||||
|
) {
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>()
|
||||||
|
val proj = arrayOf(
|
||||||
|
OpenableColumns.DISPLAY_NAME,
|
||||||
|
OpenableColumns.SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
context.contentResolver.query(
|
||||||
|
mUri,
|
||||||
|
proj,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)?.use { cursor ->
|
||||||
|
cursor.moveToFirst()
|
||||||
|
val indexName: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
|
|
||||||
|
if (indexName >= 0) {
|
||||||
|
collections[DocumentProperty.FileName] = cursor.getString(indexName)
|
||||||
|
}
|
||||||
|
|
||||||
|
val indexSize: Int = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||||
|
if (indexSize >= 0) {
|
||||||
|
val fileSize: Long = cursor.getString(indexSize).toLong()
|
||||||
|
collections[DocumentProperty.FileSize] = Formatter.formatShortFileSize(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,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,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>
|
|
|
@ -14,6 +14,8 @@
|
||||||
<string name="action_save_as">Save as</string>
|
<string name="action_save_as">Save as</string>
|
||||||
<string name="action_view_document_properties">Properties</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_invalid_date">Invalid date</string>
|
||||||
<string name="document_properties_retrieval_failed">Failed to obtain document metadata</string>
|
<string name="document_properties_retrieval_failed">Failed to obtain document metadata</string>
|
||||||
|
|
||||||
|
@ -29,4 +31,17 @@
|
||||||
<string name="password_prompt_description">Enter the password to decrypt this PDF file</string>
|
<string name="password_prompt_description">Enter the password to decrypt this PDF file</string>
|
||||||
<string name="open">Open</string>
|
<string name="open">Open</string>
|
||||||
<string name="cancel">Cancel</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>
|
</resources>
|
||||||
|
|
|
@ -4,8 +4,8 @@ buildscript {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:7.4.2")
|
classpath("com.android.tools.build:gradle:8.0.0")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10")
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableR8.fullMode=true
|
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -1,7 +1,7 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=ff7bf6a86f09b9b2c40bb8f48b25fc19cf2b2664fd1d220cd7ab833ec758d0d7
|
distributionSha256Sum=a62c5f99585dd9e1f95dab7b9415a0e698fa9dd1e6c38537faa81ac078f4d23e
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
@ -85,9 +85,6 @@ done
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
@ -197,6 +194,10 @@ if "$cygwin" || "$msys" ; then
|
||||||
done
|
done
|
||||||
fi
|
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;
|
# Collect all arguments for the java command;
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"pdfjs-dist": "3.5.141"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit eb245b8de89c8d631d175ae937136de54ea3ed51
|
|
Loading…
Reference in New Issue