Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ dependencies {
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.fragment:fragment-ktx:1.8.9")
implementation("com.google.android.material:material:1.13.0")
implementation("androidx.datastore:datastore-preferences:1.2.1")
}

fun getCommand(command: String, winExt: String = "cmd"): String {
Expand Down
53 changes: 53 additions & 0 deletions app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,11 @@
import app.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment;
import app.grapheneos.pdfviewer.fragment.JumpToPageFragment;
import app.grapheneos.pdfviewer.fragment.PasswordPromptFragment;
import app.grapheneos.pdfviewer.fragment.SettingsDialog;
import app.grapheneos.pdfviewer.ktx.ViewKt;
import app.grapheneos.pdfviewer.loader.DocumentPropertiesAsyncTaskLoader;
import app.grapheneos.pdfviewer.outline.OutlineFragment;
import app.grapheneos.pdfviewer.preferences.PdfPreferencesRepository;
import app.grapheneos.pdfviewer.viewModel.PdfViewModel;

public class PdfViewer extends AppCompatActivity implements LoaderManager.LoaderCallbacks<List<CharSequence>> {
Expand All @@ -62,6 +64,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
private static final String STATE_WEBVIEW_CRASHED = "webview_crashed";
private static final String STATE_URI = "uri";
private static final String STATE_PAGE = "page";
private static final String STATE_NUM_PAGES = "numPages";
private static final String STATE_ZOOM_RATIO = "zoomRatio";
private static final String STATE_DOCUMENT_ORIENTATION_DEGREES = "documentOrientationDegrees";
private static final String STATE_ENCRYPTED_DOCUMENT_PASSWORD = "encrypted_document_password";
Expand Down Expand Up @@ -146,9 +149,13 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
if (resultData != null) {
mUri = result.getData().getData();
mPage = 1;
mNumPages = 0;
mDocumentProperties = null;
mEncryptedDocumentPassword = "";
viewModel.clearOutline();
if (mUri != null && PreferenceHelper.INSTANCE.isResumeLastDocumentEnabled(this)) {
viewModel.prepareNewPdf(mUri, mPage);
}
loadPdf();
invalidateOptionsMenu();
}
Expand Down Expand Up @@ -229,6 +236,16 @@ public void setNumPages(int numPages) {
runOnUiThread(PdfViewer.this::invalidateOptionsMenu);
}

@JavascriptInterface
public void setFingerprint(String fingerprint) {
viewModel.setCurrentFingerprint(fingerprint);
Integer savedPage = viewModel.getPageByFingerprintBlocking();

if (savedPage != null) {
runOnUiThread(() -> onJumpToPageInDocument(savedPage));
}
}

@JavascriptInterface
public void setDocumentProperties(final String properties) {
if (mDocumentProperties != null) {
Expand Down Expand Up @@ -486,6 +503,7 @@ public void onZoomEnd() {
}
mUri = intent.getData();
mPage = 1;
mNumPages = 0;
}

if (savedInstanceState != null) {
Expand All @@ -498,9 +516,31 @@ public void onZoomEnd() {
mUri = uri;
}
mPage = savedInstanceState.getInt(STATE_PAGE);
mNumPages = savedInstanceState.getInt(STATE_NUM_PAGES);
mZoomRatio = savedInstanceState.getFloat(STATE_ZOOM_RATIO);
mDocumentOrientationDegrees = savedInstanceState.getInt(STATE_DOCUMENT_ORIENTATION_DEGREES);
mEncryptedDocumentPassword = savedInstanceState.getString(STATE_ENCRYPTED_DOCUMENT_PASSWORD);
} else {
if (mUri != null && PreferenceHelper.INSTANCE.isResumeLastDocumentEnabled(this)) {
viewModel.prepareNewPdf(mUri, mPage);
} else {
PdfPreferencesRepository.PdfState state =
viewModel.maybeLoadPdfStateBlocking();

if (state != null && state.getLastOpenedUri() != null) {
Uri uri = Uri.parse(state.getLastOpenedUri());
if (viewModel.hasUriPermission(uri)) {
mUri = uri;
mPage = state.getLastOpenedPage();
mNumPages = 0;
} else {
viewModel.clearLastOpened();
Toast.makeText(this,
R.string.msg_previous_document_unavailable,
Toast.LENGTH_LONG).show();
}
}
}
}

binding.webviewAlertReload.setOnClickListener(v -> {
Expand All @@ -526,6 +566,15 @@ private void purgeWebView() {
binding.webview.destroy();
}

@Override
protected void onStop() {
super.onStop();

if (mUri != null) {
viewModel.savePdfStateBlocking(mUri.toString(), mPage, true);
}
}

@Override
protected void onDestroy() {
super.onDestroy();
Expand Down Expand Up @@ -696,6 +745,7 @@ public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
savedInstanceState.putBoolean(STATE_WEBVIEW_CRASHED, webViewCrashed);
savedInstanceState.putParcelable(STATE_URI, mUri);
savedInstanceState.putInt(STATE_PAGE, mPage);
savedInstanceState.putInt(STATE_NUM_PAGES, mNumPages);
savedInstanceState.putFloat(STATE_ZOOM_RATIO, mZoomRatio);
savedInstanceState.putInt(STATE_DOCUMENT_ORIENTATION_DEGREES, mDocumentOrientationDegrees);
savedInstanceState.putString(STATE_ENCRYPTED_DOCUMENT_PASSWORD, mEncryptedDocumentPassword);
Expand Down Expand Up @@ -827,6 +877,9 @@ public boolean onOptionsItemSelected(MenuItem item) {
} else if (itemId == R.id.debug_action_crash_webview) {
binding.webview.loadUrl("chrome://crash");
return true;
} else if (itemId == R.id.action_settings) {
new SettingsDialog().show(getSupportFragmentManager(), "settings");
return true;
}

return super.onOptionsItemSelected(item);
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/app/grapheneos/pdfviewer/PreferenceHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package app.grapheneos.pdfviewer


import android.content.Context

object PreferenceHelper {

const val PREF_NAME = "app_prefs"
const val KEY_RESUME_LAST_DOCUMENT = "resume_last_document"

fun isResumeLastDocumentEnabled(context: Context): Boolean {
val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
return prefs.getBoolean(KEY_RESUME_LAST_DOCUMENT, true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package app.grapheneos.pdfviewer.fragment

import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.core.content.edit
import androidx.fragment.app.DialogFragment
import app.grapheneos.pdfviewer.PreferenceHelper
import app.grapheneos.pdfviewer.R
import app.grapheneos.pdfviewer.databinding.DialogSettingsBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder

class SettingsDialog : DialogFragment() {

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val binding = DialogSettingsBinding.inflate(layoutInflater)

val prefs = requireContext().getSharedPreferences(PreferenceHelper.PREF_NAME, Context.MODE_PRIVATE)
binding.switchResumeDocument.isChecked =
prefs.getBoolean(PreferenceHelper.KEY_RESUME_LAST_DOCUMENT, true)
binding.switchResumeDocument.setOnCheckedChangeListener { _, isChecked ->
prefs.edit {
putBoolean(PreferenceHelper.KEY_RESUME_LAST_DOCUMENT, isChecked)
}
}

return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.settings)
.setView(binding.root)
.create()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package app.grapheneos.pdfviewer.preferences

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.IOException
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import org.json.JSONObject

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "pdf_preferences",
corruptionHandler = ReplaceFileCorruptionHandler { emptyPreferences() }
)

class PdfPreferencesRepository (private val context: Context) {

private object PreferencesKeys {
val LAST_OPENED_URI = stringPreferencesKey("last_opened_uri")
val LAST_OPENED_PAGE = intPreferencesKey("last_opened_page")
val FILE_PAGE_POSITIONS = stringPreferencesKey("file_page_positions")
}

companion object {
private const val MAX_FILE_HISTORY = 50
}

data class PdfState(
val lastOpenedUri: String? = null,
val lastOpenedPage: Int = 1,
val filePagePositions: Map<String, Int> = emptyMap()
)

val pdfStateFlow: Flow<PdfState> = context.dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
PdfState(
lastOpenedUri = preferences[PreferencesKeys.LAST_OPENED_URI],
lastOpenedPage = preferences[PreferencesKeys.LAST_OPENED_PAGE] ?: 1,
filePagePositions = parseFilePagePositions(
preferences[PreferencesKeys.FILE_PAGE_POSITIONS]
)
)
}

suspend fun saveLastOpened(uri: String?, page: Int) {
context.dataStore.edit { preferences ->
if (uri != null) {
preferences[PreferencesKeys.LAST_OPENED_URI] = uri
} else {
preferences.remove(PreferencesKeys.LAST_OPENED_URI)
}
preferences[PreferencesKeys.LAST_OPENED_PAGE] = page
}
}

suspend fun clearLastOpened() {
context.dataStore.edit { preferences ->
preferences.remove(PreferencesKeys.LAST_OPENED_URI)
preferences.remove(PreferencesKeys.LAST_OPENED_PAGE)
}
}

suspend fun updatePagePosition(fileHash: String, page: Int) {
context.dataStore.edit { preferences ->
val currentMap = parseFilePagePositions(
preferences[PreferencesKeys.FILE_PAGE_POSITIONS]
).toMutableMap()

currentMap[fileHash] = page

if (currentMap.size > MAX_FILE_HISTORY) {
val toRemove = currentMap.keys.take(currentMap.size - MAX_FILE_HISTORY)
toRemove.forEach { currentMap.remove(it) }
}

preferences[PreferencesKeys.FILE_PAGE_POSITIONS] = serializeFilePagePositions(currentMap)
}
}

suspend fun getPageForFile(fileHash: String): Int? {
return pdfStateFlow.first().filePagePositions[fileHash]
}

private fun parseFilePagePositions(json: String?): Map<String, Int> {
if (json.isNullOrEmpty()) return emptyMap()
val jsonObject = JSONObject(json)
return buildMap {
jsonObject.keys().forEach { key ->
put(key, jsonObject.getInt(key))
}
}
}

private fun serializeFilePagePositions(map: Map<String, Int>): String {
return JSONObject(map).toString()
}
}
Loading