Compare commits

..

No commits in common. "29a004091e24f5a821c1b65c80b703019d5f3ca8" and "30b8769ed2598c81599c8e4b6cc0a6b1692cefae" have entirely different histories.

33 changed files with 1249 additions and 2353 deletions

View File

@ -10,8 +10,3 @@ updates:
schedule:
interval: daily
target-branch: main
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
target-branch: main

View File

@ -8,16 +8,13 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: npm
- name: Set up JDK 20
submodules: true
- name: Set up JDK 19
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: 20
java-version: 19
cache: gradle
- run: npm ci --ignore-scripts
- name: Build with Gradle
run: ./gradlew build --no-daemon

1
.gitignore vendored
View File

@ -6,4 +6,3 @@ keystore.properties
*.keystore
/.idea
/releases
/node_modules

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "pdfjs-dist"]
path = third_party/pdfjs-dist
url = https://github.com/mozilla/pdfjs-dist.git

View File

@ -15,7 +15,7 @@ plugins {
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
languageVersion.set(JavaLanguageVersion.of(11))
}
}
@ -76,17 +76,16 @@ android {
buildFeatures {
viewBinding = true
buildConfig = true
}
}
compileOptions {
sourceCompatibility(JavaVersion.VERSION_17)
targetCompatibility(JavaVersion.VERSION_17)
sourceCompatibility(JavaVersion.VERSION_11)
targetCompatibility(JavaVersion.VERSION_11)
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
jvmTarget = JavaVersion.VERSION_11.toString()
}
}

View File

@ -1 +1 @@
../../../../node_modules/pdfjs-dist/build/pdf.min.js
../../../../third_party/pdfjs-dist/build/pdf.min.js

View File

@ -1 +1 @@
../../../../node_modules/pdfjs-dist/build/pdf.worker.min.js
../../../../third_party/pdfjs-dist/build/pdf.worker.min.js

View File

@ -1,8 +1,3 @@
:root {
--text-layer-opacity: 0.2;
--text-layer-foreground: transparent;
}
html, body {
height: 100%;
}
@ -17,8 +12,6 @@ body {
}
#container {
--scale-factor: 1;
width: 100%;
height: 100%;
display: grid;
@ -31,26 +24,25 @@ body {
grid-column-start: 1;
}
canvas {
canvas, .textLayer {
display: inline-block;
position: relative;
}
.textLayer {
text-align: initial;
position: absolute;
overflow: hidden;
opacity: var(--text-layer-opacity);
opacity: 0.2;
line-height: 1;
}
.textLayer span,
.textLayer br {
color: var(--text-layer-foreground);
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0 0;
transform-origin: 0% 0%;
}
.textLayer .highlight {
@ -99,13 +91,3 @@ canvas {
.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);
}

View File

@ -6,8 +6,7 @@ let pdfDoc = null;
let pageRendering = false;
let renderPending = false;
let renderPendingZoom = 0;
const canvas = document.getElementById("content");
const container = document.getElementById("container");
const canvas = document.getElementById('content');
let orientationDegrees = 0;
let zoomRatio = 1;
let textLayerDiv = document.getElementById("text");
@ -20,8 +19,6 @@ let useRender;
const cache = [];
const maxCached = 6;
let isTextLayerVisible = false;
function maybeRenderNextPage() {
if (renderPending) {
pageRendering = false;
@ -64,14 +61,6 @@ 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) {
pageRendering = true;
useRender = !prerender;
@ -93,8 +82,6 @@ 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;
@ -108,7 +95,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
return;
}
const viewport = page.getViewport({scale: newZoomRatio, rotation: orientationDegrees});
const viewport = page.getViewport({scale: newZoomRatio, rotation: orientationDegrees})
if (useRender) {
if (newZoomRatio !== zoomRatio) {
@ -118,7 +105,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
zoomRatio = newZoomRatio;
}
if (zoom === 2) {
if (zoom == 2) {
pageRendering = false;
return;
}
@ -150,10 +137,10 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
}
render();
const newTextLayerDiv = textLayerDiv.cloneNode();
const textLayerFrag = document.createDocumentFragment();
task = pdfjsLib.renderTextLayer({
textContentSource: page.streamTextContent(),
container: newTextLayerDiv,
textContentStream: page.streamTextContent(),
container: textLayerFrag,
viewport: viewport
});
task.promise.then(function() {
@ -161,24 +148,14 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
render();
// 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);
const newTextLayerDiv = textLayerDiv.cloneNode();
newTextLayerDiv.style.height = newCanvas.style.height;
newTextLayerDiv.style.width = newCanvas.style.width;
if (useRender) {
textLayerDiv.replaceWith(newTextLayerDiv);
textLayerDiv = newTextLayerDiv;
container.style.setProperty("--scale-factor", newZoomRatio.toString());
}
newTextLayerDiv.appendChild(textLayerFrag);
if (cache.length === maxCached) {
cache.shift()
@ -188,9 +165,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
zoomRatio: newZoomRatio,
orientationDegrees: orientationDegrees,
canvas: newCanvas,
textLayerDiv: newTextLayerDiv,
pageWidth: viewport.width,
pageHeight: viewport.height
textLayerDiv: newTextLayerDiv
});
pageRendering = false;
@ -223,18 +198,6 @@ function isTextSelected() {
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() {
const pdfPassword = channel.getPassword();
const loadingTask = pdfjsLib.getDocument({ url: "https://localhost/placeholder.pdf", password: pdfPassword });
@ -260,7 +223,3 @@ function loadDocument() {
console.error(reason.name + ": " + reason.message);
});
}
window.onresize = () => {
setLayerTransform(canvas.clientWidth, canvas.clientHeight, textLayerDiv);
}

View File

@ -45,14 +45,12 @@ import app.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment;
import app.grapheneos.pdfviewer.fragment.PasswordPromptFragment;
import app.grapheneos.pdfviewer.fragment.JumpToPageFragment;
import app.grapheneos.pdfviewer.ktx.ViewKt;
import app.grapheneos.pdfviewer.loader.DocumentPropertiesAsyncTaskLoader;
import app.grapheneos.pdfviewer.loader.DocumentPropertiesLoader;
import app.grapheneos.pdfviewer.viewModel.PasswordStatus;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@ -191,7 +189,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
final Bundle args = new Bundle();
args.putString(KEY_PROPERTIES, properties);
runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesAsyncTaskLoader.ID, args, PdfViewer.this));
runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesLoader.ID, args, PdfViewer.this));
}
@JavascriptInterface
@ -476,14 +474,14 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
@NonNull
@Override
public Loader<List<CharSequence>> onCreateLoader(int id, Bundle args) {
return new DocumentPropertiesAsyncTaskLoader(this, args.getString(KEY_PROPERTIES), mNumPages, mUri);
return new DocumentPropertiesLoader(this, args.getString(KEY_PROPERTIES), mNumPages, mUri);
}
@Override
public void onLoadFinished(@NonNull Loader<List<CharSequence>> loader, List<CharSequence> data) {
mDocumentProperties = data;
setToolbarTitleWithDocumentName();
LoaderManager.getInstance(this).destroyLoader(DocumentPropertiesAsyncTaskLoader.ID);
LoaderManager.getInstance(this).destroyLoader(DocumentPropertiesLoader.ID);
}
@Override
@ -620,21 +618,15 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.pdf_viewer, menu);
if (BuildConfig.DEBUG) {
inflater.inflate(R.menu.pdf_viewer_debug, menu);
}
return true;
}
@Override
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_share, R.id.action_save_as));
if (BuildConfig.DEBUG) {
ids.add(R.id.debug_action_toggle_text_layer_visibility);
}
final int[] ids = {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_share, R.id.action_save_as};
if (mDocumentState < STATE_LOADED) {
for (final int id : ids) {
final MenuItem item = menu.findItem(id);
@ -699,9 +691,6 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
return true;
} else if (itemId == R.id.action_save_as) {
saveDocument();
} else if (itemId == R.id.debug_action_toggle_text_layer_visibility) {
binding.webview.evaluateJavascript("toggleTextLayerVisibility()", null);
return true;
}
return super.onOptionsItemSelected(item);

View File

@ -9,6 +9,21 @@ 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";
}
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 {
try {

View File

@ -0,0 +1,60 @@
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();
}
}

