I have to open a PDF from a URL that requires an access token (The PDF is not publically available). The PDF should not be permanently stored on the device.
If the access token wouldn't be required, the solution would look like this:
val intent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(Uri.parse(url), "application/pdf") }
startActivity(intent)
However, an access token is required and therefore this doesn't work.
I have tried downloading the file in the app and then opening the local copy of the file, like this:
val inputStream = /*get PDF as an InputStream*/
val file = File.createTempFile(
fileName,
".pdf",
context?.externalCacheDir
).apply { deleteOnExit() }
stream.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
val intent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(Uri.fromFile(file), "application/pdf") }
startActivity(intent)
but this gives me a android.os.FileUriExposedException
with the message <myFilePath> exposed beyond app through Intent.getData()
.
I have also tried the same thing using context?.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
and got the same result.
So far, the closest I got was like this:
val inputStream = /*get PDF as an InputStream*/
val file = File.createTempFile(
fileName,
".pdf",
context?.filesDir
).apply { deleteOnExit() }
stream.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
val intent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(Uri.parse(file.absolutePath), "application/pdf") }
startActivity(intent)
Doing it like this, my Samsung device offered me Samsung Notes and Microsoft Word to open the PDF and neither of them was actually able to do it. (It just opened these apps and they showed an error that they couldn't find the file they were supposed to open)
The installed PDF reader - the one that would normally open the PDF when using my two lines of code at the very top of this post - wasn't even listed.
So, how do I properly show this PDF?
Edit:
After finding this answer, I added a file Provider and now my code looks like this:
val inputStream = /*get PDF as an InputStream*/
val file = File.createTempFile(
fileName,
".pdf",
context?.getExternalFilesDir(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
Environment.DIRECTORY_DOCUMENTS
else
Environment.DIRECTORY_DOWNLOADS
).apply { deleteOnExit() }
stream.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
val documentUri: Uri = FileProvider.getUriForFile(
requireContext(),
getString(R.string.file_provider_authority),
file
)
val intent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(documentUri, "application/pdf") }
startActivity(intent)
Now the PDF can be opened properly. However the PDF is empty.
This is how I load the PDF to an InputStream:
interface DocumentsClient {
@GET("/api/something/pdf/{pdf_id}")
@Headers("Accept: application/pdf")
@Streaming
fun loadSomethingPDF(@Path("pdf_id") pdfId: Long): Call<ResponseBody>
}
fun loadSomethingPDF(accessToken: String?, pdfId: Long) =
createDocumentsClient(accessToken).loadSomethingPDF(pdfId)
private fun createDocumentsClient(accessToken: String?): DocumentsClient {
val okClientBuilder = OkHttpClient.Builder()
// add headers
okClientBuilder.addInterceptor { chain ->
val original = chain.request()
val requestBuilder = original.newBuilder()
if (accessToken != null) {
requestBuilder.header("Authorization", "Bearer $accessToken")
}
val request = requestBuilder.build()
chain.proceed(request)
}
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(okClientBuilder.build())
.build()
return retrofit.create(DocumentsClient::class.java)
}
loadSomethingPdf then gets called like this:
@Throws(IOException::class, IllegalStateException::class)
suspend fun loadSomethingPdf(accessToken: String?, pdfId: Long) = withContext(Dispatchers.IO) {
val call = remoteManager.loadSomethingPDF(accessToken, pdfId)
try {
val response = call.execute()
if (!response.isSuccessful) {
return@withContext null
}
return@withContext response.body()?.byteStream()
} catch (e: IOException) {
throw e
} catch (e: IllegalStateException) {
throw e
}
}
And that's where I get the inputStream from.