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