Compare commits

..

No commits in common. "libpdfviewer" and "b9da0073b761968a1db362dd8314bbd8dbbe27dc" have entirely different histories.

55 changed files with 760 additions and 3166 deletions

12
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
target-branch: main
- package-ecosystem: gradle
directory: "/"
schedule:
interval: daily
target-branch: main

20
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: Build application
on: [pull_request, push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: 17
cache: gradle
- name: Build with Gradle
run: ./gradlew build --no-daemon

View File

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

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

@ -1,4 +1,4 @@
Copyright © 2017-2023 GrapheneOS
Copyright © 2017-2022 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

View File

@ -1,6 +1,11 @@
Fork of GrapheneOS' [PdfViewer](https://github.com/GrapheneOS/PdfViewer) to work as a library.
## Warning !
The only goal of this library is to be integrated in [DroidFS](https://forge.chapril.org/hardcoresushi/DroidFS). Use it at your own risk !
The npm dependency has been removed. Instead, `pdf.min.js` and `pdf.worker.min.js` must be manually placed in `app/pdfjs-dist/build/`.
Simple Android PDF viewer based on pdf.js and content providers. The app
doesn't require any permissions. The PDF stream is fed into the sandboxed
WebView without giving it access to content or files. Content-Security-Policy
is used to enforce that the JavaScript and styling properties within the
WebView are entirely static content from the apk assets. It reuses the hardened
Chromium rendering stack while only exposing a tiny subset of the attack
surface compared to actual web content. The PDF rendering code itself is memory
safe with dynamic code evaluation disabled, and even if an attacker did gain
code execution by exploiting the underlying web rendering engine, they're
within the Chromium renderer sandbox with no access to the network (unlike a
browser), files, or other content.

View File

@ -9,16 +9,10 @@ if (useKeystoreProperties) {
}
plugins {
id("com.android.library")
id("com.android.application")
id("kotlin-android")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
android {
if (useKeystoreProperties) {
signingConfigs {
@ -38,19 +32,25 @@ android {
}
}
compileSdk = 33
buildToolsVersion = "33.0.2"
namespace = "app.grapheneos.pdfviewer"
compileSdk = 32
buildToolsVersion = "32.0.0"
defaultConfig {
minSdk = 21
targetSdk = 33
applicationId = "app.grapheneos.pdfviewer"
minSdk = 26
targetSdk = 31
versionCode = 12
versionName = versionCode.toString()
resourceConfigurations.add("en")
}
buildTypes {
getByName("debug") {
applicationIdSuffix = ".debug"
}
getByName("release") {
isShrinkResources = true
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
if (useKeystoreProperties) {
@ -60,6 +60,7 @@ android {
create("play") {
initWith(getByName("release"))
applicationIdSuffix = ".play"
if (useKeystoreProperties) {
signingConfig = signingConfigs.getByName("play")
}
@ -67,21 +68,20 @@ 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()
}
}
dependencies {
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.9.0")
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("com.google.android.material:material:1.5.0")
}

View File

@ -1,8 +1,7 @@
<lint>
<!-- overly aggressive check -->
<issue id="VectorPath">
<ignore path="src/main/res/drawable/ic_rotate_left_24dp.xml"/>
<ignore path="src/main/res/drawable/ic_rotate_right_24dp.xml"/>
<!-- full backups are desired -->
<issue id="AllowBackup">
<ignore path="src/main/AndroidManifest.xml"/>
</issue>
<!-- Google app indexing doesn't make any sense for this app -->

View File

@ -1,9 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.grapheneos.pdfviewer"
android:targetSandboxVersion="2">
<original-package android:name="org.grapheneos.pdfviewer" />
<application>
<application android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:allowBackup="true">
<activity android:name=".PdfViewer"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="application/pdf" />
</intent-filter>
</activity>
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"

View File

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

View File

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

View File

@ -1,56 +1,36 @@
:root {
--text-layer-opacity: 0.2;
--text-layer-foreground: transparent;
}
html, body {
height: 100%;
}
body, canvas {
padding: 0;
margin: 0;
}
canvas {
margin: auto;
display: block;
}
body {
background-color: #c0c0c0;
}
#container {
--scale-factor: 1;
width: 100%;
height: 100%;
display: grid;
place-items: center;
}
#container canvas, #container .textLayer {
/* overlay child elements on top of each other */
grid-row-start: 1;
grid-column-start: 1;
}
canvas {
display: inline-block;
position: relative;
}
.textLayer {
text-align: initial;
position: absolute;
text-align: initial;
left: 0;
top: 0;
right: 0;
bottom: 0;
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 +79,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

@ -7,10 +7,8 @@
<script src="pdf.js"></script>
</head>
<body>
<div id="container">
<canvas id="content"></canvas>
<div id="text" class="textLayer"></div>
</div>
<canvas id="content"></canvas>
<div id="text" class="textLayer"></div>
<script src="viewer.js"></script>
</body>
</html>

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,21 +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 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;
@ -91,7 +73,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
for (let i = 0; i < cache.length; i++) {
const cached = cache[i];
if (cached.pageNumber === pageNumber && cached.zoomRatio === newZoomRatio &&
cached.orientationDegrees === orientationDegrees) {
cache.orientationDegrees === orientationDegrees) {
if (useRender) {
cache.splice(i, 1);
cache.push(cached);
@ -100,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;
@ -115,15 +95,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
return;
}
const defaultZoomRatio = getDefaultZoomRatio(page, orientationDegrees);
if (cache.length === 0) {
zoomRatio = defaultZoomRatio;
newZoomRatio = defaultZoomRatio;
channel.setZoomRatio(defaultZoomRatio);
}
const viewport = page.getViewport({scale: newZoomRatio, rotation: orientationDegrees});
const viewport = page.getViewport({scale: newZoomRatio, rotation: orientationDegrees})
if (useRender) {
if (newZoomRatio !== zoomRatio) {
@ -133,7 +105,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
zoomRatio = newZoomRatio;
}
if (zoom === 2) {
if (zoom == 2) {
pageRendering = false;
return;
}
@ -165,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() {
@ -176,36 +148,24 @@ 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();
cache.shift()
}
cache.push({
pageNumber: pageNumber,
zoomRatio: newZoomRatio,
orientationDegrees: orientationDegrees,
canvas: newCanvas,
textLayerDiv: newTextLayerDiv,
pageWidth: viewport.width,
pageHeight: viewport.height
textLayerDiv: newTextLayerDiv
});
pageRendering = false;
@ -238,44 +198,15 @@ 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 });
loadingTask.onPassword = (_, error) => {
if (error === pdfjsLib.PasswordResponses.NEED_PASSWORD) {
channel.showPasswordPrompt();
} else if (error === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {
channel.invalidPassword();
}
};
loadingTask.promise.then(function (newDoc) {
channel.onLoaded();
pdfDoc = newDoc;
channel.setNumPages(pdfDoc.numPages);
pdfDoc.getMetadata().then(function (data) {
channel.setDocumentProperties(JSON.stringify(data.info));
}).catch(function (error) {
console.log("getMetadata error: " + error);
});
renderPage(channel.getPage(), false, false);
}, function (reason) {
console.error(reason.name + ": " + reason.message);
pdfjsLib.getDocument("https://localhost/placeholder.pdf").promise.then(function(newDoc) {
pdfDoc = newDoc;
channel.setNumPages(pdfDoc.numPages);
pdfDoc.getMetadata().then(function(data) {
channel.setDocumentProperties(JSON.stringify(data.info));
}).catch(function(error) {
console.log("getMetadata error: " + error);
});
}
window.onresize = () => {
setLayerTransform(canvas.clientWidth, canvas.clientHeight, textLayerDiv);
};
renderPage(channel.getPage(), false, false);
}).catch(function(error) {
console.log("getDocument error: " + error);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,10 +1,12 @@
package app.grapheneos.pdfviewer;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
@ -24,14 +26,11 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
@ -39,25 +38,23 @@ import com.google.android.material.snackbar.Snackbar;
import app.grapheneos.pdfviewer.databinding.PdfviewerBinding;
import app.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment;
import app.grapheneos.pdfviewer.fragment.PasswordPromptFragment;
import app.grapheneos.pdfviewer.fragment.JumpToPageFragment;
import app.grapheneos.pdfviewer.ktx.ViewKt;
import app.grapheneos.pdfviewer.loader.DocumentPropertiesAsyncTaskLoader;
import app.grapheneos.pdfviewer.viewModel.PasswordStatus;
import app.grapheneos.pdfviewer.loader.DocumentPropertiesLoader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequence>> {
public class PdfViewer extends AppCompatActivity implements LoaderManager.LoaderCallbacks<List<CharSequence>> {
public static final String TAG = "PdfViewer";
private static final String STATE_URI = "uri";
private static final String STATE_PAGE = "page";
private static final String STATE_ZOOM_RATIO = "zoomRatio";
private static final String STATE_DOCUMENT_ORIENTATION_DEGREES = "documentOrientationDegrees";
private static final String KEY_PROPERTIES = "properties";
private static final int MIN_WEBVIEW_RELEASE = 92;
private static final int MIN_WEBVIEW_RELEASE = 89;
private static final String CONTENT_SECURITY_POLICY =
"default-src 'none'; " +
@ -81,7 +78,6 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
"document-domain=(), " +
"encrypted-media=(), " +
"fullscreen=(), " +
"gamepad=(), " +
"geolocation=(), " +
"gyroscope=(), " +
"hid=(), " +
@ -95,37 +91,32 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
"publickey-credentials-get=(), " +
"screen-wake-lock=(), " +
"serial=(), " +
"speaker-selection=(), " +
"sync-xhr=(), " +
"usb=(), " +
"xr-spatial-tracking=()";
private static final float MIN_ZOOM_RATIO = 0.2f;
private static final float MIN_ZOOM_RATIO = 0.5f;
private static final float MAX_ZOOM_RATIO = 1.5f;
private static final int ALPHA_LOW = 130;
private static final int ALPHA_HIGH = 255;
private static final int ACTION_OPEN_DOCUMENT_REQUEST_CODE = 1;
private static final int STATE_LOADED = 1;
private static final int STATE_END = 2;
private static final int PADDING = 10;
private Uri mUri;
public int mPage;
public int mNumPages;
private float mZoomRatio = 1f;
private int mDocumentOrientationDegrees;
private int mDocumentState;
private String mEncryptedDocumentPassword;
private List<CharSequence> mDocumentProperties;
private ByteArrayInputStream mInputStream;
private InputStream mInputStream;
private PdfviewerBinding binding;
private TextView mTextView;
private Toast mToast;
AppCompatActivity activity;
String fileName;
Long fileSize;
private PasswordPromptFragment mPasswordPromptFragment;
public PasswordStatus passwordValidationViewModel;
private Snackbar snackbar;
private class Channel {
@JavascriptInterface
@ -138,21 +129,6 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
return mZoomRatio;
}
@JavascriptInterface
public void setZoomRatio(final float ratio) {
mZoomRatio = Math.max(Math.min(ratio, MAX_ZOOM_RATIO), MIN_ZOOM_RATIO);
}
@JavascriptInterface
public float getMinZoomRatio() {
return MIN_ZOOM_RATIO;
}
@JavascriptInterface
public float getMaxZoomRatio() {
return MAX_ZOOM_RATIO;
}
@JavascriptInterface
public int getDocumentOrientationDegrees() {
return mDocumentOrientationDegrees;
@ -161,7 +137,7 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
@JavascriptInterface
public void setNumPages(int numPages) {
mNumPages = numPages;
activity.runOnUiThread(activity::invalidateOptionsMenu);
runOnUiThread(PdfViewer.this::invalidateOptionsMenu);
}
@JavascriptInterface
@ -172,44 +148,19 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
final Bundle args = new Bundle();
args.putString(KEY_PROPERTIES, properties);
activity.runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this.activity).restartLoader(DocumentPropertiesAsyncTaskLoader.ID, args, PdfViewer.this));
}
@JavascriptInterface
public void showPasswordPrompt() {
if (!getPasswordPromptFragment().isAdded()) {
getPasswordPromptFragment().show(activity.getSupportFragmentManager(), PasswordPromptFragment.class.getName());
}
passwordValidationViewModel.passwordMissing();
}
@JavascriptInterface
public void invalidPassword() {
activity.runOnUiThread(() -> passwordValidationViewModel.invalid());
}
@JavascriptInterface
public void onLoaded() {
passwordValidationViewModel.validated();
if (getPasswordPromptFragment().isAdded()) {
getPasswordPromptFragment().dismiss();
}
}
@JavascriptInterface
public String getPassword() {
return mEncryptedDocumentPassword != null ? mEncryptedDocumentPassword : "";
runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesLoader.ID, args, PdfViewer.this));
}
}
public PdfViewer(@NonNull AppCompatActivity activity) {
this.activity = activity;
binding = PdfviewerBinding.inflate(activity.getLayoutInflater());
activity.setContentView(binding.getRoot());
activity.setSupportActionBar(binding.toolbar);
passwordValidationViewModel = new ViewModelProvider(activity, ViewModelProvider.AndroidViewModelFactory.getInstance(activity.getApplication())).get(PasswordStatus.class);
@Override
@SuppressLint({"SetJavaScriptEnabled", "ClickableViewAccessibility"})
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = PdfviewerBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
WindowCompat.setDecorFitsSystemWindows(activity.getWindow(), false);
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
// Margins for the toolbar are needed, so that content of the toolbar
// is not covered by a system button navigation bar when in landscape.
@ -233,7 +184,6 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
settings.setAllowFileAccess(false);
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
settings.setJavaScriptEnabled(true);
settings.setMinimumFontSize(1);
CookieManager.getInstance().setAcceptCookie(false);
@ -242,7 +192,7 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
binding.webview.setWebViewClient(new WebViewClient() {
private WebResourceResponse fromAsset(final String mime, final String path) {
try {
InputStream inputStream = activity.getAssets().open(path.substring(1));
InputStream inputStream = getAssets().open(path.substring(1));
return new WebResourceResponse(mime, null, inputStream);
} catch (IOException e) {
return null;
@ -264,7 +214,6 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
Log.d(TAG, "path " + path);
if ("/placeholder.pdf".equals(path)) {
mInputStream.reset();
return new WebResourceResponse("application/pdf", null, mInputStream);
}
@ -297,25 +246,28 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
@Override
public void onPageFinished(WebView view, String url) {
mDocumentState = STATE_LOADED;
activity.invalidateOptionsMenu();
loadPdfWithPassword(mEncryptedDocumentPassword);
invalidateOptionsMenu();
}
});
GestureHelper.attach(activity, binding.webview,
GestureHelper.attach(PdfViewer.this, binding.webview,
new GestureHelper.GestureListener() {
@Override
public boolean onTapUp() {
binding.webview.evaluateJavascript("isTextSelected()", selection -> {
if (!Boolean.parseBoolean(selection)) {
if (activity.getSupportActionBar().isShowing()) {
hideSystemUi();
} else {
showSystemUi();
if (mUri != null) {
binding.webview.evaluateJavascript("isTextSelected()", selection -> {
if (!Boolean.parseBoolean(selection)) {
if ((getWindow().getDecorView().getSystemUiVisibility() &
View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
hideSystemUi();
} else {
showSystemUi();
}
}
}
});
return true;
});
return true;
}
return false;
}
@Override
@ -334,68 +286,62 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
}
});
mTextView = new TextView(activity);
mTextView = new TextView(this);
mTextView.setBackgroundColor(Color.DKGRAY);
mTextView.setTextColor(ColorStateList.valueOf(Color.WHITE));
mTextView.setTextSize(18);
mTextView.setPadding(PADDING, 0, PADDING, 0);
}
public void onDestroy() {
binding.webview.removeJavascriptInterface("channel");
binding.getRoot().removeView(binding.webview);
binding.webview.destroy();
maybeCloseInputStream();
}
// If loaders are not being initialized in onCreate(), the result will not be delivered
// after orientation change (See FragmentHostCallback), thus initialize the
// loader manager impl so that the result will be delivered.
LoaderManager.getInstance(this);
void maybeCloseInputStream() {
InputStream stream = mInputStream;
if (stream == null) {
return;
}
mInputStream = null;
try {
stream.close();
} catch (IOException ignored) {}
}
snackbar = Snackbar.make(binding.webview, "", Snackbar.LENGTH_LONG);
private PasswordPromptFragment getPasswordPromptFragment() {
if (mPasswordPromptFragment == null) {
final Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag(PasswordPromptFragment.class.getName());
if (fragment != null) {
mPasswordPromptFragment = (PasswordPromptFragment) fragment;
} else {
mPasswordPromptFragment = new PasswordPromptFragment(this);
final Intent intent = getIntent();
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
if (!"application/pdf".equals(intent.getType())) {
snackbar.setText(R.string.invalid_mime_type).show();
return;
}
mUri = intent.getData();
mPage = 1;
}
return mPasswordPromptFragment;
}
private void setToolbarTitleWithDocumentName() {
String documentName = getCurrentDocumentName();
if (documentName != null && !documentName.isEmpty()) {
activity.getSupportActionBar().setTitle(documentName);
} else {
activity.getSupportActionBar().setTitle(R.string.app_name);
if (savedInstanceState != null) {
mUri = savedInstanceState.getParcelable(STATE_URI);
mPage = savedInstanceState.getInt(STATE_PAGE);
mZoomRatio = savedInstanceState.getFloat(STATE_ZOOM_RATIO);
mDocumentOrientationDegrees = savedInstanceState.getInt(STATE_DOCUMENT_ORIENTATION_DEGREES);
}
if (mUri != null) {
if ("file".equals(mUri.getScheme())) {
snackbar.setText(R.string.legacy_file_uri).show();
return;
}
loadPdf();
}
}
public void onResume() {
@Override
protected void onResume() {
super.onResume();
// The user could have left the activity to update the WebView
activity.invalidateOptionsMenu();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (getWebViewRelease() >= MIN_WEBVIEW_RELEASE) {
binding.webviewOutOfDateLayout.setVisibility(View.GONE);
binding.webview.setVisibility(View.VISIBLE);
} else {
binding.webview.setVisibility(View.GONE);
binding.webviewOutOfDateMessage.setText(activity.getString(R.string.webview_out_of_date_message, getWebViewRelease(), MIN_WEBVIEW_RELEASE));
binding.webviewOutOfDateLayout.setVisibility(View.VISIBLE);
}
invalidateOptionsMenu();
if (getWebViewRelease() >= MIN_WEBVIEW_RELEASE) {
binding.webviewOutOfDateLayout.setVisibility(View.GONE);
binding.webview.setVisibility(View.VISIBLE);
} else {
binding.webview.setVisibility(View.GONE);
binding.webviewOutOfDateMessage.setText(getString(R.string.webview_out_of_date_message, getWebViewRelease(), MIN_WEBVIEW_RELEASE));
binding.webviewOutOfDateLayout.setVisibility(View.VISIBLE);
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private int getWebViewRelease() {
PackageInfo webViewPackage = WebView.getCurrentWebViewPackage();
String webViewVersionName = webViewPackage.versionName;
@ -405,14 +351,13 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
@NonNull
@Override
public Loader<List<CharSequence>> onCreateLoader(int id, Bundle args) {
return new DocumentPropertiesAsyncTaskLoader(activity, args.getString(KEY_PROPERTIES), mNumPages, fileName, fileSize);
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(activity).destroyLoader(DocumentPropertiesAsyncTaskLoader.ID);
LoaderManager.getInstance(this).destroyLoader(DocumentPropertiesLoader.ID);
}
@Override
@ -420,20 +365,19 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
mDocumentProperties = null;
}
public void loadPdf(ByteArrayInputStream inputStream, String fileName, Long fileSize) {
mPage = 1;
mDocumentProperties = null;
mInputStream = inputStream;
this.fileName = fileName;
this.fileSize = fileSize;
showSystemUi();
activity.invalidateOptionsMenu();
binding.webview.loadUrl("https://localhost/viewer.html");
}
private void loadPdf() {
try {
if (mInputStream != null) {
mInputStream.close();
}
mInputStream = getContentResolver().openInputStream(mUri);
} catch (IOException e) {
snackbar.setText(R.string.io_error).show();
return;
}
public void loadPdfWithPassword(final String password) {
mEncryptedDocumentPassword = password;
binding.webview.evaluateJavascript("loadDocument()", null);
showSystemUi();
binding.webview.loadUrl("https://localhost/viewer.html");
}
private void renderPage(final int zoom) {
@ -448,11 +392,18 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
renderPage(0);
}
private void openDocument() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/pdf");
startActivityForResult(intent, ACTION_OPEN_DOCUMENT_REQUEST_CODE);
}
private void zoomIn(float value, boolean end) {
if (mZoomRatio < MAX_ZOOM_RATIO) {
mZoomRatio = Math.min(mZoomRatio + value, MAX_ZOOM_RATIO);
renderPage(end ? 1 : 2);
activity.invalidateOptionsMenu();
invalidateOptionsMenu();
}
}
@ -460,7 +411,7 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
if (mZoomRatio > MIN_ZOOM_RATIO) {
mZoomRatio = Math.max(mZoomRatio - value, MIN_ZOOM_RATIO);
renderPage(end ? 1 : 2);
activity.invalidateOptionsMenu();
invalidateOptionsMenu();
}
}
@ -483,18 +434,51 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
mPage = selected_page;
renderPage(0);
showPageNumber();
activity.invalidateOptionsMenu();
invalidateOptionsMenu();
}
}
private void showSystemUi() {
ViewKt.showSystemUi(binding.getRoot(), activity.getWindow());
activity.getSupportActionBar().show();
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
getSupportActionBar().show();
}
private void hideSystemUi() {
ViewKt.hideSystemUi(binding.getRoot(), activity.getWindow());
activity.getSupportActionBar().hide();
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_IMMERSIVE);
getSupportActionBar().hide();
}
@Override
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
savedInstanceState.putParcelable(STATE_URI, mUri);
savedInstanceState.putInt(STATE_PAGE, mPage);
savedInstanceState.putFloat(STATE_ZOOM_RATIO, mZoomRatio);
savedInstanceState.putInt(STATE_DOCUMENT_ORIENTATION_DEGREES, mDocumentOrientationDegrees);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == ACTION_OPEN_DOCUMENT_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
if (resultData != null) {
mUri = resultData.getData();
mPage = 1;
mDocumentProperties = null;
loadPdf();
invalidateOptionsMenu();
}
}
}
private void showPageNumber() {
@ -502,29 +486,27 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
mToast.cancel();
}
mTextView.setText(String.format("%s/%s", mPage, mNumPages));
mToast = new Toast(activity);
mToast = new Toast(getApplicationContext());
mToast.setGravity(Gravity.BOTTOM | Gravity.END, PADDING, PADDING);
mToast.setDuration(Toast.LENGTH_SHORT);
mToast.setView(mTextView);
mToast.show();
}
public void onCreateOptionMenu(@NonNull Menu menu) {
MenuInflater inflater = activity.getMenuInflater();
@Override
public boolean onCreateOptionsMenu(Menu menu) {
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;
}
public boolean onPrepareOptionsMenu(@NonNull Menu menu) {
final ArrayList<Integer> ids = new ArrayList<>(Arrays.asList(R.id.action_jump_to_page,
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
final int[] ids = { R.id.action_zoom_in, R.id.action_zoom_out, R.id.action_jump_to_page,
R.id.action_next, R.id.action_previous, R.id.action_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);
}
R.id.action_view_document_properties };
if (mDocumentState < STATE_LOADED) {
for (final int id : ids) {
final MenuItem item = menu.findItem(id);
@ -542,12 +524,16 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
mDocumentState = STATE_END;
}
enableDisableMenuItem(menu.findItem(R.id.action_open), getWebViewRelease() >= MIN_WEBVIEW_RELEASE);
enableDisableMenuItem(menu.findItem(R.id.action_zoom_in), mZoomRatio != MAX_ZOOM_RATIO);
enableDisableMenuItem(menu.findItem(R.id.action_zoom_out), mZoomRatio != MIN_ZOOM_RATIO);
enableDisableMenuItem(menu.findItem(R.id.action_next), mPage < mNumPages);
enableDisableMenuItem(menu.findItem(R.id.action_previous), mPage > 1);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.action_previous) {
@ -562,6 +548,15 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
} else if (itemId == R.id.action_last) {
onJumpToPageInDocument(mNumPages);
return true;
} else if (itemId == R.id.action_open) {
openDocument();
return true;
} else if (itemId == R.id.action_zoom_out) {
zoomOut(0.25f, true);
return true;
} else if (itemId == R.id.action_zoom_in) {
zoomIn(0.25f, true);
return true;
} else if (itemId == R.id.action_rotate_clockwise) {
documentOrientationChanged(90);
return true;
@ -571,32 +566,14 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
} else if (itemId == R.id.action_view_document_properties) {
DocumentPropertiesFragment
.newInstance(mDocumentProperties)
.show(activity.getSupportFragmentManager(), DocumentPropertiesFragment.TAG);
.show(getSupportFragmentManager(), DocumentPropertiesFragment.TAG);
return true;
} else if (itemId == R.id.action_jump_to_page) {
new JumpToPageFragment(this)
.show(activity.getSupportFragmentManager(), JumpToPageFragment.TAG);
return true;
} else if (itemId == R.id.debug_action_toggle_text_layer_visibility) {
binding.webview.evaluateJavascript("toggleTextLayerVisibility()", null);
new JumpToPageFragment()
.show(getSupportFragmentManager(), JumpToPageFragment.TAG);
return true;
}
return false;
}
private String getCurrentDocumentName() {
if (mDocumentProperties == null || mDocumentProperties.isEmpty()) return "";
String fileName = "";
String title = "";
for (CharSequence property : mDocumentProperties) {
if (property.toString().startsWith("File name:")) {
fileName = property.toString().replace("File name:", "");
}
if (property.toString().startsWith("Title:-")) {
title = property.toString().replace("Title:-", "");
}
}
return fileName.length() > 2 ? fileName : title;
return super.onOptionsItemSelected(item);
}
}

View File

@ -9,13 +9,20 @@ import java.text.ParseException;
import java.util.Calendar;
public class Utils {
public static String parseFileSize(long fileSize) {
final double kb = fileSize / 1000d;
private static int parseIntSafely(String field) throws ParseException {
try {
return Integer.parseInt(field);
} catch (NumberFormatException e) {
throw new ParseException("Error while parsing int", -1);
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)";
}
// Parse date as per PDF spec (complies with PDF v1.4 to v1.7)
@ -32,13 +39,14 @@ public class Utils {
final Calendar calendar = Calendar.getInstance();
final int currentYear = calendar.get(Calendar.YEAR);
int year;
// Year is required
String field = date.substring(position += 2, 6);
if (!TextUtils.isDigitsOnly(field)) {
throw new ParseException("Invalid year", position);
}
int year = parseIntSafely(field);
year = Integer.parseInt(field);
if (year > currentYear) {
year = currentYear;
}
@ -59,7 +67,7 @@ public class Utils {
if (!TextUtils.isDigitsOnly(field)) {
throw new ParseException("Invalid month", position);
}
month = parseIntSafely(field) - 1;
month = Integer.parseInt(field) - 1;
if (month > 11) {
throw new ParseException("Invalid month", position);
}
@ -70,7 +78,7 @@ public class Utils {
if (!TextUtils.isDigitsOnly(field)) {
throw new ParseException("Invalid day", position);
}
day = parseIntSafely(field);
day = Integer.parseInt(field);
if (day > 31) {
throw new ParseException("Invalid day", position);
}
@ -81,7 +89,7 @@ public class Utils {
if (!TextUtils.isDigitsOnly(field)) {
throw new ParseException("Invalid hours", position);
}
hours = parseIntSafely(field);
hours = Integer.parseInt(field);
if (hours > 23) {
throw new ParseException("Invalid hours", position);
}
@ -92,7 +100,7 @@ public class Utils {
if (!TextUtils.isDigitsOnly(field)) {
throw new ParseException("Invalid minutes", position);
}
minutes = parseIntSafely(field);
minutes = Integer.parseInt(field);
if (minutes > 59) {
throw new ParseException("Invalid minutes", position);
}
@ -103,7 +111,7 @@ public class Utils {
if (!TextUtils.isDigitsOnly(field)) {
throw new ParseException("Invalid seconds", position);
}
seconds = parseIntSafely(field);
seconds = Integer.parseInt(field);
if (seconds > 59) {
throw new ParseException("Invalid seconds", position);
}
@ -127,7 +135,7 @@ public class Utils {
if (!TextUtils.isDigitsOnly(field)) {
throw new ParseException("Invalid UTC offset hours", position);
}
offsetHours = parseIntSafely(field);
offsetHours = Integer.parseInt(field);
final int offsetHoursMinutes = offsetHours * 100 + offsetMinutes;
// Validate UTC offset (UTC-12:00 to UTC+14:00)
@ -150,7 +158,7 @@ public class Utils {
if (!TextUtils.isDigitsOnly(field)) {
throw new ParseException("Invalid UTC offset minutes", position);
}
offsetMinutes = parseIntSafely(field);
offsetMinutes = Integer.parseInt(field);
if (offsetMinutes > 59) {
throw new ParseException("Invalid UTC offset minutes", position);
}

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,65 @@
package app.grapheneos.pdfviewer.fragment;
import android.app.Dialog;
import android.os.Bundle;
import android.view.Gravity;
import android.widget.FrameLayout;
import android.widget.NumberPicker;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import app.grapheneos.pdfviewer.PdfViewer;
public class JumpToPageFragment extends DialogFragment {
public static final String TAG = "JumpToPageFragment";
private final static String STATE_PICKER_CUR = "picker_cur";
private final static String STATE_PICKER_MIN = "picker_min";
private final static String STATE_PICKER_MAX = "picker_max";
private NumberPicker mPicker;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
mPicker.setMinValue(savedInstanceState.getInt(STATE_PICKER_MIN));
mPicker.setMaxValue(savedInstanceState.getInt(STATE_PICKER_MAX));
mPicker.setValue(savedInstanceState.getInt(STATE_PICKER_CUR));
}
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
mPicker = new NumberPicker(getActivity());
mPicker.setMinValue(1);
mPicker.setMaxValue(((PdfViewer)requireActivity()).mNumPages);
mPicker.setValue(((PdfViewer)requireActivity()).mPage);
final FrameLayout layout = new FrameLayout(getActivity());
layout.addView(mPicker, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER));
return new MaterialAlertDialogBuilder(requireActivity())
.setView(layout)
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
mPicker.clearFocus();
((PdfViewer)requireActivity()).onJumpToPageInDocument(mPicker.getValue());
})
.setNegativeButton(android.R.string.cancel, null)
.create();
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putInt(STATE_PICKER_MIN, mPicker.getMinValue());
outState.putInt(STATE_PICKER_MAX, mPicker.getMaxValue());
outState.putInt(STATE_PICKER_CUR, mPicker.getValue());
}
}

View File

@ -1,58 +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(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)
}
}

View File

@ -1,103 +0,0 @@
package app.grapheneos.pdfviewer.fragment
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.WindowManager
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import app.grapheneos.pdfviewer.PdfViewer
import app.grapheneos.pdfviewer.R
import app.grapheneos.pdfviewer.databinding.PasswordDialogFragmentBinding
import app.grapheneos.pdfviewer.viewModel.PasswordStatus
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
class PasswordPromptFragment(private val pdfViewer: PdfViewer) : DialogFragment() {
private lateinit var passwordLayout : TextInputLayout
private lateinit var passwordEditText : TextInputEditText
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val passwordPrompt = MaterialAlertDialogBuilder(requireContext())
val passwordDialogFragmentBinding =
PasswordDialogFragmentBinding.inflate(LayoutInflater.from(requireContext()))
passwordLayout = passwordDialogFragmentBinding.pdfPasswordTextInputLayout
passwordEditText = passwordDialogFragmentBinding.pdfPasswordEditText
passwordPrompt.setView(passwordDialogFragmentBinding.root)
passwordEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(input: CharSequence, i: Int, i1: Int, i2: Int) {
updatePositiveButton()
}
override fun afterTextChanged(editable: Editable) {}
})
passwordEditText.setOnEditorActionListener { _, actionId, _ ->
if (actionId != IME_ACTION_DONE) return@setOnEditorActionListener false
sendPassword()
true
}
passwordPrompt.setPositiveButton(R.string.open, null)
passwordPrompt.setNegativeButton(R.string.cancel, null)
val dialog = passwordPrompt.create()
passwordPrompt.setCancelable(false)
isCancelable = false
dialog.setCanceledOnTouchOutside(false)
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
pdfViewer.passwordValidationViewModel.status.observe(
this
) {
when (it) {
PasswordStatus.Status.MissingPassword -> {
passwordEditText.editableText.clear()
passwordDialogFragmentBinding.title.setText(R.string.password_prompt_description)
}
PasswordStatus.Status.InvalidPassword -> {
passwordEditText.editableText.clear()
passwordDialogFragmentBinding.pdfPasswordTextInputLayout.error =
"invalid password"
}
PasswordStatus.Status.Validated -> {
//Activity will dismiss the dialog
}
else -> {
throw NullPointerException("status shouldn't be null")
}
}
}
return dialog
}
private fun updatePositiveButton() {
passwordLayout.error = ""
val btn = (dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
btn.isEnabled = passwordEditText.text?.isNotEmpty() ?: false
}
private fun sendPassword() {
val password = passwordEditText.text.toString()
if (!TextUtils.isEmpty(password)) {
pdfViewer.loadPdfWithPassword(password)
}
}
override fun onStart() {
super.onStart()
updatePositiveButton()
passwordEditText.requestFocus()
}
override fun onResume() {
super.onResume()
(dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
sendPassword()
}
}
}

View File

@ -1,20 +0,0 @@
package app.grapheneos.pdfviewer.ktx
import android.view.View
import android.view.Window
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
private val systemBars = WindowInsetsCompat.Type.statusBars()
fun View.hideSystemUi(window: Window) {
val controller = WindowCompat.getInsetsController(window, this)
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
controller.hide(systemBars)
}
fun View.showSystemUi(window: Window) {
WindowCompat.getInsetsController(window, this).show(systemBars)
}

View File

@ -1,51 +0,0 @@
package app.grapheneos.pdfviewer.loader;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.loader.content.AsyncTaskLoader;
import java.util.List;
public class DocumentPropertiesAsyncTaskLoader extends AsyncTaskLoader<List<CharSequence>> {
public static final String TAG = "DocumentPropertiesLoader";
public static final int ID = 1;
private final String mProperties;
private final int mNumPages;
String fileName;
Long fileSize;
public DocumentPropertiesAsyncTaskLoader(Context context, String properties, int numPages, String fileName, Long fileSize) {
super(context);
mProperties = properties;
mNumPages = numPages;
this.fileName = fileName;
this.fileSize = fileSize;
}
@Override
protected void onStartLoading() {
forceLoad();
}
@Nullable
@Override
public List<CharSequence> loadInBackground() {
DocumentPropertiesLoader loader = new DocumentPropertiesLoader(
getContext(),
mProperties,
mNumPages,
fileName,
fileSize
);
return loader.loadAsList();
}
}

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.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,88 +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.format.Formatter
import android.text.style.StyleSpan
import android.util.Log
import app.grapheneos.pdfviewer.R
import org.json.JSONException
class DocumentPropertiesLoader(
private val context: Context,
private val properties: String,
private val numPages: Int,
private val fileName: String,
private val fileSize: Long,
) {
fun loadAsList(): List<CharSequence> {
return load().map { item ->
val name = context.getString(item.key.nameResource)
val value = item.value
SpannableStringBuilder()
.append(name)
.append(":\n")
.append(value)
.apply {
setSpan(
StyleSpan(Typeface.BOLD),
0,
name.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
private fun load(): Map<DocumentProperty, String> {
val result = mutableMapOf<DocumentProperty, String>()
result.addFileProperties()
result.addPageSizeProperty()
result.addPDFJsProperties()
return result
}
private fun MutableMap<DocumentProperty, String>.addPageSizeProperty() {
this[DocumentProperty.Pages] = java.lang.String.valueOf(numPages)
}
private fun MutableMap<DocumentProperty, String>.addFileProperties() {
putAll(getFileProperties())
}
private fun MutableMap<DocumentProperty, String>.addPDFJsProperties() {
putAll(getPDFJsProperties())
}
private fun getPDFJsProperties(): Map<DocumentProperty, String> {
return try {
PDFJsPropertiesToDocumentPropertyConverter(
properties,
context.getString(R.string.document_properties_invalid_date),
parseExceptionListener = { parseException, value ->
Log.w(
DocumentPropertiesAsyncTaskLoader.TAG,
"${parseException.message} for $value at offset: ${parseException.errorOffset}"
)
}
).convert()
} catch (e: JSONException) {
Log.w(
DocumentPropertiesAsyncTaskLoader.TAG,
"invalid properties"
)
emptyMap()
}
}
private fun getFileProperties(): Map<DocumentProperty, String> {
val collections = mutableMapOf<DocumentProperty, String>()
collections[DocumentProperty.FileName] = fileName
collections[DocumentProperty.FileSize] = Formatter.formatFileSize(context, fileSize)
return collections
}
}

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,27 +0,0 @@
package app.grapheneos.pdfviewer.viewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class PasswordStatus : ViewModel() {
enum class Status {
MissingPassword,
InvalidPassword,
Validated
}
val status: MutableLiveData<Status> = MutableLiveData(Status.MissingPassword)
fun passwordMissing() {
status.postValue(Status.MissingPassword)
}
fun invalid() {
status.postValue(Status.InvalidPassword)
}
fun validated() {
status.postValue(Status.Validated)
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8.83c0,-0.53 -0.21,-1.04 -0.59,-1.41l-4.83,-4.83c-0.37,-0.38 -0.88,-0.59 -1.41,-0.59L6,2zM13,8L13,3.5L18.5,9L14,9c-0.55,0 -1,-0.45 -1,-1z" />
</vector>

View File

@ -0,0 +1,30 @@
<!--
Copyright (C) 2021 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="2.43375"
android:scaleY="2.43375"
android:translateX="24.795"
android:translateY="24.795">
<path
android:fillColor="#000000"
android:pathData="M18,4L6,4C4.9,4 4,4.9 4,6v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,6C20,4.9 19.1,4 18,4ZM9.5,11.5C9.5,12.33 8.83,13 8,13L7,13v1.25C7,14.66 6.66,15 6.25,15 5.84,15 5.5,14.66 5.5,14.25L5.5,10c0,-0.55 0.45,-1 1,-1L8,9c0.83,0 1.5,0.67 1.5,1.5zM14.5,13.5c0,0.83 -0.67,1.5 -1.5,1.5h-2c-0.28,0 -0.5,-0.22 -0.5,-0.5v-5C10.5,9.22 10.72,9 11,9h2c0.83,0 1.5,0.67 1.5,1.5zM18.5,9.75c0,0.41 -0.34,0.75 -0.75,0.75L17,10.5v1h0.75c0.41,0 0.75,0.34 0.75,0.75 0,0.41 -0.34,0.75 -0.75,0.75L17,13v1.25C17,14.66 16.66,15 16.25,15 15.84,15 15.5,14.66 15.5,14.25L15.5,10c0,-0.55 0.45,-1 1,-1h1.25c0.41,0 0.75,0.34 0.75,0.75zM7,11.5h1v-1L7,10.5ZM12,13.5h1v-3h-1z" />
</group>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M15.5,14h-0.79l-0.28,-0.27c1.2,-1.4 1.82,-3.31 1.48,-5.34 -0.47,-2.78 -2.79,-5 -5.59,-5.34 -4.23,-0.52 -7.78,3.04 -7.27,7.27 0.34,2.8 2.56,5.12 5.34,5.59 2.03,0.34 3.94,-0.28 5.34,-1.48l0.27,0.28v0.79l4.26,4.25c0.41,0.41 1.07,0.41 1.48,0l0.01,-0.01c0.41,-0.41 0.41,-1.07 0,-1.48L15.5,14zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14zM9.5,7c-0.28,0 -0.5,0.22 -0.5,0.5L9,9L7.5,9c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5L9,10v1.5c0,0.28 0.22,0.5 0.5,0.5s0.5,-0.22 0.5,-0.5L10,10h1.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5L10,9L10,7.5c0,-0.28 -0.22,-0.5 -0.5,-0.5z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M15.5,14h-0.79l-0.28,-0.27c1.2,-1.4 1.82,-3.31 1.48,-5.34 -0.47,-2.78 -2.79,-5 -5.59,-5.34 -4.23,-0.52 -7.79,3.04 -7.27,7.27 0.34,2.8 2.56,5.12 5.34,5.59 2.03,0.34 3.94,-0.28 5.34,-1.48l0.27,0.28v0.79l4.26,4.25c0.41,0.41 1.07,0.41 1.48,0l0.01,-0.01c0.41,-0.41 0.41,-1.07 0,-1.48L15.5,14zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14zM7.5,9h4c0.28,0 0.5,0.22 0.5,0.5s-0.22,0.5 -0.5,0.5h-4c-0.28,0 -0.5,-0.22 -0.5,-0.5s0.22,-0.5 0.5,-0.5z" />
</vector>

View File

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="20dp"
android:text="@string/password_prompt_description"
android:textColor="?android:attr/colorAccent"
android:textSize="20sp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/pdf_password_text_input_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:hintEnabled="false"
app:passwordToggleEnabled="true"
app:passwordToggleTint="?android:attr/colorAccent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/pdf_password_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:hint="@string/password_prompt_hint"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@ -14,6 +14,7 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#212121"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>

View File

@ -11,13 +11,31 @@
android:id="@+id/action_previous"
android:icon="@drawable/ic_navigate_before_24dp"
android:title="@string/action_previous"
app:showAsAction="always" />
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_next"
android:icon="@drawable/ic_navigate_next_24dp"
android:title="@string/action_next"
app:showAsAction="always" />
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_open"
android:icon="@drawable/ic_insert_drive_file_24dp"
android:title="@string/action_open"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_zoom_out"
android:icon="@drawable/ic_zoom_out_24dp"
android:title="@string/action_zoom_out"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_zoom_in"
android:icon="@drawable/ic_zoom_in_24dp"
android:title="@string/action_zoom_in"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_first"

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,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:shrinkMode="strict" />

View File

@ -1,14 +1,13 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">#DEFFFFFF</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">#212121</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightNavigationBar" tools:ignore="NewApi">false</item>
</style>
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.Material3.DynamicColors.Dark" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.Material3.Dark" />
</resources>

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

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@ -1,39 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<resources>
<string name="app_name">PDF Viewer</string>
<!--<string name="action_settings">Settings</string>-->
<string name="action_previous">Previous page</string>
<string name="action_next">Next page</string>
<string name="action_open">Open document</string>
<string name="action_first">First page</string>
<string name="action_last">Last page</string>
<string name="action_jump_to_page">Jump to page</string>
<string name="action_zoom_out">Zoom out</string>
<string name="action_zoom_in">Zoom in</string>
<string name="action_rotate_clockwise">Rotate clockwise</string>
<string name="action_rotate_counterclockwise">Rotate counterclockwise</string>
<string name="action_jump_to_page">Jump to page</string>
<string name="action_view_document_properties">Properties</string>
<string name="debug_action_toggle_text_layer_visibility">Toggle text layer visibility</string>
<string name="document_properties_invalid_date">Invalid date</string>
<string name="document_properties_retrieval_failed">Failed to obtain document metadata</string>
<string name="invalid_mime_type">Cannot open file with invalid MIME type</string>
<string name="legacy_file_uri">Cannot open legacy file paths from insecure apps</string>
<string name="io_error">Received I/O error trying to open content</string>
<string name="webview_out_of_date_title">WebView out-of-date</string>
<string name="webview_out_of_date_message" tools:ignore="PluralsCandidate">Your current WebView version is %1$d. The WebView should be at least version %2$d for the PDF Viewer to work.</string>
<string name="password_prompt_hint">Password</string>
<string name="password_prompt_description">Enter the password to decrypt this PDF file</string>
<string name="open">Open</string>
<string name="cancel">Cancel</string>
<string name="file_name">File name</string>
<string name="file_size">File size</string>
<string name="title">Title</string>
<string name="author">Author</string>
<string name="subject">Subject</string>
<string name="keywords">Keywords</string>
<string name="creation_date">Creation date</string>
<string name="modify_date">Modify date</string>
<string name="producer">Producer</string>
<string name="creator">Creator</string>
<string name="pdf_version">PDF version</string>
<string name="pages">Pages</string>
<string name="webview_out_of_date_message">Your current WebView version is %d. The WebView should be at least version %d for the PDF Viewer to work.</string>
</resources>

View File

@ -1,16 +1,15 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">#000000</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">#212121</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightNavigationBar" tools:ignore="NewApi">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.Material3.DynamicColors.Dark" />
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.Material3.Dark" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.Material3.DynamicColors.Light" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.Material3.Light" />
</resources>

View File

@ -1,6 +1,12 @@
plugins {
id("com.android.application") apply false
id("org.jetbrains.kotlin.android") apply false
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.1.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
}
}
allprojects {

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,8 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=38f66cd6eef217b4c35855bb11ea4e9fbc53594ccccb5fb82dfd317ef8c2c5a3
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
distributionSha256Sum=8cc27038d5dbd815759851ba53e70cf62e481b87494cc97cfd97982ada5ba634
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

30
gradlew vendored
View File

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/master/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,11 +80,14 @@ do
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
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"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -130,29 +133,22 @@ location of your Java installation."
fi
else
JAVACMD=java
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.
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
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
@ -197,10 +193,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
@ -213,12 +205,6 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

15
gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,8 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -41,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -76,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

@ -1,9 +1,3 @@
pluginManagement {
repositories {
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {

1
third_party/pdfjs-dist vendored Submodule

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