Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {
android {
namespace = "com.rajat.sample.pdfviewer"

compileSdk = 35
compileSdk = 36

defaultConfig {
applicationId = "com.rajat.sample.pdfviewer"
Expand All @@ -29,7 +29,7 @@ android {
}

kotlin {
jvmToolchain(17)
jvmToolchain(21)
}

buildTypes {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 18 additions & 18 deletions pdfViewer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,7 +38,7 @@ android {
}

kotlin {
jvmToolchain(17)
jvmToolchain(21)
}

buildFeatures {
Expand All @@ -49,35 +49,35 @@ 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:")
debugImplementation("androidx.compose.ui:ui-tooling")
// 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 {
Expand Down
28 changes: 26 additions & 2 deletions pdfViewer/src/main/java/com/rajat/pdfviewer/PdfDownloader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}")
Expand Down
58 changes: 58 additions & 0 deletions pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
[email protected] = 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()
}

Comment on lines +162 to +194
Copy link

Copilot AI Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The download is started in onCreate, but this may execute multiple times if the lifecycle owner goes through multiple create/destroy cycles. The download should be started immediately when initWithUrl is called, not deferred to onCreate. Consider starting the download directly and using lifecycle observer only for cleanup.

Suggested change
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()
}
// Start the download immediately
val lifecycleScope = lifecycleOwner.lifecycleScope
this.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()
// Add observer only for cleanup
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {

Copilot uses AI. Check for mistakes.
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
lifecycleOwner.lifecycle.removeObserver(this)
}
})
}

Comment on lines +163 to +201
Copy link

Copilot AI Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lifecycle observer is added but the PDF download is started in onCreate. If the lifecycle is already past the CREATED state when this method is called, the onCreate callback will never be triggered and the PDF will never download. Consider checking the current lifecycle state and starting the download immediately if already created, or use a different lifecycle event like onStart.

Suggested change
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)
}
})
}
val startDownload = {
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()
}
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
startDownload()
} else {
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
startDownload()
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
lifecycleOwner.lifecycle.removeObserver(this)
}
})
}

Copilot uses AI. Check for mistakes.
/**
* Initializes the PDF view with a local [File].
*
Expand Down
32 changes: 18 additions & 14 deletions pdfViewer/src/main/java/com/rajat/pdfviewer/util/FileUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.*

Expand Down Expand Up @@ -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 {
Comment on lines +78 to +79
Copy link

Copilot AI Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The function signature uses an expression body but the implementation spans multiple lines. Consider using a block body for better readability and consistency with the multi-line implementation.

Copilot uses AI. Check for mistakes.
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) {
Expand Down