Compare commits

...

10 Commits

32 changed files with 108 additions and 3434 deletions

View File

@ -1,40 +0,0 @@
{
"env": {
"browser": true,
"es2022": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
],
"no-var": [
"error"
]
},
"globals": {
"pdfjsLib": "readonly",
"channel": "readonly"
},
"ignorePatterns": [
"pdf.js",
"pdf.worker.js"
]
}

View File

@ -1,17 +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
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
target-branch: main

View File

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

View File

@ -1,11 +0,0 @@
name: Validate Gradle Wrapper
on: [pull_request, push]
jobs:
validation:
name: Validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1

1
.gitignore vendored
View File

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

View File

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

View File

@ -9,7 +9,7 @@ if (useKeystoreProperties) {
}
plugins {
id("com.android.application")
id("com.android.library")
id("kotlin-android")
}
@ -44,21 +44,13 @@ android {
namespace = "app.grapheneos.pdfviewer"
defaultConfig {
applicationId = "app.grapheneos.pdfviewer"
minSdk = 26
minSdk = 21
targetSdk = 33
versionCode = 17
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) {
@ -68,7 +60,6 @@ android {
create("play") {
initWith(getByName("release"))
applicationIdSuffix = ".play"
if (useKeystoreProperties) {
signingConfig = signingConfigs.getByName("play")
}

View File

@ -3,25 +3,7 @@
android:targetSandboxVersion="2">
<original-package android:name="org.grapheneos.pdfviewer" />
<application android:name=".App"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity android:name=".PdfViewer"
android:documentLaunchMode="always"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="application/pdf" />
</intent-filter>
</activity>
<application>
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,13 +0,0 @@
package app.grapheneos.pdfviewer
import android.app.Application
import com.google.android.material.color.DynamicColors
class App : Application() {
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
}
}

View File

@ -1,30 +0,0 @@
package app.grapheneos.pdfviewer
import android.content.Context
import android.net.Uri
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
@Throws(
FileNotFoundException::class,
IOException::class,
IllegalArgumentException::class,
OutOfMemoryError::class
)
fun saveAs(context: Context, existingUri: Uri, saveAs: Uri) {
context.asInputStream(existingUri)?.use { inputStream ->
context.asOutputStream(saveAs)?.use { outputStream ->
outputStream.write(inputStream.readBytes())
}
}
}
@Throws(FileNotFoundException::class)
private fun Context.asInputStream(uri: Uri): InputStream? = contentResolver.openInputStream(uri)
@Throws(FileNotFoundException::class)
private fun Context.asOutputStream(uri: Uri): OutputStream? = contentResolver.openOutputStream(uri)

View File

@ -1,7 +1,5 @@
package app.grapheneos.pdfviewer;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.res.ColorStateList;
import android.graphics.Color;
@ -25,9 +23,8 @@ import android.webkit.WebViewClient;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
@ -48,22 +45,17 @@ import app.grapheneos.pdfviewer.ktx.ViewKt;
import app.grapheneos.pdfviewer.loader.DocumentPropertiesAsyncTaskLoader;
import app.grapheneos.pdfviewer.viewModel.PasswordStatus;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
public class PdfViewer extends AppCompatActivity implements LoaderManager.LoaderCallbacks<List<CharSequence>> {
public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequence>> {
public static final String TAG = "PdfViewer";
private static final String STATE_URI = "uri";
private static final String STATE_PAGE = "page";
private static final String STATE_ZOOM_RATIO = "zoomRatio";
private static final String STATE_DOCUMENT_ORIENTATION_DEGREES = "documentOrientationDegrees";
private static final String STATE_ENCRYPTED_DOCUMENT_PASSWORD = "encrypted_document_password";
private static final String KEY_PROPERTIES = "properties";
private static final int MIN_WEBVIEW_RELEASE = 92;
@ -116,7 +108,6 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
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;
@ -124,43 +115,18 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
private int mDocumentState;
private String mEncryptedDocumentPassword;
private List<CharSequence> mDocumentProperties;
private InputStream mInputStream;
private ByteArrayInputStream mInputStream;
private PdfviewerBinding binding;
private TextView mTextView;
private Toast mToast;
private Snackbar snackbar;
AppCompatActivity activity;
String fileName;
Long fileSize;
private PasswordPromptFragment mPasswordPromptFragment;
public PasswordStatus passwordValidationViewModel;
private final ActivityResultLauncher<Intent> openDocumentLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> {
if (result == null) return;
if (result.getResultCode() != RESULT_OK) return;
Intent resultData = result.getData();
if (resultData != null) {
mUri = result.getData().getData();
mPage = 1;
mDocumentProperties = null;
mEncryptedDocumentPassword = "";
loadPdf();
invalidateOptionsMenu();
}
});
private final ActivityResultLauncher<Intent> saveAsLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> {
if (result == null) return;
if (result.getResultCode() != RESULT_OK) return;
Intent resultData = result.getData();
if (resultData != null) {
Uri path = resultData.getData();
if (path != null) {
saveDocumentAs(path);
}
}
});
private class Channel {
@JavascriptInterface
public int getPage() {
@ -195,7 +161,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
@JavascriptInterface
public void setNumPages(int numPages) {
mNumPages = numPages;
runOnUiThread(PdfViewer.this::invalidateOptionsMenu);
activity.runOnUiThread(activity::invalidateOptionsMenu);
}
@JavascriptInterface
@ -206,20 +172,20 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
final Bundle args = new Bundle();
args.putString(KEY_PROPERTIES, properties);
runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesAsyncTaskLoader.ID, args, PdfViewer.this));
activity.runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this.activity).restartLoader(DocumentPropertiesAsyncTaskLoader.ID, args, PdfViewer.this));
}
@JavascriptInterface
public void showPasswordPrompt() {
if (!getPasswordPromptFragment().isAdded()){
getPasswordPromptFragment().show(getSupportFragmentManager(), PasswordPromptFragment.class.getName());
if (!getPasswordPromptFragment().isAdded()) {
getPasswordPromptFragment().show(activity.getSupportFragmentManager(), PasswordPromptFragment.class.getName());
}
passwordValidationViewModel.passwordMissing();
}
@JavascriptInterface
public void invalidPassword() {
runOnUiThread(() -> passwordValidationViewModel.invalid());
activity.runOnUiThread(() -> passwordValidationViewModel.invalid());
}
@JavascriptInterface
@ -236,16 +202,14 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
}
}
@Override
@SuppressLint({"SetJavaScriptEnabled", "ClickableViewAccessibility"})
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = PdfviewerBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
passwordValidationViewModel = new ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication())).get(PasswordStatus.class);
public PdfViewer(@NonNull AppCompatActivity activity) {
this.activity = activity;
binding = PdfviewerBinding.inflate(activity.getLayoutInflater());
activity.setContentView(binding.getRoot());
activity.setSupportActionBar(binding.toolbar);
passwordValidationViewModel = new ViewModelProvider(activity, ViewModelProvider.AndroidViewModelFactory.getInstance(activity.getApplication())).get(PasswordStatus.class);
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
WindowCompat.setDecorFitsSystemWindows(activity.getWindow(), false);
// Margins for the toolbar are needed, so that content of the toolbar
// is not covered by a system button navigation bar when in landscape.
@ -278,7 +242,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
binding.webview.setWebViewClient(new WebViewClient() {
private WebResourceResponse fromAsset(final String mime, final String path) {
try {
InputStream inputStream = getAssets().open(path.substring(1));
InputStream inputStream = activity.getAssets().open(path.substring(1));
return new WebResourceResponse(mime, null, inputStream);
} catch (IOException e) {
return null;
@ -300,12 +264,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
Log.d(TAG, "path " + path);
if ("/placeholder.pdf".equals(path)) {
maybeCloseInputStream();
try {
mInputStream = getContentResolver().openInputStream(mUri);
} catch (FileNotFoundException ignored) {
snackbar.setText(R.string.error_while_opening).show();
}
mInputStream.reset();
return new WebResourceResponse("application/pdf", null, mInputStream);
}
@ -338,28 +297,25 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
@Override
public void onPageFinished(WebView view, String url) {
mDocumentState = STATE_LOADED;
invalidateOptionsMenu();
activity.invalidateOptionsMenu();
loadPdfWithPassword(mEncryptedDocumentPassword);
}
});
GestureHelper.attach(PdfViewer.this, binding.webview,
GestureHelper.attach(activity, binding.webview,
new GestureHelper.GestureListener() {
@Override
public boolean onTapUp() {
if (mUri != null) {
binding.webview.evaluateJavascript("isTextSelected()", selection -> {
if (!Boolean.parseBoolean(selection)) {
if (getSupportActionBar().isShowing()) {
hideSystemUi();
} else {
showSystemUi();
}
binding.webview.evaluateJavascript("isTextSelected()", selection -> {
if (!Boolean.parseBoolean(selection)) {
if (activity.getSupportActionBar().isShowing()) {
hideSystemUi();
} else {
showSystemUi();
}
});
return true;
}
return false;
}
});
return true;
}
@Override
@ -378,56 +334,14 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
}
});
mTextView = new TextView(this);
mTextView = new TextView(activity);
mTextView.setBackgroundColor(Color.DKGRAY);
mTextView.setTextColor(ColorStateList.valueOf(Color.WHITE));
mTextView.setTextSize(18);
mTextView.setPadding(PADDING, 0, PADDING, 0);
// If loaders are not being initialized in onCreate(), the result will not be delivered
// after orientation change (See FragmentHostCallback), thus initialize the
// loader manager impl so that the result will be delivered.
LoaderManager.getInstance(this);
snackbar = Snackbar.make(binding.getRoot(), "", Snackbar.LENGTH_LONG);
final Intent intent = getIntent();
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
if (!"application/pdf".equals(intent.getType())) {
snackbar.setText(R.string.invalid_mime_type).show();
return;
}
mUri = intent.getData();
mPage = 1;
}
if (savedInstanceState != null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@SuppressWarnings("deprecation")
final Uri uri = savedInstanceState.getParcelable(STATE_URI);
mUri = uri;
} else {
mUri = savedInstanceState.getParcelable(STATE_URI, Uri.class);
}
mPage = savedInstanceState.getInt(STATE_PAGE);
mZoomRatio = savedInstanceState.getFloat(STATE_ZOOM_RATIO);
mDocumentOrientationDegrees = savedInstanceState.getInt(STATE_DOCUMENT_ORIENTATION_DEGREES);
mEncryptedDocumentPassword = savedInstanceState.getString(STATE_ENCRYPTED_DOCUMENT_PASSWORD);
}
if (mUri != null) {
if ("file".equals(mUri.getScheme())) {
snackbar.setText(R.string.legacy_file_uri).show();
return;
}
loadPdf();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
public void onDestroy() {
binding.webview.removeJavascriptInterface("channel");
binding.getRoot().removeView(binding.webview);
binding.webview.destroy();
@ -447,11 +361,11 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
private PasswordPromptFragment getPasswordPromptFragment() {
if (mPasswordPromptFragment == null) {
final Fragment fragment = getSupportFragmentManager().findFragmentByTag(PasswordPromptFragment.class.getName());
final Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag(PasswordPromptFragment.class.getName());
if (fragment != null) {
mPasswordPromptFragment = (PasswordPromptFragment) fragment;
} else {
mPasswordPromptFragment = new PasswordPromptFragment();
mPasswordPromptFragment = new PasswordPromptFragment(this);
}
}
return mPasswordPromptFragment;
@ -460,28 +374,28 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
private void setToolbarTitleWithDocumentName() {
String documentName = getCurrentDocumentName();
if (documentName != null && !documentName.isEmpty()) {
getSupportActionBar().setTitle(documentName);
activity.getSupportActionBar().setTitle(documentName);
} else {
getSupportActionBar().setTitle(R.string.app_name);
activity.getSupportActionBar().setTitle(R.string.app_name);
}
}
@Override
protected void onResume() {
super.onResume();
public void onResume() {
// The user could have left the activity to update the WebView
invalidateOptionsMenu();
if (getWebViewRelease() >= MIN_WEBVIEW_RELEASE) {
binding.webviewOutOfDateLayout.setVisibility(View.GONE);
binding.webview.setVisibility(View.VISIBLE);
} else {
binding.webview.setVisibility(View.GONE);
binding.webviewOutOfDateMessage.setText(getString(R.string.webview_out_of_date_message, getWebViewRelease(), MIN_WEBVIEW_RELEASE));
binding.webviewOutOfDateLayout.setVisibility(View.VISIBLE);
activity.invalidateOptionsMenu();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (getWebViewRelease() >= MIN_WEBVIEW_RELEASE) {
binding.webviewOutOfDateLayout.setVisibility(View.GONE);
binding.webview.setVisibility(View.VISIBLE);
} else {
binding.webview.setVisibility(View.GONE);
binding.webviewOutOfDateMessage.setText(activity.getString(R.string.webview_out_of_date_message, getWebViewRelease(), MIN_WEBVIEW_RELEASE));
binding.webviewOutOfDateLayout.setVisibility(View.VISIBLE);
}
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private int getWebViewRelease() {
PackageInfo webViewPackage = WebView.getCurrentWebViewPackage();
String webViewVersionName = webViewPackage.versionName;
@ -491,14 +405,14 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
@NonNull
@Override
public Loader<List<CharSequence>> onCreateLoader(int id, Bundle args) {
return new DocumentPropertiesAsyncTaskLoader(this, args.getString(KEY_PROPERTIES), mNumPages, mUri);
return new DocumentPropertiesAsyncTaskLoader(activity, args.getString(KEY_PROPERTIES), mNumPages, fileName, fileSize);
}
@Override
public void onLoadFinished(@NonNull Loader<List<CharSequence>> loader, List<CharSequence> data) {
mDocumentProperties = data;
setToolbarTitleWithDocumentName();
LoaderManager.getInstance(this).destroyLoader(DocumentPropertiesAsyncTaskLoader.ID);
LoaderManager.getInstance(activity).destroyLoader(DocumentPropertiesAsyncTaskLoader.ID);
}
@Override
@ -506,20 +420,14 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
mDocumentProperties = null;
}
private void loadPdf() {
try {
if (mInputStream != null) {
mInputStream.close();
}
mInputStream = getContentResolver().openInputStream(mUri);
} catch (IOException e) {
snackbar.setText(R.string.error_while_opening).show();
return;
}
mDocumentState = 0;
public void loadPdf(ByteArrayInputStream inputStream, String fileName, Long fileSize) {
mPage = 1;
mDocumentProperties = null;
mInputStream = inputStream;
this.fileName = fileName;
this.fileSize = fileSize;
showSystemUi();
invalidateOptionsMenu();
activity.invalidateOptionsMenu();
binding.webview.loadUrl("https://localhost/viewer.html");
}
@ -540,30 +448,11 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
renderPage(0);
}
private void openDocument() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/pdf");
openDocumentLauncher.launch(intent);
}
private void shareDocument() {
if (mUri != null) {
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setDataAndTypeAndNormalize(mUri, "application/pdf");
shareIntent.putExtra(Intent.EXTRA_STREAM, mUri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(shareIntent, getString(R.string.action_share)));
} else {
Log.w(TAG, "Cannot share unexpected null URI");
}
}
private void zoomIn(float value, boolean end) {
if (mZoomRatio < MAX_ZOOM_RATIO) {
mZoomRatio = Math.min(mZoomRatio + value, MAX_ZOOM_RATIO);
renderPage(end ? 1 : 2);
invalidateOptionsMenu();
activity.invalidateOptionsMenu();
}
}
@ -571,7 +460,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
if (mZoomRatio > MIN_ZOOM_RATIO) {
mZoomRatio = Math.max(mZoomRatio - value, MIN_ZOOM_RATIO);
renderPage(end ? 1 : 2);
invalidateOptionsMenu();
activity.invalidateOptionsMenu();
}
}
@ -594,28 +483,18 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
mPage = selected_page;
renderPage(0);
showPageNumber();
invalidateOptionsMenu();
activity.invalidateOptionsMenu();
}
}
private void showSystemUi() {
ViewKt.showSystemUi(binding.getRoot(), getWindow());
getSupportActionBar().show();
ViewKt.showSystemUi(binding.getRoot(), activity.getWindow());
activity.getSupportActionBar().show();
}
private void hideSystemUi() {
ViewKt.hideSystemUi(binding.getRoot(), getWindow());
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);
savedInstanceState.putString(STATE_ENCRYPTED_DOCUMENT_PASSWORD, mEncryptedDocumentPassword);
ViewKt.hideSystemUi(binding.getRoot(), activity.getWindow());
activity.getSupportActionBar().hide();
}
private void showPageNumber() {
@ -623,30 +502,26 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
mToast.cancel();
}
mTextView.setText(String.format("%s/%s", mPage, mNumPages));
mToast = new Toast(getApplicationContext());
mToast = new Toast(activity);
mToast.setGravity(Gravity.BOTTOM | Gravity.END, PADDING, PADDING);
mToast.setDuration(Toast.LENGTH_SHORT);
mToast.setView(mTextView);
mToast.show();
}
@Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
public void onCreateOptionMenu(@NonNull Menu menu) {
MenuInflater inflater = activity.getMenuInflater();
inflater.inflate(R.menu.pdf_viewer, menu);
if (BuildConfig.DEBUG) {
inflater.inflate(R.menu.pdf_viewer_debug, menu);
}
return true;
}
@Override
public boolean onPrepareOptionsMenu(@NonNull Menu menu) {
final ArrayList<Integer> ids = new ArrayList<>(Arrays.asList(R.id.action_jump_to_page,
R.id.action_next, R.id.action_previous, R.id.action_first, R.id.action_last,
R.id.action_rotate_clockwise, R.id.action_rotate_counterclockwise,
R.id.action_view_document_properties, R.id.action_share, R.id.action_save_as));
R.id.action_view_document_properties));
if (BuildConfig.DEBUG) {
ids.add(R.id.debug_action_toggle_text_layer_visibility);
}
@ -667,16 +542,12 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
mDocumentState = STATE_END;
}
enableDisableMenuItem(menu.findItem(R.id.action_open), getWebViewRelease() >= MIN_WEBVIEW_RELEASE);
enableDisableMenuItem(menu.findItem(R.id.action_share), mUri != null);
enableDisableMenuItem(menu.findItem(R.id.action_next), mPage < mNumPages);
enableDisableMenuItem(menu.findItem(R.id.action_previous), mPage > 1);
enableDisableMenuItem(menu.findItem(R.id.action_save_as), mUri != null);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.action_previous) {
@ -691,9 +562,6 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
} else if (itemId == R.id.action_last) {
onJumpToPageInDocument(mNumPages);
return true;
} else if (itemId == R.id.action_open) {
openDocument();
return true;
} else if (itemId == R.id.action_rotate_clockwise) {
documentOrientationChanged(90);
return true;
@ -703,31 +571,18 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
} else if (itemId == R.id.action_view_document_properties) {
DocumentPropertiesFragment
.newInstance(mDocumentProperties)
.show(getSupportFragmentManager(), DocumentPropertiesFragment.TAG);
.show(activity.getSupportFragmentManager(), DocumentPropertiesFragment.TAG);
return true;
} else if (itemId == R.id.action_jump_to_page) {
new JumpToPageFragment()
.show(getSupportFragmentManager(), JumpToPageFragment.TAG);
new JumpToPageFragment(this)
.show(activity.getSupportFragmentManager(), JumpToPageFragment.TAG);
return true;
} else if (itemId == R.id.action_share) {
shareDocument();
return true;
} else if (itemId == R.id.action_save_as) {
saveDocument();
} else if (itemId == R.id.debug_action_toggle_text_layer_visibility) {
binding.webview.evaluateJavascript("toggleTextLayerVisibility()", null);
return true;
}
return super.onOptionsItemSelected(item);
}
private void saveDocument() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/pdf");
intent.putExtra(Intent.EXTRA_TITLE, getCurrentDocumentName());
saveAsLauncher.launch(intent);
return false;
}
private String getCurrentDocumentName() {
@ -744,12 +599,4 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
}
return fileName.length() > 2 ? fileName : title;
}
private void saveDocumentAs(Uri uri) {
try {
KtUtilsKt.saveAs(this, mUri, uri);
} catch (IOException | OutOfMemoryError | IllegalArgumentException e) {
snackbar.setText(R.string.error_while_saving).show();
}
}
}

