From 801aedc8a37b5eed517fb2f436376f4ea3830ab9 Mon Sep 17 00:00:00 2001 From: Tim McIntosh Date: Tue, 18 Mar 2025 21:25:21 -0700 Subject: [PATCH 01/12] Update gradle + kotlin --- app/build.gradle.kts | 2 +- build.gradle.kts | 6 +++--- pdfViewer/build.gradle.kts | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 84b9183..67bc832 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -96,7 +96,7 @@ dependencies { implementation("com.google.android.material:material:1.12.0") implementation("androidx.test.espresso:espresso-contrib:3.6.1") - val kotlin_version = "1.9.21" + val kotlin_version = "2.1.10" implementation(kotlin("stdlib")) implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) //noinspection GradleDependency diff --git a/build.gradle.kts b/build.gradle.kts index 7e79b6f..3fa10b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("com.android.application") version "8.8.1" apply false - id("org.jetbrains.kotlin.android") version "1.9.20" apply false - id("com.android.library") version "8.8.1" apply false + id("com.android.application") version "8.9.0" apply false + id("org.jetbrains.kotlin.android") version "2.1.10" apply false + id("com.android.library") version "8.9.0" apply false } diff --git a/pdfViewer/build.gradle.kts b/pdfViewer/build.gradle.kts index 4c7078b..44dfe28 100644 --- a/pdfViewer/build.gradle.kts +++ b/pdfViewer/build.gradle.kts @@ -4,6 +4,7 @@ import com.vanniktech.maven.publish.SonatypeHost plugins { id("com.android.library") id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") id("kotlin-parcelize") id("org.jetbrains.dokka") version "1.9.20" id("com.vanniktech.maven.publish") version "0.28.0" @@ -52,7 +53,7 @@ android { dependencies { implementation("androidx.compose.material3:material3-android:1.3.1") - val kotlin_version = "1.9.21" + val kotlin_version = "2.1.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") @@ -121,4 +122,4 @@ mavenPublishing { } } -} \ No newline at end of file +} From a3a9e5ba99365fa11a8d0dfa523dbc465bf41670 Mon Sep 17 00:00:00 2001 From: Tim McIntosh Date: Wed, 2 Apr 2025 21:35:50 -0700 Subject: [PATCH 02/12] pdf_renderview.xml: Remove apparently-unused WebView. --- pdfViewer/src/main/res/layout/pdf_rendererview.xml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pdfViewer/src/main/res/layout/pdf_rendererview.xml b/pdfViewer/src/main/res/layout/pdf_rendererview.xml index 91cfae6..3eba997 100644 --- a/pdfViewer/src/main/res/layout/pdf_rendererview.xml +++ b/pdfViewer/src/main/res/layout/pdf_rendererview.xml @@ -12,14 +12,8 @@ android:scrollbars="vertical" tools:listitem="@layout/list_item_pdf_page" /> - - - \ No newline at end of file + From 82523fa61ae220b0b6b24f22af986df4e785d769 Mon Sep 17 00:00:00 2001 From: Tim McIntosh Date: Wed, 2 Apr 2025 21:39:37 -0700 Subject: [PATCH 03/12] list_item_pdf_page.xml: Change layout_height to "match_parent" so that 4 unloaded pages are not displayed initially (my layout is almost full-screen), even though only (part of) a single page is displayed after loading. --- pdfViewer/src/main/res/layout/list_item_pdf_page.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pdfViewer/src/main/res/layout/list_item_pdf_page.xml b/pdfViewer/src/main/res/layout/list_item_pdf_page.xml index 37a982a..8b5078d 100644 --- a/pdfViewer/src/main/res/layout/list_item_pdf_page.xml +++ b/pdfViewer/src/main/res/layout/list_item_pdf_page.xml @@ -3,7 +3,7 @@ android:id="@+id/container_view" android:layout_width="match_parent" android:minHeight="200dp" - android:layout_height="wrap_content"> + android:layout_height="match_parent"> - \ No newline at end of file + From 9485878ca3bcba004c7b31fe082a558d7c6cfe3a Mon Sep 17 00:00:00 2001 From: Tim McIntosh Date: Wed, 2 Apr 2025 21:43:18 -0700 Subject: [PATCH 04/12] CacheManager.kt: Reduce cacheSize to 1/16 of maxMemory() to help with memory footprint, especially on older configurations like Nexus 5X / API 23. Also, use `value.allocationByteCount` rather than `value.byteCount`, as recommended by API documentation. --- .../src/main/java/com/rajat/pdfviewer/util/CacheManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/util/CacheManager.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/util/CacheManager.kt index 35083f0..44eac0c 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/util/CacheManager.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/util/CacheManager.kt @@ -38,9 +38,9 @@ class CacheManager( private fun createMemoryCache(): LruCache { val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() - val cacheSize = maxMemory / 6 + val cacheSize = maxMemory / 16 return object : LruCache(cacheSize) { - override fun sizeOf(key: Int, value: Bitmap): Int = value.byteCount / 1024 + override fun sizeOf(key: Int, value: Bitmap): Int = value.allocationByteCount / 1024 } } From 1820a78fd9cc6e7b273efbdb3383d6fca5e68db1 Mon Sep 17 00:00:00 2001 From: Tim McIntosh Date: Wed, 2 Apr 2025 22:27:49 -0700 Subject: [PATCH 05/12] PdfRenderCore.kt: renderPage(): Replace `Bitmap` parameter with `Size`, as the bitmap is not actually used, except for its size, and eliminate redundant `success` parameter in the completion routine (the `bitmap` parameter null/non-null status already indicates failure/success). --- .../com/rajat/pdfviewer/PdfRendererCore.kt | 30 ++++++------------- .../com/rajat/pdfviewer/PdfViewAdapter.kt | 12 +++----- 2 files changed, 13 insertions(+), 29 deletions(-) diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererCore.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererCore.kt index d1da19e..cfa5e07 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererCore.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererCore.kt @@ -18,7 +18,6 @@ import java.nio.file.Files import java.nio.file.Paths import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger -import kotlin.math.abs open class PdfRendererCore( private val context: Context, @@ -84,19 +83,19 @@ open class PdfRendererCore( fun renderPage( pageNo: Int, - bitmap: Bitmap, - onBitmapReady: ((success: Boolean, pageNo: Int, bitmap: Bitmap?) -> Unit)? = null + size: Size, + onBitmapReady: ((pageNo: Int, bitmap: Bitmap?) -> Unit)? = null ) { val startTime = System.nanoTime() if (pageNo >= getPageCount()) { - onBitmapReady?.invoke(false, pageNo, null) + onBitmapReady?.invoke(pageNo, null) return } getBitmapFromCache(pageNo)?.let { cachedBitmap -> scope.launch(Dispatchers.Main) { - onBitmapReady?.invoke(true, pageNo, cachedBitmap) + onBitmapReady?.invoke(pageNo, cachedBitmap) if (enableDebugMetrics) { Log.d("PdfRendererCore", "Page $pageNo loaded from cache") } @@ -108,7 +107,6 @@ open class PdfRendererCore( renderJobs[pageNo]?.cancel() renderJobs[pageNo] = scope.launch { - var success = false var renderedBitmap: Bitmap? = null renderLock.withLock { @@ -117,14 +115,13 @@ open class PdfRendererCore( try { val aspectRatio = pdfPage.width.toFloat() / pdfPage.height - val height = bitmap.height + val height = size.height val width = (height * aspectRatio).toInt() val tempBitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, height) pdfPage.render(tempBitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) addBitmapToMemoryCache(pageNo, tempBitmap) - success = true renderedBitmap = tempBitmap } catch (e: Exception) { @@ -144,26 +141,19 @@ open class PdfRendererCore( updateAggregateMetrics(pageNo, renderTime) withContext(Dispatchers.Main) { - onBitmapReady?.invoke(success, pageNo, renderedBitmap) + onBitmapReady?.invoke(pageNo, renderedBitmap) } } } suspend fun renderPageAsync(pageNo: Int, width: Int, height: Int): Bitmap? { return suspendCancellableCoroutine { continuation -> - val bitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, height) - renderPage(pageNo, bitmap) { success, _, renderedBitmap -> - if (success) { - continuation.resume(renderedBitmap ?: bitmap, null) - } else { - CommonUtils.Companion.BitmapPool.recycleBitmap(bitmap) - continuation.resume(null, null) - } + renderPage(pageNo, Size(width, maxOf(1, height))) { _, renderedBitmap -> + continuation.resume(renderedBitmap, null) } } } - private fun updateAggregateMetrics(page: Int, duration: Long) { totalPagesRendered++ totalRenderTime += duration @@ -186,9 +176,7 @@ open class PdfRendererCore( if (renderJobs[pageNo]?.isActive != true) { renderJobs[pageNo]?.cancel() renderJobs[pageNo] = scope.launch { - val bitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, height) - renderPage(pageNo, bitmap) { success, _, _ -> - if (!success) CommonUtils.Companion.BitmapPool.recycleBitmap(bitmap) + renderPage(pageNo, Size(width, maxOf(1, height))) { _, _ -> } } } diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt index 65f6e35..bb7c809 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt @@ -2,6 +2,7 @@ package com.rajat.pdfviewer import android.content.Context import android.graphics.Rect +import android.util.Size import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -9,7 +10,6 @@ import android.view.animation.AlphaAnimation import android.view.animation.LinearInterpolator import androidx.recyclerview.widget.RecyclerView import com.rajat.pdfviewer.databinding.ListItemPdfPageBinding -import com.rajat.pdfviewer.util.CommonUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -54,11 +54,10 @@ internal class PdfViewAdapter( updateLayoutParams(height) - val bitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, maxOf(1, height)) - renderer.renderPage(position, bitmap) { success, pageNo, renderedBitmap -> - if (success && pageNo == position) { + renderer.renderPage(position, Size(width, maxOf(1, height))) { pageNo, renderedBitmap -> + if (renderedBitmap != null && pageNo == position) { CoroutineScope(Dispatchers.Main).launch { - pageView.setImageBitmap(renderedBitmap ?: bitmap) + pageView.setImageBitmap(renderedBitmap) applyFadeInAnimation(pageView) pageLoadingLayout.pdfViewPageLoadingProgress.visibility = View.GONE @@ -68,10 +67,7 @@ internal class PdfViewAdapter( width = pageView.width.takeIf { it > 0 } ?: context.resources.displayMetrics.widthPixels, height = pageView.height.takeIf { it > 0 } ?: context.resources.displayMetrics.heightPixels ) - } - } else { - CommonUtils.Companion.BitmapPool.recycleBitmap(bitmap) } } } From f1b51959f01124e5c47ec23cd4eeeb1663e5592e Mon Sep 17 00:00:00 2001 From: Tim McIntosh Date: Wed, 2 Apr 2025 21:34:51 -0700 Subject: [PATCH 06/12] Remove PdfViewerActivity, which is not used in my configuration, but has caused build failure due to missing resources in the past. --- .../com/rajat/pdfviewer/PdfViewerActivity.kt | 525 ------------------ .../com/rajat/pdfviewer/util/ViewerStyle.kt | 53 -- .../main/res/layout/activity_pdf_viewer.xml | 59 -- 3 files changed, 637 deletions(-) delete mode 100644 pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewerActivity.kt delete mode 100644 pdfViewer/src/main/java/com/rajat/pdfviewer/util/ViewerStyle.kt delete mode 100644 pdfViewer/src/main/res/layout/activity_pdf_viewer.xml diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewerActivity.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewerActivity.kt deleted file mode 100644 index 8bdec48..0000000 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewerActivity.kt +++ /dev/null @@ -1,525 +0,0 @@ -package com.rajat.pdfviewer - -import android.Manifest.permission -import android.app.AlertDialog -import android.content.Context -import android.content.DialogInterface -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.os.Environment -import android.text.TextUtils -import android.util.Log -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View.GONE -import android.view.View.VISIBLE -import android.view.Window -import android.widget.Toast -import androidx.activity.SystemBarStyle -import androidx.activity.enableEdgeToEdge -import androidx.activity.result.contract.ActivityResultContracts -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.DrawableCompat -import androidx.lifecycle.lifecycleScope -import com.rajat.pdfviewer.databinding.ActivityPdfViewerBinding -import com.rajat.pdfviewer.util.CacheStrategy -import com.rajat.pdfviewer.util.EdgeToEdgeHelper -import com.rajat.pdfviewer.util.FileUtils.createPdfDocumentUri -import com.rajat.pdfviewer.util.FileUtils.fileFromAsset -import com.rajat.pdfviewer.util.FileUtils.uriToFile -import com.rajat.pdfviewer.util.NetworkUtil.checkInternetConnection -import com.rajat.pdfviewer.util.ThemeValidator -import com.rajat.pdfviewer.util.ToolbarStyle -import com.rajat.pdfviewer.util.ToolbarTitleBehavior -import com.rajat.pdfviewer.util.ViewerStrings -import com.rajat.pdfviewer.util.ViewerStrings.Companion.getMessageForError -import com.rajat.pdfviewer.util.ViewerStyle -import com.rajat.pdfviewer.util.saveTo -import org.jetbrains.annotations.TestOnly -import java.io.File -import java.net.SocketTimeoutException -import java.net.UnknownHostException - -/** - * Created by Rajat on 11,July,2020 - */ - -class PdfViewerActivity : AppCompatActivity() { - - private lateinit var file_not_downloaded_yet: String - private lateinit var file_saved_to_downloads: String - private lateinit var file_saved_successfully: String - private lateinit var error_no_internet_connection: String - private lateinit var permission_required: String - private lateinit var permission_required_title: String - private lateinit var error_pdf_corrupted: String - private lateinit var pdf_viewer_retry: String - private lateinit var pdf_viewer_grant: String - private lateinit var pdf_viewer_cancel: String - private lateinit var pdf_viewer_error: String - private var menuItem: MenuItem? = null - private var fileUrl: String? = null - private lateinit var headers: HeaderData - private lateinit var binding: ActivityPdfViewerBinding - private val viewModel: PdfViewerViewModel by viewModels() - private var downloadedFilePath: String? = null - private var isDownloadButtonEnabled = false - private lateinit var cacheStrategy: CacheStrategy - - companion object { - const val FILE_URL = "pdf_file_url" - const val FILE_TITLE = "pdf_file_title" - const val ENABLE_FILE_DOWNLOAD = "enable_download" - const val FROM_ASSETS = "from_assests" - const val TITLE_BEHAVIOR = "title_behavior" - const val ENABLE_ZOOM = "enable_zoom" - var enableDownload = false - var isPDFFromPath = false - var isFromAssets = false - var SAVE_TO_DOWNLOADS = true - var isZoomEnabled = true - const val CACHE_STRATEGY = "cache_strategy" - - fun launchPdfFromUrl( - context: Context?, - pdfUrl: String?, - pdfTitle: String?, - saveTo: saveTo, - enableDownload: Boolean = true, - enableZoom: Boolean = true, - headers: Map = emptyMap(), - toolbarTitleBehavior: ToolbarTitleBehavior? = null, - cacheStrategy: CacheStrategy = CacheStrategy.MAXIMIZE_PERFORMANCE - ): Intent { - val intent = Intent(context, PdfViewerActivity::class.java) - intent.putExtra(FILE_URL, pdfUrl) - intent.putExtra(FILE_TITLE, pdfTitle) - intent.putExtra(ENABLE_FILE_DOWNLOAD, enableDownload) - intent.putExtra("headers", HeaderData(headers)) - intent.putExtra(ENABLE_ZOOM, enableZoom) - toolbarTitleBehavior?.let { - intent.putExtra(TITLE_BEHAVIOR, it.ordinal) - } - intent.putExtra(CACHE_STRATEGY, cacheStrategy.ordinal) - isPDFFromPath = false - SAVE_TO_DOWNLOADS = saveTo == com.rajat.pdfviewer.util.saveTo.DOWNLOADS - return intent - } - - fun launchPdfFromPath( - context: Context?, - path: String?, - pdfTitle: String?, - saveTo: saveTo, - fromAssets: Boolean = false, - enableZoom: Boolean = true, - toolbarTitleBehavior: ToolbarTitleBehavior? = null, - cacheStrategy: CacheStrategy = CacheStrategy.MAXIMIZE_PERFORMANCE - ): Intent { - val intent = Intent(context, PdfViewerActivity::class.java) - intent.putExtra(FILE_URL, path) - intent.putExtra(FILE_TITLE, pdfTitle) - intent.putExtra(ENABLE_FILE_DOWNLOAD, false) - intent.putExtra(FROM_ASSETS, fromAssets) - toolbarTitleBehavior?.let { - intent.putExtra(TITLE_BEHAVIOR, it.ordinal) - } - intent.putExtra(ENABLE_ZOOM, enableZoom) - intent.putExtra(CACHE_STRATEGY, cacheStrategy.ordinal) - isPDFFromPath = true - SAVE_TO_DOWNLOADS = saveTo == com.rajat.pdfviewer.util.saveTo.DOWNLOADS - return intent - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - setTheme(R.style.Theme_PdfView_SelectedTheme) - ThemeValidator.validatePdfViewerTheme(this) - super.onCreate(savedInstanceState) - - // Inflate layout once (previously done twice) - binding = ActivityPdfViewerBinding.inflate(layoutInflater) - setContentView(binding.root) - - // Apply edge-to-edge window - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - applyEdgeToEdge(window) - } - - // Setup Toolbar - configureToolbar() - - // Apply theme attributes (background & progress bar styles) - applyThemeAttributes() - - // Retrieve intent extras - extractIntentExtras() - - // Initialize the PDF viewer - init() - } - - private fun configureToolbar() { - val toolbarStyle = ToolbarStyle.from(this, intent) - val toolbarTitle = intent.getStringExtra(FILE_TITLE) ?: "PDF" - - try { - // Check if system ActionBar exists (theme includes windowActionBar) - supportActionBar?.hide() // Hide it (avoids double toolbar) - } catch (e: IllegalStateException) { - // Do nothing — if it crashes here, we’ll fallback safely below - Log.w("PdfViewer-configureToolbar", "supportActionBar check failed: ${e.message}") - } - - // Use our custom toolbar always - binding.myToolbar.visibility = VISIBLE - try { - setSupportActionBar(binding.myToolbar) - supportActionBar?.setDisplayShowTitleEnabled(false) - } catch (e: IllegalStateException) { - Log.e("PdfViewer-configureToolbar", "Can't setSupportActionBar(): ${e.message}") - // fallback — don't set toolbar, maybe layout-only mode - } - - toolbarStyle.applyTo(binding.myToolbar, binding.toolbarTitle) - binding.toolbarTitle.text = toolbarTitle - } - - private fun applyEdgeToEdge(window: Window) { - val isDarkMode = EdgeToEdgeHelper.isDarkModeEnabled(resources.configuration.uiMode) - val toolbarColor = ToolbarStyle.from(this, intent).toolbarColor - - // Must be called from ComponentActivity - enableEdgeToEdge( - statusBarStyle = if (isDarkMode) { - SystemBarStyle.dark(toolbarColor) - } else { - SystemBarStyle.light(toolbarColor, toolbarColor) - } - ) - - // apply insets via helper - EdgeToEdgeHelper.applyInsets(window, binding.root, isDarkMode) - } - - private fun applyThemeAttributes() { - ViewerStyle.from(this).applyTo(binding) - } - - private fun extractIntentExtras() { - enableDownload = intent.getBooleanExtra(ENABLE_FILE_DOWNLOAD, false) - isFromAssets = intent.getBooleanExtra(FROM_ASSETS, false) - - headers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra("headers", HeaderData::class.java) - } else { - intent.getParcelableExtra("headers") - } ?: HeaderData(emptyMap()) - - isZoomEnabled = intent.getBooleanExtra(ENABLE_ZOOM, true) - - val strategyOrdinal = - intent.getIntExtra(CACHE_STRATEGY, CacheStrategy.MAXIMIZE_PERFORMANCE.ordinal) - cacheStrategy = CacheStrategy.entries.getOrElse(strategyOrdinal) { - CacheStrategy.MAXIMIZE_PERFORMANCE - } - - // Apply themed strings with fallback - ViewerStrings.from(this).also { strings -> - error_pdf_corrupted = strings.errorPdfCorrupted - error_no_internet_connection = strings.errorNoInternet - file_saved_successfully = strings.fileSavedSuccessfully - file_saved_to_downloads = strings.fileSavedToDownloads - file_not_downloaded_yet = strings.fileNotDownloadedYet - permission_required = strings.permissionRequired - permission_required_title = strings.permissionRequiredTitle - pdf_viewer_error = strings.genericError - pdf_viewer_retry = strings.retry - pdf_viewer_cancel = strings.cancel - pdf_viewer_grant = strings.grant - } - } - - private fun init() { - binding.pdfView.statusListener = object : PdfRendererView.StatusCallBack { - override fun onPdfLoadStart() { - true.showProgressBar() - updateDownloadButtonState(false) - } - - override fun onPdfLoadProgress( - progress: Int, downloadedBytes: Long, totalBytes: Long? - ) { - //Download is in progress - true.showProgressBar() - } - - override fun onPdfLoadSuccess(absolutePath: String) { - runOnUiThread { - false.showProgressBar() - downloadedFilePath = absolutePath - if (menuItem == null) { - isDownloadButtonEnabled = true // ✅ Store state so it applies later - } else { - updateDownloadButtonState(true) - } - } - } - - override fun onError(error: Throwable) { - runOnUiThread { - false.showProgressBar() - val strings = ViewerStrings.from(this@PdfViewerActivity) - val errorMessage = strings.getMessageForError(error) - showErrorDialog(errorMessage, isRetryable(error)) - } - } - - override fun onPageChanged(currentPage: Int, totalPage: Int) { - //Page change. Not require - } - } - - if (intent.extras!!.containsKey(FILE_URL)) { - fileUrl = intent.extras!!.getString(FILE_URL) - if (isPDFFromPath) { - initPdfViewerWithPath(this.fileUrl) - } else { - if (checkInternetConnection(this)) { - loadFileFromNetwork(this.fileUrl) - } else { - Toast.makeText( - this, error_no_internet_connection, Toast.LENGTH_SHORT - ).show() - } - } - } - } - - - private fun isRetryable(error: Throwable): Boolean { - return error is UnknownHostException || error is SocketTimeoutException || error.message?.contains( - "Failed to download" - ) == true || error.message?.contains("Incomplete download") == true - } - - private fun showErrorDialog(message: String, shouldRetry: Boolean) { - val strings = ViewerStrings.from(this) - val builder = AlertDialog.Builder(this) - .setTitle(strings.errorDialogTitle) - .setMessage(message) - if (shouldRetry) { - builder.setPositiveButton(pdf_viewer_retry) { _, _ -> loadFileFromNetwork(fileUrl) } - } - builder.setNegativeButton(pdf_viewer_cancel, null).show() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - val inflater: MenuInflater = menuInflater - inflater.inflate(R.menu.menu, menu) - menuItem = menu.findItem(R.id.download) - menuItem?.isVisible = enableDownload - - // Apply download icon tint from theme - val toolbarStyle = ToolbarStyle.from(this, intent) - menuItem?.icon?.mutate()?.let { - val wrappedIcon = DrawableCompat.wrap(it) - DrawableCompat.setTint(wrappedIcon, toolbarStyle.downloadIconTint) - menuItem?.icon = wrappedIcon - } - - updateDownloadButtonState(isDownloadButtonEnabled) - return true - } - - @TestOnly - fun isDownloadButtonVisible(): Boolean = menuItem?.isVisible == true - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - // Handle item selection. - return when (item.itemId) { - R.id.download -> { - checkAndStartDownload() - true - } - - android.R.id.home -> { - finish() - true - } - - else -> super.onOptionsItemSelected(item) - } - } - - private fun loadFileFromNetwork(fileUrl: String?) { - initPdfViewer( - fileUrl - ) - } - - private fun initPdfViewer(fileUrl: String?) { - if (TextUtils.isEmpty(fileUrl)) onPdfError("") - //Initiating PDf Viewer with URL - try { - binding.pdfView.setZoomEnabled(isZoomEnabled) - binding.pdfView.initWithUrl( - fileUrl!!, - headers, - lifecycleScope, - lifecycle = lifecycle, - cacheStrategy = cacheStrategy - ) - } catch (e: Exception) { - onPdfError(e.toString()) - } - } - - private fun initPdfViewerWithPath(filePath: String?) { - if (TextUtils.isEmpty(filePath)) { - onPdfError("") - return - } - try { - val file = if (filePath!!.startsWith("content://")) { - uriToFile(applicationContext, Uri.parse(filePath)) - } else if (isFromAssets) { - fileFromAsset(this, filePath) - } else { - File(filePath) - } - binding.pdfView.setZoomEnabled(isZoomEnabled) - binding.pdfView.initWithFile(file, cacheStrategy) - } catch (e: Exception) { - onPdfError(e.toString()) - } - } - - private fun onPdfError(e: String) { - Log.e("Pdf render error", e) - AlertDialog.Builder(this).setTitle(pdf_viewer_error).setMessage(error_pdf_corrupted) - .setPositiveButton(pdf_viewer_retry) { dialog, which -> - runOnUiThread { - init() - } - }.setNegativeButton(pdf_viewer_cancel, null).show() - } - - private fun Boolean.showProgressBar() { - binding.progressBar.visibility = if (this) VISIBLE else GONE - } - - private val requestPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - startDownload() - } else { - // Show an AlertDialog here - AlertDialog.Builder(this).setTitle(permission_required_title) - .setMessage(permission_required) - .setPositiveButton(pdf_viewer_grant) { dialog: DialogInterface, which: Int -> - // Request the permission again - requestStoragePermission() - }.setNegativeButton(pdf_viewer_cancel, null).show() - } - } - - private fun requestStoragePermission() { - requestPermissionLauncher.launch(permission.WRITE_EXTERNAL_STORAGE) - } - - private fun checkAndStartDownload() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - // For OS versions below Android 11, use the old method - if (ContextCompat.checkSelfPermission( - this, permission.WRITE_EXTERNAL_STORAGE - ) == PackageManager.PERMISSION_GRANTED - ) { - startDownload() - } else { - // Request the permission - requestPermissionLauncher.launch(permission.WRITE_EXTERNAL_STORAGE) - } - } else { - // For Android 13 and above, use scoped storage or MediaStore APIs - startDownload() - } - } - - private fun startDownload() { - val fileName = intent.getStringExtra(FILE_TITLE) ?: "downloaded_file.pdf" - downloadedFilePath?.let { filePath -> - if (SAVE_TO_DOWNLOADS) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - saveFileToPublicDirectoryScopedStorage(filePath, fileName) - } else { - saveFileToPublicDirectoryLegacy(filePath, fileName) - } - } else { - promptUserForLocation(fileName) - } - } ?: Toast.makeText(this, file_not_downloaded_yet, Toast.LENGTH_SHORT).show() - } - - private fun promptUserForLocation(fileName: String) { - val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/pdf" - putExtra(Intent.EXTRA_TITLE, fileName) - } - createFileLauncher.launch(intent) - } - - private val createFileLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - result.data?.data?.let { uri -> - contentResolver.openOutputStream(uri)?.use { outputStream -> - downloadedFilePath?.let { filePath -> - File(filePath).inputStream().copyTo(outputStream) - } - } - Toast.makeText(this, file_saved_successfully, Toast.LENGTH_SHORT).show() - } - } - } - - private fun saveFileToPublicDirectoryScopedStorage(filePath: String, fileName: String) { - val contentResolver = applicationContext.contentResolver - val uri = createPdfDocumentUri(contentResolver, fileName) - contentResolver.openOutputStream(uri)?.use { outputStream -> - File(filePath).inputStream().copyTo(outputStream) - } - Toast.makeText(this, file_saved_to_downloads, Toast.LENGTH_SHORT).show() - } - - private fun saveFileToPublicDirectoryLegacy(filePath: String, fileName: String) { - val destinationFile = File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName - ) - File(filePath).copyTo(destinationFile, overwrite = true) - Toast.makeText(this, file_saved_to_downloads, Toast.LENGTH_SHORT).show() - } - - override fun onDestroy() { - super.onDestroy() - binding.pdfView.closePdfRender() - } - - private fun updateDownloadButtonState(isEnabled: Boolean) { - isDownloadButtonEnabled = isEnabled - - menuItem?.let { item -> - item.isEnabled = isEnabled - item.icon?.alpha = if (isEnabled) 255 else 100 // Adjust opacity for disabled state - } - } - -} \ No newline at end of file diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/util/ViewerStyle.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/util/ViewerStyle.kt deleted file mode 100644 index f50cce0..0000000 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/util/ViewerStyle.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.rajat.pdfviewer.util - -import android.content.Context -import android.util.Log -import androidx.core.content.ContextCompat -import com.rajat.pdfviewer.R -import com.rajat.pdfviewer.databinding.ActivityPdfViewerBinding - -/** - * Handles general view styling like background & progress bar - */ -data class ViewerStyle( - val backgroundColor: Int, - val progressBarDrawableResId: Int -) { - fun applyTo(binding: ActivityPdfViewerBinding) { - try { - binding.parentLayout.setBackgroundColor(backgroundColor) - binding.progressBar.indeterminateDrawable = ContextCompat.getDrawable( - binding.root.context, progressBarDrawableResId - ) - } catch (e: Exception) { - Log.w("ViewerStyle", "Failed to apply style: ${e.localizedMessage}") - } - } - - companion object { - fun from(context: Context): ViewerStyle { - val typedArray = context.theme.obtainStyledAttributes( - R.styleable.PdfRendererView - ) - - val backgroundColor = ThemeUtils.getColorFromTypedArray( - typedArray, - R.styleable.PdfRendererView_pdfView_backgroundColor, - ContextCompat.getColor(context, R.color.pdf_viewer_surface) - ) - - val progressBarDrawableResId = ThemeUtils.getResIdFromTypedArray( - typedArray, - R.styleable.PdfRendererView_pdfView_progressBar, - R.drawable.pdf_viewer_progress_circle - ) - - typedArray.recycle() - - return ViewerStyle( - backgroundColor = backgroundColor, - progressBarDrawableResId = progressBarDrawableResId - ) - } - } -} \ No newline at end of file diff --git a/pdfViewer/src/main/res/layout/activity_pdf_viewer.xml b/pdfViewer/src/main/res/layout/activity_pdf_viewer.xml deleted file mode 100644 index a6dd6f6..0000000 --- a/pdfViewer/src/main/res/layout/activity_pdf_viewer.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - From 96798a87ae9bb532daa0556f417c661003620193 Mon Sep 17 00:00:00 2001 From: Tim McIntosh Date: Wed, 2 Apr 2025 22:29:14 -0700 Subject: [PATCH 07/12] PdfViewAdapter.kt: Clear bitmap whenever view is offscreen in attempt to save memory on older devices where memory is scarce (e.g. Nexus 5X / API 23). --- .../com/rajat/pdfviewer/PdfViewAdapter.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt index bb7c809..9492841 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt @@ -2,6 +2,7 @@ package com.rajat.pdfviewer import android.content.Context import android.graphics.Rect +//import android.util.Log import android.util.Size import android.view.LayoutInflater import android.view.View @@ -34,9 +35,47 @@ internal class PdfViewAdapter( holder.bind(position) } + override fun onViewRecycled(holder: PdfPageViewHolder) { + holder.recycle() + } + + override fun onViewDetachedFromWindow(holder: PdfPageViewHolder) { + holder.detach() + } + + override fun onViewAttachedToWindow(holder: PdfPageViewHolder) { + holder.attach(holder.bindingAdapterPosition) + } + inner class PdfPageViewHolder(private val itemBinding: ListItemPdfPageBinding) : RecyclerView.ViewHolder(itemBinding.root) { + private var detached = false + + private fun clearBitmap() { + with(itemBinding) { + pageView.setImageBitmap(null); + } + detached = true + } + + fun attach(position: Int) { +// Log.d("PdfViewAdapter", "Attached to window: $bindingAdapterPosition") + if ( detached ) + bind(position) + } + + fun detach() { +// Log.d("PdfViewAdapter", "Detached from window: $bindingAdapterPosition") + clearBitmap() + } + + fun recycle() { +// Log.d("PdfViewAdapter", "Recycled page: $bindingAdapterPosition") + clearBitmap() + } + fun bind(position: Int) { with(itemBinding) { +// Log.d("PdfViewAdapter", "Binding page: $position") pageLoadingLayout.pdfViewPageLoadingProgress.visibility = if (enableLoadingForPages) View.VISIBLE else View.GONE // Before we trigger rendering, explicitly ensure that cached bitmaps are used @@ -60,6 +99,7 @@ internal class PdfViewAdapter( pageView.setImageBitmap(renderedBitmap) applyFadeInAnimation(pageView) pageLoadingLayout.pdfViewPageLoadingProgress.visibility = View.GONE + this@PdfPageViewHolder.detached = false // Prefetch here renderer.prefetchPagesAround( From 8d1498887ec2dd7356b6af1bfe3e2814b00ba3e6 Mon Sep 17 00:00:00 2001 From: Tim McIntosh Date: Mon, 7 Apr 2025 23:45:47 -0700 Subject: [PATCH 08/12] Cleanup warning. --- pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererCore.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererCore.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererCore.kt index cfa5e07..ef4db1c 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererCore.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererCore.kt @@ -146,6 +146,7 @@ open class PdfRendererCore( } } + @OptIn(ExperimentalCoroutinesApi::class) suspend fun renderPageAsync(pageNo: Int, width: Int, height: Int): Bitmap? { return suspendCancellableCoroutine { continuation -> renderPage(pageNo, Size(width, maxOf(1, height))) { _, renderedBitmap -> From c0204db3acc6c9a95bdf2f87bedaa33a1f8afca3 Mon Sep 17 00:00:00 2001 From: Tim McIntosh Date: Thu, 10 Apr 2025 19:42:39 -0700 Subject: [PATCH 09/12] Upgrade AGP to 8.9.1 --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3fa10b1..8bedc37 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("com.android.application") version "8.9.0" apply false + id("com.android.application") version "8.9.1" apply false id("org.jetbrains.kotlin.android") version "2.1.10" apply false - id("com.android.library") version "8.9.0" apply false + id("com.android.library") version "8.9.1" apply false } From 50f5755f302f6a7b433cf8aa4635792f49925969 Mon Sep 17 00:00:00 2001 From: Tim McIntosh Date: Sun, 27 Apr 2025 14:10:01 -0400 Subject: [PATCH 10/12] Upgrade AGP to 8.9.2 --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8bedc37..19192bc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("com.android.application") version "8.9.1" apply false + id("com.android.application") version "8.9.2" apply false id("org.jetbrains.kotlin.android") version "2.1.10" apply false - id("com.android.library") version "8.9.1" apply false + id("com.android.library") version "8.9.2" apply false } From 6cd26ae1a4441d7283dbee2f2808b1cfcd7de47c Mon Sep 17 00:00:00 2001 From: Tim McIntosh Date: Thu, 15 May 2025 14:31:23 -0400 Subject: [PATCH 11/12] Update AGP from 8.9.2 to 8.10.0. --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 19192bc..994ee4b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("com.android.application") version "8.9.2" apply false + id("com.android.application") version "8.10.0" apply false id("org.jetbrains.kotlin.android") version "2.1.10" apply false - id("com.android.library") version "8.9.2" apply false + id("com.android.library") version "8.10.0" apply false } From d1368801a17dfd13dcfb05b004b18ea31bdee8fa Mon Sep 17 00:00:00 2001 From: Tim McIntosh Date: Thu, 15 May 2025 15:18:17 -0400 Subject: [PATCH 12/12] Update to API 36 (Android 16) --- app/build.gradle.kts | 4 ++-- pdfViewer/build.gradle.kts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 67bc832..7b3d0da 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,12 +8,12 @@ plugins { android { namespace = "com.rajat.sample.pdfviewer" - compileSdk = 35 + compileSdk = 36 defaultConfig { applicationId = "com.rajat.sample.pdfviewer" minSdk = 23 - targetSdk = 35 + targetSdk = 36 versionCode = 2 versionName = "1.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/pdfViewer/build.gradle.kts b/pdfViewer/build.gradle.kts index 44dfe28..0069168 100644 --- a/pdfViewer/build.gradle.kts +++ b/pdfViewer/build.gradle.kts @@ -12,7 +12,7 @@ plugins { android { namespace = "com.rajat.pdfviewer" - compileSdk = 35 + compileSdk = 36 defaultConfig { minSdk = 23