diff --git a/app/build.gradle b/app/build.gradle index 8442110..c905722 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -66,6 +66,12 @@ android { } dependencies { + def room_version = "2.3.0" + + // Room + implementation "androidx.room:room-runtime:$room_version" + annotationProcessor "androidx.room:room-compiler:$room_version" + implementation 'androidx.appcompat:appcompat:1.3.0-beta01' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' diff --git a/app/src/main/java/com/gsnathan/pdfviewer/AppDatabase.java b/app/src/main/java/com/gsnathan/pdfviewer/AppDatabase.java new file mode 100644 index 0000000..e2b7b65 --- /dev/null +++ b/app/src/main/java/com/gsnathan/pdfviewer/AppDatabase.java @@ -0,0 +1,24 @@ +package com.gsnathan.pdfviewer; + +import android.content.Context; + +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; + +@Database(entities = {SavedLocation.class}, version = 1, exportSchema = false) +public abstract class AppDatabase extends RoomDatabase { + private static AppDatabase INSTANCE = null; + private static String DATABASE_NAME = "app-db.db"; + + public static AppDatabase getInstance(Context context) { + if (INSTANCE != null) { + return INSTANCE; + } + String location = context.getCacheDir().getAbsolutePath() + "/" + DATABASE_NAME; + INSTANCE = Room.databaseBuilder(context, AppDatabase.class, location).build(); + return INSTANCE; + } + + public abstract SavedLocationDao savedLocationDao(); +} diff --git a/app/src/main/java/com/gsnathan/pdfviewer/MainActivity.java b/app/src/main/java/com/gsnathan/pdfviewer/MainActivity.java index 8b62c55..f5426d5 100755 --- a/app/src/main/java/com/gsnathan/pdfviewer/MainActivity.java +++ b/app/src/main/java/com/gsnathan/pdfviewer/MainActivity.java @@ -35,17 +35,18 @@ import android.net.Uri; import android.os.Bundle; import android.os.Environment; +import android.os.Handler; +import android.os.Looper; import android.os.StrictMode; import android.preference.PreferenceManager; import android.print.PrintManager; import android.provider.OpenableColumns; import android.util.Log; -import android.view.WindowManager; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; -import android.view.Window; +import android.view.WindowManager; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; @@ -75,6 +76,12 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -82,8 +89,15 @@ public class MainActivity extends CyaneaAppCompatActivity { private static final String TAG = "MainActivity"; + /** For performance reasons, we won't hash the entire PDF but only up to this many bytes. */ + private static final int HASH_SIZE = 1024 * 1024; + + private final Executor executor = Executors.newSingleThreadExecutor(); + private final Handler handler = new Handler(Looper.getMainLooper()); + private PrintManager mgr; private SharedPreferences prefManager; + private AppDatabase appDb; private Uri uri; private int pageNumber = 0; @@ -91,6 +105,7 @@ public class MainActivity extends CyaneaAppCompatActivity { private String pdfFileName = ""; private byte[] downloadedPdfFileContent; + private String fileContentHash = null; private boolean isBottomNavigationHidden = false; private boolean isFullscreenToggled = false; @@ -122,7 +137,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewBinding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(viewBinding.getRoot()); - + Constants.THUMBNAIL_RATIO = 1f; setBottomBarListeners(); @@ -133,6 +148,7 @@ protected void onCreate(Bundle savedInstanceState) { prefManager = PreferenceManager.getDefaultSharedPreferences(this); mgr = (PrintManager) getSystemService(PRINT_SERVICE); + appDb = AppDatabase.getInstance(getApplicationContext()); onFirstInstall(); onFirstUpdate(); @@ -253,18 +269,59 @@ private void setBottomBarListeners() { }); } + String computeHash() { + try { + MessageDigest digester = MessageDigest.getInstance("MD5"); + if (downloadedPdfFileContent != null) { + int size = Math.min(HASH_SIZE, downloadedPdfFileContent.length); + digester.update(downloadedPdfFileContent, 0, size); + } else { + InputStream is = getContentResolver().openInputStream(uri); + byte[] buffer = new byte[HASH_SIZE]; + int amountRead = is.read(buffer); + if (amountRead == -1) { + return null; + } + digester.update(buffer, 0, amountRead); + } + return String.format("%032x", new BigInteger(1, digester.digest())); + } catch (NoSuchAlgorithmException | IOException e) { + return null; + } + } + void configurePdfViewAndLoad(PDFView.Configurator viewConfigurator) { + if (pageNumber == 0) { // attempt to find a saved location + executor.execute(() -> { // off UI thread + fileContentHash = computeHash(); + Integer maybePageNumber = fileContentHash == null + ? Integer.valueOf(0) + : appDb.savedLocationDao().findSavedPage(fileContentHash); + handler.post(() -> // back on UI thread + configurePdfViewAndLoadWithPageNumber( + viewConfigurator, + maybePageNumber != null ? maybePageNumber : 0 + ) + ); + }); + } else { + configurePdfViewAndLoadWithPageNumber(viewConfigurator, pageNumber); + } + } + + void configurePdfViewAndLoadWithPageNumber(PDFView.Configurator viewConfigurator, int pageNum) { if (!prefManager.getBoolean("pdftheme_pref", false)) { viewBinding.pdfView.setBackgroundColor(Color.LTGRAY); } else { viewBinding.pdfView.setBackgroundColor(0xFF212121); } + viewBinding.pdfView.useBestQuality(prefManager.getBoolean("quality_pref", false)); viewBinding.pdfView.setMinZoom(0.5f); viewBinding.pdfView.setMidZoom(2.0f); viewBinding.pdfView.setMaxZoom(5.0f); viewConfigurator - .defaultPage(pageNumber) + .defaultPage(pageNum) .onPageChange(this::setCurrentPage) .enableAnnotationRendering(true) .enableAntialiasing(prefManager.getBoolean("alias_pref", true)) @@ -447,6 +504,13 @@ void navToSettings() { } private void setCurrentPage(int page, int pageCount) { + String hash = fileContentHash; // Don't want fileContentHash to change out from under us + if (hash != null) { + executor.execute(() -> // off UI thread + appDb.savedLocationDao().insert(new SavedLocation(hash, pageNumber)) + ); + } + pageNumber = page; setTitle(String.format("%s %s / %s", pdfFileName + " ", page + 1, pageCount)); } @@ -546,4 +610,3 @@ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { } } } - diff --git a/app/src/main/java/com/gsnathan/pdfviewer/SavedLocation.java b/app/src/main/java/com/gsnathan/pdfviewer/SavedLocation.java new file mode 100644 index 0000000..946fc4c --- /dev/null +++ b/app/src/main/java/com/gsnathan/pdfviewer/SavedLocation.java @@ -0,0 +1,23 @@ +package com.gsnathan.pdfviewer; + +import androidx.annotation.NonNull; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +import org.jetbrains.annotations.NotNull; + +@Entity +public class SavedLocation { + public SavedLocation(@NotNull String hash, int pageNumber) { + this.hash = hash; + this.pageNumber = pageNumber; + } + + @PrimaryKey + @NonNull + public String hash; + + @ColumnInfo(name = "pageNumber") + public int pageNumber; +} diff --git a/app/src/main/java/com/gsnathan/pdfviewer/SavedLocationDao.java b/app/src/main/java/com/gsnathan/pdfviewer/SavedLocationDao.java new file mode 100644 index 0000000..63a8c0e --- /dev/null +++ b/app/src/main/java/com/gsnathan/pdfviewer/SavedLocationDao.java @@ -0,0 +1,15 @@ +package com.gsnathan.pdfviewer; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +@Dao +public interface SavedLocationDao { + @Query("SELECT pageNumber FROM SavedLocation WHERE hash = :hash") + Integer findSavedPage(String hash); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(SavedLocation saveLocations); +}