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 22517fa83..97d6aa6f6 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.viewmodel.PdfViewerViewModel;
+
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
-import org.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment;
-import org.grapheneos.pdfviewer.fragment.JumpToPageFragment;
-import org.grapheneos.pdfviewer.loader.DocumentPropertiesLoader;
-
-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);
+ mViewModel = new ViewModelProvider(requireActivity()).get(PdfViewerViewModel.class);
- mWebView = findViewById(R.id.webview);
+ mWebView = view.findViewById(R.id.webview);
- mWebView.setOnApplyWindowInsetsListener((view, insets) -> {
- windowInsetTop = insets.getSystemWindowInsetTop();
+ 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,20 +246,20 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request
@Override
public void onPageFinished(WebView view, String url) {
mDocumentState = STATE_LOADED;
- invalidateOptionsMenu();
+ requireActivity().invalidateOptionsMenu();
}
});
showSystemUi();
- 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 {
@@ -272,72 +288,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);
+ mSnackbar = Snackbar.make(mWebView, "", Snackbar.LENGTH_LONG);
- snackbar = 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();
+ }
+
mWebView.loadUrl("https://localhost/viewer.html");
}
@@ -346,33 +347,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 +403,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 +443,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 +471,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 +497,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 +522,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