libpdfviewer/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java

550 lines
20 KiB
Java
Raw Normal View History

package app.grapheneos.pdfviewer;
import android.content.pm.PackageInfo;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.net.Uri;
2022-03-06 21:39:38 +01:00
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
2019-08-17 01:42:51 +02:00
import android.view.View;
2022-02-20 17:29:02 +01:00
import android.view.ViewGroup;
import android.webkit.CookieManager;
import android.webkit.JavascriptInterface;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
2022-03-06 21:39:38 +01:00
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
2022-02-20 17:29:02 +01:00
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
2022-02-20 21:21:25 +01:00
import androidx.core.view.WindowCompat;
2022-02-20 17:29:02 +01:00
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
2022-05-06 19:12:13 +02:00
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
2020-04-11 19:21:36 +02:00
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;
2022-05-12 13:23:55 +02:00
import app.grapheneos.pdfviewer.ktx.ViewKt;
import app.grapheneos.pdfviewer.loader.DocumentPropertiesLoader;
2022-05-06 19:12:13 +02:00
import app.grapheneos.pdfviewer.viewModel.PasswordStatus;
2022-09-20 16:00:48 +02:00
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
2022-03-06 21:39:38 +01:00
public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequence>> {
public static final String TAG = "PdfViewer";
private static final String KEY_PROPERTIES = "properties";
private static final int MIN_WEBVIEW_RELEASE = 89;
private static final String CONTENT_SECURITY_POLICY =
"default-src 'none'; " +
"form-action 'none'; " +
"connect-src https://localhost/placeholder.pdf; " +
"img-src blob: 'self'; " +
2021-11-21 21:41:15 +01:00
"script-src 'self'; " +
"style-src 'self'; " +
"frame-ancestors 'none'; " +
"base-uri 'none'";
private static final String PERMISSIONS_POLICY =
"accelerometer=(), " +
"ambient-light-sensor=(), " +
"autoplay=(), " +
"battery=(), " +
"camera=(), " +
"clipboard-read=(), " +
"clipboard-write=(), " +
"display-capture=(), " +
"document-domain=(), " +
"encrypted-media=(), " +
"fullscreen=(), " +
"gamepad=(), " +
"geolocation=(), " +
"gyroscope=(), " +
"hid=(), " +
"idle-detection=(), " +
"interest-cohort=(), " +
"magnetometer=(), " +
"microphone=(), " +
"midi=(), " +
"payment=(), " +
"picture-in-picture=(), " +
"publickey-credentials-get=(), " +
"screen-wake-lock=(), " +
"serial=(), " +
"speaker-selection=(), " +
"sync-xhr=(), " +
"usb=(), " +
"xr-spatial-tracking=()";
2019-11-18 11:49:58 +01:00
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 STATE_LOADED = 1;
private static final int STATE_END = 2;
private static final int PADDING = 10;
public int mPage;
public int mNumPages;
2019-11-18 11:49:58 +01:00
private float mZoomRatio = 1f;
2017-11-14 18:29:10 +01:00
private int mDocumentOrientationDegrees;
private int mDocumentState;
private String mEncryptedDocumentPassword;
private List<CharSequence> mDocumentProperties;
2022-09-20 16:00:48 +02:00
private ByteArrayInputStream mInputStream;
private PdfviewerBinding binding;
private TextView mTextView;
private Toast mToast;
2022-03-06 21:39:38 +01:00
AppCompatActivity activity;
2022-02-18 15:03:53 +01:00
String fileName;
Long fileSize;
private PasswordPromptFragment mPasswordPromptFragment;
2022-05-06 19:12:13 +02:00
public PasswordStatus passwordValidationViewModel;
private class Channel {
@JavascriptInterface
public int getPage() {
return mPage;
}
@JavascriptInterface
2019-11-18 11:49:58 +01:00
public float getZoomRatio() {
return mZoomRatio;
}
2017-11-14 18:29:10 +01:00
@JavascriptInterface
public int getDocumentOrientationDegrees() {
return mDocumentOrientationDegrees;
}
@JavascriptInterface
public void setNumPages(int numPages) {
mNumPages = numPages;
2022-03-06 21:39:38 +01:00
activity.runOnUiThread(activity::invalidateOptionsMenu);
}
@JavascriptInterface
public void setDocumentProperties(final String properties) {
if (mDocumentProperties != null) {
throw new SecurityException("mDocumentProperties not null");
}
final Bundle args = new Bundle();
args.putString(KEY_PROPERTIES, properties);
2022-03-06 21:39:38 +01:00
activity.runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this.activity).restartLoader(DocumentPropertiesLoader.ID, args, PdfViewer.this));
}
@JavascriptInterface
public void showPasswordPrompt() {
2022-10-06 18:22:57 +02:00
if (!getPasswordPromptFragment().isAdded()) {
getPasswordPromptFragment().show(activity.getSupportFragmentManager(), PasswordPromptFragment.class.getName());
}
2022-05-06 19:12:13 +02:00
passwordValidationViewModel.passwordMissing();
}
@JavascriptInterface
public void invalidPassword() {
2022-10-06 18:22:57 +02:00
activity.runOnUiThread(() -> passwordValidationViewModel.invalid());
}
2022-05-06 19:07:20 +02:00
@JavascriptInterface
public void onLoaded() {
2022-05-06 19:12:13 +02:00
passwordValidationViewModel.validated();
if (getPasswordPromptFragment().isAdded()) {
getPasswordPromptFragment().dismiss();
}
}
@JavascriptInterface
public String getPassword() {
return mEncryptedDocumentPassword != null ? mEncryptedDocumentPassword : "";
}
}
2022-03-06 21:39:38 +01:00
public PdfViewer(@NonNull AppCompatActivity activity) {
this.activity = activity;
2022-09-20 16:00:48 +02:00
binding = PdfviewerBinding.inflate(activity.getLayoutInflater());
2022-03-06 21:39:38 +01:00
activity.setContentView(binding.getRoot());
2022-09-20 16:00:48 +02:00
activity.setSupportActionBar(binding.toolbar);
2022-10-06 18:22:57 +02:00
passwordValidationViewModel = new ViewModelProvider(activity, ViewModelProvider.AndroidViewModelFactory.getInstance(activity.getApplication())).get(PasswordStatus.class);
2022-09-20 16:00:48 +02:00
WindowCompat.setDecorFitsSystemWindows(activity.getWindow(), false);
2022-02-20 21:21:25 +01:00
2022-02-20 17:29:02 +01:00
// Margins for the toolbar are needed, so that content of the toolbar
// is not covered by a system button navigation bar when in landscape.
ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
mlp.leftMargin = insets.left;
mlp.rightMargin = insets.right;
v.setLayoutParams(mlp);
return windowInsets;
});
binding.webview.setBackgroundColor(Color.TRANSPARENT);
if (BuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true);
}
final WebSettings settings = binding.webview.getSettings();
settings.setAllowContentAccess(false);
settings.setAllowFileAccess(false);
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
settings.setJavaScriptEnabled(true);
settings.setMinimumFontSize(1);
CookieManager.getInstance().setAcceptCookie(false);
binding.webview.addJavascriptInterface(new Channel(), "channel");
binding.webview.setWebViewClient(new WebViewClient() {
private WebResourceResponse fromAsset(final String mime, final String path) {
try {
2022-03-06 21:39:38 +01:00
InputStream inputStream = activity.getAssets().open(path.substring(1));
return new WebResourceResponse(mime, null, inputStream);
} catch (IOException e) {
return null;
}
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
if (!"GET".equals(request.getMethod())) {
return null;
}
final Uri url = request.getUrl();
if (!"localhost".equals(url.getHost())) {
return null;
}
final String path = url.getPath();
Log.d(TAG, "path " + path);
if ("/placeholder.pdf".equals(path)) {
2022-09-20 16:00:48 +02:00
mInputStream.reset();
return new WebResourceResponse("application/pdf", null, mInputStream);
}
if ("/viewer.html".equals(path)) {
final WebResourceResponse response = fromAsset("text/html", path);
HashMap<String, String> headers = new HashMap<>();
headers.put("Content-Security-Policy", CONTENT_SECURITY_POLICY);
headers.put("Permissions-Policy", PERMISSIONS_POLICY);
2019-07-01 07:52:34 +02:00
headers.put("X-Content-Type-Options", "nosniff");
response.setResponseHeaders(headers);
return response;
}
if ("/viewer.css".equals(path)) {
return fromAsset("text/css", path);
}
if ("/viewer.js".equals(path) || "/pdf.js".equals(path) || "/pdf.worker.js".equals(path)) {
return fromAsset("application/javascript", path);
}
return null;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
return true;
}
@Override
public void onPageFinished(WebView view, String url) {
mDocumentState = STATE_LOADED;
2022-02-18 15:03:53 +01:00
activity.invalidateOptionsMenu();
loadPdfWithPassword(mEncryptedDocumentPassword);
}
});
2022-03-06 21:39:38 +01:00
GestureHelper.attach(activity, binding.webview,
new GestureHelper.GestureListener() {
2019-08-17 01:42:51 +02:00
@Override
public boolean onTapUp() {
2022-09-20 16:00:48 +02:00
binding.webview.evaluateJavascript("isTextSelected()", selection -> {
if (!Boolean.parseBoolean(selection)) {
2022-10-06 18:22:57 +02:00
if (activity.getSupportActionBar().isShowing()) {
2022-09-20 16:00:48 +02:00
hideSystemUi();
} else {
showSystemUi();
2019-08-17 01:42:51 +02:00
}
2022-09-20 16:00:48 +02:00
}
});
return true;
2019-08-17 01:42:51 +02:00
}
2019-10-05 12:06:00 +02:00
@Override
2020-04-04 17:57:15 +02:00
public void onZoomIn(float value) {
2020-04-04 18:22:51 +02:00
zoomIn(value, false);
2019-10-05 12:06:00 +02:00
}
@Override
2020-04-04 17:57:15 +02:00
public void onZoomOut(float value) {
2020-04-04 18:22:51 +02:00
zoomOut(value, false);
2019-10-05 12:06:00 +02:00
}
2020-04-04 17:54:34 +02:00
@Override
public void onZoomEnd() {
zoomEnd();
}
2019-10-05 12:06:00 +02:00
});
2022-03-06 21:39:38 +01:00
mTextView = new TextView(activity);
mTextView.setBackgroundColor(Color.DKGRAY);
mTextView.setTextColor(ColorStateList.valueOf(Color.WHITE));
mTextView.setTextSize(18);
mTextView.setPadding(PADDING, 0, PADDING, 0);
2022-02-18 15:03:53 +01:00
}
2022-09-20 16:00:48 +02:00
public void onDestroy() {
binding.webview.removeJavascriptInterface("channel");
binding.getRoot().removeView(binding.webview);
binding.webview.destroy();
maybeCloseInputStream();
}
void maybeCloseInputStream() {
InputStream stream = mInputStream;
if (stream == null) {
return;
}
mInputStream = null;
try {
stream.close();
} catch (IOException ignored) {}
}
private PasswordPromptFragment getPasswordPromptFragment() {
if (mPasswordPromptFragment == null) {
2022-09-20 16:00:48 +02:00
final Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag(PasswordPromptFragment.class.getName());
if (fragment != null) {
mPasswordPromptFragment = (PasswordPromptFragment) fragment;
} else {
2022-09-20 16:00:48 +02:00
mPasswordPromptFragment = new PasswordPromptFragment(this);
}
}
return mPasswordPromptFragment;
2022-02-18 15:03:53 +01:00
}
2022-03-06 21:39:38 +01:00
public void onResume() {
// The user could have left the activity to update the WebView
2022-09-20 16:00:48 +02:00
activity.invalidateOptionsMenu();
2022-03-06 21:39:38 +01:00
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);
2020-05-28 20:02:44 +02:00
}
}
}
2022-03-06 21:39:38 +01:00
@RequiresApi(api = Build.VERSION_CODES.O)
private int getWebViewRelease() {
PackageInfo webViewPackage = WebView.getCurrentWebViewPackage();
String webViewVersionName = webViewPackage.versionName;
return Integer.parseInt(webViewVersionName.substring(0, webViewVersionName.indexOf(".")));
}
@NonNull
@Override
public Loader<List<CharSequence>> onCreateLoader(int id, Bundle args) {
2022-02-18 15:03:53 +01:00
return new DocumentPropertiesLoader(activity, args.getString(KEY_PROPERTIES), mNumPages, fileName, fileSize);
}
@Override
public void onLoadFinished(@NonNull Loader<List<CharSequence>> loader, List<CharSequence> data) {
mDocumentProperties = data;
2022-02-18 15:03:53 +01:00
LoaderManager.getInstance(activity).destroyLoader(DocumentPropertiesLoader.ID);
}
@Override
public void onLoaderReset(@NonNull Loader<List<CharSequence>> loader) {
mDocumentProperties = null;
}
2022-09-20 16:00:48 +02:00
public void loadPdf(ByteArrayInputStream inputStream, String fileName, Long fileSize) {
2022-02-18 15:03:53 +01:00
mPage = 1;
mDocumentProperties = null;
mInputStream = inputStream;
this.fileName = fileName;
this.fileSize = fileSize;
showSystemUi();
2022-02-18 15:03:53 +01:00
activity.invalidateOptionsMenu();
binding.webview.loadUrl("https://localhost/viewer.html");
}
public void loadPdfWithPassword(final String password) {
mEncryptedDocumentPassword = password;
binding.webview.evaluateJavascript("loadDocument()", null);
}
private void renderPage(final int zoom) {
binding.webview.evaluateJavascript("onRenderPage(" + zoom + ")", null);
}
2017-11-14 18:29:10 +01:00
private void documentOrientationChanged(final int orientationDegreesOffset) {
mDocumentOrientationDegrees = (mDocumentOrientationDegrees + orientationDegreesOffset) % 360;
if (mDocumentOrientationDegrees < 0) {
mDocumentOrientationDegrees += 360;
}
renderPage(0);
2017-11-14 18:29:10 +01:00
}
2020-04-04 18:22:51 +02:00
private void zoomIn(float value, boolean end) {
2019-11-18 11:49:58 +01:00
if (mZoomRatio < MAX_ZOOM_RATIO) {
2020-04-04 18:27:35 +02:00
mZoomRatio = Math.min(mZoomRatio + value, MAX_ZOOM_RATIO);
2020-04-04 18:22:51 +02:00
renderPage(end ? 1 : 2);
2022-02-18 15:03:53 +01:00
activity.invalidateOptionsMenu();
2019-10-05 12:06:00 +02:00
}
}
2020-04-04 18:22:51 +02:00
private void zoomOut(float value, boolean end) {
2019-11-18 11:49:58 +01:00
if (mZoomRatio > MIN_ZOOM_RATIO) {
2020-04-04 18:27:35 +02:00
mZoomRatio = Math.max(mZoomRatio - value, MIN_ZOOM_RATIO);
2020-04-04 18:22:51 +02:00
renderPage(end ? 1 : 2);
2022-02-18 15:03:53 +01:00
activity.invalidateOptionsMenu();
2019-10-05 12:06:00 +02:00
}
}
2020-04-04 17:54:34 +02:00
private void zoomEnd() {
renderPage(1);
2020-04-04 17:54:34 +02:00
}
private static void enableDisableMenuItem(MenuItem item, boolean enable) {
if (enable) {
item.setEnabled(true);
item.getIcon().setAlpha(ALPHA_HIGH);
} else {
item.setEnabled(false);
item.getIcon().setAlpha(ALPHA_LOW);
}
}
2017-11-22 16:46:10 +01:00
public void onJumpToPageInDocument(final int selected_page) {
if (selected_page >= 1 && selected_page <= mNumPages && mPage != selected_page) {
mPage = selected_page;
renderPage(0);
showPageNumber();
2022-02-18 15:03:53 +01:00
activity.invalidateOptionsMenu();
}
}
2019-08-17 01:42:51 +02:00
private void showSystemUi() {
2023-02-01 22:52:07 +01:00
ViewKt.showSystemUi(binding.getRoot(), activity.getWindow());
2022-09-20 16:00:48 +02:00
activity.getSupportActionBar().show();
2019-08-17 01:42:51 +02:00
}
private void hideSystemUi() {
2023-02-01 22:52:07 +01:00
ViewKt.hideSystemUi(binding.getRoot(), activity.getWindow());
2022-09-20 16:00:48 +02:00
activity.getSupportActionBar().hide();
}
private void showPageNumber() {
if (mToast != null) {
mToast.cancel();
}
mTextView.setText(String.format("%s/%s", mPage, mNumPages));
2022-02-18 15:03:53 +01:00
mToast = new Toast(activity);
mToast.setGravity(Gravity.BOTTOM | Gravity.END, PADDING, PADDING);
mToast.setDuration(Toast.LENGTH_SHORT);
mToast.setView(mTextView);
mToast.show();
}
2023-02-01 22:52:07 +01:00
public void onCreateOptionMenu(@NonNull Menu menu) {
2022-09-20 16:00:48 +02:00
MenuInflater inflater = activity.getMenuInflater();
inflater.inflate(R.menu.pdf_viewer, menu);
}
2022-10-02 15:11:07 +02:00
public boolean onPrepareOptionsMenu(@NonNull Menu menu) {
final int[] ids = {R.id.action_jump_to_page, R.id.action_next, R.id.action_previous,
R.id.action_first, R.id.action_last, R.id.action_rotate_clockwise,
2022-09-20 16:00:48 +02:00
R.id.action_rotate_counterclockwise, R.id.action_view_document_properties};
if (mDocumentState < STATE_LOADED) {
for (final int id : ids) {
final MenuItem item = menu.findItem(id);
if (item.isVisible()) {
item.setVisible(false);
}
}
} else if (mDocumentState == STATE_LOADED) {
for (final int id : ids) {
final MenuItem item = menu.findItem(id);
if (!item.isVisible()) {
item.setVisible(true);
}
}
mDocumentState = STATE_END;
}
enableDisableMenuItem(menu.findItem(R.id.action_next), mPage < mNumPages);
enableDisableMenuItem(menu.findItem(R.id.action_previous), mPage > 1);
2019-11-18 11:49:58 +01:00
return true;
}
public boolean onOptionsItemSelected(MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.action_previous) {
onJumpToPageInDocument(mPage - 1);
return true;
} else if (itemId == R.id.action_next) {
onJumpToPageInDocument(mPage + 1);
return true;
} else if (itemId == R.id.action_first) {
onJumpToPageInDocument(1);
return true;
} else if (itemId == R.id.action_last) {
onJumpToPageInDocument(mNumPages);
return true;
} else if (itemId == R.id.action_rotate_clockwise) {
documentOrientationChanged(90);
return true;
} else if (itemId == R.id.action_rotate_counterclockwise) {
documentOrientationChanged(-90);
return true;
} else if (itemId == R.id.action_view_document_properties) {
DocumentPropertiesFragment
.newInstance(mDocumentProperties)
2022-02-18 15:03:53 +01:00
.show(activity.getSupportFragmentManager(), DocumentPropertiesFragment.TAG);
return true;
} else if (itemId == R.id.action_jump_to_page) {
2022-02-18 15:03:53 +01:00
JumpToPageFragment.newInstance(this)
.show(activity.getSupportFragmentManager(), JumpToPageFragment.TAG);
return true;
}
2022-02-18 15:03:53 +01:00
return false;
}
}