From b1ff81bd2b58df90415095fc06dd4b85c74cc87d Mon Sep 17 00:00:00 2001
From: inthewaves <26474149+inthewaves@users.noreply.github.com>
Date: Wed, 26 Aug 2020 17:57:52 -0700
Subject: [PATCH 1/7] switch to using Fragments and ViewModel
- Host the WebView and all of its related code inside of a Fragment
instead of an Activity. Fragments can give us more flexibility in the
future on how we can display the WebView, how we could manage some
bottom menu via a BottomNavigationView or similar, etc.
- Use a ViewModel and LiveData architecture in order to move away from
Loaders, which are now deprecated. Almost all state information for the
viewer (like current page, zoom ratio) is now stored in the ViewModel,
which survives configuration changes.
- Rewrite the document property parsing in DocumentPropertiesLoader in
Kotlin, taking advantage of Kotlin coroutines to do asynchronous
parsing of the document properties.
- Dynamically update the properties dialog after PDF loads.
If viewing the PDF properties while the PDF loads, before, the dialog
would show an error message. Now, the error message will be swapped out
with the parsed info as soon the document properties have been parsed.
This is done using an Observer on LiveData.
- Use an alpha version of AndroidX Fragment so that we can use a simpler
way to pass data between two Fragments:
https://developer.android.com/training/basics/fragments/pass-data-between
- Alpha version of Fragments also has ActivityResultLauncher, which
simplifies the SAF launch
https://developer.android.com/training/basics/intents/result
"While the underlying startActivityForResult() and onActivityResult()
APIs are available on the Activity class on all API levels, it is
strongly recommended to use the Activity Result APIs introduced in
AndroidX Activity 1.2.0-alpha02 and Fragment 1.3.0-alpha02."
Closes #69, closes #70
---
app/build.gradle | 10 +
app/src/debug/res/values/strings.xml | 4 +
app/src/main/AndroidManifest.xml | 37 +-
.../pdfviewer/PdfViewerActivity.java | 24 ++
...{PdfViewer.java => PdfViewerFragment.java} | 359 +++++++++---------
.../fragment/DocumentPropertiesFragment.java | 56 +--
.../fragment/JumpToPageFragment.java | 22 +-
.../loader/DocumentPropertiesLoader.java | 149 --------
.../pdfviewer/viewmodel/PdfViewerViewModel.kt | 119 ++++++
.../main/res/layout/activity_pdf_viewer.xml | 9 +
.../{webview.xml => fragment_webview.xml} | 0
.../{pdf_viewer.xml => menu_pdf_viewer.xml} | 0
build.gradle | 2 +
13 files changed, 412 insertions(+), 379 deletions(-)
create mode 100644 app/src/debug/res/values/strings.xml
create mode 100644 app/src/main/java/org/grapheneos/pdfviewer/PdfViewerActivity.java
rename app/src/main/java/org/grapheneos/pdfviewer/{PdfViewer.java => PdfViewerFragment.java} (57%)
delete mode 100644 app/src/main/java/org/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.java
create mode 100644 app/src/main/java/org/grapheneos/pdfviewer/viewmodel/PdfViewerViewModel.kt
create mode 100644 app/src/main/res/layout/activity_pdf_viewer.xml
rename app/src/main/res/layout/{webview.xml => fragment_webview.xml} (100%)
rename app/src/main/res/menu/{pdf_viewer.xml => menu_pdf_viewer.xml} (100%)
diff --git a/app/build.gradle b/app/build.gradle
index 19506255c..ad62f2073 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,4 +1,5 @@
apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
android {
compileSdkVersion 29
@@ -36,6 +37,12 @@ dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
+ implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+ implementation "androidx.core:core-ktx:1.3.1"
+ implementation "androidx.fragment:fragment:1.3.0-alpha08"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
}
def props = new Properties()
@@ -61,3 +68,6 @@ if (propFile.canRead()) {
println 'signing.properties not found'
android.buildTypes.release.signingConfig = null
}
+repositories {
+ mavenCentral()
+}
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
new file mode 100644
index 000000000..c4caf6728
--- /dev/null
+++ b/app/src/debug/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ PDF Viewer Debug
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index dc055a63e..29ec74253 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,29 +1,38 @@
-
-
+ package="org.grapheneos.pdfviewer"
+ android:targetSandboxVersion="2">
+
+
+
+
+
+
-
-
+
+
-
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerActivity.java b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerActivity.java
new file mode 100644
index 000000000..cedd17726
--- /dev/null
+++ b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerActivity.java
@@ -0,0 +1,24 @@
+package org.grapheneos.pdfviewer;
+
+import android.os.Bundle;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+public class PdfViewerActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_pdf_viewer);
+
+ if (savedInstanceState == null) {
+ PdfViewerFragment fragment = PdfViewerFragment.newInstance();
+ getSupportFragmentManager()
+ .beginTransaction()
+ .setPrimaryNavigationFragment(fragment)
+ .add(R.id.pdf_fragment_container, fragment)
+ .commit();
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
similarity index 57%
rename from app/src/main/java/org/grapheneos/pdfviewer/PdfViewer.java
rename to app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
index 606a1da79..63c7d7a98 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewer.java
+++ b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
@@ -1,7 +1,6 @@
package org.grapheneos.pdfviewer;
import android.annotation.SuppressLint;
-import android.app.Activity;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
@@ -9,10 +8,12 @@
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
+import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
+import android.view.ViewGroup;
import android.webkit.CookieManager;
import android.webkit.JavascriptInterface;
import android.webkit.WebResourceRequest;
@@ -23,124 +24,139 @@
import android.widget.TextView;
import android.widget.Toast;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.loader.app.LoaderManager;
-import androidx.loader.content.Loader;
+import androidx.activity.result.ActivityResultCallback;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.snackbar.Snackbar;
import org.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment;
import org.grapheneos.pdfviewer.fragment.JumpToPageFragment;
-import org.grapheneos.pdfviewer.loader.DocumentPropertiesLoader;
+import org.grapheneos.pdfviewer.viewmodel.PdfViewerViewModel;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
-public class PdfViewer extends AppCompatActivity implements LoaderManager.LoaderCallbacks> {
+public class PdfViewerFragment extends Fragment {
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 String CONTENT_SECURITY_POLICY =
- "default-src 'none'; " +
- "form-action 'none'; " +
- "connect-src https://localhost/placeholder.pdf; " +
- "img-src blob: 'self'; " +
- "script-src 'self' 'resource://pdf.js'; " +
- "style-src 'self'; " +
- "frame-ancestors 'none'; " +
- "base-uri 'none'";
+ "default-src 'none'; " +
+ "form-action 'none'; " +
+ "connect-src https://localhost/placeholder.pdf; " +
+ "img-src blob: 'self'; " +
+ "script-src 'self' 'resource://pdf.js'; " +
+ "style-src 'self'; " +
+ "frame-ancestors 'none'; " +
+ "base-uri 'none'";
private static final String FEATURE_POLICY =
- "accelerometer 'none'; " +
- "ambient-light-sensor 'none'; " +
- "autoplay 'none'; " +
- "camera 'none'; " +
- "encrypted-media 'none'; " +
- "fullscreen 'none'; " +
- "geolocation 'none'; " +
- "gyroscope 'none'; " +
- "magnetometer 'none'; " +
- "microphone 'none'; " +
- "midi 'none'; " +
- "payment 'none'; " +
- "picture-in-picture 'none'; " +
- "speaker 'none'; " +
- "sync-xhr 'none'; " +
- "usb 'none'; " +
- "vr 'none'";
+ "accelerometer 'none'; " +
+ "ambient-light-sensor 'none'; " +
+ "autoplay 'none'; " +
+ "camera 'none'; " +
+ "encrypted-media 'none'; " +
+ "fullscreen 'none'; " +
+ "geolocation 'none'; " +
+ "gyroscope 'none'; " +
+ "magnetometer 'none'; " +
+ "microphone 'none'; " +
+ "midi 'none'; " +
+ "payment 'none'; " +
+ "picture-in-picture 'none'; " +
+ "speaker 'none'; " +
+ "sync-xhr 'none'; " +
+ "usb 'none'; " +
+ "vr 'none'";
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 int windowInsetTop;
- private List mDocumentProperties;
+ private WebView mWebView;
+ private int mWindowInsetsTop;
private InputStream mInputStream;
- private WebView mWebView;
- private TextView mTextView;
private Toast mToast;
- private Snackbar snackbar;
+ private TextView mTextView;
+ private Snackbar mSnackbar;
+
+ private PdfViewerViewModel mViewModel;
private class Channel {
@JavascriptInterface
public int getWindowInsetTop() {
- return windowInsetTop;
+ return mWindowInsetsTop;
}
@JavascriptInterface
public int getPage() {
- return mPage;
+ return mViewModel.getPage();
}
@JavascriptInterface
public float getZoomRatio() {
- return mZoomRatio;
+ return mViewModel.getZoomRatio();
}
@JavascriptInterface
public int getDocumentOrientationDegrees() {
- return mDocumentOrientationDegrees;
+ return mViewModel.getDocumentOrientationDegrees();
}
@JavascriptInterface
public void setNumPages(int numPages) {
- mNumPages = numPages;
- runOnUiThread(PdfViewer.this::invalidateOptionsMenu);
+ mViewModel.setNumPages(numPages);
+ requireActivity().runOnUiThread(() -> requireActivity().invalidateOptionsMenu());
}
@JavascriptInterface
public void setDocumentProperties(final String properties) {
- if (mDocumentProperties != null) {
- throw new SecurityException("mDocumentProperties not null");
+ final List list = mViewModel.getDocumentProperties().getValue();
+ if (list != null && list.isEmpty()) {
+ mViewModel.loadProperties(properties);
+ } else {
+ Log.d(TAG, "setDocumentProperties: did not load in properties");
}
-
- final Bundle args = new Bundle();
- args.putString(KEY_PROPERTIES, properties);
- runOnUiThread(() -> {
- LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesLoader.ID, args, PdfViewer.this);
- });
}
}
+ public PdfViewerFragment() {
+ // Required empty public constructor
+ }
+
+ public static PdfViewerFragment newInstance() {
+ return new PdfViewerFragment();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ getParentFragmentManager().setFragmentResultListener(JumpToPageFragment.REQUEST_KEY,
+ this, (requestKey, result) -> {
+ final int newPage = result.getInt(JumpToPageFragment.BUNDLE_KEY);
+ onJumpToPageInDocument(newPage);
+ });
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_webview, container, false);
+ }
+
// Can be removed once minSdkVersion >= 26
@SuppressWarnings("deprecation")
private void disableSaveFormData(final WebSettings settings) {
@@ -149,15 +165,15 @@ private void disableSaveFormData(final WebSettings settings) {
@Override
@SuppressLint({"SetJavaScriptEnabled", "ClickableViewAccessibility"})
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- setContentView(R.layout.webview);
- mWebView = findViewById(R.id.webview);
+ mViewModel = new ViewModelProvider(requireActivity()).get(PdfViewerViewModel.class);
- mWebView.setOnApplyWindowInsetsListener((view, insets) -> {
- windowInsetTop = insets.getSystemWindowInsetTop();
+ mWebView = view.findViewById(R.id.webview);
+
+ mWebView.setOnApplyWindowInsetsListener((v, insets) -> {
+ mWindowInsetsTop = insets.getSystemWindowInsetTop();
mWebView.evaluateJavascript("updateInset()", null);
return insets;
});
@@ -176,7 +192,7 @@ protected void onCreate(Bundle savedInstanceState) {
mWebView.setWebViewClient(new WebViewClient() {
private WebResourceResponse fromAsset(final String mime, final String path) {
try {
- InputStream inputStream = getAssets().open(path.substring(1));
+ InputStream inputStream = requireActivity().getAssets().open(path.substring(1));
return new WebResourceResponse(mime, null, inputStream);
} catch (IOException e) {
return null;
@@ -230,18 +246,18 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request
@Override
public void onPageFinished(WebView view, String url) {
mDocumentState = STATE_LOADED;
- invalidateOptionsMenu();
+ requireActivity().invalidateOptionsMenu();
}
});
- GestureHelper.attach(PdfViewer.this, mWebView,
+ GestureHelper.attach(getContext(), mWebView,
new GestureHelper.GestureListener() {
@Override
public boolean onTapUp() {
- if (mUri != null) {
+ if (mViewModel.getUri() != null) {
mWebView.evaluateJavascript("isTextSelected()", selection -> {
if (!Boolean.valueOf(selection)) {
- if ((getWindow().getDecorView().getSystemUiVisibility() &
+ if ((requireActivity().getWindow().getDecorView().getSystemUiVisibility() &
View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
hideSystemUi();
} else {
@@ -270,73 +286,57 @@ public void onZoomEnd() {
}
});
- mTextView = new TextView(this);
+ mTextView = new TextView(getContext());
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(mWebView, "", Snackbar.LENGTH_LONG);
+ mSnackbar = Snackbar.make(mWebView, "", Snackbar.LENGTH_LONG);
- final Intent intent = getIntent();
- if (Intent.ACTION_VIEW.equals(intent.getAction())) {
+ final Intent intent = requireActivity().getIntent();
+ if (savedInstanceState == null && Intent.ACTION_VIEW.equals(intent.getAction())) {
if (!"application/pdf".equals(intent.getType())) {
- snackbar.setText(R.string.invalid_mime_type).show();
+ mSnackbar.setText(R.string.invalid_mime_type).show();
return;
}
- mUri = intent.getData();
- mPage = 1;
- }
- 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);
+ mViewModel.setUri(intent.getData());
+ mViewModel.setPage(1);
}
- if (mUri != null) {
- if ("file".equals(mUri.getScheme())) {
- snackbar.setText(R.string.legacy_file_uri).show();
+ final Uri uri = mViewModel.getUri();
+ if (uri != null) {
+ if ("file".equals(uri.getScheme())) {
+ mSnackbar.setText(R.string.legacy_file_uri).show();
return;
}
- loadPdf();
+ loadPdf(savedInstanceState == null);
}
}
- @Override
- public Loader> onCreateLoader(int id, Bundle args) {
- return new DocumentPropertiesLoader(this, args.getString(KEY_PROPERTIES), mNumPages, mUri);
- }
-
- @Override
- public void onLoadFinished(Loader> loader, List data) {
- mDocumentProperties = data;
- LoaderManager.getInstance(this).destroyLoader(DocumentPropertiesLoader.ID);
- }
-
- @Override
- public void onLoaderReset(Loader> loader) {
- mDocumentProperties = null;
- }
+ private void loadPdf(boolean isLoadingNewPdf) {
+ final Uri uri = mViewModel.getUri();
+ if (uri == null) {
+ return;
+ }
- private void loadPdf() {
try {
if (mInputStream != null) {
mInputStream.close();
}
- mInputStream = getContentResolver().openInputStream(mUri);
+ mInputStream = requireActivity().getContentResolver().openInputStream(uri);
} catch (IOException e) {
- snackbar.setText(R.string.io_error).show();
+ mSnackbar.setText(R.string.io_error).show();
+ mViewModel.clearDocumentProperties();
return;
}
+ if (isLoadingNewPdf) {
+ mViewModel.clearDocumentProperties();
+ }
+
showSystemUi();
mWebView.loadUrl("https://localhost/viewer.html");
}
@@ -346,33 +346,44 @@ private void renderPage(final int zoom) {
}
private void documentOrientationChanged(final int orientationDegreesOffset) {
- mDocumentOrientationDegrees = (mDocumentOrientationDegrees + orientationDegreesOffset) % 360;
- if (mDocumentOrientationDegrees < 0) {
- mDocumentOrientationDegrees += 360;
+ int newOrientation = (mViewModel.getDocumentOrientationDegrees()
+ + orientationDegreesOffset) % 360;
+ if (newOrientation < 0) {
+ newOrientation += 360;
}
+ mViewModel.setDocumentOrientationDegrees(newOrientation);
renderPage(0);
}
+ private ActivityResultLauncher mGetDocumentUriLauncher = registerForActivityResult(
+ new ActivityResultContracts.GetContent(), new ActivityResultCallback() {
+ @Override
+ public void onActivityResult(Uri uri) {
+ mViewModel.setUri(uri);
+ mViewModel.setPage(1);
+ loadPdf(true);
+ }
+ });
+
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);
+ mGetDocumentUriLauncher.launch("application/pdf");
}
private void zoomIn(float value, boolean end) {
- if (mZoomRatio < MAX_ZOOM_RATIO) {
- mZoomRatio = Math.min(mZoomRatio + value, MAX_ZOOM_RATIO);
+ final float zoomRatio = mViewModel.getZoomRatio();
+ if (zoomRatio < MAX_ZOOM_RATIO) {
+ mViewModel.setZoomRatio(Math.min(zoomRatio + value, MAX_ZOOM_RATIO));
renderPage(end ? 1 : 2);
- invalidateOptionsMenu();
+ requireActivity().invalidateOptionsMenu();
}
}
private void zoomOut(float value, boolean end) {
- if (mZoomRatio > MIN_ZOOM_RATIO) {
- mZoomRatio = Math.max(mZoomRatio - value, MIN_ZOOM_RATIO);
+ final float zoomRatio = mViewModel.getZoomRatio();
+ if (zoomRatio > MIN_ZOOM_RATIO) {
+ mViewModel.setZoomRatio(Math.max(zoomRatio - value, MIN_ZOOM_RATIO));
renderPage(end ? 1 : 2);
- invalidateOptionsMenu();
+ requireActivity().invalidateOptionsMenu();
}
}
@@ -391,61 +402,39 @@ private static void enableDisableMenuItem(MenuItem item, boolean enable) {
}
public void onJumpToPageInDocument(final int selected_page) {
- if (selected_page >= 1 && selected_page <= mNumPages && mPage != selected_page) {
- mPage = selected_page;
+ if (selected_page >= 1
+ && selected_page <= mViewModel.getNumPages()
+ && mViewModel.getPage() != selected_page) {
+ mViewModel.setPage(selected_page);
renderPage(0);
showPageNumber();
- invalidateOptionsMenu();
+ requireActivity().invalidateOptionsMenu();
}
}
private void showSystemUi() {
- getWindow().getDecorView().setSystemUiVisibility(
+ requireActivity().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_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
private void hideSystemUi() {
- getWindow().getDecorView().setSystemUiVisibility(
+ requireActivity().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);
- }
-
- @Override
- public void onSaveInstanceState(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();
- }
- }
+ 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);
}
private void showPageNumber() {
if (mToast != null) {
mToast.cancel();
}
- mTextView.setText(String.format("%s/%s", mPage, mNumPages));
- mToast = new Toast(getApplicationContext());
+ mTextView.setText(String.format("%s/%s", mViewModel.getPage(), mViewModel.getNumPages()));
+ mToast = new Toast(getContext());
mToast.setGravity(Gravity.BOTTOM | Gravity.END, PADDING, PADDING);
mToast.setDuration(Toast.LENGTH_SHORT);
mToast.setView(mTextView);
@@ -453,19 +442,17 @@ private void showPageNumber() {
}
@Override
- public boolean onCreateOptionsMenu(Menu menu) {
- super.onCreateOptionsMenu(menu);
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.pdf_viewer, menu);
- return true;
+ public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.menu_pdf_viewer, menu);
+ super.onCreateOptionsMenu(menu, inflater);
}
@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,
+ public void onPrepareOptionsMenu(@NonNull 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 };
+ R.id.action_view_document_properties};
if (mDocumentState < STATE_LOADED) {
for (final int id : ids) {
final MenuItem item = menu.findItem(id);
@@ -483,23 +470,25 @@ public boolean onPrepareOptionsMenu(Menu menu) {
mDocumentState = STATE_END;
}
- 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;
+ enableDisableMenuItem(menu.findItem(R.id.action_zoom_in),
+ mViewModel.getZoomRatio() != MAX_ZOOM_RATIO);
+ enableDisableMenuItem(menu.findItem(R.id.action_zoom_out),
+ mViewModel.getZoomRatio() != MIN_ZOOM_RATIO);
+ enableDisableMenuItem(menu.findItem(R.id.action_next),
+ mViewModel.getPage() < mViewModel.getNumPages());
+ enableDisableMenuItem(menu.findItem(R.id.action_previous),
+ mViewModel.getPage() > 1);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_previous:
- onJumpToPageInDocument(mPage - 1);
+ onJumpToPageInDocument(mViewModel.getPage() - 1);
return true;
case R.id.action_next:
- onJumpToPageInDocument(mPage + 1);
+ onJumpToPageInDocument(mViewModel.getPage() + 1);
return true;
case R.id.action_first:
@@ -507,7 +496,7 @@ public boolean onOptionsItemSelected(MenuItem item) {
return true;
case R.id.action_last:
- onJumpToPageInDocument(mNumPages);
+ onJumpToPageInDocument(mViewModel.getNumPages());
return true;
case R.id.action_open:
@@ -532,17 +521,17 @@ public boolean onOptionsItemSelected(MenuItem item) {
case R.id.action_view_document_properties:
DocumentPropertiesFragment
- .newInstance(mDocumentProperties)
- .show(getSupportFragmentManager(), DocumentPropertiesFragment.TAG);
+ .newInstance()
+ .show(getParentFragmentManager(), DocumentPropertiesFragment.TAG);
return true;
case R.id.action_jump_to_page:
new JumpToPageFragment()
- .show(getSupportFragmentManager(), JumpToPageFragment.TAG);
+ .show(getParentFragmentManager(), JumpToPageFragment.TAG);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.java b/app/src/main/java/org/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.java
index a2870654e..990fa6326 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.java
+++ b/app/src/main/java/org/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.java
@@ -8,36 +8,33 @@
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
-
-import java.util.ArrayList;
-import java.util.List;
+import androidx.lifecycle.ViewModelProvider;
import org.grapheneos.pdfviewer.R;
+import org.grapheneos.pdfviewer.viewmodel.PdfViewerViewModel;
+
+import java.util.Collections;
+import java.util.List;
public class DocumentPropertiesFragment extends DialogFragment {
public static final String TAG = "DocumentPropertiesFragment";
- private static final String KEY_DOCUMENT_PROPERTIES = "document_properties";
-
- private List mDocumentProperties;
-
- public static DocumentPropertiesFragment newInstance(final List metaData) {
- final DocumentPropertiesFragment fragment = new DocumentPropertiesFragment();
- final Bundle args = new Bundle();
+ private ArrayAdapter mAdapter;
+ private PdfViewerViewModel mModel;
- args.putCharSequenceArrayList(KEY_DOCUMENT_PROPERTIES, (ArrayList) metaData);
- fragment.setArguments(args);
-
- return fragment;
+ public static DocumentPropertiesFragment newInstance() {
+ return new DocumentPropertiesFragment();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- if (getArguments() != null) {
- mDocumentProperties = getArguments().getStringArrayList(KEY_DOCUMENT_PROPERTIES);
- }
+ mModel = new ViewModelProvider(requireActivity()).get(PdfViewerViewModel.class);
+
+ List list = mModel.getDocumentProperties().getValue();
+ mAdapter = new ArrayAdapter<>(requireActivity(), android.R.layout.simple_list_item_1,
+ list != null ? list : Collections.emptyList());
}
@NonNull
@@ -46,14 +43,23 @@ public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity activity = requireActivity();
final AlertDialog.Builder dialog = new AlertDialog.Builder(activity)
.setPositiveButton(android.R.string.ok, null);
+ dialog.setAdapter(mAdapter, null);
+
+ final List list = mModel.getDocumentProperties().getValue();
+ dialog.setTitle(getTitleStringIdForPropertiesState(list));
+
+ final AlertDialog alertDialog = dialog.create();
+ mModel.getDocumentProperties().observe(requireActivity(), charSequences -> {
+ alertDialog.setTitle(getTitleStringIdForPropertiesState(charSequences));
+ mAdapter.notifyDataSetChanged();
+ });
+
+ return alertDialog;
+ }
- 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();
+ private int getTitleStringIdForPropertiesState(final List properties) {
+ return properties == null || properties.isEmpty()
+ ? R.string.document_properties_retrieval_failed
+ : R.string.action_view_document_properties;
}
}
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/fragment/JumpToPageFragment.java b/app/src/main/java/org/grapheneos/pdfviewer/fragment/JumpToPageFragment.java
index 53c236f5f..4e0b8bab1 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/fragment/JumpToPageFragment.java
+++ b/app/src/main/java/org/grapheneos/pdfviewer/fragment/JumpToPageFragment.java
@@ -9,12 +9,16 @@
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
+import androidx.lifecycle.ViewModelProvider;
-import org.grapheneos.pdfviewer.PdfViewer;
+import org.grapheneos.pdfviewer.viewmodel.PdfViewerViewModel;
public class JumpToPageFragment extends DialogFragment {
public static final String TAG = "JumpToPageFragment";
+ public static final String REQUEST_KEY = "jumpToPage";
+ public static final String BUNDLE_KEY = "jumpToPageBundle";
+
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";
@@ -33,24 +37,30 @@ public void onActivityCreated(Bundle savedInstanceState) {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final PdfViewerViewModel model =
+ new ViewModelProvider(requireActivity()).get(PdfViewerViewModel.class);
+
mPicker = new NumberPicker(getActivity());
mPicker.setMinValue(1);
- mPicker.setMaxValue(((PdfViewer)getActivity()).mNumPages);
- mPicker.setValue(((PdfViewer)getActivity()).mPage);
+ mPicker.setMaxValue(model.getNumPages());
+ mPicker.setValue(model.getPage());
- final FrameLayout layout = new FrameLayout(getActivity());
+ final FrameLayout layout = new FrameLayout(requireActivity());
layout.addView(mPicker, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER));
- return new AlertDialog.Builder(getActivity())
+ return new AlertDialog.Builder(requireActivity())
.setView(layout)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
mPicker.clearFocus();
- ((PdfViewer)getActivity()).onJumpToPageInDocument(mPicker.getValue());
+
+ Bundle result = new Bundle();
+ result.putInt(BUNDLE_KEY, mPicker.getValue());
+ getParentFragmentManager().setFragmentResult(REQUEST_KEY, result);
}
})
.setNegativeButton(android.R.string.cancel, null)
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.java b/app/src/main/java/org/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.java
deleted file mode 100644
index f271c055d..000000000
--- a/app/src/main/java/org/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.java
+++ /dev/null
@@ -1,149 +0,0 @@
-package org.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 org.grapheneos.pdfviewer.R;
-import org.grapheneos.pdfviewer.Utils;
-
-public class DocumentPropertiesLoader extends AsyncTaskLoader> {
- public static final String TAG = "DocumentPropertiesLoader";
-
- public static final int ID = 1;
-
- private final String mProperties;
- private final int mNumPages;
- 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 loadInBackground() {
- final Context context = getContext();
-
- final String[] names = context.getResources().getStringArray(R.array.property_names);
- final List 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.valueOf(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 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 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;
- }
-}
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/PdfViewerViewModel.kt b/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/PdfViewerViewModel.kt
new file mode 100644
index 000000000..f4d9ca274
--- /dev/null
+++ b/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/PdfViewerViewModel.kt
@@ -0,0 +1,119 @@
+package org.grapheneos.pdfviewer.viewmodel
+
+import android.app.Application
+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.annotation.NonNull
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.grapheneos.pdfviewer.R
+import org.grapheneos.pdfviewer.Utils
+import org.json.JSONException
+import org.json.JSONObject
+import java.text.ParseException
+
+private const val TAG = "PdfViewerViewModel"
+
+private const val MISSING_STRING = "-"
+
+class PdfViewerViewModel(application: Application) : AndroidViewModel(application) {
+ var uri: Uri? = null
+ var page: Int = 0
+ var numPages: Int = 0
+ var zoomRatio: Float = 1f
+ var documentOrientationDegrees: Int = 0
+
+ private val documentProperties: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ @NonNull
+ fun getDocumentProperties(): LiveData> = documentProperties
+
+ fun clearDocumentProperties() {
+ val list = documentProperties.value as? ArrayList ?: ArrayList()
+ list.clear()
+ documentProperties.postValue(list)
+ }
+
+ fun loadProperties(propertiesString: String) {
+ if (uri == null) {
+ Log.w(TAG, "Failed to parse properties: Uri is null")
+ clearDocumentProperties()
+ return
+ }
+
+ viewModelScope.launch(Dispatchers.Default) {
+ val context = getApplication()
+ val names = context.resources.getStringArray(R.array.property_names);
+ val properties = ArrayList()
+
+ val cursor: Cursor? = context.contentResolver.query(uri!!, null, null, null, null);
+ cursor?.let {
+ it.moveToFirst();
+
+ val indexOfName = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ if (indexOfName >= 0) {
+ properties.add(getProperty(null, names[0], it.getString(indexOfName)))
+ }
+
+ val indexOfSize = it.getColumnIndex(OpenableColumns.SIZE)
+ if (indexOfSize >= 0) {
+ val fileSize = it.getString(indexOfSize).toLong()
+ properties.add(getProperty(null, names[1], Utils.parseFileSize(fileSize)))
+ }
+ }
+ cursor?.close()
+
+ val specNames = arrayOf("Title", "Author", "Subject", "Keywords", "CreationDate",
+ "ModDate", "Producer", "Creator", "PDFFormatVersion", numPages.toString())
+ try {
+ val json = JSONObject(propertiesString)
+ for (i in 2 until names.size) {
+ properties.add(getProperty(json, names[i], specNames[i - 2]))
+ }
+
+ Log.d(TAG, "Successfully parsed properties")
+ val list = documentProperties.value as ArrayList
+ list.run {
+ clear()
+ addAll(properties)
+ documentProperties.postValue(this)
+ }
+ } catch (e: JSONException) {
+ clearDocumentProperties()
+ Log.e(TAG, "Failed to parse properties: ${e.message}", e)
+ }
+ }
+ }
+
+ private fun getProperty(json: JSONObject?, name: String, specName: String): CharSequence {
+ val property = SpannableStringBuilder(name).append(":\n")
+ val value = json?.optString(specName, MISSING_STRING) ?: specName
+
+ val valueToAppend = if (specName.endsWith("Date") && value != MISSING_STRING) {
+ try {
+ Utils.parseDate(value)
+ } catch (e: ParseException) {
+ Log.w(TAG, "${e.message} for $value at offset: ${e.errorOffset}")
+ val context = getApplication()
+ context.getString(R.string.document_properties_invalid_date)
+ }
+ } else {
+ value
+ }
+ property.append(valueToAppend)
+ property.setSpan(StyleSpan(Typeface.BOLD), 0, name.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ return property
+ }
+}
diff --git a/app/src/main/res/layout/activity_pdf_viewer.xml b/app/src/main/res/layout/activity_pdf_viewer.xml
new file mode 100644
index 000000000..776c35ac8
--- /dev/null
+++ b/app/src/main/res/layout/activity_pdf_viewer.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/webview.xml b/app/src/main/res/layout/fragment_webview.xml
similarity index 100%
rename from app/src/main/res/layout/webview.xml
rename to app/src/main/res/layout/fragment_webview.xml
diff --git a/app/src/main/res/menu/pdf_viewer.xml b/app/src/main/res/menu/menu_pdf_viewer.xml
similarity index 100%
rename from app/src/main/res/menu/pdf_viewer.xml
rename to app/src/main/res/menu/menu_pdf_viewer.xml
diff --git a/build.gradle b/build.gradle
index 4b7409b41..dd815d8b7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,12 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
+ ext.kotlin_version = '1.4.0'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.1'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
From 25fed4eb4a063cf12e94ad801ce493114ba5a400 Mon Sep 17 00:00:00 2001
From: inthewaves <26474149+inthewaves@users.noreply.github.com>
Date: Thu, 27 Aug 2020 13:04:44 -0700
Subject: [PATCH 2/7] save/load state using ViewModel Saved State module
- Save state using the ViewModel Saved State module, since the ViewModel
alone doesn't survive system-initiated process death. The ViewModel is
good for configuration changes.
https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate
- Save the document properties state to prevent reparsing and to show
the properties if the Activity gets killed while the properties dialog
is open.
- Don't load null URIs from open document action.
---
.../pdfviewer/PdfViewerFragment.java | 29 +++++++---
.../fragment/DocumentPropertiesFragment.java | 32 +++++++----
.../pdfviewer/viewmodel/PdfViewerViewModel.kt | 55 ++++++++++++++-----
3 files changed, 82 insertions(+), 34 deletions(-)
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
index 63c7d7a98..333de84af 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
+++ b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
@@ -44,7 +44,7 @@
import java.util.List;
public class PdfViewerFragment extends Fragment {
- public static final String TAG = "PdfViewer";
+ public static final String TAG = PdfViewerFragment.class.getSimpleName();
private static final String CONTENT_SECURITY_POLICY =
"default-src 'none'; " +
@@ -125,9 +125,10 @@ public void setNumPages(int numPages) {
public void setDocumentProperties(final String properties) {
final List list = mViewModel.getDocumentProperties().getValue();
if (list != null && list.isEmpty()) {
- mViewModel.loadProperties(properties);
+ mViewModel.loadProperties(properties, requireActivity().getApplicationContext());
} else {
- Log.d(TAG, "setDocumentProperties: did not load in properties");
+ Log.d(TAG, "setDocumentProperties: did not load in properties, since "
+ + (list == null ? "list is null" : "list is not empty"));
}
}
}
@@ -163,11 +164,15 @@ private void disableSaveFormData(final WebSettings settings) {
settings.setSaveFormData(false);
}
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mViewModel.saveState();
+ }
+
@Override
@SuppressLint({"SetJavaScriptEnabled", "ClickableViewAccessibility"})
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
-
-
mViewModel = new ViewModelProvider(requireActivity()).get(PdfViewerViewModel.class);
mWebView = view.findViewById(R.id.webview);
@@ -295,7 +300,7 @@ public void onZoomEnd() {
mSnackbar = Snackbar.make(mWebView, "", Snackbar.LENGTH_LONG);
final Intent intent = requireActivity().getIntent();
- if (savedInstanceState == null && Intent.ACTION_VIEW.equals(intent.getAction())) {
+ if (Intent.ACTION_VIEW.equals(intent.getAction())) {
if (!"application/pdf".equals(intent.getType())) {
mSnackbar.setText(R.string.invalid_mime_type).show();
return;
@@ -305,6 +310,10 @@ public void onZoomEnd() {
mViewModel.setPage(1);
}
+ if (savedInstanceState != null) {
+ mViewModel.restoreState();
+ }
+
final Uri uri = mViewModel.getUri();
if (uri != null) {
if ("file".equals(uri.getScheme())) {
@@ -359,9 +368,11 @@ private void documentOrientationChanged(final int orientationDegreesOffset) {
new ActivityResultContracts.GetContent(), new ActivityResultCallback() {
@Override
public void onActivityResult(Uri uri) {
- mViewModel.setUri(uri);
- mViewModel.setPage(1);
- loadPdf(true);
+ if (uri != null) {
+ mViewModel.setUri(uri);
+ mViewModel.setPage(1);
+ loadPdf(true);
+ }
}
});
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.java b/app/src/main/java/org/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.java
index 990fa6326..88037ce29 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.java
+++ b/app/src/main/java/org/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.java
@@ -3,11 +3,13 @@
import android.app.Activity;
import android.app.Dialog;
import android.os.Bundle;
+import android.util.Log;
import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
+import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import org.grapheneos.pdfviewer.R;
@@ -21,6 +23,7 @@ public class DocumentPropertiesFragment extends DialogFragment {
private ArrayAdapter mAdapter;
private PdfViewerViewModel mModel;
+ private Observer> mPropertiesObserver;
public static DocumentPropertiesFragment newInstance() {
return new DocumentPropertiesFragment();
@@ -29,12 +32,12 @@ public static DocumentPropertiesFragment newInstance() {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ }
- mModel = new ViewModelProvider(requireActivity()).get(PdfViewerViewModel.class);
-
- List list = mModel.getDocumentProperties().getValue();
- mAdapter = new ArrayAdapter<>(requireActivity(), android.R.layout.simple_list_item_1,
- list != null ? list : Collections.emptyList());
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mModel.getDocumentProperties().removeObserver(mPropertiesObserver);
}
@NonNull
@@ -43,16 +46,25 @@ public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity activity = requireActivity();
final AlertDialog.Builder dialog = new AlertDialog.Builder(activity)
.setPositiveButton(android.R.string.ok, null);
- dialog.setAdapter(mAdapter, null);
+ mModel = new ViewModelProvider(requireActivity()).get(PdfViewerViewModel.class);
final List list = mModel.getDocumentProperties().getValue();
+ mAdapter = new ArrayAdapter<>(requireActivity(), android.R.layout.simple_list_item_1,
+ list != null ? list : Collections.emptyList());
+ dialog.setAdapter(mAdapter, null);
+
dialog.setTitle(getTitleStringIdForPropertiesState(list));
final AlertDialog alertDialog = dialog.create();
- mModel.getDocumentProperties().observe(requireActivity(), charSequences -> {
- alertDialog.setTitle(getTitleStringIdForPropertiesState(charSequences));
- mAdapter.notifyDataSetChanged();
- });
+ mPropertiesObserver = new Observer>() {
+ @Override
+ public void onChanged(List charSequences) {
+ Log.d(TAG, "Properties changed!");
+ alertDialog.setTitle(getTitleStringIdForPropertiesState(charSequences));
+ mAdapter.notifyDataSetChanged();
+ }
+ };
+ mModel.getDocumentProperties().observe(requireActivity(), mPropertiesObserver);
return alertDialog;
}
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/PdfViewerViewModel.kt b/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/PdfViewerViewModel.kt
index f4d9ca274..027e70afa 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/PdfViewerViewModel.kt
+++ b/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/PdfViewerViewModel.kt
@@ -1,6 +1,6 @@
package org.grapheneos.pdfviewer.viewmodel
-import android.app.Application
+import android.content.Context
import android.database.Cursor
import android.graphics.Typeface
import android.net.Uri
@@ -10,10 +10,7 @@ import android.text.Spanned
import android.text.style.StyleSpan
import android.util.Log
import androidx.annotation.NonNull
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.grapheneos.pdfviewer.R
@@ -26,15 +23,40 @@ private const val TAG = "PdfViewerViewModel"
private const val MISSING_STRING = "-"
-class PdfViewerViewModel(application: Application) : AndroidViewModel(application) {
+private const val STATE_URI = "uri"
+private const val STATE_PAGE = "page"
+private const val STATE_ZOOMRATIO = "zoomRatio"
+private const val STATE_ORIENTATIONDEGREES = "orientationDegrees"
+private const val STATE_DOCUMENT_PROPERTIES = "documentProperties"
+
+class PdfViewerViewModel(private val state: SavedStateHandle) : ViewModel() {
+ private val documentProperties: MutableLiveData> by lazy {
+ MutableLiveData>(
+ state.get(STATE_DOCUMENT_PROPERTIES) ?: ArrayList())
+ }
var uri: Uri? = null
var page: Int = 0
var numPages: Int = 0
var zoomRatio: Float = 1f
var documentOrientationDegrees: Int = 0
- private val documentProperties: MutableLiveData> by lazy {
- MutableLiveData>()
+ fun restoreState() {
+ state.run {
+ uri = get(STATE_URI)
+ page = get(STATE_PAGE) ?: 0
+ zoomRatio = get(STATE_ZOOMRATIO) ?: 1f
+ documentOrientationDegrees = get(STATE_ORIENTATIONDEGREES) ?: 0
+ }
+ }
+
+ fun saveState() {
+ state.run {
+ set(STATE_URI, uri)
+ set(STATE_PAGE, page)
+ set(STATE_ZOOMRATIO, zoomRatio)
+ set(STATE_ORIENTATIONDEGREES, documentOrientationDegrees)
+ set(STATE_DOCUMENT_PROPERTIES, documentProperties.value as ArrayList)
+ }
}
@NonNull
@@ -44,9 +66,10 @@ class PdfViewerViewModel(application: Application) : AndroidViewModel(applicatio
val list = documentProperties.value as? ArrayList ?: ArrayList()
list.clear()
documentProperties.postValue(list)
+ Log.d(TAG, "clearDocumentProperties")
}
- fun loadProperties(propertiesString: String) {
+ fun loadProperties(propertiesString: String, context: Context) {
if (uri == null) {
Log.w(TAG, "Failed to parse properties: Uri is null")
clearDocumentProperties()
@@ -54,7 +77,6 @@ class PdfViewerViewModel(application: Application) : AndroidViewModel(applicatio
}
viewModelScope.launch(Dispatchers.Default) {
- val context = getApplication()
val names = context.resources.getStringArray(R.array.property_names);
val properties = ArrayList()
@@ -64,13 +86,15 @@ class PdfViewerViewModel(application: Application) : AndroidViewModel(applicatio
val indexOfName = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (indexOfName >= 0) {
- properties.add(getProperty(null, names[0], it.getString(indexOfName)))
+ properties.add(getProperty(null, names[0], it.getString(indexOfName),
+ context))
}
val indexOfSize = it.getColumnIndex(OpenableColumns.SIZE)
if (indexOfSize >= 0) {
val fileSize = it.getString(indexOfSize).toLong()
- properties.add(getProperty(null, names[1], Utils.parseFileSize(fileSize)))
+ properties.add(getProperty(null, names[1], Utils.parseFileSize(fileSize),
+ context))
}
}
cursor?.close()
@@ -80,7 +104,7 @@ class PdfViewerViewModel(application: Application) : AndroidViewModel(applicatio
try {
val json = JSONObject(propertiesString)
for (i in 2 until names.size) {
- properties.add(getProperty(json, names[i], specNames[i - 2]))
+ properties.add(getProperty(json, names[i], specNames[i - 2], context))
}
Log.d(TAG, "Successfully parsed properties")
@@ -97,7 +121,9 @@ class PdfViewerViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- private fun getProperty(json: JSONObject?, name: String, specName: String): CharSequence {
+ private fun getProperty(
+ json: JSONObject?, name: String, specName: String, context: Context
+ ): CharSequence {
val property = SpannableStringBuilder(name).append(":\n")
val value = json?.optString(specName, MISSING_STRING) ?: specName
@@ -106,7 +132,6 @@ class PdfViewerViewModel(application: Application) : AndroidViewModel(applicatio
Utils.parseDate(value)
} catch (e: ParseException) {
Log.w(TAG, "${e.message} for $value at offset: ${e.errorOffset}")
- val context = getApplication()
context.getString(R.string.document_properties_invalid_date)
}
} else {
From 58dbcb41604a4a6a470edd5bec81a7852991d4d1 Mon Sep 17 00:00:00 2001
From: inthewaves <26474149+inthewaves@users.noreply.github.com>
Date: Thu, 27 Aug 2020 16:54:35 -0700
Subject: [PATCH 3/7] don't store state of document properties
Now, we just reconstruct the properties if the app goes through some
system-initiated process death. For configuration changes, the app
still retains the properties in the ViewModel.
Also,
- Move the WebView into a container so that it can be cleared without
giving errors.
- Refactor document parsing even more (+ move into separate file) and
conform to Kotlin style guide
---
.../pdfviewer/PdfViewerFragment.java | 41 ++++++--
.../viewmodel/DocumentPropertiesParser.kt | 88 +++++++++++++++++
.../pdfviewer/viewmodel/PdfViewerViewModel.kt | 99 +++----------------
app/src/main/res/layout/fragment_webview.xml | 15 ++-
4 files changed, 149 insertions(+), 94 deletions(-)
create mode 100644 app/src/main/java/org/grapheneos/pdfviewer/viewmodel/DocumentPropertiesParser.kt
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
index 333de84af..b2758a17c 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
+++ b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
@@ -1,6 +1,7 @@
package org.grapheneos.pdfviewer;
import android.annotation.SuppressLint;
+import android.app.Activity;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
@@ -21,6 +22,7 @@
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
+import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
@@ -91,9 +93,22 @@ public class PdfViewerFragment extends Fragment {
private Toast mToast;
private TextView mTextView;
private Snackbar mSnackbar;
+ private FrameLayout mWebViewContainer;
private PdfViewerViewModel mViewModel;
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ Log.d(TAG, "onDestroy()");
+ if (mWebView != null && mWebViewContainer != null) {
+ //mWebViewContainer.removeView(mWebView);
+ //mWebView.removeAllViews();
+ //mWebView.destroy();
+ }
+
+ }
+
private class Channel {
@JavascriptInterface
public int getWindowInsetTop() {
@@ -118,17 +133,16 @@ public int getDocumentOrientationDegrees() {
@JavascriptInterface
public void setNumPages(int numPages) {
mViewModel.setNumPages(numPages);
- requireActivity().runOnUiThread(() -> requireActivity().invalidateOptionsMenu());
+ if (getActivity() != null) {
+ requireActivity().runOnUiThread(requireActivity()::invalidateOptionsMenu);
+ }
}
@JavascriptInterface
public void setDocumentProperties(final String properties) {
final List list = mViewModel.getDocumentProperties().getValue();
- if (list != null && list.isEmpty()) {
+ if (list != null && list.isEmpty() && getActivity() != null) {
mViewModel.loadProperties(properties, requireActivity().getApplicationContext());
- } else {
- Log.d(TAG, "setDocumentProperties: did not load in properties, since "
- + (list == null ? "list is null" : "list is not empty"));
}
}
}
@@ -141,6 +155,7 @@ public static PdfViewerFragment newInstance() {
return new PdfViewerFragment();
}
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -175,6 +190,7 @@ public void onSaveInstanceState(@NonNull Bundle outState) {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
mViewModel = new ViewModelProvider(requireActivity()).get(PdfViewerViewModel.class);
+ mWebViewContainer = view.findViewById(R.id.webview_container);
mWebView = view.findViewById(R.id.webview);
mWebView.setOnApplyWindowInsetsListener((v, insets) -> {
@@ -196,6 +212,10 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
mWebView.setWebViewClient(new WebViewClient() {
private WebResourceResponse fromAsset(final String mime, final String path) {
+ if (getActivity() == null) {
+ return null;
+ }
+
try {
InputStream inputStream = requireActivity().getAssets().open(path.substring(1));
return new WebResourceResponse(mime, null, inputStream);
@@ -224,6 +244,10 @@ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceReque
if ("/viewer.html".equals(path)) {
final WebResourceResponse response = fromAsset("text/html", path);
+ if (response == null) {
+ return null;
+ }
+
HashMap headers = new HashMap();
headers.put("Content-Security-Policy", CONTENT_SECURITY_POLICY);
headers.put("Feature-Policy", FEATURE_POLICY);
@@ -251,7 +275,10 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request
@Override
public void onPageFinished(WebView view, String url) {
mDocumentState = STATE_LOADED;
- requireActivity().invalidateOptionsMenu();
+
+ if (getActivity() != null) {
+ requireActivity().invalidateOptionsMenu();
+ }
}
});
@@ -261,7 +288,7 @@ public void onPageFinished(WebView view, String url) {
public boolean onTapUp() {
if (mViewModel.getUri() != null) {
mWebView.evaluateJavascript("isTextSelected()", selection -> {
- if (!Boolean.valueOf(selection)) {
+ if (!Boolean.valueOf(selection) && getActivity() != null) {
if ((requireActivity().getWindow().getDecorView().getSystemUiVisibility() &
View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
hideSystemUi();
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/DocumentPropertiesParser.kt b/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/DocumentPropertiesParser.kt
new file mode 100644
index 000000000..29fb07f42
--- /dev/null
+++ b/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/DocumentPropertiesParser.kt
@@ -0,0 +1,88 @@
+package org.grapheneos.pdfviewer.viewmodel
+
+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 org.grapheneos.pdfviewer.R
+import org.grapheneos.pdfviewer.Utils
+import org.json.JSONException
+import org.json.JSONObject
+import java.text.ParseException
+
+private const val TAG = "DocumentPropertiesParser"
+private const val MISSING_STRING = "-"
+
+internal fun parsePropertiesString(
+ propertiesString: String,
+ context: Context,
+ numPages: Int,
+ uri: Uri?
+): ArrayList? {
+ val properties = ArrayList()
+ val names = context.resources.getStringArray(R.array.property_names);
+ val cursor: Cursor? = context.contentResolver.query(uri!!, null, null, null, null);
+ cursor?.let {
+ it.moveToFirst();
+
+ val indexOfName = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ if (indexOfName >= 0) {
+ properties.add(
+ getProperty(null, names[0], it.getString(indexOfName), context)
+ )
+ }
+
+ val indexOfSize = it.getColumnIndex(OpenableColumns.SIZE)
+ if (indexOfSize >= 0) {
+ val fileSize = it.getString(indexOfSize).toLong()
+ properties.add(
+ getProperty(null, names[1], Utils.parseFileSize(fileSize), context)
+ )
+ }
+ }
+ cursor?.close()
+
+ val specNames = arrayOf("Title", "Author", "Subject", "Keywords", "CreationDate",
+ "ModDate", "Producer", "Creator", "PDFFormatVersion", numPages.toString())
+ try {
+ val json = JSONObject(propertiesString)
+ for (i in 2 until names.size) {
+ properties.add(getProperty(json, names[i], specNames[i - 2], context))
+ }
+
+ Log.d(TAG, "Successfully parsed properties")
+ return properties
+ } catch (e: JSONException) {
+ Log.e(TAG, "Failed to parse properties: ${e.message}", e)
+ }
+ return null
+}
+
+private fun getProperty(
+ json: JSONObject?,
+ name: String,
+ specName: String,
+ context: Context
+): CharSequence {
+ val property = SpannableStringBuilder(name).append(":\n")
+ val value = json?.optString(specName, MISSING_STRING) ?: specName
+
+ val valueToAppend = if (specName.endsWith("Date") && value != MISSING_STRING) {
+ try {
+ Utils.parseDate(value)
+ } catch (e: ParseException) {
+ Log.w(TAG, "${e.message} for $value at offset: ${e.errorOffset}")
+ context.getString(R.string.document_properties_invalid_date)
+ }
+ } else {
+ value
+ }
+ property.append(valueToAppend)
+ property.setSpan(StyleSpan(Typeface.BOLD), 0, name.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ return property
+}
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/PdfViewerViewModel.kt b/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/PdfViewerViewModel.kt
index 027e70afa..885dedfd8 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/PdfViewerViewModel.kt
+++ b/app/src/main/java/org/grapheneos/pdfviewer/viewmodel/PdfViewerViewModel.kt
@@ -1,38 +1,23 @@
package org.grapheneos.pdfviewer.viewmodel
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.annotation.NonNull
import androidx.lifecycle.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import org.grapheneos.pdfviewer.R
-import org.grapheneos.pdfviewer.Utils
-import org.json.JSONException
-import org.json.JSONObject
-import java.text.ParseException
private const val TAG = "PdfViewerViewModel"
-private const val MISSING_STRING = "-"
-
private const val STATE_URI = "uri"
private const val STATE_PAGE = "page"
-private const val STATE_ZOOMRATIO = "zoomRatio"
-private const val STATE_ORIENTATIONDEGREES = "orientationDegrees"
-private const val STATE_DOCUMENT_PROPERTIES = "documentProperties"
+private const val STATE_ZOOM_RATIO = "zoomRatio"
+private const val STATE_ORIENTATION_DEGREES = "orientationDegrees"
class PdfViewerViewModel(private val state: SavedStateHandle) : ViewModel() {
private val documentProperties: MutableLiveData> by lazy {
- MutableLiveData>(
- state.get(STATE_DOCUMENT_PROPERTIES) ?: ArrayList())
+ MutableLiveData>(ArrayList())
}
var uri: Uri? = null
var page: Int = 0
@@ -44,8 +29,8 @@ class PdfViewerViewModel(private val state: SavedStateHandle) : ViewModel() {
state.run {
uri = get(STATE_URI)
page = get(STATE_PAGE) ?: 0
- zoomRatio = get(STATE_ZOOMRATIO) ?: 1f
- documentOrientationDegrees = get(STATE_ORIENTATIONDEGREES) ?: 0
+ zoomRatio = get(STATE_ZOOM_RATIO) ?: 1f
+ documentOrientationDegrees = get(STATE_ORIENTATION_DEGREES) ?: 0
}
}
@@ -53,9 +38,8 @@ class PdfViewerViewModel(private val state: SavedStateHandle) : ViewModel() {
state.run {
set(STATE_URI, uri)
set(STATE_PAGE, page)
- set(STATE_ZOOMRATIO, zoomRatio)
- set(STATE_ORIENTATIONDEGREES, documentOrientationDegrees)
- set(STATE_DOCUMENT_PROPERTIES, documentProperties.value as ArrayList)
+ set(STATE_ZOOM_RATIO, zoomRatio)
+ set(STATE_ORIENTATION_DEGREES, documentOrientationDegrees)
}
}
@@ -77,68 +61,17 @@ class PdfViewerViewModel(private val state: SavedStateHandle) : ViewModel() {
}
viewModelScope.launch(Dispatchers.Default) {
- val names = context.resources.getStringArray(R.array.property_names);
- val properties = ArrayList()
-
- val cursor: Cursor? = context.contentResolver.query(uri!!, null, null, null, null);
- cursor?.let {
- it.moveToFirst();
-
- val indexOfName = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
- if (indexOfName >= 0) {
- properties.add(getProperty(null, names[0], it.getString(indexOfName),
- context))
- }
-
- val indexOfSize = it.getColumnIndex(OpenableColumns.SIZE)
- if (indexOfSize >= 0) {
- val fileSize = it.getString(indexOfSize).toLong()
- properties.add(getProperty(null, names[1], Utils.parseFileSize(fileSize),
- context))
- }
- }
- cursor?.close()
-
- val specNames = arrayOf("Title", "Author", "Subject", "Keywords", "CreationDate",
- "ModDate", "Producer", "Creator", "PDFFormatVersion", numPages.toString())
- try {
- val json = JSONObject(propertiesString)
- for (i in 2 until names.size) {
- properties.add(getProperty(json, names[i], specNames[i - 2], context))
- }
-
- Log.d(TAG, "Successfully parsed properties")
- val list = documentProperties.value as ArrayList
- list.run {
- clear()
- addAll(properties)
- documentProperties.postValue(this)
- }
- } catch (e: JSONException) {
+ val propertiesList = parsePropertiesString(propertiesString, context, numPages, uri)
+ if (propertiesList == null) {
clearDocumentProperties()
- Log.e(TAG, "Failed to parse properties: ${e.message}", e)
- }
- }
- }
-
- private fun getProperty(
- json: JSONObject?, name: String, specName: String, context: Context
- ): CharSequence {
- val property = SpannableStringBuilder(name).append(":\n")
- val value = json?.optString(specName, MISSING_STRING) ?: specName
-
- val valueToAppend = if (specName.endsWith("Date") && value != MISSING_STRING) {
- try {
- Utils.parseDate(value)
- } catch (e: ParseException) {
- Log.w(TAG, "${e.message} for $value at offset: ${e.errorOffset}")
- context.getString(R.string.document_properties_invalid_date)
+ } else {
+ documentProperties.postValue(
+ (documentProperties.value as ArrayList).apply {
+ clear()
+ addAll(propertiesList)
+ }
+ )
}
- } else {
- value
}
- property.append(valueToAppend)
- property.setSpan(StyleSpan(Typeface.BOLD), 0, name.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
- return property
}
}
diff --git a/app/src/main/res/layout/fragment_webview.xml b/app/src/main/res/layout/fragment_webview.xml
index 36674fc11..b1b668776 100644
--- a/app/src/main/res/layout/fragment_webview.xml
+++ b/app/src/main/res/layout/fragment_webview.xml
@@ -1,5 +1,12 @@
-
+
+
+
+
+
From b62535bbc290df1e2f5f5dc810216c5f4ad9c165 Mon Sep 17 00:00:00 2001
From: inthewaves <26474149+inthewaves@users.noreply.github.com>
Date: Thu, 27 Aug 2020 23:41:13 -0700
Subject: [PATCH 4/7] clear WebView after Fragment's view is destroyed
At times, the WebView would execute methods via the Channel even when
the Fragment was in a destroyed state. This crashed the app, since the
Fragment wasn't tied to an Activity. This fixes that by explicitly
stopping the WebView when the Fragment's view is destroyed.
Fixes #71
---
.../pdfviewer/PdfViewerFragment.java | 43 +++++++++++++++----
1 file changed, 34 insertions(+), 9 deletions(-)
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
index b2758a17c..115b0b49c 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
+++ b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
@@ -1,7 +1,6 @@
package org.grapheneos.pdfviewer;
import android.annotation.SuppressLint;
-import android.app.Activity;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
@@ -85,6 +84,8 @@ public class PdfViewerFragment extends Fragment {
private static final int STATE_END = 2;
private static final int PADDING = 10;
+ private static final String JAVASCRIPT_INTERFACE_NAME = "channel";
+
private int mDocumentState;
private WebView mWebView;
private int mWindowInsetsTop;
@@ -97,16 +98,35 @@ public class PdfViewerFragment extends Fragment {
private PdfViewerViewModel mViewModel;
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (mWebViewContainer != null) {
+ mWebViewContainer.removeAllViews();
+ if (mWebView != null) {
+ mWebView.removeJavascriptInterface(JAVASCRIPT_INTERFACE_NAME);
+
+ // Load about:blank to reset the view state and release page resources.
+ mWebView.loadUrl("about:blank");
+ mWebView.onPause();
+
+ // Note: This pauses JS, layout, parsing timers for all WebViews.
+ // Need to resume timers when creating again.
+ mWebView.pauseTimers();
+ mWebView.getSettings().setJavaScriptEnabled(false);
+ mWebView.removeAllViews();
+ }
+ }
+ }
+
@Override
public void onDestroy() {
super.onDestroy();
- Log.d(TAG, "onDestroy()");
- if (mWebView != null && mWebViewContainer != null) {
- //mWebViewContainer.removeView(mWebView);
- //mWebView.removeAllViews();
- //mWebView.destroy();
+ if (mWebView != null) {
+ // TODO: Figure out how to call mWebView.destroy() without getting an exception in
+ // logcat of application calling on a destroyed WebView
+ mWebView = null;
}
-
}
private class Channel {
@@ -143,6 +163,9 @@ public void setDocumentProperties(final String properties) {
final List list = mViewModel.getDocumentProperties().getValue();
if (list != null && list.isEmpty() && getActivity() != null) {
mViewModel.loadProperties(properties, requireActivity().getApplicationContext());
+ } else {
+ Log.d(TAG, "setDocumentProperties: not loading properties because " +
+ (list == null ? "list is null" : "list is not empty"));
}
}
}
@@ -192,6 +215,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
mWebViewContainer = view.findViewById(R.id.webview_container);
mWebView = view.findViewById(R.id.webview);
+ // Resume timers if onDestroyView was called
+ mWebView.resumeTimers();
mWebView.setOnApplyWindowInsetsListener((v, insets) -> {
mWindowInsetsTop = insets.getSystemWindowInsetTop();
@@ -208,7 +233,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
CookieManager.getInstance().setAcceptCookie(false);
- mWebView.addJavascriptInterface(new Channel(), "channel");
+ mWebView.addJavascriptInterface(new Channel(), JAVASCRIPT_INTERFACE_NAME);
mWebView.setWebViewClient(new WebViewClient() {
private WebResourceResponse fromAsset(final String mime, final String path) {
@@ -324,7 +349,7 @@ public void onZoomEnd() {
mTextView.setTextSize(18);
mTextView.setPadding(PADDING, 0, PADDING, 0);
- mSnackbar = Snackbar.make(mWebView, "", Snackbar.LENGTH_LONG);
+ mSnackbar = Snackbar.make(mWebViewContainer, "", Snackbar.LENGTH_LONG);
final Intent intent = requireActivity().getIntent();
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
From 6260bcba91c1cd3769a0c0f3ed56fba10f52f659 Mon Sep 17 00:00:00 2001
From: inthewaves <26474149+inthewaves@users.noreply.github.com>
Date: Fri, 28 Aug 2020 00:14:06 -0700
Subject: [PATCH 5/7] don't use androidx.fragment 1.3.0-alpha08 for now
This means we won't be able to use ActivityResultLauncher and
FragmentResult owners and listeners. This can be reverted later when
it's more stable.
---
app/build.gradle | 1 -
.../pdfviewer/PdfViewerFragment.java | 53 +++++++++++--------
.../fragment/JumpToPageFragment.java | 17 ++++--
3 files changed, 42 insertions(+), 29 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index ad62f2073..307abcc8d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -39,7 +39,6 @@ dependencies {
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "androidx.core:core-ktx:1.3.1"
- implementation "androidx.fragment:fragment:1.3.0-alpha08"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
index 115b0b49c..e1f0da3d7 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
+++ b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
@@ -1,6 +1,7 @@
package org.grapheneos.pdfviewer;
import android.annotation.SuppressLint;
+import android.app.Activity;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
@@ -25,9 +26,6 @@
import android.widget.TextView;
import android.widget.Toast;
-import androidx.activity.result.ActivityResultCallback;
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
@@ -76,6 +74,9 @@ public class PdfViewerFragment extends Fragment {
"usb 'none'; " +
"vr 'none'";
+ private static final int REQUEST_CODE_JUMP_PAGE = 1000;
+ private static final int REQUEST_CODE_ACTION_OPEN_DOCUMENT = 1001;
+
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;
@@ -183,11 +184,25 @@ public static PdfViewerFragment newInstance() {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
- getParentFragmentManager().setFragmentResultListener(JumpToPageFragment.REQUEST_KEY,
- this, (requestKey, result) -> {
- final int newPage = result.getInt(JumpToPageFragment.BUNDLE_KEY);
- onJumpToPageInDocument(newPage);
- });
+ }
+
+ // Can be replaced when support for passing results between two Fragments
+ // via new APIs on FragmentManager arrive (Androidx Fragment 1.3.0)
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (requestCode == REQUEST_CODE_JUMP_PAGE) {
+ if (resultCode == Activity.RESULT_OK) {
+ final int newPage = data.getIntExtra(JumpToPageFragment.INTENT_KEY, -1);
+ onJumpToPageInDocument(newPage);
+ }
+ } else if (requestCode == REQUEST_CODE_ACTION_OPEN_DOCUMENT) {
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ mViewModel.setUri(data.getData());
+ mViewModel.setPage(1);
+ loadPdf(true);
+ requireActivity().invalidateOptionsMenu();
+ }
+ }
}
@Override
@@ -416,20 +431,11 @@ private void documentOrientationChanged(final int orientationDegreesOffset) {
renderPage(0);
}
- private ActivityResultLauncher mGetDocumentUriLauncher = registerForActivityResult(
- new ActivityResultContracts.GetContent(), new ActivityResultCallback() {
- @Override
- public void onActivityResult(Uri uri) {
- if (uri != null) {
- mViewModel.setUri(uri);
- mViewModel.setPage(1);
- loadPdf(true);
- }
- }
- });
-
private void openDocument() {
- mGetDocumentUriLauncher.launch("application/pdf");
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("application/pdf");
+ startActivityForResult(intent, REQUEST_CODE_ACTION_OPEN_DOCUMENT);
}
private void zoomIn(float value, boolean end) {
@@ -589,8 +595,9 @@ public boolean onOptionsItemSelected(MenuItem item) {
return true;
case R.id.action_jump_to_page:
- new JumpToPageFragment()
- .show(getParentFragmentManager(), JumpToPageFragment.TAG);
+ final JumpToPageFragment fragment = new JumpToPageFragment();
+ fragment.setTargetFragment(this, REQUEST_CODE_JUMP_PAGE);
+ fragment.show(getParentFragmentManager(), JumpToPageFragment.TAG);
return true;
default:
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/fragment/JumpToPageFragment.java b/app/src/main/java/org/grapheneos/pdfviewer/fragment/JumpToPageFragment.java
index 4e0b8bab1..cf490b687 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/fragment/JumpToPageFragment.java
+++ b/app/src/main/java/org/grapheneos/pdfviewer/fragment/JumpToPageFragment.java
@@ -1,7 +1,9 @@
package org.grapheneos.pdfviewer.fragment;
+import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
+import android.content.Intent;
import android.os.Bundle;
import android.view.Gravity;
import android.widget.FrameLayout;
@@ -16,8 +18,8 @@
public class JumpToPageFragment extends DialogFragment {
public static final String TAG = "JumpToPageFragment";
- public static final String REQUEST_KEY = "jumpToPage";
- public static final String BUNDLE_KEY = "jumpToPageBundle";
+ public static final int REQUEST_CODE = 1000;
+ public static final String INTENT_KEY = "jumpToPageBundle";
private final static String STATE_PICKER_CUR = "picker_cur";
private final static String STATE_PICKER_MIN = "picker_min";
@@ -57,10 +59,15 @@ public Dialog onCreateDialog(Bundle savedInstanceState) {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
mPicker.clearFocus();
+ if (getTargetFragment() == null) {
+ return;
+ }
- Bundle result = new Bundle();
- result.putInt(BUNDLE_KEY, mPicker.getValue());
- getParentFragmentManager().setFragmentResult(REQUEST_KEY, result);
+ Intent data = new Intent();
+ data.putExtra(INTENT_KEY, mPicker.getValue());
+
+ getTargetFragment().onActivityResult(REQUEST_CODE, Activity.RESULT_OK,
+ data);
}
})
.setNegativeButton(android.R.string.cancel, null)
From 95ed5a9aef5d72f189bb8d932338cf3284d1bd49 Mon Sep 17 00:00:00 2001
From: inthewaves <26474149+inthewaves@users.noreply.github.com>
Date: Fri, 28 Aug 2020 00:14:32 -0700
Subject: [PATCH 6/7] update appcompat and material to 1.2.0
---
app/build.gradle | 4 ++--
.../main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 307abcc8d..201af19f3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -35,8 +35,8 @@ android {
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
- implementation 'androidx.appcompat:appcompat:1.1.0'
- implementation 'com.google.android.material:material:1.1.0'
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'com.google.android.material:material:1.2.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "androidx.core:core-ktx:1.3.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
index e1f0da3d7..155de1d16 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
+++ b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
@@ -604,4 +604,4 @@ public boolean onOptionsItemSelected(MenuItem item) {
return super.onOptionsItemSelected(item);
}
}
-}
\ No newline at end of file
+}
From 0020f2cf205b6cc0e00126b5d8f5a78d91b54f32 Mon Sep 17 00:00:00 2001
From: inthewaves <26474149+inthewaves@users.noreply.github.com>
Date: Sat, 29 Aug 2020 22:38:49 -0700
Subject: [PATCH 7/7] Revert "don't use androidx.fragment 1.3.0-alpha08 for
now"
This reverts commit 6260bcba
---
app/build.gradle | 1 +
.../pdfviewer/PdfViewerFragment.java | 53 ++++++++-----------
.../fragment/JumpToPageFragment.java | 17 ++----
3 files changed, 29 insertions(+), 42 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 201af19f3..77d9ab80a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -39,6 +39,7 @@ dependencies {
implementation 'com.google.android.material:material:1.2.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "androidx.core:core-ktx:1.3.1"
+ implementation "androidx.fragment:fragment:1.3.0-alpha08"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
index 155de1d16..a472ac7fb 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
+++ b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewerFragment.java
@@ -1,7 +1,6 @@
package org.grapheneos.pdfviewer;
import android.annotation.SuppressLint;
-import android.app.Activity;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
@@ -26,6 +25,9 @@
import android.widget.TextView;
import android.widget.Toast;
+import androidx.activity.result.ActivityResultCallback;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
@@ -74,9 +76,6 @@ public class PdfViewerFragment extends Fragment {
"usb 'none'; " +
"vr 'none'";
- private static final int REQUEST_CODE_JUMP_PAGE = 1000;
- private static final int REQUEST_CODE_ACTION_OPEN_DOCUMENT = 1001;
-
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;
@@ -184,25 +183,11 @@ public static PdfViewerFragment newInstance() {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
- }
-
- // Can be replaced when support for passing results between two Fragments
- // via new APIs on FragmentManager arrive (Androidx Fragment 1.3.0)
- @Override
- public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- if (requestCode == REQUEST_CODE_JUMP_PAGE) {
- if (resultCode == Activity.RESULT_OK) {
- final int newPage = data.getIntExtra(JumpToPageFragment.INTENT_KEY, -1);
- onJumpToPageInDocument(newPage);
- }
- } else if (requestCode == REQUEST_CODE_ACTION_OPEN_DOCUMENT) {
- if (resultCode == Activity.RESULT_OK && data != null) {
- mViewModel.setUri(data.getData());
- mViewModel.setPage(1);
- loadPdf(true);
- requireActivity().invalidateOptionsMenu();
- }
- }
+ getParentFragmentManager().setFragmentResultListener(JumpToPageFragment.REQUEST_KEY,
+ this, (requestKey, result) -> {
+ final int newPage = result.getInt(JumpToPageFragment.BUNDLE_KEY);
+ onJumpToPageInDocument(newPage);
+ });
}
@Override
@@ -431,11 +416,20 @@ private void documentOrientationChanged(final int orientationDegreesOffset) {
renderPage(0);
}
+ private ActivityResultLauncher mGetDocumentUriLauncher = registerForActivityResult(
+ new ActivityResultContracts.GetContent(), new ActivityResultCallback() {
+ @Override
+ public void onActivityResult(Uri uri) {
+ if (uri != null) {
+ mViewModel.setUri(uri);
+ mViewModel.setPage(1);
+ loadPdf(true);
+ }
+ }
+ });
+
private void openDocument() {
- Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("application/pdf");
- startActivityForResult(intent, REQUEST_CODE_ACTION_OPEN_DOCUMENT);
+ mGetDocumentUriLauncher.launch("application/pdf");
}
private void zoomIn(float value, boolean end) {
@@ -595,9 +589,8 @@ public boolean onOptionsItemSelected(MenuItem item) {
return true;
case R.id.action_jump_to_page:
- final JumpToPageFragment fragment = new JumpToPageFragment();
- fragment.setTargetFragment(this, REQUEST_CODE_JUMP_PAGE);
- fragment.show(getParentFragmentManager(), JumpToPageFragment.TAG);
+ new JumpToPageFragment()
+ .show(getParentFragmentManager(), JumpToPageFragment.TAG);
return true;
default:
diff --git a/app/src/main/java/org/grapheneos/pdfviewer/fragment/JumpToPageFragment.java b/app/src/main/java/org/grapheneos/pdfviewer/fragment/JumpToPageFragment.java
index cf490b687..4e0b8bab1 100644
--- a/app/src/main/java/org/grapheneos/pdfviewer/fragment/JumpToPageFragment.java
+++ b/app/src/main/java/org/grapheneos/pdfviewer/fragment/JumpToPageFragment.java
@@ -1,9 +1,7 @@
package org.grapheneos.pdfviewer.fragment;
-import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
-import android.content.Intent;
import android.os.Bundle;
import android.view.Gravity;
import android.widget.FrameLayout;
@@ -18,8 +16,8 @@
public class JumpToPageFragment extends DialogFragment {
public static final String TAG = "JumpToPageFragment";
- public static final int REQUEST_CODE = 1000;
- public static final String INTENT_KEY = "jumpToPageBundle";
+ public static final String REQUEST_KEY = "jumpToPage";
+ public static final String BUNDLE_KEY = "jumpToPageBundle";
private final static String STATE_PICKER_CUR = "picker_cur";
private final static String STATE_PICKER_MIN = "picker_min";
@@ -59,15 +57,10 @@ public Dialog onCreateDialog(Bundle savedInstanceState) {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
mPicker.clearFocus();
- if (getTargetFragment() == null) {
- return;
- }
- Intent data = new Intent();
- data.putExtra(INTENT_KEY, mPicker.getValue());
-
- getTargetFragment().onActivityResult(REQUEST_CODE, Activity.RESULT_OK,
- data);
+ Bundle result = new Bundle();
+ result.putInt(BUNDLE_KEY, mPicker.getValue());
+ getParentFragmentManager().setFragmentResult(REQUEST_KEY, result);
}
})
.setNegativeButton(android.R.string.cancel, null)