I am trying to display a pdf which is downloaded from network and saved to internal storage. If pdf is already present inside internal storage then it is not downloaded from the network. The issue happens when I download the pdf from network for the first time. If pdf is already present in internal storage then I don't face this issue.
Below is my code
class MainActivity : ComponentActivity() {
private var file: File? = null
private var mFileDescriptor: ParcelFileDescriptor? = null
private var mPdfRenderer: PdfRenderer? = null
var mCurrentPage: PdfRenderer.Page? = null
private lateinit var myViewModel: MyViewModel
var downloadID: Long = 0
var downloadManager: DownloadManager? = null
private fun renderPDF() {
mFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
if (mFileDescriptor != null) {
mPdfRenderer = PdfRenderer(mFileDescriptor!!)
Log.i("pdfrenderd ",mPdfRenderer?.pageCount.toString())
myViewModel.isLoaded()
}
}
private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
private fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
private fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
private fun isGoogleDriveUri(uri: Uri): Boolean {
return (
"com.google.android.apps.docs.storage" == uri.authority ||
"com.google.android.apps.docs.storage.legacy" == uri.authority
)
}
private fun File.copyInputStreamToFile(inputStream: InputStream) {
this.outputStream().use { fileOut ->
inputStream.copyTo(fileOut)
}
}
suspend fun copyFileToInternalStorage(path: String) {
val outputFile = File(filesDir, "mypdfs.pdf")
outputFile.copyInputStreamToFile(File(path).inputStream())
file = outputFile
renderPDF()
}
private fun getMediaDocumentPath(
context: Context,
uri: Uri?,
selection: String?,
selectionArgs: Array<String>?
): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)
try {
if (uri == null) return null
cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}
private val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE == action) {
val fileUrl: Uri? = downloadManager?.getUriForDownloadedFile(downloadID)
lifecycleScope.launch {
withContext(Dispatchers.IO) {
val path = getRealPathFromURI(fileUrl)
if (path != null) {
copyFileToInternalStorage(path)
}
}
}
}
}
}
private fun getDriveFilePath(uri: Uri, context: Context): String? {
val returnCursor = context.contentResolver.query(
uri,
null,
null,
null,
null
)
val nameIndex = returnCursor!!.getColumnIndex(OpenableColumns.DISPLAY_NAME)
returnCursor.moveToFirst()
val name = returnCursor.getString(nameIndex)
returnCursor.close()
val file = File(context.cacheDir, name)
try {
val inputStream = context.contentResolver.openInputStream(uri)
val outputStream = FileOutputStream(file)
var read = 0
val maxBufferSize = 1 * 1024 * 1024
val bytesAvailable = inputStream!!.available()
val bufferSize = min(bytesAvailable, maxBufferSize)
val buffers = ByteArray(bufferSize)
while (inputStream.read(buffers).also { read = it } != -1) {
outputStream.write(buffers, 0, read)
}
inputStream.close()
outputStream.close()
} catch (e: Exception) {
context.unregisterReceiver(onDownloadComplete)
}
return file.path
}
private fun getFilePath(context: Context, uri: Uri?): String? {
var cursor: Cursor? = null
val projection = arrayOf(MediaStore.MediaColumns.DISPLAY_NAME)
try {
if (uri == null) return null
cursor = context.contentResolver.query(
uri,
projection,
null,
null,
null
)
if (cursor != null && cursor!!.moveToFirst()) {
val index = cursor!!.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
return cursor!!.getString(index)
}
} finally {
cursor?.close()
}
return null
}
private fun getDataColumn(
context: Context,
uri: Uri?,
selection: String?,
selectionArgs: Array<String>?
): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)
try {
if (uri == null) return null
cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
if (cursor != null && cursor!!.moveToFirst()) {
val index = cursor!!.getColumnIndexOrThrow(column)
return cursor!!.getString(index)
}
} finally {
cursor?.close()
}
return null
}
fun getRealPathFromURI(uri: Uri?): String? {
when {
// DocumentProvider
DocumentsContract.isDocumentUri(this, uri) -> {
when {
// ExternalStorageProvider
uri?.let { isExternalStorageDocument(it) } == true -> {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
// This is for checking Main Memory
return if ("primary".equals(type, ignoreCase = true)) {
if (split.size > 1) {
Environment.getExternalStorageDirectory()
.toString() + "/" + split[1]
} else {
Environment.getExternalStorageDirectory().toString() + "/"
}
// This is for checking SD Card
} else {
"storage" + "/" + docId.replace(":", "/")
}
}
uri?.let { isDownloadsDocument(it) } == true -> {
val fileName = getFilePath(this, uri)
if (fileName != null) {
return Environment.getExternalStorageDirectory()
.toString() + "/Download/" + fileName
}
var id = DocumentsContract.getDocumentId(uri)
if (id.startsWith("raw:")) {
id = id.replaceFirst("raw:".toRegex(), "")
val file = File(id)
if (file.exists()) return id
}
val contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"),
java.lang.Long.valueOf(id)
)
return getDataColumn(this, contentUri, null, null)
}
uri?.let { isMediaDocument(it) } == true -> {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":").toTypedArray()
val type = split[0]
val contentUri: Uri?
when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
else -> {
// non-media files i.e documents and other files
contentUri = MediaStore.Files.getContentUri("external")
val selection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.MediaColumns.RELATIVE_PATH + "=?"
} else {
"_id=?"
}
val selectionArgs = arrayOf(Environment.DIRECTORY_DOCUMENTS)
return getMediaDocumentPath(
this,
contentUri,
selection,
selectionArgs
)
}
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
return getDataColumn(this, contentUri, selection, selectionArgs)
}
uri?.let { isGoogleDriveUri(it) } == true -> {
return getDriveFilePath(uri, this)
}
else -> {
unregisterReceiver(onDownloadComplete)
}
}
}
"content".equals(uri?.scheme, ignoreCase = true) -> {
// Return the remote address
return if (uri?.let { isGooglePhotosUri(it) } == true) {
uri.lastPathSegment
} else {
getDataColumn(
this,
uri,
null,
null
)
}
}
"file".equals(uri?.scheme, ignoreCase = true) -> {
return uri?.path
}
}
return null
}
private fun downloadFile(url: String) {
try {
val uri = Uri.parse(url)
val request = DownloadManager.Request(uri)
request.setAllowedNetworkTypes(
DownloadManager.Request.NETWORK_MOBILE or DownloadManager.Request.NETWORK_WIFI
)
request.setNotificationVisibility(
DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
)
request.setDestinationInExternalFilesDir(
this,
Environment.DIRECTORY_DOCUMENTS,
"myfile.pdf"
)
downloadManager =
getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
downloadID = downloadManager!!.enqueue(request)
} catch (e: Exception) {
unregisterReceiver(onDownloadComplete)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myViewModel = ViewModelProvider(this).get(MyViewModel::class.java)
registerReceiver(
onDownloadComplete,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
val outputFile = File(filesDir, "mypdfs.pdf")
if (outputFile.exists()) {
file = outputFile
renderPDF()
} else {
downloadFile("https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf")
}
setContent {
PdfLazyTheme {
var scale by remember { mutableStateOf(0.5f) }
val loaded by myViewModel.isLoading.collectAsState()
// A surface container using the 'background' color from the theme
if (loaded) {
if ((mPdfRenderer?.pageCount ?: 0) <= 0) {
Text(text = "Pdf is not ready123")
} else {
LazyColumn(
) {
items(mPdfRenderer?.pageCount!!) { message ->
if (null != mCurrentPage) {
mCurrentPage?.close()
}
mCurrentPage = mPdfRenderer?.openPage(message)
val bitmap = mCurrentPage?.width?.let {
mCurrentPage?.height?.let { it1 ->
Bitmap.createBitmap(
it, it1,
Bitmap.Config.ARGB_8888
)
}
}
bitmap?.let {
mCurrentPage?.render(
it,
null,
Matrix().apply { postRotate(0F) },
PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY
)
}
Image(
bitmap = bitmap!!.asImageBitmap(),
contentDescription = "some useful description",
modifier = Modifier
.fillMaxWidth()
.border(width = 1.dp, color = Color.Gray)
.aspectRatio(1f)
.pointerInput(Unit) {
detectZoom { zoom ->
scale *= zoom
}
}
.graphicsLayer {
scaleX = maxOf(1f, minOf(3f, scale));
scaleY = maxOf(1f, minOf(3f, scale))
}
.zIndex(scale)
.shadow(
elevation = 4.dp,
spotColor = Color.Gray,
)
)
}
}
}
} else {
Text(text = "Pdf is not ready")
}
}
}
}
}
class MyViewModel : ViewModel() {
private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow()
fun isLoaded(){
_isLoading.value = true
}
}
So for the first time when pdf is downloaded I am changing isLoading
flow value to true inside renderPDF()
so it should I have displayed my LazyColumn
because mPdfRenderer
has pageCount
greater than zero but instead it displays my text component displaying Pdf is not ready123, which I don't understand why. If I open the app again then it displays lazy column properly as pdf is saved in internal storage.
_isLoading
is initially set to true
, then after loading the file it is set again to true
. It is not detected as a change, so the UI does not update.
In various places in the code this property is named either "loading" or "loaded", which feels the opposite. I guess this inconsistency in naming caused you to interpret false
and true
values differently in different places.