View File

@ -1,53 +0,0 @@
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
}
}
}

View File

@ -0,0 +1,61 @@
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());
}
}

View File

@ -1,60 +0,0 @@
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)
}
}

View File

@ -1,48 +0,0 @@
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();
}
}

View File

@ -0,0 +1,149 @@
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;
}
}

View File

@ -1,111 +0,0 @@
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
}
}

View File

@ -1,35 +0,0 @@
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);
}

View File

@ -1,47 +0,0 @@
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
}
}

View File

@ -1,10 +0,0 @@
<?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>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="property_names">
<item>File name</item>
<item>File size</item>
<item>Title</item>
<item>Author</item>
<item>Subject</item>
<item>Keywords</item>
<item>Creation date</item>
<item>Modify date</item>
<item>Producer</item>
<item>Creator</item>
<item>PDF version</item>
<item>Pages</item>
</string-array>
</resources>

View File

@ -14,8 +14,6 @@
<string name="action_save_as">Save as</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>
@ -31,17 +29,4 @@
<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>

View File

@ -4,8 +4,8 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.0.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20")
classpath("com.android.tools.build:gradle:7.4.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10")
}
}

View File

@ -1,3 +1,4 @@
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.

View File

@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=a62c5f99585dd9e1f95dab7b9415a0e698fa9dd1e6c38537faa81ac078f4d23e
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
distributionSha256Sum=ff7bf6a86f09b9b2c40bb8f48b25fc19cf2b2664fd1d220cd7ab833ec758d0d7
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

7
gradlew vendored
View File

@ -85,6 +85,9 @@ done
APP_BASE_NAME=${0##*/}
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.
MAX_FD=maximum
@ -194,10 +197,6 @@ 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

1285
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +0,0 @@
{
"dependencies": {
"pdfjs-dist": "3.5.141"
}
}

3
setup
View File

@ -1,3 +0,0 @@
#!/bin/bash
npm ci --ignore-scripts

1
third_party/pdfjs-dist vendored Submodule

@ -0,0 +1 @@
Subproject commit eb245b8de89c8d631d175ae937136de54ea3ed51