first commit

This commit is contained in:
ocean 2025-09-02 15:10:52 +08:00
commit 2e19fd0b28
92 changed files with 3872 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@ -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

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
PDF Reader Pro

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

18
.idea/deploymentTargetSelector.xml generated Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-08-29T07:07:15.234652Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=21181FDF6006C9" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

19
.idea/gradle.xml generated Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -0,0 +1,61 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

10
.idea/migrations.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

15
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CMakeSettings">
<configurations>
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
</configurations>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

17
.idea/runConfigurations.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

56
app/build.gradle.kts Normal file
View File

@ -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)
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -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)
}
}

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Android 11+ 存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Android 13+ 媒体权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- 读取所有文件权限Android 11+ 需要特殊处理) -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<application
android:name=".PDFReaderApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PDFReaderPro"
android:requestLegacyExternalStorage="true"
tools:targetApi="36">
<activity
android:name=".ui.act.SplashActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.PDFReaderPro">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.act.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.PDFReaderPro">
<!-- 处理PDF文件打开 -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="application/pdf" />
<data android:scheme="file" />
<data android:scheme="content" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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) }
}
}

View File

@ -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<List<BookmarkEntity>>
@Query("SELECT * FROM bookmarks WHERE pdfHash = :pdfHash AND pageNumber = :pageNumber")
suspend fun getBookmarksByPage(pdfHash: String, pageNumber: Int): List<BookmarkEntity>
@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)
}

View File

@ -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<List<NoteEntity>>
@Query("SELECT * FROM notes WHERE pdfHash = :pdfHash AND pageNumber = :pageNumber")
suspend fun getNotesByPage(pdfHash: String, pageNumber: Int): List<NoteEntity>
@Query("SELECT * FROM notes WHERE pdfHash = :pdfHash AND noteType = :noteType")
fun getNotesByType(pdfHash: String, noteType: String): Flow<List<NoteEntity>>
@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)
}

View File

@ -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<List<PdfDocumentEntity>>
@Query("SELECT * FROM pdf_documents WHERE fileName LIKE '%' || :query || '%' OR metadataTitle LIKE '%' || :query || '%'")
fun searchDocuments(query: String): Flow<List<PdfDocumentEntity>>
@Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC")
fun getAllDocuments(): Flow<List<PdfDocumentEntity>>
@Delete
suspend fun delete(document: PdfDocumentEntity)
@Query("DELETE FROM pdf_documents WHERE fileHash = :fileHash")
suspend fun deleteByHash(fileHash: String)
}

View File

@ -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<List<PdfDocumentEntity>>
@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)
}

View File

@ -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"
}
}

View File

@ -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()
)

View File

@ -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()
)

View File

@ -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修改时间
)

View File

@ -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 // 退出时的页码
)

View File

@ -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<List<PdfDocumentEntity>> = pdfDao.getAllDocuments()
fun getFavoriteDocuments(): Flow<List<PdfDocumentEntity>> = pdfDao.getFavoriteDocuments()
fun searchDocuments(query: String): Flow<List<PdfDocumentEntity>> = 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<List<PdfDocumentEntity>> = 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<List<BookmarkEntity>> = bookmarkDao.getBookmarksByPdf(pdfHash)
suspend fun getBookmarksByPage(pdfHash: String, page: Int): List<BookmarkEntity> = 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<List<NoteEntity>> = noteDao.getNotesByPdf(pdfHash)
suspend fun getNotesByPage(pdfHash: String, page: Int): List<NoteEntity> = noteDao.getNotesByPage(pdfHash, page)
fun getNotesByType(pdfHash: String, noteType: String): Flow<List<NoteEntity>> = noteDao.getNotesByType(pdfHash, noteType)
// 组合查询
suspend fun getPdfWithDetails(pdfHash: String): Flow<PdfDetails> {
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<BookmarkEntity>,
val notes: List<NoteEntity>
)

View File

@ -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"
}
}

View File

@ -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<String>): Set<String> {
return preferences.getStringSet(key, defaultValue)!!
}
override fun setStringSet(key: String, value: Set<String>) {
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)
}

View File

@ -0,0 +1,98 @@
package com.all.pdfreader.pro.app.sp.store
import kotlin.reflect.KProperty
class Store(val provider: StoreProvider) {
interface Delegate<T> {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)
}
fun int(key: String, defaultValue: Int): Delegate<Int> {
return object : Delegate<Int> {
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<Long> {
return object : Delegate<Long> {
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<String> {
return object : Delegate<String> {
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<String>): Delegate<Set<String>> {
return object : Delegate<Set<String>> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Set<String> {
return provider.getStringSet(key, defaultValue)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set<String>) {
provider.setStringSet(key, value)
}
}
}
fun boolean(key: String, defaultValue: Boolean): Delegate<Boolean> {
return object : Delegate<Boolean> {
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 <T : Enum<T>> enum(key: String, defaultValue: T, values: Array<T>): Delegate<T> {
return object : Delegate<T> {
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 <T> typedString(key: String, from: (String) -> T?, to: (T?) -> String): Delegate<T?> {
return object : Delegate<T?> {
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))
}
}
}
}

View File

@ -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<String>): Set<String>
fun setStringSet(key: String, value: Set<String>)
fun getBoolean(key: String, defaultValue: Boolean): Boolean
fun setBoolean(key: String, value: Boolean)
}

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View File

@ -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() {
// 启动页禁止返回
}
}

View File

@ -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<PdfDocumentEntity>,
private val onItemClick: (PdfDocumentEntity) -> Unit,
private val onMoreClick: (PdfDocumentEntity) -> Unit
) : RecyclerView.Adapter<PdfAdapter.PdfViewHolder>() {
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<PdfDocumentEntity>) {
pdfList.clear()
pdfList.addAll(newList)
notifyDataSetChanged()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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() {
}
}

View File

@ -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数据")
}
}
}
}
}

View File

@ -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() {
}
}

View File

@ -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() {
}
}

View File

@ -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", "📂 检测到文件变化,可以执行增量扫描...")
}
}
}
}

View File

@ -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<File> {
val pdfFiles = mutableListOf<File>()
// 应用私有目录(无需权限)
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<File>) {
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<File>) {
if (!directory.exists() || !directory.canRead()) return
val maxDepth = 3 // 限制递归深度,避免扫描过深
scanDirectoryRecursive(directory, result, 0, maxDepth)
}
private fun scanDirectoryRecursive(
directory: File, result: MutableList<File>, 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<File>) {
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<File> {
return if (StoragePermissionHelper.hasBasicStoragePermission(context)) {
scanMediaStoreWithPermission(context)
} else {
emptyList()
}
}
private fun scanMediaStoreWithPermission(context: Context): List<File> {
val pdfFiles = mutableListOf<File>()
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<File>) {
// 通过文件扩展名搜索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<File>) {
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
)
}

View File

@ -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))
} ?: ""
}
}

View File

@ -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)
}
}

View File

@ -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<String> {
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)
}
}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M512,0a512,512 0,1 0,0 1024A512,512 0,1 0,512 0zM717.3,666.9a36,36 0,0 1,-51 50.9L512,563 357.7,717.7a35.8,35.8 0,0 1,-50.9 0,36 36,0 0,1 0,-50.9L461.1,512l-154.5,-154.9a36,36 0,0 1,50.9 -50.9L512,461l154.3,-154.8a36,36 0,0 1,51 50.9L562.9,512l154.5,154.9z"
android:fillColor="#000000"
android:fillAlpha="0.25"/>
</vector>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 按下状态 -->
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#3B5C8E"/> <!-- 按下颜色:深一点 -->
<corners android:radius="12dp"/>
</shape>
</item>
<!-- 默认状态 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#4E78BA"/> <!-- 默认颜色 -->
<corners android:radius="12dp"/>
</shape>
</item>
</selector>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="12dp" />
<solid android:color="@color/white" />
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="10dp"/>
<solid android:color="@color/grey"/>
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:topLeftRadius="10dp"
android:topRightRadius="10dp" />
<solid android:color="@color/grey" />
</shape>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M398.6,954.2 L179.3,680.2c0,0 -11.1,-102.2 87.7,-59.2 57,25.6 77.4,79.3 77.4,79.3l2,-417.4c0,0 58.9,-87.9 110.2,0l0.9,264.3c0,0 63.8,-86.7 138.7,6.1 0,0 56.2,-94.5 135.3,4.6 0,0 56,-109.3 112.3,9.5l0,331.8c0,0 -3.3,25.9 -48.9,55.1L398.6,954.2 398.6,954.2zM398.6,954.2"
android:fillColor="#f8a128"/>
<!-- <path-->
<!-- android:pathData="M400.6,71C278.2,71 178.9,170.3 178.9,292.6c0,82.1 45.2,153 111.5,191.2l0,-68.5c-34.3,-30.4 -56.4,-74.2 -56.4,-123.6 0,-91.4 74.2,-165.6 165.6,-165.6 91.4,0 165.6,74.2 165.6,165.6 0,48.4 -21.2,91.6 -54.3,121.9l0,70.1c66.1,-38.4 111.2,-109.1 111.2,-191.1C622.2,170.3 522.9,71 400.6,71L400.6,71zM400.6,71"-->
<!-- android:fillColor="#f8a128"/>-->
</vector>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M576,192a64,64 0,1 0,-128 0,64 64,0 0,0 128,0zM576,512a64,64 0,1 0,-128 0,64 64,0 0,0 128,0zM512,896a64,64 0,1 1,0 -128,64 64,0 0,1 0,128z"
android:fillColor="#5A5A5A"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="256dp"
android:viewportWidth="2048"
android:viewportHeight="1024">
<path
android:pathData="M0,512C0,332.1 145.3,186.2 325,186.2H1630A325.6,325.6 0,0 1,1954.9 512C1954.9,691.9 1809.6,837.8 1629.9,837.8H324.9A325.4,325.4 0,0 1,0 512z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M1536,1024a512,512 0,1 0,0 -1024,512 512,0 0,0 0,1024z"
android:fillColor="#4E78BA"/>
</vector>

View File

@ -0,0 +1,198 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_color"
android:orientation="vertical">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<FrameLayout
android:id="@+id/fragment_fl"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<LinearLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical">
<LinearLayout
android:id="@+id/pnLayout"
android:layout_width="match_parent"
android:layout_height="58dp"
android:background="@drawable/dr_rounded_corner_top_bg_grey"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:visibility="visible">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@mipmap/ic_launcher_round" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/notice"
android:textColor="@color/black"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:text="@string/permission_notice"
android:textColor="@color/black_60"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/pnGoBtn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/dr_click_btn_bg"
android:paddingStart="24dp"
android:paddingTop="4dp"
android:paddingEnd="24dp"
android:paddingBottom="4dp"
android:text="@string/go"
android:textColor="@color/white"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="68dp"
android:background="@color/white">
<LinearLayout
android:id="@+id/home_ll_btn"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/home_iv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/home_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/home"
android:textColor="@color/black"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/recently_ll_btn"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/recently_iv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/recently_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/recently"
android:textColor="@color/black"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/favorite_ll_btn"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/favorite_iv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/favorite_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/favorite"
android:textColor="@color/black"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/tools_layout_btn"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/tools_iv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/tools_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/tools"
android:textColor="@color/black"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="@color/bg_color"
android:orientation="vertical">
<!-- App Logo -->
<ImageView
android:id="@+id/app_logo"
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@mipmap/ic_launcher" />
<!-- App Name -->
<TextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/app_name"
android:textColor="@color/black"
android:textSize="28sp"
android:textStyle="bold" />
<!-- App Description -->
<TextView
android:id="@+id/app_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:alpha="0.8"
android:text="@string/splash_desc"
android:textColor="@color/black"
android:textSize="16sp" />
<!-- Loading Indicator -->
<ProgressBar
android:id="@+id/loading_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:indeterminate="true"
android:indeterminateTint="@android:color/black" />
</LinearLayout>

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingTop="12dp"
tools:ignore="RtlSymmetry">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/tvFileImg"
android:layout_width="54dp"
android:layout_height="54dp"
android:src="@mipmap/ic_launcher_round" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/tvFileName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textColor="@color/black"
android:maxLines="1"
android:ellipsize="marquee"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tvFileDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2月27,2025"
android:textColor="@color/black_60"
android:textSize="14sp" />
<TextView
android:id="@+id/tvFileSize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="8.1MB"
android:textColor="@color/black_60"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/moreBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:gravity="center">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/more" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="@color/line_color" />
</LinearLayout>

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/dr_rounded_corner_12_bg_white"
android:orientation="vertical"
android:padding="16dp"
tools:ignore="UseCompoundDrawables">
<!-- 关闭按钮 -->
<LinearLayout
android:id="@+id/closeBtn"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_gravity="end"
android:gravity="center">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/close_round" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:src="@mipmap/ic_launcher"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:gravity="center"
android:text="@string/permission_required"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/pr_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:gravity="center"
android:text="@string/permission_required_desc_1"
android:textColor="@color/black_80"
android:textSize="16sp" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp">
<LinearLayout
android:id="@+id/allow_layout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@drawable/dr_rounded_corner_bg_grey"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="@string/permission_required_desc_2"
android:textColor="@color/black_80"
android:textSize="14sp" />
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/permission_required_open_btn" />
</LinearLayout>
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentEnd="true"
android:layout_marginTop="16dp"
android:src="@drawable/finger" />
</RelativeLayout>
<LinearLayout
android:id="@+id/allow_access_btn"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@drawable/dr_click_btn_bg"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/allow_access"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_color"
android:orientation="vertical">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/favorite"/>
</LinearLayout>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_color"
android:orientation="vertical">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:background="@color/bg_color"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/recently"/>
</LinearLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_color"
android:orientation="vertical">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/tools"/>
</LinearLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="black_80">#CC000000</color>
<color name="black_60">#99000000</color>
<color name="white">#FFFFFFFF</color>
<color name="grey">#E6E6E6</color>
<color name="bg_color">#F6F6F6</color>
<color name="line_color">#E0E0E0</color>
</resources>

View File

@ -0,0 +1,15 @@
<resources>
<string name="app_name">PDF Reader Pro</string>
<string name="home">Home</string>
<string name="recently">Recently</string>
<string name="favorite">Favorite</string>
<string name="tools">Tools</string>
<string name="splash_desc">Professional PDF Document Reader</string>
<string name="permission_required">Permission Required</string>
<string name="permission_required_desc_1">To Read and edit your files,Please allow %1$s to access all files</string>
<string name="permission_required_desc_2">Allow access to manage all files</string>
<string name="allow_access">Allow Access</string>
<string name="go">Go</string>
<string name="notice">Notice</string>
<string name="permission_notice">Permission is required to access files</string>
</resources>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="CustomDialogStyle" parent="Theme.AppCompat.Light.Dialog.Alert">
<item name="android:windowMinWidthMajor">90%</item>
<item name="android:windowMinWidthMinor">90%</item>
</style>
<style name="BottomSheetDialogStyle" parent="Theme.AppCompat.Light.Dialog">
<item name="android:windowFrame">@null</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
<item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
<item name="android:windowCloseOnTouchOutside">false</item>
<item name="android:windowIsTranslucent">true</item>
</style>
</resources>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.PDFReaderPro" parent="Theme.AppCompat.DayNight.NoActionBar" />
<!-- SplashActivity theme -->
<style name="Theme.PDFReaderPro.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>
<!-- PermissionActivity Dialog theme -->
<style name="Theme.PDFReaderPro.Dialog" parent="Theme.PDFReaderPro">
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:backgroundDimAmount">0.5</item>
</style>
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -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)
}
}

6
build.gradle.kts Normal file
View File

@ -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
}

23
gradle.properties Normal file
View File

@ -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

38
gradle/libs.versions.toml Normal file
View File

@ -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" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -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

185
gradlew vendored Executable file
View File

@ -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" "$@"

89
gradlew.bat vendored Normal file
View File

@ -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

24
settings.gradle.kts Normal file
View File

@ -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")