commit 2e19fd0b28e9fdcc3e54acdd80cd20bc9e369a02
Author: ocean <503259349@qq.com>
Date: Tue Sep 2 15:10:52 2025 +0800
first commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..34e2d1f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+/release
+/debug
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..4b87ca8
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+PDF Reader Pro
\ No newline at end of file
diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..56ba133
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..639c779
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..7061a0d
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..274ccf3
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..9833294
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,56 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.devtools.ksp)
+}
+
+android {
+ namespace = "com.all.pdfreader.pro.app"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "com.all.pdfreader.pro.app"
+ minSdk = 24
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ buildFeatures {
+ viewBinding = true
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.swiperefreshlayout)
+ implementation(libs.androidx.recyclerview)
+ implementation(libs.protolite.well.known.types)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ implementation(libs.immersionbar)
+ implementation(libs.immersionbar.ktx)
+ implementation(libs.androidx.room.runtime)
+ ksp(libs.androidx.room.compiler)
+ implementation(libs.androidx.room.ktx)
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/all/pdfreader/pro/app/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/all/pdfreader/pro/app/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..5d5bd74
--- /dev/null
+++ b/app/src/androidTest/java/com/all/pdfreader/pro/app/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.all.pdfreader.pro.app
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.all.pdfreader.pro.app", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1174aaf
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/PDFReaderApplication.kt b/app/src/main/java/com/all/pdfreader/pro/app/PDFReaderApplication.kt
new file mode 100644
index 0000000..ab51d65
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/PDFReaderApplication.kt
@@ -0,0 +1,26 @@
+package com.all.pdfreader.pro.app
+
+import android.app.Application
+import android.content.Context
+import com.all.pdfreader.pro.app.room.repository.PdfRepository
+
+class PDFReaderApplication : Application() {
+
+ companion object {
+ private lateinit var instance: PDFReaderApplication
+
+ fun getInstance(): PDFReaderApplication = instance
+
+ fun getContext(): Context = instance.applicationContext
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ instance = this
+
+ // 初始化数据库
+ PdfRepository.initialize(this)
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/converter/DateConverter.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/converter/DateConverter.kt
new file mode 100644
index 0000000..9069dfc
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/room/converter/DateConverter.kt
@@ -0,0 +1,17 @@
+package com.all.pdfreader.pro.app.room.converter
+
+import androidx.room.TypeConverter
+import java.util.*
+
+class DateConverter {
+
+ @TypeConverter
+ fun fromTimestamp(value: Long?): Date? {
+ return value?.let { Date(it) }
+ }
+
+ @TypeConverter
+ fun dateToTimestamp(date: Date?): Long? {
+ return date?.time
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/converter/UriConverter.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/converter/UriConverter.kt
new file mode 100644
index 0000000..ff01954
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/room/converter/UriConverter.kt
@@ -0,0 +1,17 @@
+package com.all.pdfreader.pro.app.room.converter
+
+import android.net.Uri
+import androidx.room.TypeConverter
+
+class UriConverter {
+
+ @TypeConverter
+ fun fromUri(uri: Uri?): String? {
+ return uri?.toString()
+ }
+
+ @TypeConverter
+ fun toUri(uriString: String?): Uri? {
+ return uriString?.let { Uri.parse(it) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/dao/BookmarkDao.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/dao/BookmarkDao.kt
new file mode 100644
index 0000000..4c07d34
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/room/dao/BookmarkDao.kt
@@ -0,0 +1,36 @@
+package com.all.pdfreader.pro.app.room.dao
+
+import androidx.room.*
+import com.all.pdfreader.pro.app.room.entity.BookmarkEntity
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface BookmarkDao {
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(bookmark: BookmarkEntity): Long
+
+ @Update
+ suspend fun update(bookmark: BookmarkEntity)
+
+ @Delete
+ suspend fun delete(bookmark: BookmarkEntity)
+
+ @Query("SELECT * FROM bookmarks WHERE id = :bookmarkId")
+ suspend fun getById(bookmarkId: Long): BookmarkEntity?
+
+ @Query("SELECT * FROM bookmarks WHERE pdfHash = :pdfHash ORDER BY pageNumber ASC, createTime ASC")
+ fun getBookmarksByPdf(pdfHash: String): Flow>
+
+ @Query("SELECT * FROM bookmarks WHERE pdfHash = :pdfHash AND pageNumber = :pageNumber")
+ suspend fun getBookmarksByPage(pdfHash: String, pageNumber: Int): List
+
+ @Query("SELECT COUNT(*) FROM bookmarks WHERE pdfHash = :pdfHash")
+ suspend fun getBookmarkCount(pdfHash: String): Int
+
+ @Query("DELETE FROM bookmarks WHERE pdfHash = :pdfHash")
+ suspend fun deleteAllByPdf(pdfHash: String)
+
+ @Query("DELETE FROM bookmarks WHERE pdfHash = :pdfHash AND pageNumber = :pageNumber")
+ suspend fun deleteByPage(pdfHash: String, pageNumber: Int)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/dao/NoteDao.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/dao/NoteDao.kt
new file mode 100644
index 0000000..3333fd4
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/room/dao/NoteDao.kt
@@ -0,0 +1,39 @@
+package com.all.pdfreader.pro.app.room.dao
+
+import androidx.room.*
+import com.all.pdfreader.pro.app.room.entity.NoteEntity
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface NoteDao {
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(note: NoteEntity): Long
+
+ @Update
+ suspend fun update(note: NoteEntity)
+
+ @Delete
+ suspend fun delete(note: NoteEntity)
+
+ @Query("SELECT * FROM notes WHERE id = :noteId")
+ suspend fun getById(noteId: Long): NoteEntity?
+
+ @Query("SELECT * FROM notes WHERE pdfHash = :pdfHash ORDER BY pageNumber ASC, createTime ASC")
+ fun getNotesByPdf(pdfHash: String): Flow>
+
+ @Query("SELECT * FROM notes WHERE pdfHash = :pdfHash AND pageNumber = :pageNumber")
+ suspend fun getNotesByPage(pdfHash: String, pageNumber: Int): List
+
+ @Query("SELECT * FROM notes WHERE pdfHash = :pdfHash AND noteType = :noteType")
+ fun getNotesByType(pdfHash: String, noteType: String): Flow>
+
+ @Query("SELECT COUNT(*) FROM notes WHERE pdfHash = :pdfHash")
+ suspend fun getNoteCount(pdfHash: String): Int
+
+ @Query("DELETE FROM notes WHERE pdfHash = :pdfHash")
+ suspend fun deleteAllByPdf(pdfHash: String)
+
+ @Query("DELETE FROM notes WHERE pdfHash = :pdfHash AND pageNumber = :pageNumber")
+ suspend fun deleteByPage(pdfHash: String, pageNumber: Int)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/dao/PdfDocumentDao.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/dao/PdfDocumentDao.kt
new file mode 100644
index 0000000..fe93834
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/room/dao/PdfDocumentDao.kt
@@ -0,0 +1,36 @@
+package com.all.pdfreader.pro.app.room.dao
+
+import androidx.room.*
+import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface PdfDocumentDao {
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertOrUpdate(document: PdfDocumentEntity)
+
+ @Update
+ suspend fun update(document: PdfDocumentEntity)
+
+ @Query("SELECT * FROM pdf_documents WHERE fileHash = :fileHash")
+ suspend fun getByHash(fileHash: String): PdfDocumentEntity?
+
+ @Query("SELECT * FROM pdf_documents WHERE filePath = :filePath")
+ suspend fun getByPath(filePath: String): PdfDocumentEntity?
+
+ @Query("SELECT * FROM pdf_documents WHERE isFavorite = 1 ORDER BY addedToFavoriteTime DESC")
+ fun getFavoriteDocuments(): Flow>
+
+ @Query("SELECT * FROM pdf_documents WHERE fileName LIKE '%' || :query || '%' OR metadataTitle LIKE '%' || :query || '%'")
+ fun searchDocuments(query: String): Flow>
+
+ @Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC")
+ fun getAllDocuments(): Flow>
+
+ @Delete
+ suspend fun delete(document: PdfDocumentEntity)
+
+ @Query("DELETE FROM pdf_documents WHERE fileHash = :fileHash")
+ suspend fun deleteByHash(fileHash: String)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/dao/RecentReadDao.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/dao/RecentReadDao.kt
new file mode 100644
index 0000000..1c5900b
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/room/dao/RecentReadDao.kt
@@ -0,0 +1,36 @@
+package com.all.pdfreader.pro.app.room.dao
+
+import androidx.room.*
+import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
+import com.all.pdfreader.pro.app.room.entity.RecentReadEntity
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface RecentReadDao {
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertOrUpdate(recentRead: RecentReadEntity)
+
+ @Query("SELECT * FROM recently_read WHERE pdfHash = :pdfHash")
+ suspend fun getByPdfHash(pdfHash: String): RecentReadEntity?
+
+ @Query("""
+ SELECT pdf_documents.*
+ FROM pdf_documents
+ INNER JOIN recently_read ON pdf_documents.fileHash = recently_read.pdfHash
+ ORDER BY recently_read.lastOpenedTime DESC
+ """)
+ fun getRecentReadDocuments(): Flow>
+
+ @Query("UPDATE recently_read SET lastOpenedTime = :time, openedCount = openedCount + 1 WHERE pdfHash = :pdfHash")
+ suspend fun updateOpenTime(pdfHash: String, time: Long = System.currentTimeMillis())
+
+ @Query("UPDATE recently_read SET totalReadTime = totalReadTime + :additionalTime WHERE pdfHash = :pdfHash")
+ suspend fun addReadTime(pdfHash: String, additionalTime: Long)
+
+ @Query("DELETE FROM recently_read WHERE pdfHash = :pdfHash")
+ suspend fun deleteByPdfHash(pdfHash: String)
+
+ @Query("DELETE FROM recently_read WHERE lastOpenedTime < :cutoffTime")
+ suspend fun deleteOldRecents(cutoffTime: Long)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/database/PdfDatabase.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/database/PdfDatabase.kt
new file mode 100644
index 0000000..a2a228e
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/room/database/PdfDatabase.kt
@@ -0,0 +1,32 @@
+package com.all.pdfreader.pro.app.room.database
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import com.all.pdfreader.pro.app.room.converter.DateConverter
+import com.all.pdfreader.pro.app.room.converter.UriConverter
+import com.all.pdfreader.pro.app.room.dao.*
+import com.all.pdfreader.pro.app.room.entity.*
+
+@Database(
+ entities = [
+ PdfDocumentEntity::class,
+ RecentReadEntity::class,
+ BookmarkEntity::class,
+ NoteEntity::class
+ ],
+ version = 1,
+ exportSchema = false
+)
+@TypeConverters(DateConverter::class, UriConverter::class)
+abstract class PdfDatabase : RoomDatabase() {
+
+ abstract fun pdfDocumentDao(): PdfDocumentDao
+ abstract fun recentReadDao(): RecentReadDao
+ abstract fun bookmarkDao(): BookmarkDao
+ abstract fun noteDao(): NoteDao
+
+ companion object {
+ const val DATABASE_NAME = "pdf_database"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/entity/BookmarkEntity.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/entity/BookmarkEntity.kt
new file mode 100644
index 0000000..f3eddca
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/room/entity/BookmarkEntity.kt
@@ -0,0 +1,28 @@
+package com.all.pdfreader.pro.app.room.entity
+
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import androidx.room.PrimaryKey
+
+@Entity(
+ tableName = "bookmarks",
+ foreignKeys = [ForeignKey(
+ entity = PdfDocumentEntity::class,
+ parentColumns = ["fileHash"],
+ childColumns = ["pdfHash"],
+ onDelete = ForeignKey.CASCADE
+ )],
+ indices = [Index(value = ["pdfHash"])]
+)
+data class BookmarkEntity(
+ @PrimaryKey(autoGenerate = true)
+ val id: Long = 0,
+
+ val pdfHash: String, // 关联PdfDocumentEntity的fileHash
+ val pageNumber: Int, // 页码(从0开始)
+ val label: String, // 书签标签
+ val positionX: Float = 0f, // 页面内X位置
+ val positionY: Float = 0f, // 页面内Y位置
+ val createTime: Long = System.currentTimeMillis()
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/entity/NoteEntity.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/entity/NoteEntity.kt
new file mode 100644
index 0000000..77dfc4c
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/room/entity/NoteEntity.kt
@@ -0,0 +1,33 @@
+package com.all.pdfreader.pro.app.room.entity
+
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import androidx.room.PrimaryKey
+
+@Entity(
+ tableName = "notes",
+ foreignKeys = [ForeignKey(
+ entity = PdfDocumentEntity::class,
+ parentColumns = ["fileHash"],
+ childColumns = ["pdfHash"],
+ onDelete = ForeignKey.CASCADE
+ )],
+ indices = [Index(value = ["pdfHash"])]
+)
+data class NoteEntity(
+ @PrimaryKey(autoGenerate = true)
+ val id: Long = 0,
+
+ val pdfHash: String, // 关联PdfDocumentEntity的fileHash
+ val pageNumber: Int, // 页码(从0开始)
+ val noteType: String, // 注释类型: HIGHLIGHT, TEXT_NOTE, DRAWING
+ val content: String, // 注释内容(文本或序列化的绘制数据)
+ val positionX: Float, // 页面内X位置
+ val positionY: Float, // 页面内Y位置
+ val width: Float = 0f, // 注释宽度
+ val height: Float = 0f, // 注释高度
+ val color: Int, // 注释颜色
+ val createTime: Long = System.currentTimeMillis(),
+ val updateTime: Long = System.currentTimeMillis()
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/entity/PdfDocumentEntity.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/entity/PdfDocumentEntity.kt
new file mode 100644
index 0000000..c5d6997
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/room/entity/PdfDocumentEntity.kt
@@ -0,0 +1,31 @@
+package com.all.pdfreader.pro.app.room.entity
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "pdf_documents")
+data class PdfDocumentEntity(
+ @PrimaryKey
+ val fileHash: String, // 文件内容哈希(MD5/SHA-1)
+
+ val filePath: String, // 当前文件路径
+ val fileName: String, // 文件名
+ val fileSize: Long, // 文件大小(字节)
+ val lastModified: Long, // 文件最后修改时间
+ val pageCount: Int, // 总页数
+
+ val isFavorite: Boolean = false, // 是否收藏
+ val addedToFavoriteTime: Long? = null, // 收藏时间
+
+ val lastOpenedTime: Long = 0, // 最后打开时间
+ val lastReadPage: Int = 0, // 最后阅读页码
+ val readingProgress: Float = 0f, // 阅读进度(0-100)
+
+ val thumbnailPath: String? = null, // 缩略图本地路径
+ val metadataTitle: String? = null, // PDF元数据标题
+ val metadataAuthor: String? = null, // PDF元数据作者
+ val metadataSubject: String? = null, // PDF元数据主题
+ val metadataKeywords: String? = null, // PDF元数据关键词
+ val metadataCreationDate: Long? = null, // PDF创建时间
+ val metadataModificationDate: Long? = null // PDF修改时间
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/entity/RecentReadEntity.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/entity/RecentReadEntity.kt
new file mode 100644
index 0000000..ebcc9b0
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/room/entity/RecentReadEntity.kt
@@ -0,0 +1,27 @@
+package com.all.pdfreader.pro.app.room.entity
+
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import androidx.room.PrimaryKey
+
+@Entity(
+ tableName = "recently_read",
+ foreignKeys = [ForeignKey(
+ entity = PdfDocumentEntity::class,
+ parentColumns = ["fileHash"],
+ childColumns = ["pdfHash"],
+ onDelete = ForeignKey.CASCADE
+ )],
+ indices = [Index(value = ["pdfHash"])]
+)
+data class RecentReadEntity(
+ @PrimaryKey(autoGenerate = true)
+ val id: Long = 0,
+
+ val pdfHash: String, // 关联PdfDocumentEntity的fileHash
+ val lastOpenedTime: Long, // 最后打开时间
+ val openedCount: Int = 1, // 打开次数
+ val totalReadTime: Long = 0, // 总阅读时长(毫秒)
+ val exitPage: Int = 0 // 退出时的页码
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/repository/PdfRepository.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/repository/PdfRepository.kt
new file mode 100644
index 0000000..11ab84e
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/room/repository/PdfRepository.kt
@@ -0,0 +1,154 @@
+package com.all.pdfreader.pro.app.room.repository
+
+import android.content.Context
+import androidx.room.Room
+import com.all.pdfreader.pro.app.room.database.PdfDatabase
+import com.all.pdfreader.pro.app.room.entity.*
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import java.security.MessageDigest
+
+class PdfRepository private constructor(context: Context) {
+
+ private val database = Room.databaseBuilder(
+ context,
+ PdfDatabase::class.java,
+ PdfDatabase.DATABASE_NAME
+ ).build()
+
+ private val pdfDao = database.pdfDocumentDao()
+ private val recentDao = database.recentReadDao()
+ private val bookmarkDao = database.bookmarkDao()
+ private val noteDao = database.noteDao()
+
+ // PDF文档相关操作
+ suspend fun insertOrUpdateDocument(document: PdfDocumentEntity) {
+ pdfDao.insertOrUpdate(document)
+ }
+
+ suspend fun getDocumentByHash(fileHash: String): PdfDocumentEntity? {
+ return pdfDao.getByHash(fileHash)
+ }
+
+ suspend fun getDocumentByPath(filePath: String): PdfDocumentEntity? {
+ return pdfDao.getByPath(filePath)
+ }
+
+ fun getAllDocuments(): Flow> = pdfDao.getAllDocuments()
+ fun getFavoriteDocuments(): Flow> = pdfDao.getFavoriteDocuments()
+ fun searchDocuments(query: String): Flow> = pdfDao.searchDocuments(query)
+
+ suspend fun updateFavoriteStatus(fileHash: String, isFavorite: Boolean) {
+ val document = pdfDao.getByHash(fileHash)?.copy(
+ isFavorite = isFavorite,
+ addedToFavoriteTime = if (isFavorite) System.currentTimeMillis() else null
+ )
+ document?.let { pdfDao.update(it) }
+ }
+
+ suspend fun updateReadingProgress(fileHash: String, page: Int, progress: Float) {
+ val document = pdfDao.getByHash(fileHash)?.copy(
+ lastOpenedTime = System.currentTimeMillis(),
+ lastReadPage = page,
+ readingProgress = progress
+ )
+ document?.let { pdfDao.update(it) }
+ }
+
+ // 最近阅读相关操作
+ suspend fun addToRecent(pdfHash: String, page: Int = 0) {
+ val existing = recentDao.getByPdfHash(pdfHash)
+ if (existing != null) {
+ recentDao.updateOpenTime(pdfHash)
+ } else {
+ recentDao.insertOrUpdate(RecentReadEntity(pdfHash = pdfHash, lastOpenedTime = System.currentTimeMillis()))
+ }
+ }
+
+ fun getRecentReadDocuments(): Flow> = recentDao.getRecentReadDocuments()
+
+ // 书签相关操作
+ suspend fun addBookmark(bookmark: BookmarkEntity): Long = bookmarkDao.insert(bookmark)
+ suspend fun updateBookmark(bookmark: BookmarkEntity) = bookmarkDao.update(bookmark)
+ suspend fun deleteBookmark(bookmark: BookmarkEntity) = bookmarkDao.delete(bookmark)
+ fun getBookmarksByPdf(pdfHash: String): Flow> = bookmarkDao.getBookmarksByPdf(pdfHash)
+ suspend fun getBookmarksByPage(pdfHash: String, page: Int): List = bookmarkDao.getBookmarksByPage(pdfHash, page)
+
+ // 注释相关操作
+ suspend fun addNote(note: NoteEntity): Long = noteDao.insert(note)
+ suspend fun updateNote(note: NoteEntity) = noteDao.update(note)
+ suspend fun deleteNote(note: NoteEntity) = noteDao.delete(note)
+ fun getNotesByPdf(pdfHash: String): Flow> = noteDao.getNotesByPdf(pdfHash)
+ suspend fun getNotesByPage(pdfHash: String, page: Int): List = noteDao.getNotesByPage(pdfHash, page)
+ fun getNotesByType(pdfHash: String, noteType: String): Flow> = noteDao.getNotesByType(pdfHash, noteType)
+
+ // 组合查询
+ suspend fun getPdfWithDetails(pdfHash: String): Flow {
+ return combine(
+ pdfDao.getByHash(pdfHash)?.let { kotlinx.coroutines.flow.flowOf(it) } ?: kotlinx.coroutines.flow.flowOf(null),
+ bookmarkDao.getBookmarksByPdf(pdfHash),
+ noteDao.getNotesByPdf(pdfHash)
+ ) { document, bookmarks, notes ->
+ PdfDetails(document, bookmarks, notes)
+ }
+ }
+
+ // 工具方法
+ suspend fun generateFileHash(filePath: String): String {
+ return try {
+ val file = java.io.File(filePath)
+ if (!file.exists()) return ""
+
+ val digest = MessageDigest.getInstance("MD5")
+ file.inputStream().use { input ->
+ val buffer = ByteArray(8192)
+ var bytes = input.read(buffer)
+ while (bytes >= 0) {
+ digest.update(buffer, 0, bytes)
+ bytes = input.read(buffer)
+ }
+ }
+ digest.digest().joinToString("") { "%02x".format(it) }
+ } catch (e: Exception) {
+ ""
+ }
+ }
+
+ // 数据清理
+ suspend fun deleteDocument(fileHash: String) {
+ pdfDao.deleteByHash(fileHash)
+ recentDao.deleteByPdfHash(fileHash)
+ bookmarkDao.deleteAllByPdf(fileHash)
+ noteDao.deleteAllByPdf(fileHash)
+ }
+
+ companion object {
+ @Volatile
+ private var INSTANCE: PdfRepository? = null
+
+ fun initialize(context: Context) {
+ if (INSTANCE == null) {
+ synchronized(this) {
+ if (INSTANCE == null) {
+ INSTANCE = PdfRepository(context.applicationContext)
+ }
+ }
+ }
+ }
+
+ fun getInstance(): PdfRepository {
+ return INSTANCE ?: throw IllegalStateException("PdfRepository must be initialized first")
+ }
+
+ // 向后兼容的方法
+ fun getInstance(context: Context): PdfRepository {
+ return getInstance()
+ }
+ }
+}
+
+data class PdfDetails(
+ val document: PdfDocumentEntity?,
+ val bookmarks: List,
+ val notes: List
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/sp/AppStore.kt b/app/src/main/java/com/all/pdfreader/pro/app/sp/AppStore.kt
new file mode 100644
index 0000000..6b4042b
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/sp/AppStore.kt
@@ -0,0 +1,21 @@
+package com.all.pdfreader.pro.app.sp
+
+import android.content.Context
+import com.all.pdfreader.pro.app.sp.store.Store
+import com.all.pdfreader.pro.app.sp.store.asStoreProvider
+
+class AppStore(context: Context) {
+ private val store = Store(
+ context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE).asStoreProvider()
+ )
+
+ //权限提示的对话框是否已经展示过
+ var isShowPermissionsDialogPrompt: Boolean by store.boolean(
+ key = PERMISSIONS_DIALOG_PROMPT, defaultValue = false
+ )
+
+ companion object {
+ private const val FILE_NAME = "prp_sp_name"
+ private const val PERMISSIONS_DIALOG_PROMPT = "permissions_dialog_prompt"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/sp/store/Providers.kt b/app/src/main/java/com/all/pdfreader/pro/app/sp/store/Providers.kt
new file mode 100644
index 0000000..4972470
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/sp/store/Providers.kt
@@ -0,0 +1,60 @@
+package com.all.pdfreader.pro.app.sp.store
+
+import android.content.SharedPreferences
+import androidx.core.content.edit
+
+class SharedPreferenceProvider(private val preferences: SharedPreferences) : StoreProvider {
+ override fun getInt(key: String, defaultValue: Int): Int {
+ return preferences.getInt(key, defaultValue)
+ }
+
+ override fun setInt(key: String, value: Int) {
+ preferences.edit {
+ putInt(key, value)
+ }
+ }
+
+ override fun getLong(key: String, defaultValue: Long): Long {
+ return preferences.getLong(key, defaultValue)
+ }
+
+ override fun setLong(key: String, value: Long) {
+ preferences.edit {
+ putLong(key, value)
+ }
+ }
+
+ override fun getString(key: String, defaultValue: String): String {
+ return preferences.getString(key, defaultValue)!!
+ }
+
+ override fun setString(key: String, value: String) {
+ preferences.edit {
+ putString(key, value)
+ }
+ }
+
+ override fun getStringSet(key: String, defaultValue: Set): Set {
+ return preferences.getStringSet(key, defaultValue)!!
+ }
+
+ override fun setStringSet(key: String, value: Set) {
+ preferences.edit {
+ putStringSet(key, value)
+ }
+ }
+
+ override fun getBoolean(key: String, defaultValue: Boolean): Boolean {
+ return preferences.getBoolean(key, defaultValue)
+ }
+
+ override fun setBoolean(key: String, value: Boolean) {
+ preferences.edit {
+ putBoolean(key, value)
+ }
+ }
+}
+
+fun SharedPreferences.asStoreProvider(): StoreProvider {
+ return SharedPreferenceProvider(this)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/sp/store/Store.kt b/app/src/main/java/com/all/pdfreader/pro/app/sp/store/Store.kt
new file mode 100644
index 0000000..7b28a33
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/sp/store/Store.kt
@@ -0,0 +1,98 @@
+package com.all.pdfreader.pro.app.sp.store
+
+import kotlin.reflect.KProperty
+
+class Store(val provider: StoreProvider) {
+ interface Delegate {
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): T
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)
+ }
+
+ fun int(key: String, defaultValue: Int): Delegate {
+ return object : Delegate {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
+ return provider.getInt(key, defaultValue)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
+ provider.setInt(key, value)
+ }
+ }
+ }
+
+ fun long(key: String, defaultValue: Long): Delegate {
+ return object : Delegate {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Long {
+ return provider.getLong(key, defaultValue)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Long) {
+ provider.setLong(key, value)
+ }
+ }
+ }
+
+ fun string(key: String, defaultValue: String): Delegate {
+ return object : Delegate {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): String {
+ return provider.getString(key, defaultValue)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
+ provider.setString(key, value)
+ }
+ }
+ }
+
+ fun stringSet(key: String, defaultValue: Set): Delegate> {
+ return object : Delegate> {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Set {
+ return provider.getStringSet(key, defaultValue)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set) {
+ provider.setStringSet(key, value)
+ }
+ }
+ }
+
+ fun boolean(key: String, defaultValue: Boolean): Delegate {
+ return object : Delegate {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean {
+ return provider.getBoolean(key, defaultValue)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
+ provider.setBoolean(key, value)
+ }
+ }
+ }
+
+ fun > enum(key: String, defaultValue: T, values: Array): Delegate {
+ return object : Delegate {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): T {
+ val name = provider.getString(key, defaultValue.name)
+
+ return values.find { name == it.name } ?: defaultValue
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
+ provider.setString(key, value.name)
+ }
+ }
+ }
+
+ fun typedString(key: String, from: (String) -> T?, to: (T?) -> String): Delegate {
+ return object : Delegate {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): T? {
+ val value = provider.getString(key, to(null))
+
+ return from(value)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
+ provider.setString(key, to(value))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/sp/store/StoreProvider.kt b/app/src/main/java/com/all/pdfreader/pro/app/sp/store/StoreProvider.kt
new file mode 100644
index 0000000..bdeff99
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/sp/store/StoreProvider.kt
@@ -0,0 +1,18 @@
+package com.all.pdfreader.pro.app.sp.store
+
+interface StoreProvider {
+ fun getInt(key: String, defaultValue: Int): Int
+ fun setInt(key: String, value: Int)
+
+ fun getLong(key: String, defaultValue: Long): Long
+ fun setLong(key: String, value: Long)
+
+ fun getString(key: String, defaultValue: String): String
+ fun setString(key: String, value: String)
+
+ fun getStringSet(key: String, defaultValue: Set): Set
+ fun setStringSet(key: String, value: Set)
+
+ fun getBoolean(key: String, defaultValue: Boolean): Boolean
+ fun setBoolean(key: String, value: Boolean)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/BaseActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/BaseActivity.kt
new file mode 100644
index 0000000..3f5bc3e
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/BaseActivity.kt
@@ -0,0 +1,67 @@
+package com.all.pdfreader.pro.app.ui.act
+
+import android.app.Activity
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import com.all.pdfreader.pro.app.room.repository.PdfRepository
+import com.all.pdfreader.pro.app.sp.AppStore
+import com.all.pdfreader.pro.app.util.StoragePermissionHelper
+
+abstract class BaseActivity : AppCompatActivity() {
+
+ protected abstract val TAG: String
+ protected val appStore by lazy { AppStore(this) }
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.d("ocean", "🚀 ${javaClass.simpleName} onCreate")
+ }
+
+ override fun onStart() {
+ super.onStart()
+ Log.d("ocean", "🔄 ${javaClass.simpleName} onStart")
+ }
+
+ override fun onResume() {
+ super.onResume()
+ Log.d("ocean", "📱 ${javaClass.simpleName} onResume")
+ }
+
+ override fun onPause() {
+ super.onPause()
+ Log.d("ocean", "⏸️ ${javaClass.simpleName} onPause")
+ }
+
+ override fun onStop() {
+ super.onStop()
+ Log.d("ocean", "🛑 ${javaClass.simpleName} onStop")
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ Log.d("ocean", "💀 ${javaClass.simpleName} onDestroy")
+ }
+
+ protected fun logDebug(message: String) {
+ Log.d("ocean", "$TAG: $message")
+ }
+
+ protected fun logError(message: String, throwable: Throwable? = null) {
+ Log.e("ocean", "$TAG: $message", throwable)
+ }
+
+ protected fun logInfo(message: String) {
+ Log.i("ocean", "$TAG: $message")
+ }
+
+ protected fun logWarning(message: String) {
+ Log.w("ocean", "$TAG: $message")
+ }
+
+ //获取数据库实例
+ protected fun getRepository(): PdfRepository {
+ return PdfRepository.getInstance()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MainActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MainActivity.kt
new file mode 100644
index 0000000..d856d31
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MainActivity.kt
@@ -0,0 +1,296 @@
+package com.all.pdfreader.pro.app.ui.act
+
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import com.all.pdfreader.pro.app.R
+import com.all.pdfreader.pro.app.databinding.ActivityMainBinding
+import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
+import com.all.pdfreader.pro.app.room.repository.PdfRepository
+import com.all.pdfreader.pro.app.ui.dialog.PermissionDialogFragment
+import com.all.pdfreader.pro.app.ui.fragment.FavoriteFrag
+import com.all.pdfreader.pro.app.ui.fragment.HomeFrag
+import com.all.pdfreader.pro.app.ui.fragment.RecentlyFrag
+import com.all.pdfreader.pro.app.ui.fragment.ToolsFrag
+import com.all.pdfreader.pro.app.util.FileChangeObserver
+import com.all.pdfreader.pro.app.util.FileUtils
+import com.all.pdfreader.pro.app.util.PdfMetadataExtractor
+import com.all.pdfreader.pro.app.util.ScanManager
+import com.all.pdfreader.pro.app.util.StoragePermissionHelper
+import com.gyf.immersionbar.ImmersionBar
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.util.concurrent.TimeUnit
+
+class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback,
+ PermissionDialogFragment.CloseCallback {
+
+ override val TAG: String = "MainActivity"
+
+ private lateinit var binding: ActivityMainBinding
+ private val pdfRepository = getRepository()
+ private lateinit var fileChangeObserver: FileChangeObserver
+
+ private val homeFragment = HomeFrag()
+ private val recentlyFragment = RecentlyFrag()
+ private val favoriteFragment = FavoriteFrag()
+ private val toolsFragment = ToolsFrag()
+
+ private var activeFragment: Fragment = homeFragment
+ private val fragmentTag = "ACTIVE_FRAGMENT"
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
+ .navigationBarColor(R.color.white).init()
+
+ setupFragments()
+ setupNavigation()
+ if (savedInstanceState != null) {
+ val restoredFragment =
+ supportFragmentManager.getFragment(savedInstanceState, fragmentTag)
+ if (restoredFragment != null) {
+ activeFragment = restoredFragment
+ }
+ supportFragmentManager.beginTransaction().hide(homeFragment).hide(recentlyFragment)
+ .hide(favoriteFragment).hide(toolsFragment).show(activeFragment).commit()
+ }
+ updateSelectedNav(activeFragment)
+ }
+
+ private fun scanningStrategy() {
+ // 智能扫描策略
+ if (ScanManager.shouldScan(this)) {
+ logDebug("🔄 需要扫描PDF文件 (首次启动或超过24小时)")
+ if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
+ scanAndLoadPdfFiles()
+ } else {
+ logDebug("❌ 权限不足,跳过扫描")
+ }
+ } else {
+ val lastScan = ScanManager.getLastScanTime(this)
+ val hoursAgo = TimeUnit.MILLISECONDS.toHours(System.currentTimeMillis() - lastScan)
+ logDebug("⏭️ 跳过扫描,上次扫描在${hoursAgo}小时前")
+ }
+ }
+
+ private fun setupFragments() {
+ supportFragmentManager.beginTransaction().add(R.id.fragment_fl, toolsFragment, "TOOLS")
+ .hide(toolsFragment).add(R.id.fragment_fl, favoriteFragment, "FAVORITE")
+ .hide(favoriteFragment).add(R.id.fragment_fl, recentlyFragment, "RECENTLY")
+ .hide(recentlyFragment).add(R.id.fragment_fl, homeFragment, "HOME").commit()
+ }
+
+ //按钮点击事件
+ private fun setupNavigation() {
+ binding.homeLlBtn.setOnClickListener { switchFragment(homeFragment) }
+ binding.recentlyLlBtn.setOnClickListener { switchFragment(recentlyFragment) }
+ binding.favoriteLlBtn.setOnClickListener { switchFragment(favoriteFragment) }
+ binding.toolsLayoutBtn.setOnClickListener { switchFragment(toolsFragment) }
+
+ binding.pnGoBtn.setOnClickListener {
+ //直接跳转到权限设置页面
+ requestPermissions()
+ }
+ }
+
+ private fun switchFragment(target: Fragment) {
+ if (target == activeFragment) return
+ supportFragmentManager.beginTransaction().hide(activeFragment).show(target).commit()
+ activeFragment = target
+ updateSelectedNav(target)
+ }
+
+ private fun updateSelectedNav(fragment: Fragment) {
+ binding.homeIv.alpha = 0.5f
+ binding.recentlyIv.alpha = 0.5f
+ binding.favoriteIv.alpha = 0.5f
+ binding.toolsIv.alpha = 0.5f
+ if (fragment is ToolsFrag) {//工具界面不展示权限
+ binding.pnLayout.visibility = View.GONE
+ }
+ val targetIcon = when (fragment) {
+ is HomeFrag -> binding.homeIv
+ is RecentlyFrag -> binding.recentlyIv
+ is FavoriteFrag -> binding.favoriteIv
+ is ToolsFrag -> binding.toolsIv
+ else -> null
+ }
+ targetIcon?.apply {
+ alpha = 1f
+ animate().scaleX(1.2f).scaleY(1.2f).setDuration(150).withEndAction {
+ animate().scaleX(1f).scaleY(1f).setDuration(150).start()
+ }.start()
+ }
+ }
+
+ fun scanAndLoadPdfFiles(callback: (Boolean) -> Unit = {}) {
+ if (!StoragePermissionHelper.hasBasicStoragePermission(this)) {
+ logDebug("权限不足")
+ callback.invoke(false)
+ return
+ }
+ lifecycleScope.launch {
+ withContext(Dispatchers.IO) {
+ try {
+ logDebug("🔍 开始扫描PDF文件...")
+
+ // 扫描应用私有目录(无需权限)
+ val privateFiles = FileUtils.scanPdfFiles(this@MainActivity)
+ logDebug("📁 应用私有目录找到: ${privateFiles.size} 个PDF文件")
+ privateFiles.forEach { file ->
+ logDebug(" 📄 ${file.name} (${FileUtils.formatFileSize(file.length())})")
+ }
+
+ // 扫描MediaStore(需要权限)
+ val mediaStoreFiles = FileUtils.scanPdfFilesFromMediaStore(this@MainActivity)
+ logDebug("📱 MediaStore找到: ${mediaStoreFiles.size} 个PDF文件")
+ mediaStoreFiles.forEach { file ->
+ logDebug(" 📱 ${file.name} (${FileUtils.formatFileSize(file.length())})")
+ }
+
+ // 合并并去重
+ val allFiles = (privateFiles + mediaStoreFiles).distinctBy { it.absolutePath }
+ logDebug("📊 总计扫描到: ${allFiles.size} 个PDF文件")
+
+ // 处理每个PDF文件
+ allFiles.forEachIndexed { index, file ->
+ logDebug("🔄 处理文件 ${index + 1}/${allFiles.size}: ${file.name}")
+
+ if (FileUtils.isPdfFile(file)) {
+ val fileHash = FileUtils.calculateFileHash(file.absolutePath)
+ logDebug(" 🔑 文件哈希: $fileHash")
+
+ if (fileHash != null) {
+ val existingDoc = pdfRepository.getDocumentByHash(fileHash)
+
+ if (existingDoc == null) {
+ logDebug(" 🆕 发现新PDF文件: ${file.name}")
+ val metadata =
+ PdfMetadataExtractor.extractMetadata(file.absolutePath)
+ val document = PdfDocumentEntity(
+ fileHash = fileHash,
+ filePath = file.absolutePath,
+ fileName = file.name,
+ fileSize = file.length(),
+ lastModified = file.lastModified(),
+ pageCount = metadata?.pageCount ?: 0,
+ metadataTitle = metadata?.title,
+ metadataAuthor = metadata?.author,
+ metadataSubject = metadata?.subject,
+ metadataKeywords = metadata?.keywords,
+ metadataCreationDate = metadata?.creationDate?.time,
+ metadataModificationDate = metadata?.modificationDate?.time
+ )
+ pdfRepository.insertOrUpdateDocument(document)
+ logDebug(" ✅ 已保存到数据库: ${file.name}")
+ } else {
+ logDebug(" 📋 文件已存在: ${file.name}")
+ if (existingDoc.filePath != file.absolutePath) {
+ logDebug(" 🔄 更新文件路径: ${existingDoc.filePath} -> ${file.absolutePath}")
+ val updatedDoc = existingDoc.copy(
+ filePath = file.absolutePath,
+ lastModified = file.lastModified()
+ )
+ pdfRepository.insertOrUpdateDocument(updatedDoc)
+ }
+ }
+ }
+ }
+ }
+
+ // 打印数据库中的总记录数
+ pdfRepository.getAllDocuments().collect { docs ->
+ logDebug("📊 数据库中共有: ${docs.size} 个PDF记录")
+ docs.forEach { doc ->
+ logDebug(
+ " 📖 ${doc.fileName} - ${doc.pageCount}页 - ${
+ FileUtils.formatFileSize(
+ doc.fileSize
+ )
+ } - ${doc.thumbnailPath}"
+ )
+ }
+
+ // 标记扫描完成
+ ScanManager.markScanComplete(this@MainActivity)
+ val lastScanTime = ScanManager.getLastScanTime(this@MainActivity)
+ logDebug("✅ 扫描完成,记录时间: ${java.util.Date(lastScanTime)}")
+ callback.invoke(true)
+ }
+
+ } catch (e: Exception) {
+ logError("❌ 扫描出错: ${e.message}", e)
+ callback.invoke(false)
+ }
+ }
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ supportFragmentManager.putFragment(outState, fragmentTag, activeFragment)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ logDebug("main onResume")
+ if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
+ // 有授权才初始化文件变化监听器
+ fileChangeObserver = FileChangeObserver(this, lifecycle)
+ scanningStrategy()
+ binding.pnLayout.visibility = View.GONE
+ } else {
+ binding.pnLayout.visibility = View.VISIBLE
+ val dialog = PermissionDialogFragment()
+ //如果之前展示过授权对话框,则不再展示
+ if (!appStore.isShowPermissionsDialogPrompt) {
+ dialog.show(supportFragmentManager, TAG)
+ }
+ }
+ }
+
+ // 授权后续操作
+ override fun onPermissionGranted() {
+ logDebug("main onPermissionGranted")
+ //授权成功后:隐藏授权提示,开始扫描文件
+ binding.pnLayout.visibility = View.GONE
+ fileChangeObserver = FileChangeObserver(this, lifecycle)
+ scanningStrategy()
+ }
+
+ override fun onClose() {
+ logDebug("main onClose")
+ //关闭对话框进行权限入口显示。
+ if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
+ binding.pnLayout.visibility = View.GONE
+ } else {
+ binding.pnLayout.visibility = View.VISIBLE
+ }
+ }
+
+ private fun requestPermissions() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ StoragePermissionHelper.openAllFilesAccessSettings(this)
+ } else {
+ val permissions = StoragePermissionHelper.getRequiredPermissions()
+ multiplePermissionsLauncher.launch(permissions)
+ }
+ }
+
+ private val multiplePermissionsLauncher =
+ registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
+ val allGranted = permissions.values.all { it }
+ if (allGranted) {
+ onPermissionGranted()
+ } else {
+ onClose()
+ }
+ }
+}
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplashActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplashActivity.kt
new file mode 100644
index 0000000..72ab439
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplashActivity.kt
@@ -0,0 +1,57 @@
+package com.all.pdfreader.pro.app.ui.act
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import com.all.pdfreader.pro.app.databinding.ActivitySplashBinding
+import com.gyf.immersionbar.ImmersionBar
+
+@SuppressLint("CustomSplashScreen")
+class SplashActivity : BaseActivity() {
+
+ override val TAG: String = "SplashActivity"
+
+ private lateinit var binding: ActivitySplashBinding
+
+ companion object {
+ private const val SPLASH_DELAY = 3000L // 启动页显示时长
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivitySplashBinding.inflate(layoutInflater)
+ ImmersionBar
+ .with(this)
+ .fullScreen(true)
+ .statusBarDarkFont(true)
+ .transparentNavigationBar()
+ .init()
+
+ // 设置启动页布局
+ setContentView(binding.root)
+
+ // 延迟跳转到权限检查
+ Handler(Looper.getMainLooper()).postDelayed({
+ navigateToNext()
+ }, SPLASH_DELAY)
+ }
+
+ private fun navigateToNext() {
+ val intent = Intent(this, MainActivity::class.java)
+// val intent = if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
+// Intent(this, MainActivity::class.java)
+// } else {
+// Intent(this, PermissionActivity::class.java)
+// }
+ startActivity(intent)
+ finish()
+ }
+
+ @Deprecated("Deprecated in Java")
+ @SuppressLint("MissingSuperCall")
+ override fun onBackPressed() {
+ // 启动页禁止返回
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/PdfAdapter.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/PdfAdapter.kt
new file mode 100644
index 0000000..0e9f65e
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/PdfAdapter.kt
@@ -0,0 +1,50 @@
+package com.all.pdfreader.pro.app.ui.adapter
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.all.pdfreader.pro.app.databinding.AdapterPdfItemBinding
+import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
+import com.all.pdfreader.pro.app.util.FileUtils.toFormatFileSize
+import com.all.pdfreader.pro.app.util.FileUtils.toSlashDate
+
+class PdfAdapter(
+ private var pdfList: MutableList,
+ private val onItemClick: (PdfDocumentEntity) -> Unit,
+ private val onMoreClick: (PdfDocumentEntity) -> Unit
+) : RecyclerView.Adapter() {
+
+ inner class PdfViewHolder(val binding: AdapterPdfItemBinding) :
+ RecyclerView.ViewHolder(binding.root)
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfViewHolder {
+ val binding =
+ AdapterPdfItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return PdfViewHolder(binding)
+ }
+
+ @SuppressLint("SetTextI18n")
+ override fun onBindViewHolder(holder: PdfViewHolder, position: Int) {
+ val item = pdfList[position]
+ holder.binding.tvFileName.text = item.fileName
+ holder.binding.tvFileSize.text = item.fileSize.toFormatFileSize()
+ holder.binding.tvFileDate.text = item.lastModified.toSlashDate()
+
+ holder.binding.root.setOnClickListener {
+ onItemClick(item)
+ }
+ holder.binding.moreBtn.setOnClickListener {
+ onMoreClick(item)
+ }
+ }
+
+ override fun getItemCount(): Int = pdfList.size
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun updateData(newList: List) {
+ pdfList.clear()
+ pdfList.addAll(newList)
+ notifyDataSetChanged()
+ }
+}
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/PermissionDialogFragment.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/PermissionDialogFragment.kt
new file mode 100644
index 0000000..8dfcde3
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/PermissionDialogFragment.kt
@@ -0,0 +1,117 @@
+package com.all.pdfreader.pro.app.ui.dialog
+
+import android.app.Activity
+import android.app.Dialog
+import android.content.Intent
+import android.graphics.Color
+import android.os.Build
+import android.os.Bundle
+import android.provider.Settings
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
+import androidx.core.graphics.drawable.toDrawable
+import androidx.core.net.toUri
+import androidx.fragment.app.DialogFragment
+import com.all.pdfreader.pro.app.R
+import com.all.pdfreader.pro.app.databinding.DialogPermissionBinding
+import com.all.pdfreader.pro.app.sp.AppStore
+import com.all.pdfreader.pro.app.util.StoragePermissionHelper
+
+class PermissionDialogFragment : DialogFragment() {
+
+ private lateinit var binding: DialogPermissionBinding
+
+ private val multiplePermissionsLauncher =
+ registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
+ val allGranted = permissions.values.all { it }
+ if (allGranted) {
+ onPermissionGranted()
+ } else {
+ showPermissionDeniedDialog()
+ }
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val dialog = Dialog(requireContext(), R.style.BottomSheetDialogStyle)
+ binding = DialogPermissionBinding.inflate(layoutInflater)
+ dialog.setContentView(binding.root)
+ AppStore(requireActivity()).isShowPermissionsDialogPrompt = true
+ // 设置对话框在底部显示并左右铺满
+ dialog.window?.let { window ->
+ window.setLayout(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ window.setGravity(Gravity.BOTTOM)
+ window.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
+ }
+
+ dialog.setCancelable(false)
+ dialog.setCanceledOnTouchOutside(false)
+
+ binding.prDesc.text =
+ getString(R.string.permission_required_desc_1, getString(R.string.app_name))
+
+ binding.allowAccessBtn.setOnClickListener {
+ if (checkPermissions()) {
+ onPermissionGranted()
+ } else {
+ requestPermissions()
+ }
+ dismiss()
+ }
+
+ binding.closeBtn.setOnClickListener {
+ dismiss()
+ (activity as? CloseCallback)?.onClose()
+ }
+ return dialog
+ }
+
+ private fun checkPermissions(): Boolean {
+ return StoragePermissionHelper.hasBasicStoragePermission(requireContext())
+ }
+
+ private fun requestPermissions() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ StoragePermissionHelper.openAllFilesAccessSettings(requireContext() as Activity)
+ } else {
+ val permissions = StoragePermissionHelper.getRequiredPermissions()
+ multiplePermissionsLauncher.launch(permissions)
+ }
+ }
+
+ private fun showPermissionDeniedDialog() {
+ AlertDialog.Builder(requireContext())
+ .setTitle("权限被拒绝")
+ .setMessage("无法访问PDF文件。请在设置中授予存储权限")
+ .setPositiveButton("前往设置") { _, _ ->
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.data = "package:${requireContext().packageName}".toUri()
+ startActivity(intent)
+ dismiss()
+ }
+ .setNegativeButton("退出") { _, _ ->
+ dismiss()
+ }
+ .setCancelable(false)
+ .show()
+ }
+
+ //有权限直接响应授权成功
+ private fun onPermissionGranted() {
+ dismiss()
+ (activity as? PermissionCallback)?.onPermissionGranted()
+ }
+
+ interface PermissionCallback {
+ fun onPermissionGranted()
+ }
+
+ interface CloseCallback {
+ fun onClose()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/BaseFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/BaseFrag.kt
new file mode 100644
index 0000000..e8b57a8
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/BaseFrag.kt
@@ -0,0 +1,66 @@
+package com.all.pdfreader.pro.app.ui.fragment
+
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.all.pdfreader.pro.app.databinding.FragmentFavoriteBinding
+import com.all.pdfreader.pro.app.room.repository.PdfRepository
+import com.all.pdfreader.pro.app.sp.AppStore
+
+abstract class BaseFrag : Fragment() {
+ protected abstract val TAG: String
+ protected val appStore by lazy { AppStore(requireActivity()) }
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.d("ocean", "🚀 ${javaClass.simpleName} onCreate")
+ }
+
+ override fun onStart() {
+ super.onStart()
+ Log.d("ocean", "🔄 ${javaClass.simpleName} onStart")
+ }
+
+ override fun onResume() {
+ super.onResume()
+ Log.d("ocean", "📱 ${javaClass.simpleName} onResume")
+ }
+
+ override fun onPause() {
+ super.onPause()
+ Log.d("ocean", "⏸️ ${javaClass.simpleName} onPause")
+ }
+
+ override fun onStop() {
+ super.onStop()
+ Log.d("ocean", "🛑 ${javaClass.simpleName} onStop")
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ Log.d("ocean", "💀 ${javaClass.simpleName} onDestroy")
+ }
+
+ protected fun logDebug(message: String) {
+ Log.d("ocean", "$TAG: $message")
+ }
+
+ protected fun logError(message: String, throwable: Throwable? = null) {
+ Log.e("ocean", "$TAG: $message", throwable)
+ }
+
+ protected fun logInfo(message: String) {
+ Log.i("ocean", "$TAG: $message")
+ }
+
+ protected fun logWarning(message: String) {
+ Log.w("ocean", "$TAG: $message")
+ }
+
+ //获取数据库实例
+ protected fun getRepository(): PdfRepository {
+ return PdfRepository.getInstance()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/FavoriteFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/FavoriteFrag.kt
new file mode 100644
index 0000000..f1f79dc
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/FavoriteFrag.kt
@@ -0,0 +1,28 @@
+package com.all.pdfreader.pro.app.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.all.pdfreader.pro.app.databinding.FragmentFavoriteBinding
+
+class FavoriteFrag : Fragment() {
+ private lateinit var binding: FragmentFavoriteBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentFavoriteBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ initView()
+ }
+
+ private fun initView() {
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt
new file mode 100644
index 0000000..5333dfd
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt
@@ -0,0 +1,67 @@
+package com.all.pdfreader.pro.app.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.all.pdfreader.pro.app.databinding.FragmentHomeBinding
+import com.all.pdfreader.pro.app.room.repository.PdfRepository
+import com.all.pdfreader.pro.app.ui.act.MainActivity
+import com.all.pdfreader.pro.app.ui.adapter.PdfAdapter
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.launch
+
+class HomeFrag : BaseFrag() {
+ override val TAG: String = "HomeFrag"
+ private lateinit var binding: FragmentHomeBinding
+ private lateinit var adapter: PdfAdapter
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentHomeBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ initView()
+ observeDocuments()
+ }
+
+ private fun initView() {
+ adapter = PdfAdapter(pdfList = mutableListOf(), onItemClick = { pdf ->
+ Toast.makeText(requireContext(), "点击: ${pdf.fileName}", Toast.LENGTH_SHORT).show()
+ }, onMoreClick = { pdf ->
+ Toast.makeText(requireContext(), "更多操作: ${pdf.fileName}", Toast.LENGTH_SHORT).show()
+ })
+
+ binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
+ binding.recyclerView.adapter = adapter
+
+ // 下拉刷新示例
+ binding.swipeRefreshLayout.setOnRefreshListener {
+ lifecycleScope.launch {
+ (activity as? MainActivity)?.scanAndLoadPdfFiles { b ->
+ binding.swipeRefreshLayout.isRefreshing = false
+ }
+ }
+ }
+ }
+
+ private fun observeDocuments() {
+ lifecycleScope.launch {
+ viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ PdfRepository.getInstance().getAllDocuments().collect { list ->
+ adapter.updateData(list)
+ logDebug("更新adapter数据")
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/RecentlyFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/RecentlyFrag.kt
new file mode 100644
index 0000000..b276eda
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/RecentlyFrag.kt
@@ -0,0 +1,28 @@
+package com.all.pdfreader.pro.app.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.all.pdfreader.pro.app.databinding.FragmentRecentlyBinding
+
+class RecentlyFrag : Fragment() {
+ private lateinit var binding: FragmentRecentlyBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentRecentlyBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ initView()
+ }
+
+ private fun initView() {
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt
new file mode 100644
index 0000000..a65fcbe
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt
@@ -0,0 +1,28 @@
+package com.all.pdfreader.pro.app.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.all.pdfreader.pro.app.databinding.FragmentToolsBinding
+
+class ToolsFrag : Fragment() {
+ private lateinit var binding: FragmentToolsBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentToolsBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ initView()
+ }
+
+ private fun initView() {
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/FileChangeObserver.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/FileChangeObserver.kt
new file mode 100644
index 0000000..a99b29e
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/util/FileChangeObserver.kt
@@ -0,0 +1,96 @@
+@file:Suppress("DEPRECATION")
+
+package com.all.pdfreader.pro.app.util
+
+import android.content.Context
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.MediaStore
+import android.util.Log
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlin.collections.distinctBy
+import kotlin.collections.plus
+
+class FileChangeObserver(
+ private val context: Context,
+ private val lifecycle: Lifecycle
+) : LifecycleObserver {
+
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+ private var contentObserver: ContentObserver? = null
+ private var isObserving = false
+
+ // 防抖机制,避免频繁触发
+ private var lastScanTime = 0L
+ private val debounceDelay = 3000L // 3秒防抖
+
+ init {
+ lifecycle.addObserver(this)
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_START)
+ fun startObserving() {
+ if (isObserving) return
+
+ contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
+ override fun onChange(selfChange: Boolean, uri: Uri?) {
+ super.onChange(selfChange, uri)
+ handleFileChange()
+ }
+ }
+
+ try {
+ context.contentResolver.registerContentObserver(
+ MediaStore.Files.getContentUri("external"),
+ true,
+ contentObserver!!
+ )
+ isObserving = true
+ Log.d("ocean", "📡 开始监听文件变化")
+ } catch (e: Exception) {
+ Log.e("ocean", "❌ 注册文件变化监听器失败", e)
+ }
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+ fun stopObserving() {
+ if (!isObserving) return
+
+ try {
+ contentObserver?.let { observer ->
+ context.contentResolver.unregisterContentObserver(observer)
+ }
+ isObserving = false
+ Log.d("ocean", "📡 停止监听文件变化")
+ } catch (e: Exception) {
+ Log.e("ocean", "❌ 注销文件变化监听器失败", e)
+ }
+ }
+
+ private fun handleFileChange() {
+ val currentTime = System.currentTimeMillis()
+ if (currentTime - lastScanTime < debounceDelay) {
+ return // 防抖
+ }
+
+ lastScanTime = currentTime
+
+ scope.launch {
+ delay(debounceDelay) // 额外延迟,避免频繁扫描
+
+ if (StoragePermissionHelper.hasBasicStoragePermission(context)) {
+ Log.d("ocean", "📂 检测到文件变化,可以执行增量扫描...")
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt
new file mode 100644
index 0000000..131ddea
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt
@@ -0,0 +1,328 @@
+package com.all.pdfreader.pro.app.util
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore
+import android.provider.OpenableColumns
+import android.util.Log
+import android.webkit.MimeTypeMap
+import androidx.annotation.RequiresApi
+import java.io.File
+import java.io.InputStream
+import java.security.MessageDigest
+import com.all.pdfreader.pro.app.util.StoragePermissionHelper
+import java.text.SimpleDateFormat
+import java.time.Instant
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.util.Date
+import java.util.Locale
+
+object FileUtils {
+
+ private val PDF_MIME_TYPES = setOf(
+ "application/pdf", "application/x-pdf"
+ )
+
+ fun scanPdfFiles(context: Context): List {
+ val pdfFiles = mutableListOf()
+
+ // 应用私有目录(无需权限)
+ Log.d("ocean", "📁 扫描应用私有目录...")
+ scanDirectory(context.filesDir, pdfFiles)
+ context.getExternalFilesDir(null)?.let { scanDirectory(it, pdfFiles) }
+
+ // 外部存储根目录(需要权限)
+ if (StoragePermissionHelper.hasBasicStoragePermission(context)) {
+ Log.d("ocean", "📂 扫描外部存储目录...")
+
+ // 扫描常见的PDF存储目录
+ val externalStorage = android.os.Environment.getExternalStorageDirectory()
+ scanCommonDirectories(externalStorage, pdfFiles)
+
+ // 扫描Download目录
+ android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOWNLOADS)
+ ?.let { scanDirectory(it, pdfFiles) }
+
+ // 扫描Documents目录
+ android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOCUMENTS)
+ ?.let { scanDirectory(it, pdfFiles) }
+
+ // 扫描内部存储根目录
+ scanDirectoryRecursively(externalStorage, pdfFiles)
+ }
+
+ Log.d("ocean", "📊 文件扫描完成,共找到 ${pdfFiles.size} 个PDF文件")
+ return pdfFiles.distinct()
+ }
+
+ private fun scanDirectory(directory: File, result: MutableList) {
+ if (!directory.exists() || !directory.canRead()) return
+
+ try {
+ directory.listFiles()?.forEach { file ->
+ when {
+ file.isDirectory -> scanDirectory(file, result)
+ file.isFile && file.extension.equals("pdf", ignoreCase = true) -> {
+ result.add(file)
+ Log.d(
+ "ocean", " 📄 找到PDF: ${file.name} (${formatFileSize(file.length())})"
+ )
+ }
+ }
+ }
+ } catch (e: SecurityException) {
+ Log.d("ocean", "⚠️ 跳过目录:${directory.path} (权限不足)")
+ }
+ }
+
+ private fun scanDirectoryRecursively(directory: File, result: MutableList) {
+ if (!directory.exists() || !directory.canRead()) return
+
+ val maxDepth = 3 // 限制递归深度,避免扫描过深
+ scanDirectoryRecursive(directory, result, 0, maxDepth)
+ }
+
+ private fun scanDirectoryRecursive(
+ directory: File, result: MutableList, currentDepth: Int, maxDepth: Int
+ ) {
+ if (!directory.exists() || !directory.canRead() || currentDepth > maxDepth) return
+
+ try {
+ directory.listFiles()?.forEach { file ->
+ when {
+ file.isDirectory && !isSystemDirectory(file) -> {
+ scanDirectoryRecursive(file, result, currentDepth + 1, maxDepth)
+ }
+
+ file.isFile && file.extension.equals("pdf", ignoreCase = true) -> {
+ result.add(file)
+ }
+ }
+ }
+ } catch (e: SecurityException) {
+ Log.d("ocean", "⚠️ 跳过目录:${directory.path} (权限不足)")
+ }
+ }
+
+ private fun scanCommonDirectories(root: File, result: MutableList) {
+ val commonDirs = listOf(
+ "Download", "Documents", "Books", "PDF", "Ebooks", "阅读", "书籍", "资料"
+ )
+
+ commonDirs.forEach { dirName ->
+ val dir = File(root, dirName)
+ if (dir.exists() && dir.canRead()) {
+ Log.d("ocean", "📂 扫描常见目录:${dir.path}")
+ scanDirectory(dir, result)
+ }
+ }
+ }
+
+ private fun isSystemDirectory(file: File): Boolean {
+ val systemDirs = listOf("Android", ".", "__", "lost+found", "LOST.DIR")
+ return systemDirs.any { file.name.startsWith(it, ignoreCase = true) }
+ }
+
+ fun scanPdfFilesFromMediaStore(context: Context): List {
+ return if (StoragePermissionHelper.hasBasicStoragePermission(context)) {
+ scanMediaStoreWithPermission(context)
+ } else {
+ emptyList()
+ }
+ }
+
+ private fun scanMediaStoreWithPermission(context: Context): List {
+ val pdfFiles = mutableListOf()
+ val projection = arrayOf(
+ MediaStore.Files.FileColumns.DATA,
+ MediaStore.Files.FileColumns.MIME_TYPE,
+ MediaStore.Files.FileColumns.DISPLAY_NAME,
+ MediaStore.Files.FileColumns.SIZE
+ )
+
+ // 更宽松的搜索条件,支持多种PDF MIME类型
+ val selection =
+ "${MediaStore.Files.FileColumns.MIME_TYPE} IN (?, ?) OR ${MediaStore.Files.FileColumns.DISPLAY_NAME} LIKE ?"
+ val selectionArgs = arrayOf(
+ "application/pdf", "application/x-pdf", "%.pdf"
+ )
+
+ try {
+ context.contentResolver.query(
+ MediaStore.Files.getContentUri("external"),
+ projection,
+ selection,
+ selectionArgs,
+ MediaStore.Files.FileColumns.DATE_MODIFIED + " DESC"
+ )?.use { cursor ->
+ val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)
+
+ while (cursor.moveToNext()) {
+ val filePath = cursor.getString(dataColumn)
+ filePath?.let {
+ val file = File(it)
+ if (file.exists() && file.canRead() && isPdfFile(file)) {
+ pdfFiles.add(file)
+ Log.d("ocean", " 📱 MediaStore找到: ${file.name}")
+ }
+ }
+ }
+ }
+
+ // 额外的文件扩展名搜索(作为MediaStore的补充)
+ scanByExtension(context, pdfFiles)
+
+ } catch (e: SecurityException) {
+ Log.d("ocean", "⚠️ MediaStore扫描权限不足")
+ } catch (e: Exception) {
+ Log.d("ocean", "❌ MediaStore扫描错误: ${e.message}")
+ }
+
+ return pdfFiles
+ }
+
+ private fun scanByExtension(context: Context, result: MutableList) {
+ // 通过文件扩展名搜索PDF文件,作为MediaStore的补充
+ val directories = listOf(
+ android.os.Environment.getExternalStorageDirectory(),
+ android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOWNLOADS),
+ android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOCUMENTS),
+ File(android.os.Environment.getExternalStorageDirectory(), "Books"),
+ File(android.os.Environment.getExternalStorageDirectory(), "PDF"),
+ File(android.os.Environment.getExternalStorageDirectory(), "Ebooks")
+ )
+
+ directories.forEach { dir ->
+ if (dir.exists() && dir.canRead()) {
+ findPdfFilesByExtension(dir, result)
+ }
+ }
+ }
+
+ private fun findPdfFilesByExtension(directory: File, result: MutableList) {
+ if (!directory.exists() || !directory.canRead()) return
+
+ try {
+ directory.listFiles()?.forEach { file ->
+ when {
+ file.isDirectory && !isSystemDirectory(file) -> findPdfFilesByExtension(
+ file, result
+ )
+
+ file.isFile && file.extension.equals("pdf", ignoreCase = true) -> {
+ if (!result.contains(file)) {
+ result.add(file)
+ }
+ }
+ }
+ }
+ } catch (e: SecurityException) {
+ Log.d("ocean", "⚠️ 跳过扩展名扫描目录:${directory.path}")
+ }
+ }
+
+ fun getFileInfoFromUri(context: Context, uri: Uri): FileInfo? {
+ return try {
+ context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
+
+ val name = if (nameIndex != -1) cursor.getString(nameIndex) else ""
+ val size =
+ if (sizeIndex != -1 && !cursor.isNull(sizeIndex)) cursor.getLong(sizeIndex) else 0L
+
+ FileInfo(
+ name = name, size = size, uri = uri
+ )
+ } else null
+ }
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ fun calculateFileHash(filePath: String): String? {
+ return try {
+ val file = File(filePath)
+ if (!file.exists() || !file.canRead()) return null
+
+ calculateHash(file.inputStream())
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ fun calculateFileHash(uri: Uri, context: Context): String? {
+ return try {
+ context.contentResolver.openInputStream(uri)?.use { inputStream ->
+ calculateHash(inputStream)
+ }
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ private fun calculateHash(inputStream: InputStream): String {
+ val digest = MessageDigest.getInstance("MD5")
+ val buffer = ByteArray(8192)
+ var bytes = inputStream.read(buffer)
+
+ while (bytes >= 0) {
+ digest.update(buffer, 0, bytes)
+ bytes = inputStream.read(buffer)
+ }
+
+ return digest.digest().joinToString("") { "%02x".format(it) }
+ }
+
+ fun isPdfFile(file: File): Boolean {
+ return file.exists() && file.isFile && file.extension.equals(
+ "pdf", ignoreCase = true
+ ) && file.length() > 0
+ }
+
+ fun getFileExtension(fileName: String): String {
+ return fileName.substringAfterLast('.', "")
+ }
+
+ private const val kb = 1024.0
+ private const val mb = kb * 1024
+ private const val gb = mb * 1024
+
+ @SuppressLint("DefaultLocale")
+ fun formatFileSize(size: Long): String {
+ return when {
+ size <= 0 -> "0 B"
+ size < kb -> "$size B"
+ size < mb -> String.format("%.2f KB", size / kb)
+ size < gb -> String.format("%.2f MB", size / mb)
+ else -> String.format("%.2f GB", size / gb)
+ }
+ }
+
+ @SuppressLint("DefaultLocale")
+ fun Long.toFormatFileSize(): String {
+ val size = this.toDouble()
+ return when {
+ size <= 0 -> "0 B"
+ size < kb -> "$size B"
+ size < mb -> String.format("%.2f KB", size / kb)
+ size < gb -> String.format("%.2f MB", size / mb)
+ else -> String.format("%.2f GB", size / gb)
+ }
+ }
+
+ fun Long.toSlashDate(): String {
+ val sdf = SimpleDateFormat("M/d/yyyy", Locale.ENGLISH)
+ return sdf.format(Date(this))
+ }
+
+
+ data class FileInfo(
+ val name: String, val size: Long, val uri: Uri
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfMetadataExtractor.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfMetadataExtractor.kt
new file mode 100644
index 0000000..9a499f7
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfMetadataExtractor.kt
@@ -0,0 +1,154 @@
+package com.all.pdfreader.pro.app.util
+
+import android.content.Context
+import android.net.Uri
+import android.graphics.pdf.PdfRenderer
+import android.os.ParcelFileDescriptor
+import java.io.File
+import java.io.InputStream
+import java.util.*
+
+object PdfMetadataExtractor {
+
+ data class PdfMetadata(
+ val title: String?,
+ val author: String?,
+ val subject: String?,
+ val keywords: String?,
+ val creator: String?,
+ val producer: String?,
+ val creationDate: Date?,
+ val modificationDate: Date?,
+ val pageCount: Int,
+ val fileSize: Long
+ )
+
+ fun extractMetadata(filePath: String): PdfMetadata? {
+ return try {
+ val file = File(filePath)
+ if (!file.exists()) return null
+
+ val fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
+ extractFromFileDescriptor(fileDescriptor, file.length())
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ fun extractMetadata(uri: Uri, context: Context): PdfMetadata? {
+ return try {
+ context.contentResolver.openFileDescriptor(uri, "r")?.use { fileDescriptor ->
+ extractFromFileDescriptor(fileDescriptor, 0L)
+ }
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ private fun extractFromFileDescriptor(fileDescriptor: ParcelFileDescriptor, fileSize: Long): PdfMetadata? {
+ return try {
+ PdfRenderer(fileDescriptor).use { renderer ->
+ val pageCount = renderer.pageCount
+
+ // 使用文件基本信息
+ PdfMetadata(
+ title = null,
+ author = null,
+ subject = null,
+ keywords = null,
+ creator = null,
+ producer = null,
+ creationDate = null,
+ modificationDate = null,
+ pageCount = pageCount,
+ fileSize = fileSize
+ )
+ }
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ fun getBasicInfo(file: File): PdfMetadata? {
+ return try {
+ val fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
+ PdfRenderer(fileDescriptor).use { renderer ->
+ PdfMetadata(
+ title = null,
+ author = null,
+ subject = null,
+ keywords = null,
+ creator = null,
+ producer = null,
+ creationDate = null,
+ modificationDate = null,
+ pageCount = renderer.pageCount,
+ fileSize = file.length()
+ )
+ }
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ fun extractThumbnail(filePath: String, outputPath: String): Boolean {
+ return try {
+ val file = File(filePath)
+ if (!file.exists()) return false
+
+ val fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
+ PdfRenderer(fileDescriptor).use { renderer ->
+ if (renderer.pageCount > 0) {
+ val page = renderer.openPage(0)
+ val bitmap = android.graphics.Bitmap.createBitmap(
+ page.width,
+ page.height,
+ android.graphics.Bitmap.Config.ARGB_8888
+ )
+ page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
+ page.close()
+
+ java.io.FileOutputStream(outputPath).use { fos ->
+ bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 80, fos)
+ }
+ bitmap.recycle()
+ true
+ } else {
+ false
+ }
+ }
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ fun isValidPdf(file: File): Boolean {
+ return try {
+ val fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
+ PdfRenderer(fileDescriptor).use { renderer ->
+ renderer.pageCount > 0
+ }
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ fun sanitizeFilename(filename: String): String {
+ return filename.replace(Regex("[\\\\/:*?\"\u003c\u003e|]"), "_").trim()
+ }
+
+ fun extractTitleFromFilename(filename: String): String {
+ return filename.removeSuffix(".pdf").removeSuffix(".PDF")
+ }
+
+ fun formatDate(date: Date?): String {
+ return date?.let {
+ val calendar = Calendar.getInstance()
+ calendar.time = it
+ String.format(Locale.US, "%04d-%02d-%02d",
+ calendar.get(Calendar.YEAR),
+ calendar.get(Calendar.MONTH) + 1,
+ calendar.get(Calendar.DAY_OF_MONTH))
+ } ?: ""
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/ScanManager.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/ScanManager.kt
new file mode 100644
index 0000000..66dcea3
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/util/ScanManager.kt
@@ -0,0 +1,52 @@
+package com.all.pdfreader.pro.app.util
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.core.content.edit
+import java.util.concurrent.TimeUnit
+
+object ScanManager {
+
+ private const val PREF_NAME = "scan_preferences"
+ private const val KEY_FIRST_SCAN_DONE = "first_scan_done"
+ private const val KEY_LAST_SCAN_TIME = "last_scan_time"
+ private const val SCAN_INTERVAL_HOURS = 24L
+
+ fun shouldScan(context: Context): Boolean {
+ val prefs = getSharedPreferences(context)
+ val currentTime = System.currentTimeMillis()
+
+ // 首次启动需要扫描
+ if (!prefs.getBoolean(KEY_FIRST_SCAN_DONE, false)) {
+ return true
+ }
+
+ // 检查24小时间隔
+ val lastScanTime = prefs.getLong(KEY_LAST_SCAN_TIME, 0)
+ val hoursSinceLastScan = TimeUnit.MILLISECONDS.toHours(currentTime - lastScanTime)
+
+ return hoursSinceLastScan >= SCAN_INTERVAL_HOURS
+ }
+
+ fun markScanComplete(context: Context) {
+ getSharedPreferences(context).edit {
+ putBoolean(KEY_FIRST_SCAN_DONE, true)
+ putLong(KEY_LAST_SCAN_TIME, System.currentTimeMillis())
+ }
+ }
+
+ fun getLastScanTime(context: Context): Long {
+ return getSharedPreferences(context).getLong(KEY_LAST_SCAN_TIME, 0)
+ }
+
+ fun resetScanState(context: Context) {
+ getSharedPreferences(context).edit {
+ putBoolean(KEY_FIRST_SCAN_DONE, false)
+ putLong(KEY_LAST_SCAN_TIME, 0)
+ }
+ }
+
+ private fun getSharedPreferences(context: Context): SharedPreferences {
+ return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/StoragePermissionHelper.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/StoragePermissionHelper.kt
new file mode 100644
index 0000000..1327b1f
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/util/StoragePermissionHelper.kt
@@ -0,0 +1,83 @@
+package com.all.pdfreader.pro.app.util
+
+import android.Manifest
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.Settings
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
+
+object StoragePermissionHelper {
+
+ // 适配不同Android版本的权限
+ fun getRequiredPermissions(): Array {
+ return when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
+ // Android 13+
+ arrayOf(
+ Manifest.permission.READ_MEDIA_IMAGES,
+ Manifest.permission.READ_MEDIA_VIDEO
+ )
+ }
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
+ // Android 11-12
+ arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
+ }
+ else -> {
+ // Android 10及以下
+ arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
+ }
+ }
+ }
+
+ // 检查是否有基本存储权限
+ fun hasBasicStoragePermission(context: Context): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ // Android 11+ 检查是否拥有所有文件访问权限
+ Environment.isExternalStorageManager()
+ } else {
+ // Android 10及以下检查基本存储权限
+ val permissions = getRequiredPermissions()
+ permissions.all { permission ->
+ ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
+ }
+ }
+ }
+
+ // 请求权限
+ fun requestPermissions(activity: Activity, requestCode: Int) {
+ val permissions = getRequiredPermissions()
+ ActivityCompat.requestPermissions(activity, permissions, requestCode)
+ }
+
+ // 打开所有文件访问设置(Android 11+)
+ fun openAllFilesAccessSettings(activity: Activity) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ try {
+ val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
+ intent.data = "package:${activity.packageName}".toUri()
+ activity.startActivity(intent)
+ } catch (e: Exception) {
+ // 兜底:打开通用“所有文件访问权限”界面
+ val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
+ activity.startActivity(intent)
+ }
+ }
+ }
+
+ // 检查是否需要请求权限
+ fun shouldShowPermissionRationale(activity: Activity): Boolean {
+ val permissions = getRequiredPermissions()
+ return permissions.any { permission ->
+ ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/close_round.xml b/app/src/main/res/drawable/close_round.xml
new file mode 100644
index 0000000..db73430
--- /dev/null
+++ b/app/src/main/res/drawable/close_round.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/dr_click_btn_bg.xml b/app/src/main/res/drawable/dr_click_btn_bg.xml
new file mode 100644
index 0000000..8ff4b23
--- /dev/null
+++ b/app/src/main/res/drawable/dr_click_btn_bg.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/dr_rounded_corner_12_bg_white.xml b/app/src/main/res/drawable/dr_rounded_corner_12_bg_white.xml
new file mode 100644
index 0000000..0f1f658
--- /dev/null
+++ b/app/src/main/res/drawable/dr_rounded_corner_12_bg_white.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dr_rounded_corner_bg_grey.xml b/app/src/main/res/drawable/dr_rounded_corner_bg_grey.xml
new file mode 100644
index 0000000..78d42d6
--- /dev/null
+++ b/app/src/main/res/drawable/dr_rounded_corner_bg_grey.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dr_rounded_corner_top_bg_grey.xml b/app/src/main/res/drawable/dr_rounded_corner_top_bg_grey.xml
new file mode 100644
index 0000000..dd2cef9
--- /dev/null
+++ b/app/src/main/res/drawable/dr_rounded_corner_top_bg_grey.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/finger.xml b/app/src/main/res/drawable/finger.xml
new file mode 100644
index 0000000..79e2ad9
--- /dev/null
+++ b/app/src/main/res/drawable/finger.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/more.xml b/app/src/main/res/drawable/more.xml
new file mode 100644
index 0000000..7a3cb3b
--- /dev/null
+++ b/app/src/main/res/drawable/more.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/permission_required_open_btn.xml b/app/src/main/res/drawable/permission_required_open_btn.xml
new file mode 100644
index 0000000..427f1eb
--- /dev/null
+++ b/app/src/main/res/drawable/permission_required_open_btn.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..b73c530
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml
new file mode 100644
index 0000000..0d75c20
--- /dev/null
+++ b/app/src/main/res/layout/activity_splash.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/adapter_pdf_item.xml b/app/src/main/res/layout/adapter_pdf_item.xml
new file mode 100644
index 0000000..b5541ea
--- /dev/null
+++ b/app/src/main/res/layout/adapter_pdf_item.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_permission.xml b/app/src/main/res/layout/dialog_permission.xml
new file mode 100644
index 0000000..e01268a
--- /dev/null
+++ b/app/src/main/res/layout/dialog_permission.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_favorite.xml b/app/src/main/res/layout/fragment_favorite.xml
new file mode 100644
index 0000000..fb3ce0e
--- /dev/null
+++ b/app/src/main/res/layout/fragment_favorite.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
new file mode 100644
index 0000000..2a1214d
--- /dev/null
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_recently.xml b/app/src/main/res/layout/fragment_recently.xml
new file mode 100644
index 0000000..8ae653c
--- /dev/null
+++ b/app/src/main/res/layout/fragment_recently.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_tools.xml b/app/src/main/res/layout/fragment_tools.xml
new file mode 100644
index 0000000..c3ca252
--- /dev/null
+++ b/app/src/main/res/layout/fragment_tools.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..10ce468
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,15 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #CC000000
+ #99000000
+ #FFFFFFFF
+ #E6E6E6
+ #F6F6F6
+ #E0E0E0
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..daf19d6
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,15 @@
+
+ PDF Reader Pro
+ Home
+ Recently
+ Favorite
+ Tools
+ Professional PDF Document Reader
+ Permission Required
+ To Read and edit your files,Please allow %1$s to access all files
+ Allow access to manage all files
+ Allow Access
+ Go
+ Notice
+ Permission is required to access files
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..a845da4
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..1cf1feb
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/all/pdfreader/pro/app/ExampleUnitTest.kt b/app/src/test/java/com/all/pdfreader/pro/app/ExampleUnitTest.kt
new file mode 100644
index 0000000..e199701
--- /dev/null
+++ b/app/src/test/java/com/all/pdfreader/pro/app/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.all.pdfreader.pro.app
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..9c75ed0
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,6 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.devtools.ksp) apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..1ca9e9a
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,38 @@
+[versions]
+appcompat = "1.7.1"
+agp = "8.10.1"
+kotlin = "2.0.21"
+ksp = "2.0.21-1.0.27"
+coreKtx = "1.17.0"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+espressoCore = "3.7.0"
+lifecycleRuntimeKtx = "2.9.2"
+immersionbar = "3.2.2"
+immersionbarKtx = "3.2.2"
+room_version = "2.7.2"
+swiperefreshlayout = "1.1.0"
+recyclerview = "1.4.0"
+protoliteWellKnownTypes = "18.0.1"
+
+[libraries]
+androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room_version" }
+androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room_version" }
+androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room_version" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+immersionbar = { group = "com.geyifeng.immersionbar", name = "immersionbar", version.ref = "immersionbar" }
+immersionbar-ktx = { group = "com.geyifeng.immersionbar", name = "immersionbar-ktx", version.ref = "immersionbarKtx" }
+androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }
+androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
+protolite-well-known-types = { group = "com.google.firebase", name = "protolite-well-known-types", version.ref = "protoliteWellKnownTypes" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..24dd3af
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Aug 26 14:34:14 CST 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..c012862
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,24 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "PDF Reader Pro"
+include(":app")
+
\ No newline at end of file