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)