androidbase64

android - download base64 encoded pdf file and open it


I'm trying to find a solution to simply download a base64 encoded pdf file of a webservice and open it with an preinstalled pdf viewer. My application targets Android R. I tried something like this but I dont't want a picker to open.

This is my code so far. It is just downloading the file and converts it to a bytearray. The next step should by saving the file and opening it.

  lifecycleScope.launch {
            withContext(Dispatchers.IO) {
                try {

                    Snackbar.make(binding.root, getString(R.string.load_document_started), Snackbar.LENGTH_LONG).show()
                    
                    val documentData = DocumentDao().get(document.id, montageOrder)
                    val docAsByte = Base64.decode(documentData.data, Base64.DEFAULT)

                    val currentDateString  = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())

                    val fileName = document.documentType.
                    lowercase()
                        .replace("ä", "ae")
                        .replace("ü", "ue")
                        .replace("ö", "oe") +
                     "_" + currentDateString

                               val file = File(requireContext().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), fileName)
                    val fileStream = FileOutputStream(file)
                    fileStream.write(docAsByte)
                    fileStream.close()


                    val target = Intent(Intent.ACTION_VIEW)
                    target.setDataAndType(Uri.fromFile(file), "application/pdf")
                    target.flags = Intent.FLAG_ACTIVITY_NO_HISTORY

                    val intent = Intent.createChooser(target, "Yolo");
                    startActivity(intent)


                } catch (e: Exception) {
                    Log.e(TAG, "Dokument konnte nicht geladen werden: " + e.message, e)
                    Snackbar.make(binding.root, getString(R.string.exception_could_not_load_document), Snackbar.LENGTH_LONG).show()
                }
            }
        }

This results in a FileUriExposedException

Another aproach was using the SAF

    lateinit var docAsByte : ByteArray

private val createFileLauncher = registerForActivityResult(CreatePdfDocument()) { uri ->

    lifecycleScope.launch {
        withContext(Dispatchers.IO) {
            val stream = requireContext().contentResolver.openOutputStream(uri)
            stream?.write(docAsByte)
            stream?.close()

            val target = Intent(Intent.ACTION_VIEW)
            target.setDataAndType(uri, "application/pdf")
            target.flags = Intent.FLAG_ACTIVITY_NO_HISTORY

            startActivity(target)
        }
    }


}


private fun setGui() {
    _binding?.lvDocuments?.adapter = DocumentAdapter(requireContext(), montageOrder.documents)
    _binding?.lvDocuments?.setOnItemClickListener  { parent, _, position, _ ->
        val document : Document =  parent.getItemAtPosition(position) as Document

        lifecycleScope.launch {
            withContext(Dispatchers.IO) {
                try {

                    val currentDateString  = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())

                    val fileName = document.documentType.
                    lowercase()
                        .replace("ä", "ae")
                        .replace("ü", "ue")
                        .replace("ö", "oe") +
                            "_" +
                            montageOrder.orderNumber +
                            "_" +
                            currentDateString +
                            ".pdf"

                    Snackbar.make(binding.root, getString(R.string.load_document_started), Snackbar.LENGTH_LONG).show()

                    val documentData = DocumentDao().get(document.id, montageOrder)
                    docAsByte = Base64.decode(documentData.data, Base64.DEFAULT)


                    createFileLauncher.launch(fileName)

                } catch (e: Exception) {
                    Log.e(TAG, "Dokument konnte nicht geladen werden: " + e.message, e)
                    Snackbar.make(binding.root, getString(R.string.exception_could_not_load_document), Snackbar.LENGTH_LONG).show()
                }
            }
        }
    }
}

Everything works fine except for opening. But if I open the pdf via file explorer it works.


Solution

  • Solved it this way

    Add provider_paths.xml to xml resource folder

    <paths xmlns:android="http://schemas.android.com/apk/res/android">
        <external-path name="external_files" path="."/>
    </paths>
    

    In your manifest add a FileProvider:

     <application>
        <provider
             android:name="androidx.core.content.FileProvider"
             android:authorities="${applicationId}.provider"
             android:exported="false"
             android:grantUriPermissions="true">
             <meta-data
                 android:name="android.support.FILE_PROVIDER_PATHS"
                 android:resource="@xml/provider_paths" />
         </provider>
     </application>
    

    Prepare to download files to any directory your app owns, such as getFilesDir(), getExternalFilesDir(), getCacheDir() or getExternalCacheDir().

    val privateDir = context.getFilesDir()
    

    Download file taking its progress into account (DIY):

    val downloadedFile = myFancyMethodToDownloadToAnyDir(url, privateDir, fileName)
    

    Copy it to Downloads folder:

    private val DOWNLOAD_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
    
    val finalUri : Uri? = copyFileToDownloads(context, downloadedFile)
    
    fun copyFileToDownloads(context: Context, downloadedFile: File): Uri? {
        val resolver = context.contentResolver
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            val contentValues = ContentValues().apply {
                put(MediaStore.MediaColumns.DISPLAY_NAME, getName(downloadedFile))
                put(MediaStore.MediaColumns.MIME_TYPE, getMimeType(downloadedFile))
                put(MediaStore.MediaColumns.SIZE, getFileSize(downloadedFile))
            }
            resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
        } else {
            val authority = "${context.packageName}.provider"
            val destinyFile = File(DOWNLOAD_DIR, getName(downloadedFile))
            FileProvider.getUriForFile(context, authority, destinyFile)
        }?.also { downloadedUri ->
            resolver.openOutputStream(downloadedUri).use { outputStream ->
                val brr = ByteArray(1024)
                var len: Int
                val bufferedInputStream = BufferedInputStream(FileInputStream(downloadedFile.absoluteFile))
                while ((bufferedInputStream.read(brr, 0, brr.size).also { len = it }) != -1) {
                    outputStream?.write(brr, 0, len)
                }
                outputStream?.flush()
                bufferedInputStream.close()
            }
        }
    

    Once in download folder you can open file from app like this:

    val authority = "${context.packageName}.provider"
    val intent = Intent(Intent.ACTION_VIEW).apply {
        setDataAndType(finalUri, getMimeTypeForUri(finalUri))
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
            addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
        } else {
            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        }
    }
    try {
        context.startActivity(Intent.createChooser(intent, chooseAppToOpenWith))
    } catch (e: Exception) {
        Toast.makeText(context, "Error opening file", Toast.LENGTH_LONG).show()
    }
    
    //Kitkat or above
    fun getMimeTypeForUri(context: Context, finalUri: Uri) : String =
        DocumentFile.fromSingleUri(context, finalUri)?.type ?: "application/octet-stream"
    
    //Just in case this is for Android 4.3 or below
    fun getMimeTypeForFile(finalFile: File) : String =
        DocumentFile.fromFile(it)?.type ?: "application/octet-stream"