diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 797a070..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index e9b8f62..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -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 19 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: 19 - cache: gradle - - name: Build with Gradle - run: ./gradlew build --no-daemon diff --git a/.github/workflows/validate-gradle-wrapper.yml b/.github/workflows/validate-gradle-wrapper.yml deleted file mode 100644 index b5f0b31..0000000 --- a/.github/workflows/validate-gradle-wrapper.yml +++ /dev/null @@ -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 diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 5a9ae89..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "pdfjs-dist"] - path = app/pdfjs-dist - url = https://github.com/mozilla/pdfjs-dist.git diff --git a/LICENSE b/LICENSE index 19f3d8d..4ee63d2 100644 --- a/LICENSE +++ b/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 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5595679..3d2ff8c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,6 +13,12 @@ plugins { id("kotlin-android") } +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + android { if (useKeystoreProperties) { signingConfigs { @@ -33,7 +39,7 @@ android { } compileSdk = 33 - buildToolsVersion = "33.0.0" + buildToolsVersion = "33.0.2" namespace = "app.grapheneos.pdfviewer" @@ -61,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.5.1") - implementation("com.google.android.material:material:1.7.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.9.0") } diff --git a/app/src/main/assets/viewer.css b/app/src/main/assets/viewer.css index 48bf80a..883b8c2 100644 --- a/app/src/main/assets/viewer.css +++ b/app/src/main/assets/viewer.css @@ -1,3 +1,8 @@ +:root { + --text-layer-opacity: 0.2; + --text-layer-foreground: transparent; +} + html, body { height: 100%; } @@ -12,6 +17,8 @@ body { } #container { + --scale-factor: 1; + width: 100%; height: 100%; display: grid; @@ -24,25 +31,26 @@ body { grid-column-start: 1; } -canvas, .textLayer { +canvas { display: inline-block; position: relative; } .textLayer { text-align: initial; + 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 { @@ -91,3 +99,13 @@ canvas, .textLayer { .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); +} diff --git a/app/src/main/assets/viewer.js b/app/src/main/assets/viewer.js index 34fc49d..074478f 100644 --- a/app/src/main/assets/viewer.js +++ b/app/src/main/assets/viewer.js @@ -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; @@ -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,6 +238,18 @@ 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 }); @@ -207,7 +259,7 @@ function loadDocument() { } else if (error === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) { channel.invalidPassword(); } - } + }; loadingTask.promise.then(function (newDoc) { channel.onLoaded(); @@ -223,3 +275,7 @@ function loadDocument() { console.error(reason.name + ": " + reason.message); }); } + +window.onresize = () => { + setLayerTransform(canvas.clientWidth, canvas.clientHeight, textLayerDiv); +}; diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index 2bcad8a..796acba 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -42,12 +42,14 @@ 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.DocumentPropertiesLoader; +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; @@ -55,7 +57,7 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks LoaderManager.getInstance(PdfViewer.this.activity).restartLoader(DocumentPropertiesLoader.ID, args, PdfViewer.this)); + activity.runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this.activity).restartLoader(DocumentPropertiesAsyncTaskLoader.ID, args, PdfViewer.this)); } @JavascriptInterface @@ -354,6 +371,15 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks> onCreateLoader(int id, Bundle args) { - return new DocumentPropertiesLoader(activity, args.getString(KEY_PROPERTIES), mNumPages, fileName, fileSize); + return new DocumentPropertiesAsyncTaskLoader(activity, args.getString(KEY_PROPERTIES), mNumPages, fileName, fileSize); } @Override public void onLoadFinished(@NonNull Loader> loader, List data) { mDocumentProperties = data; - LoaderManager.getInstance(activity).destroyLoader(DocumentPropertiesLoader.ID); + setToolbarTitleWithDocumentName(); + LoaderManager.getInstance(activity).destroyLoader(DocumentPropertiesAsyncTaskLoader.ID); } @Override @@ -485,12 +512,19 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks 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)); + 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); @@ -540,10 +574,29 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks 2 ? fileName : title; + } } diff --git a/app/src/main/java/app/grapheneos/pdfviewer/Utils.java b/app/src/main/java/app/grapheneos/pdfviewer/Utils.java index 3199b70..d7388a7 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/Utils.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/Utils.java @@ -9,21 +9,6 @@ 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 { diff --git a/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.java b/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.java deleted file mode 100644 index 47f039b..0000000 --- a/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.java +++ /dev/null @@ -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 mDocumentProperties; - - public static DocumentPropertiesFragment newInstance(final List metaData) { - final DocumentPropertiesFragment fragment = new DocumentPropertiesFragment(); - final Bundle args = new Bundle(); - - args.putCharSequenceArrayList(KEY_DOCUMENT_PROPERTIES, (ArrayList) 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(); - } -} diff --git a/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt b/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt new file mode 100644 index 0000000..49d175f --- /dev/null +++ b/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt @@ -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 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): DocumentPropertiesFragment { + val fragment = DocumentPropertiesFragment() + val args = Bundle() + args.putCharSequenceArrayList( + KEY_DOCUMENT_PROPERTIES, + metaData as ArrayList + ) + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.java b/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.java deleted file mode 100644 index 99ff01c..0000000 --- a/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.java +++ /dev/null @@ -1,62 +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; - PdfViewer pdfViewer; - - public static JumpToPageFragment newInstance(PdfViewer pdfViewer) { - JumpToPageFragment f = new JumpToPageFragment(); - f.pdfViewer = pdfViewer; - return f; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - mPicker = new NumberPicker(getActivity()); - mPicker.setMinValue(1); - mPicker.setMaxValue(pdfViewer.mNumPages); - mPicker.setValue(pdfViewer.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.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()); - } -} diff --git a/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.kt b/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.kt new file mode 100644 index 0000000..da7021b --- /dev/null +++ b/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.kt @@ -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) + } +} diff --git a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesAsyncTaskLoader.java b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesAsyncTaskLoader.java new file mode 100644 index 0000000..25050d5 --- /dev/null +++ b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesAsyncTaskLoader.java @@ -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> { + + 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 loadInBackground() { + + DocumentPropertiesLoader loader = new DocumentPropertiesLoader( + getContext(), + mProperties, + mNumPages, + fileName, + fileSize + ); + + return loader.loadAsList(); + } +} diff --git a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.java b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.java deleted file mode 100644 index cff47c9..0000000 --- a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.java +++ /dev/null @@ -1,115 +0,0 @@ -package app.grapheneos.pdfviewer.loader; - -import android.content.Context; -import android.graphics.Typeface; -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> { - 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 DocumentPropertiesLoader(Context context, String properties, int numPages, String fileName, Long fileSize) { - super(context); - - mProperties = properties; - mNumPages = numPages; - this.fileName = fileName; - this.fileSize = fileSize; - } - - @Override - public List loadInBackground() { - final Context context = getContext(); - - final String[] names = context.getResources().getStringArray(R.array.property_names); - final List properties = new ArrayList<>(names.length); - - properties.add(getProperty(null, names[0], fileName)); - properties.add(getProperty(null, names[1], Utils.parseFileSize(fileSize))); - - 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 properties) { - if (!isReset() && isStarted()) { - super.deliverResult(properties); - } - } - - @Override - protected void onStartLoading() { - forceLoad(); - } - - @Override - protected void onStopLoading() { - cancelLoad(); - } - - @Override - protected void onReset() { - super.onReset(); - - onStopLoading(); - } - - 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; - } -} diff --git a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt new file mode 100644 index 0000000..e2fdb7c --- /dev/null +++ b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt @@ -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 { + 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 { + val result = mutableMapOf() + result.addFileProperties() + result.addPageSizeProperty() + result.addPDFJsProperties() + return result + } + + private fun MutableMap.addPageSizeProperty() { + this[DocumentProperty.Pages] = java.lang.String.valueOf(numPages) + } + + private fun MutableMap.addFileProperties() { + putAll(getFileProperties()) + } + + private fun MutableMap.addPDFJsProperties() { + putAll(getPDFJsProperties()) + } + + private fun getPDFJsProperties(): Map { + 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 { + val collections = mutableMapOf() + collections[DocumentProperty.FileName] = fileName + collections[DocumentProperty.FileSize] = Formatter.formatFileSize(context, fileSize) + return collections + } +} diff --git a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentProperty.kt b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentProperty.kt new file mode 100644 index 0000000..bee1873 --- /dev/null +++ b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentProperty.kt @@ -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); +} diff --git a/app/src/main/java/app/grapheneos/pdfviewer/loader/PDFJsPropertiesToDocumentPropertyConverter.kt b/app/src/main/java/app/grapheneos/pdfviewer/loader/PDFJsPropertiesToDocumentPropertyConverter.kt new file mode 100644 index 0000000..3bf8137 --- /dev/null +++ b/app/src/main/java/app/grapheneos/pdfviewer/loader/PDFJsPropertiesToDocumentPropertyConverter.kt @@ -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 { + val result = mutableMapOf() + + val json = JSONObject(properties) + addJsonProperties(json, result) + return result + } + + private fun addJsonProperties( + json: JSONObject, + collections: MutableMap + ) { + 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 + } +} diff --git a/app/src/main/res/menu/pdf_viewer.xml b/app/src/main/res/menu/pdf_viewer.xml index 488ecf2..c086f9b 100644 --- a/app/src/main/res/menu/pdf_viewer.xml +++ b/app/src/main/res/menu/pdf_viewer.xml @@ -11,13 +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" /> + app:showAsAction="always" /> + + + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml deleted file mode 100644 index d9d1b85..0000000 --- a/app/src/main/res/values/arrays.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - File name - File size - Title - Author - Subject - Keywords - Creation date - Modify date - Producer - Creator - PDF version - Pages - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64cfcb4..c876c13 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,8 @@ Rotate counterclockwise Properties + Toggle text layer visibility + Invalid date Failed to obtain document metadata @@ -21,4 +23,17 @@ Enter the password to decrypt this PDF file Open Cancel + + File name + File size + Title + Author + Subject + Keywords + Creation date + Modify date + Producer + Creator + PDF version + Pages diff --git a/build.gradle.kts b/build.gradle.kts index 78e1149..93d1639 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,18 +1,6 @@ -buildscript { - repositories { - // dependabot cannot handle google() - maven { - url = uri("https://dl.google.com/dl/android/maven2") - } - // dependabot cannot handle mavenCentral() - maven { - url = uri("https://repo.maven.apache.org/maven2") - } - } - dependencies { - classpath("com.android.tools.build:gradle:7.3.1") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.22") - } +plugins { + id("com.android.application") apply false + id("org.jetbrains.kotlin.android") apply false } allprojects { diff --git a/gradle.properties b/gradle.properties index e6948d1..e696167 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 android.useAndroidX=true -android.enableR8.fullMode=true kotlin.code.style=official diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml new file mode 100644 index 0000000..d9be56a --- /dev/null +++ b/gradle/verification-metadata.xml @@ -0,0 +1,2179 @@ + + + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e583..033e24c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b916c04..a7c2bd1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-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 diff --git a/gradlew b/gradlew index a69d9cb..fcb6fca 100755 --- a/gradlew +++ b/gradlew @@ -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 diff --git a/gradlew.bat b/gradlew.bat index 53a6b23..6689b85 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% diff --git a/settings.gradle.kts b/settings.gradle.kts index cf78a48..c311f9a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,9 @@ +pluginManagement { + repositories { + google() + mavenCentral() + } +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories {