diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7281239..e056568 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,7 +9,7 @@ plugins { android { namespace = "com.rajat.sample.pdfviewer" - compileSdk = 35 + compileSdk = 36 defaultConfig { applicationId = "com.rajat.sample.pdfviewer" @@ -29,7 +29,7 @@ android { } kotlin { - jvmToolchain(17) + jvmToolchain(21) } buildTypes { @@ -92,30 +92,30 @@ android { dependencies { implementation("com.google.android.material:material:1.12.0") - implementation("androidx.test.espresso:espresso-contrib:3.6.1") - val kotlin_version = "2.1.20" + implementation("androidx.test.espresso:espresso-contrib:3.7.0") + val kotlin_version = "2.2.10" implementation(kotlin("stdlib")) implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) //noinspection GradleDependency implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version") - implementation("androidx.core:core-ktx:1.15.0") - implementation("androidx.appcompat:appcompat:1.7.0") + implementation("androidx.core:core-ktx:1.17.0") + implementation("androidx.appcompat:appcompat:1.7.1") implementation("androidx.constraintlayout:constraintlayout:2.2.1") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.2") implementation("androidx.compose.ui:ui-graphics") testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.2.1") - androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") + androidTestImplementation("androidx.test.ext:junit:1.3.0") + androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0") implementation(project(":pdfViewer")) // implementation("io.github.afreakyelf:Pdf-Viewer:2.1.1") - testImplementation("androidx.test:core:1.6.1") - androidTestImplementation("androidx.test:rules:1.6.1") - androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1") + testImplementation("androidx.test:core:1.7.0") + androidTestImplementation("androidx.test:rules:1.7.0") + androidTestImplementation("androidx.test.ext:junit-ktx:1.3.0") implementation("androidx.recyclerview:recyclerview:1.4.0") // Check for the latest version available // compose - implementation(platform("androidx.compose:compose-bom:2025.03.00")) + implementation(platform("androidx.compose:compose-bom:2025.08.00")) // Choose one of the following: // Material Design 3 @@ -130,16 +130,16 @@ dependencies { // Android Studio Preview support implementation("androidx.compose.ui:ui-tooling-preview") - androidTestImplementation(platform("androidx.compose:compose-bom:2025.04.01")) + androidTestImplementation(platform("androidx.compose:compose-bom:2025.08.00")) debugImplementation("androidx.compose.ui:ui-tooling") // UI Tests androidTestImplementation("androidx.compose.ui:ui-test-junit4") debugImplementation("androidx.compose.ui:ui-test-manifest") - androidTestImplementation("androidx.test.ext:junit:1.2.1") - androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") - androidTestImplementation("androidx.test:rules:1.6.1") - androidTestImplementation("androidx.test:runner:1.6.1") + androidTestImplementation("androidx.test.ext:junit:1.3.0") + androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0") + androidTestImplementation("androidx.test:rules:1.7.0") + androidTestImplementation("androidx.test:runner:1.7.0") // Optional - Integration with activities diff --git a/build.gradle.kts b/build.gradle.kts index f36dd09..1b1859f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ plugins { - id("com.android.application") version "8.10.0" apply false - id("com.android.library") version "8.10.0" apply false - id("org.jetbrains.kotlin.android") version "2.1.20" apply false - id("org.jetbrains.kotlin.plugin.compose") version "2.1.20" apply false + id("com.android.application") version "8.12.1" apply false + id("com.android.library") version "8.12.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.10" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.2.10" apply false } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 796759a..c9bfe2c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip diff --git a/pdfViewer/build.gradle.kts b/pdfViewer/build.gradle.kts index 6aa55fb..7e410e5 100644 --- a/pdfViewer/build.gradle.kts +++ b/pdfViewer/build.gradle.kts @@ -6,13 +6,13 @@ plugins { id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.plugin.compose") id("kotlin-parcelize") - id("org.jetbrains.dokka") version "1.9.20" + id("org.jetbrains.dokka") version "2.0.0" id("com.vanniktech.maven.publish") version "0.28.0" } android { namespace = "com.rajat.pdfviewer" - compileSdk = 35 + compileSdk = 36 defaultConfig { minSdk = 21 @@ -38,7 +38,7 @@ android { } kotlin { - jvmToolchain(17) + jvmToolchain(21) } buildFeatures { @@ -49,26 +49,26 @@ android { } dependencies { - implementation("androidx.compose.material3:material3-android:1.3.1") - val kotlin_version = "2.1.20" + implementation("androidx.compose.material3:material3-android:1.3.2") + val kotlin_version = "2.2.10" implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version") - implementation("androidx.core:core-ktx:1.15.0") - implementation("androidx.appcompat:appcompat:1.7.0") + implementation("androidx.core:core-ktx:1.17.0") + implementation("androidx.appcompat:appcompat:1.7.1") implementation("androidx.recyclerview:recyclerview:1.4.0") - implementation("androidx.constraintlayout:constraintlayout:2.2.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1") + implementation("androidx.constraintlayout:constraintlayout:2.2.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") implementation("com.google.android.material:material:1.12.0") testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.2.1") - androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") + androidTestImplementation("androidx.test.ext:junit:1.3.0") + androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0") // ViewModel - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7") - implementation("androidx.activity:activity-ktx:1.10.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.2") + implementation("androidx.activity:activity-ktx:1.10.1") // compose - implementation(platform("androidx.compose:compose-bom:2025.04.01")) - androidTestImplementation(platform("androidx.compose:compose-bom:2025.04.01")) + implementation(platform("androidx.compose:compose-bom:2025.08.00")) + androidTestImplementation(platform("androidx.compose:compose-bom:2025.08.00")) implementation("androidx.compose.ui:ui") // Android Studio Preview support implementation("androidx.compose.ui:ui-tooling-preview:") @@ -76,8 +76,8 @@ dependencies { // UI Tests androidTestImplementation("androidx.compose.ui:ui-test-junit4") debugImplementation("androidx.compose.ui:ui-test-manifest") - implementation("androidx.activity:activity-compose:1.10.0") - implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("androidx.activity:activity-compose:1.10.1") + implementation("com.squareup.okhttp3:okhttp:5.1.0") } mavenPublishing { diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfDownloader.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfDownloader.kt index da58e01..78e2dc2 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfDownloader.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfDownloader.kt @@ -13,13 +13,18 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext +import okhttp3.Call +import okhttp3.Callback import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Request import okhttp3.Response import java.io.File import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException class PdfDownloader( private val coroutineScope: CoroutineScope, @@ -175,13 +180,32 @@ class PdfDownloader( } } - private fun makeNetworkRequest(downloadUrl: String): Response { + private suspend fun makeNetworkRequest(downloadUrl: String): Response { val requestBuilder = Request.Builder().url(downloadUrl) headers.headers.forEach { (key, value) -> requestBuilder.addHeader(key, value) } - return httpClient.newCall(requestBuilder.build()).execute() + return httpClient.awaitCall(requestBuilder.build()) } + private suspend fun OkHttpClient.awaitCall(request: Request): Response = + suspendCancellableCoroutine { cont -> + val call = newCall(request) + + cont.invokeOnCancellation { + call.cancel() + } + + call.enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + if (cont.isActive) cont.resumeWithException(e) + } + + override fun onResponse(call: Call, response: Response) { + if (cont.isActive) cont.resume(response) + } + }) + } + private fun validateResponse(response: Response) { if (!response.isSuccessful) { throw DownloadFailedException("Failed to download PDF, HTTP Status: ${response.code}") diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt index 512a5c2..89bc1ff 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt @@ -15,9 +15,13 @@ import android.view.LayoutInflater import android.view.WindowManager import android.widget.FrameLayout import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -141,6 +145,60 @@ class PdfRendererView @JvmOverloads constructor( )).start() } + /** + * Initializes the PDF view with a remote URL. Downloads and renders the PDF. + * + * @param url The URL of the PDF file. + * @param headers Optional HTTP headers. + * @param lifecycleOwner The LifecycleOwner to bind the download lifecycle. + * @param cacheStrategy Cache strategy to apply. + */ + fun initWithUrl( + url: String, + headers: HeaderData = HeaderData(), + lifecycleOwner: LifecycleOwner, + cacheStrategy: CacheStrategy = CacheStrategy.MAXIMIZE_PERFORMANCE + ) { + + lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + super.onCreate(owner) + val lifecycleScope = lifecycleOwner.lifecycleScope + this@PdfRendererView.cacheStrategy = cacheStrategy + PdfDownloader( + lifecycleScope, + headers, + url, + cacheStrategy, + PdfDownloadCallback( + context, + onStart = { + statusListener?.onPdfLoadStart() + }, + onProgress = { progress, current, total -> + statusListener?.onPdfLoadProgress(progress, current, total) + }, + onSuccess = { + try { + initWithFile(it, cacheStrategy) + statusListener?.onPdfLoadSuccess(it.absolutePath) + } catch (e: Exception) { + statusListener?.onError(e) + } + }, + onError = { + statusListener?.onError(it) + } + )).start() + } + + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + lifecycleOwner.lifecycle.removeObserver(this) + } + }) + } + /** * Initializes the PDF view with a local [File]. * diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/util/FileUtils.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/util/FileUtils.kt index 4999c89..544487b 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/util/FileUtils.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/util/FileUtils.kt @@ -11,6 +11,8 @@ import android.os.ParcelFileDescriptor import android.provider.MediaStore import android.util.Log import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withContext import java.io.* @@ -73,23 +75,25 @@ object FileUtils { ?.forEach { it.delete() } } - fun writeFile(inputStream: InputStream, file: File, totalLength: Long, onProgress: (Long) -> Unit) { - FileOutputStream(file).use { outputStream -> - val data = ByteArray(8192) - var totalBytesRead = 0L - var bytesRead: Int - while (inputStream.read(data).also { bytesRead = it } != -1) { - outputStream.write(data, 0, bytesRead) - totalBytesRead += bytesRead - try { - onProgress(totalBytesRead) - } catch (e: Exception) { - Log.w(TAG, "Progress callback failed: ${e.message}", e) + suspend fun writeFile(inputStream: InputStream, file: File, totalLength: Long, onProgress: (Long) -> Unit) = + coroutineScope { + FileOutputStream(file).use { outputStream -> + val data = ByteArray(8192) + var totalBytesRead = 0L + var bytesRead: Int + while (inputStream.read(data).also { bytesRead = it } != -1) { + ensureActive() + outputStream.write(data, 0, bytesRead) + totalBytesRead += bytesRead + try { + onProgress(totalBytesRead) + } catch (e: Exception) { + Log.w(TAG, "Progress callback failed: ${e.message}", e) + } } + outputStream.flush() } - outputStream.flush() } - } suspend fun isValidPdf(file: File?): Boolean = withContext(Dispatchers.IO) { if (file == null || !file.exists() || file.length() < 4) {