View File

@ -10,7 +10,7 @@ import androidx.fragment.app.DialogFragment
import app.grapheneos.pdfviewer.PdfViewer
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class JumpToPageFragment : DialogFragment() {
class JumpToPageFragment(private val pdfViewer: PdfViewer) : DialogFragment() {
companion object {
const val TAG = "JumpToPageFragment"
@ -23,16 +23,14 @@ class JumpToPageFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val viewerActivity: PdfViewer = (requireActivity() as PdfViewer)
if (savedInstanceState != null) {
mPicker.minValue = savedInstanceState.getInt(STATE_PICKER_MIN)
mPicker.maxValue = savedInstanceState.getInt(STATE_PICKER_MAX)
mPicker.value = savedInstanceState.getInt(STATE_PICKER_CUR)
} else {
mPicker.minValue = 1
mPicker.maxValue = viewerActivity.mNumPages
mPicker.value = viewerActivity.mPage
mPicker.maxValue = pdfViewer.mNumPages
mPicker.value = pdfViewer.mPage
}
val layout = FrameLayout(requireActivity())
layout.addView(
@ -46,7 +44,7 @@ class JumpToPageFragment : DialogFragment() {
.setView(layout)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
mPicker.clearFocus()
viewerActivity.onJumpToPageInDocument(mPicker.value)
pdfViewer.onJumpToPageInDocument(mPicker.value)
}
.setNegativeButton(android.R.string.cancel, null)
.create()

View File

@ -9,6 +9,7 @@ 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
@ -19,7 +20,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
class PasswordPromptFragment : DialogFragment() {
class PasswordPromptFragment(private val pdfViewer: PdfViewer) : DialogFragment() {
private lateinit var passwordLayout : TextInputLayout
private lateinit var passwordEditText : TextInputEditText
@ -50,7 +51,7 @@ class PasswordPromptFragment : DialogFragment() {
isCancelable = false
dialog.setCanceledOnTouchOutside(false)
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
(requireActivity() as PdfViewer).passwordValidationViewModel.status.observe(
pdfViewer.passwordValidationViewModel.status.observe(
this
) {
when (it) {
@ -83,7 +84,7 @@ class PasswordPromptFragment : DialogFragment() {
private fun sendPassword() {
val password = passwordEditText.text.toString()
if (!TextUtils.isEmpty(password)) {
(activity as PdfViewer).loadPdfWithPassword(password)
pdfViewer.loadPdfWithPassword(password)
}
}

View File

@ -1,7 +1,6 @@
package app.grapheneos.pdfviewer.loader;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.loader.content.AsyncTaskLoader;
@ -16,14 +15,17 @@ public class DocumentPropertiesAsyncTaskLoader extends AsyncTaskLoader<List<Char
private final String mProperties;
private final int mNumPages;
private final Uri mUri;
public DocumentPropertiesAsyncTaskLoader(Context context, String properties, int numPages, Uri uri) {
String fileName;
Long fileSize;
public DocumentPropertiesAsyncTaskLoader(Context context, String properties, int numPages, String fileName, Long fileSize) {
super(context);
mProperties = properties;
mNumPages = numPages;
mUri = uri;
this.fileName = fileName;
this.fileSize = fileSize;
}
@ -40,7 +42,8 @@ public class DocumentPropertiesAsyncTaskLoader extends AsyncTaskLoader<List<Char
getContext(),
mProperties,
mNumPages,
mUri
fileName,
fileSize
);
return loader.loadAsList();

View File

@ -2,14 +2,11 @@ package app.grapheneos.pdfviewer.loader
import android.content.Context
import android.graphics.Typeface
import android.net.Uri
import android.provider.OpenableColumns
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.format.Formatter
import android.text.style.StyleSpan
import android.util.Log
import androidx.core.database.getLongOrNull
import app.grapheneos.pdfviewer.R
import org.json.JSONException
@ -17,7 +14,8 @@ class DocumentPropertiesLoader(
private val context: Context,
private val properties: String,
private val numPages: Int,
private val mUri: Uri
private val fileName: String,
private val fileSize: Long,
) {
fun loadAsList(): List<CharSequence> {
@ -83,31 +81,8 @@ class DocumentPropertiesLoader(
private fun getFileProperties(): Map<DocumentProperty, String> {
val collections = mutableMapOf<DocumentProperty, String>()
val proj = arrayOf(
OpenableColumns.DISPLAY_NAME,
OpenableColumns.SIZE
)
context.contentResolver.query(
mUri,
proj,
null,
null
)?.use { cursor ->
cursor.moveToFirst()
val indexName: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (indexName >= 0) {
collections[DocumentProperty.FileName] = cursor.getString(indexName)
}
val indexSize: Int = cursor.getColumnIndex(OpenableColumns.SIZE)
if (indexSize >= 0) {
val fileSize = cursor.getLongOrNull(indexSize)
collections[DocumentProperty.FileSize] =
fileSize?.let { Formatter.formatFileSize(context, it) } ?: context.getString(R.string.unknown_file_size)
}
}
collections[DocumentProperty.FileName] = fileName
collections[DocumentProperty.FileSize] = Formatter.formatFileSize(context, fileSize)
return collections
}
}

View File

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

View File

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

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM19,19L5,19L5,5h11.17L19,7.83L19,19zM12,12c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3zM6,6h9v4L6,10z"/>
</vector>

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

View File

@ -19,12 +19,6 @@
android:title="@string/action_next"
app:showAsAction="always" />
<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_first"
android:icon="@drawable/ic_first_page_24dp"
@ -55,18 +49,6 @@
android:title="@string/action_rotate_counterclockwise"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_share"
android:icon="@drawable/ic_share_24dp"
android:title="@string/action_share"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_save_as"
android:icon="@drawable/ic_save_24dp"
android:title="@string/action_save_as"
app:showAsAction="never" />
<item
android:id="@+id/action_view_document_properties"
android:title="@string/action_view_document_properties"

View File

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

View File

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

View File

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

View File

@ -4,14 +4,11 @@
<!--<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_rotate_clockwise">Rotate clockwise</string>
<string name="action_rotate_counterclockwise">Rotate counterclockwise</string>
<string name="action_share">Share</string>
<string name="action_save_as">Save as</string>
<string name="action_view_document_properties">Properties</string>
<string name="debug_action_toggle_text_layer_visibility">Toggle text layer visibility</string>
@ -19,11 +16,6 @@
<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="error_while_opening">Error encountered trying to open content</string>
<string name="error_while_saving">Error encountered while saving</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>
@ -44,6 +36,4 @@
<string name="creator">Creator</string>
<string name="pdf_version">PDF version</string>
<string name="pages">Pages</string>
<string name="unknown_file_size">Unknown</string>
</resources>

View File

@ -1,6 +1,6 @@
plugins {
id("com.android.application") version "8.0.2" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
id("com.android.application") apply false
id("org.jetbrains.kotlin.android") apply false
}
allprojects {

2872
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
{
"dependencies": {
"pdfjs-dist": "3.8.162"
},
"devDependencies": {
"eslint": "^8.44.0"
}
}

3
setup
View File

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