androidkotlinandroid-jetpack-composekotlin-flowpdfrenderer

Weird race condition while setting flow value of a view model


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.


Solution

  • _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.