cwac-provider icon indicating copy to clipboard operation
cwac-provider copied to clipboard

Wrong PDF opened but download works

Open Berki2021 opened this issue 5 years ago • 14 comments

When I open a pdf file from the assets folder, the wrong pdf file is shown but the name of the pdf file is correct (e.g I open Note_1, Note_2 is shown but the name is Note_1). When I download this file with my reader, the correct file is downloaded and later shown.

class MainActivity : AppCompatActivity() {
private lateinit var noteViewModel: NoteViewModel

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val adapter = NoteAdapter()
    recycler_view.apply {
        layoutManager = LinearLayoutManager(this.context)
        recycler_view.setHasFixedSize(true)
        this.adapter = adapter
    }

    noteViewModel = ViewModelProvider(
        this,
        ViewModelProvider.AndroidViewModelFactory.getInstance(this.application)
    ).get(NoteViewModel::class.java)

    noteViewModel.getAllNotes().observe(this, Observer {
        adapter.submitList(it)
    })

    adapter.setOnItemClickListener(object : NoteAdapter.OnItemClickListener {
        override fun onItemClick(note: Note) {
            // Open PDF by name
            note.readFromAsset(baseContext)
        }
    })
 }
}

And here my Note class:

data class Note(var title: String, var description: String, var priority: Int, val pdfName: String) {
@PrimaryKey(autoGenerate = true)
 var id: Int = 0

fun readFromAsset(context: Context) {
    val intent = Intent(Intent.ACTION_VIEW, getUri())
        .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

    context.startActivity(intent)
}

private fun getUri(): Uri? {
    val AUTHORITY = "com.example.viewpdfroom"
    val PROVIDER = Uri.parse("content://$AUTHORITY")

    return (PROVIDER
        .buildUpon()
        .appendPath(StreamProvider.getUriPrefix(AUTHORITY))
        .appendPath("assets/$pdfName")
        .build())
}
}

You can find the full code here: https://github.com/Berki2021/ViewPDFRoom

Berki2021 avatar May 22 '20 19:05 Berki2021

OK, I'll peek at this over the weekend.

commonsguy avatar May 22 '20 19:05 commonsguy

OK, I'll peek at this over the weekend.

Thanks! I am looking forward to your reply

Berki2021 avatar May 22 '20 19:05 Berki2021

OK, this is very strange. AFAICT, my code is doing the right thing. I am creating the right asset path and am passing it to openFd() on Android's AssetManager. Inexplicably, it appears as though I am getting an AssetFileDescriptor on the wrong content returned to me.

What is even stranger is that I am only getting that result from Google Drive's PDF viewer. On the same device, Adobe Reader works fine. And I can't see what I am doing differently — it appears as though both apps are going through the same code paths in StreamProvider. So, this is going to take some time to track down what's going on.

What PDF viewer are you using?


Note that your sample project crashes. baseContext is rarely used in Android app development; do not use it to start an activity. I changed line 38 of MainActivity.kt to note.readPDFFromAsset(this@MainActivity).

Another problem is your Uri assembly. Use:

provider
            .buildUpon()
            .appendPath(StreamProvider.getUriPrefix(authority))
            .appendPath("assets")
            .appendPath(pdfName)
            .build()

Otherwise, you wind up encoding the / in assets/$pdfName.

Neither of these should impact the problem — I can reproduce it with Drive after making these changes. I'm just mentioning them, since I encountered them while trying this out.

commonsguy avatar May 23 '20 21:05 commonsguy

Okay, this is really weird. I've changed my code as you said and nothing changed (obviously).

Now comes the weird part: I've used Googles PDF viewer as well and got the same error as yours (it opened the wrong pdf file). After that, I've changed my code to this to test it better:

fun readPDFFromAsset(context: Context) {
    val intent = Intent(Intent.ACTION_VIEW, getUri())
        .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

    context.startActivity(Intent.createChooser(intent, "Open with"))
}

The weird part is, that this problem only appears when using the built-in google drive pdf viewer!!!

The following programs work perfectly:

  • Word
  • onedrive pdf-viewer
  • hp pdf print service
  • adobe acrobat reader
  • foxit pdf reader
  • pdf reader for android (reader tools) => wouldn't use it, full of ads…
  • pdf reader for android 2020 (iVilia Studio) => wouldn't use it, full of ads…

The following programs don't work properly: (maybe these apps use the google pdf engine?)

  • easy pdf-reader (rizwan chaudhary) => "Error when loading the pdf file"
  • pdf reader (pfr inc) => "this file manager doesn't allow opening files with third app
  • pdf viewer (tapi llc) => "Application error: An unexpected error has occurred: Format specifier '%1$s'
  • pdf reader (librera) => "File not found /eac4df25-9e04-4d23-add7-e0af20cb76d0/assets/Note_1.pdf
  • google pdf viewer and google drive pdf viewer (note that those are not the same!)

6 out of 13 pdf viewers don't work..

Is there a proper way to enforce the user to use a pdf reader other than googles pdf reader (or do I have to tell them to use adobe pdf reader explicitly...?) I think nearly every android device has the onedrive pdf reader? Duh, sometimes I hate google...

I will try to find a solution as well, but this is really weird...

Berki2021 avatar May 24 '20 09:05 Berki2021

In terms of your failure cases, the non-Google ones may just be bugs in the PDF viewer. For example, the "File not found" one probably is just grabbing the path from the Uri and trying to open a file using it, pretending that all Uri values have file schemes. If you are in position to try opening a PDF attachment from an email in those apps, it would be interesting to see which of those failure cases still fail (clearly bug) and which now work (suggesting that limitations intrinsic to the asset as a data source is what's tripping them up).

From a compatibility standpoint, there is little question that copying the asset to a file, then using FileProvider to serve it, will be better than StreamProvider. Assets, being entries in a ZIP-style archive, are a bit less flexible than are simple files. The downside of the copy-asset-to-file approach is the extra disk space that it consumes.

In terms of what the Google viewers are doing, I have absolutely no explanation for that behavior.

Is there a proper way to enforce the user to use a pdf reader other than googles pdf reader

Not really. The closest thing that you could do would be to use queryIntentActivities() on PackageManager to find everything that supports startActivity() for your Intent. You would filter the resulting list, throwing out known problematic apps, then present your own UI to ask the user to choose a PDF viewer among the remainder. You would then craft an explicit Intent, identifying that particular activity plus having the rest of your existing Intent contents, and use that for startActivity(), to drive straight to the chosen activity. IOW, you do the same sort of stuff that a launcher does when presenting a list of MAIN/LAUNCHER activities to the user, just for your Intent instead of the launcher Intent.

I think nearly every android device has the onedrive pdf reader?

Other than a few Samsung models, I never see that app pre-installed.

but this is really weird...

I am going to try to budget time to write a separate AssetProvider that just handles the serve-assets scenario. This library is old, and I wrote it back when I was being a moron about libraries and focusing on features vs. maintainability.

(nowadays, I have broadened my scope and am a moron about many other things :grin:)

I will see what happens when I use it with your assets and see if there is any behavior change in the Google PDF viewer.

commonsguy avatar May 24 '20 13:05 commonsguy

If you are in position to try opening a PDF attachment from an email in those apps, it would be interesting to see which of those failure cases still fail (clearly bug) and which now work (suggesting that limitations intrinsic to the asset as a data source is what's tripping them up).

I've taken the chance to test the above written case and came to following conclusion (I've sent several pdfs from one email to another using gmail and opening the files from the gmail client):

These work fine:

  • Word
  • onedrive pdf viewer
  • hp pdf print service
  • adobe acrobat reader
  • foxit pdf reader
  • pdf reader for android
  • easy pdf-reader (rizwan chaudhary) => didn't work before
  • pdf reader (librera) => didn't work before, but it recognized the pdf file as a word file

These do not work:

  • pdf reader (pfr inc) => "this file manager doesn't allow opening files with third app" (that's literally the worst app I've ever downloaded... I don't get why it has a million downloads)
  • pdf viewer (tapi llc) => "Application error: An unexpected error has occurred: Format specifier '%1$s'
  • pdf reader for android 2020 (iVilia Studio) => didn't open anything, worked before

So in this case, 3 out of 10 didn't work and are maybe just bad written? (because they look like they are..)

I am going to try to budget time to write a separate AssetProvider that just handles the serve-assets scenario.

That's really kind, thank you very much! Even tho it would serve for a small part of my further application, it is at least one of the most important ones. Our customers need quick access to some of our pdf files... (we are a small business, and I am just a student) If you need any help, just contact me! I may be not the best programmer, but I am stubborn 😄

The other option would be to transfer the pdf files from the asset folder to the internal storage after downloading the app and read the files from there. Not the perfect solution, but it serves the purpose of just reading pdf files... (if it is even possible due to security concerns)

I hope I don't have the same pain when programming the app in ios / swift 😞

Berki2021 avatar May 24 '20 19:05 Berki2021

These do not work

They probably do not handle content Uri values well. That's surprising, since it is 2020 and file Uri values have been being phased out for nearly four years. Gmail, for example, has been using them for at least that long. So those apps have been limited for quite some time, and there isn't a whole lot you can do about that.

The other option would be to transfer the pdf files from the asset folder to the internal storage after downloading the app and read the files from there. Not the perfect solution, but it serves the purpose of just reading pdf files... (if it is even possible due to security concerns)

Be careful about terminology. My guess is that when you wrote "internal storage" you really mean what the Android SDK calls external storage and not what the Android SDK refers to as internal storage. Unfortunately, Google uses different terms for the SDK's external storage, calling it "internal storage" when talking to users. :man_facepalming:

Copying the assets to the SDK's internal storage is easy. You would use FileProvider to make the PDF available. This sample Java app from one of my books demonstrates the whole process. However, your three failing PDF viewers will still fail, in all likelihood.

Copying the assets to the SDK's external storage is a slightly bigger hassle. If you use getExternalFilesDir() on Context to get the directory to write to, you're fine, but the directory is not the most user-friendly location (Android/your.application.id/files/) and the PDFs would be removed when your app is uninstalled. Given some restrictions for writing elsewhere on external storage introduced in Android 10, the Storage Access Framework (ACTION_CREATE_DOCUMENT) would be your most likely alternative, allowing the user to choose where to place the content. In either case, the users wind up with independent access to the PDFs and can open them using whatever app they choose.

commonsguy avatar May 24 '20 19:05 commonsguy

Be careful about terminology. My guess is that when you wrote "internal storage" you really mean what the Android SDK calls external storage and not what the Android SDK refers to as internal storage

Wait, I still don't got it. So lets say we have a phone, which has an internal storage of 128 gb and can extend its storage with a 64 gb sd card.

Copying the assets to the SDK's internal storage is easy.

Which of the above given example is the "internal storage" now? The 128 gb yes? So when google says "external storage" they mean the "internal storage" and not the sd card? So I could just copy my pdf files from the assets folder to the 128 gb "internal storage" and everything should work properly?

However, your three failing PDF viewers will still fail, in all likelihood.

Yeah I have to say, if one of my users use one of these pdf viewers, all hope is lost for them and I would just refer them to use adobe acrobat like everyone else does...

Copying the assets to the SDK's external storage is a slightly bigger hassle.

This would mean the 64 gb sd card? If it refers to the 128 gb storage, do I have to do this in order to open a pdf?

Berki2021 avatar May 24 '20 20:05 Berki2021

So lets say we have a phone, which has an internal storage of 128 gb and can extend its storage with a 64 gb sd card.

From a space standpoint, that is both what the SDK refers to as "internal storage" and "external storage" — those are simply separate directory structures with separate access privileges.

Which of the above given example is the "internal storage" now?

I recommend that you read the blog posts that I linked to on what internal storage and external storage mean, from the standpoint of the Android SDK. Roughly speaking, if the user can see it when plugging in a USB cable, it is external storage.

So when google says "external storage" they mean the "internal storage" and not the sd card?

When the Android SDK documentation refers to "external storage", it refers to what the user will see often labeled as "internal storage" when plugging in a USB cable.

So I could just copy my pdf files from the assets folder to the 128 gb "internal storage" and everything should work properly?

That will depend a ton on what "work properly" means. Your app's ability to launch a PDF viewer will not dramatically improve (the three broken apps will still be broken). However, the user also has direct access to those PDFs and may have other options for getting them to her favorite PDF viewer. For example, those broken PDF apps might have a built in "open a PDF" option that will work better. The user will also be able to copy the PDFs to her desktop, if desired.

This would mean the 64 gb sd card?

I term SD cards removable storage. Google's docs, and their SDK support for SD cards, is a mess.

commonsguy avatar May 24 '20 20:05 commonsguy

However, the user also has direct access to those PDFs and may have other options for getting them to her favorite PDF viewer.

My user is really tech-savvy so the ability to directly access those PDF files would not enhance nor worsen my current situation 😄

I recommend that you read the blog posts that I linked to on what internal storage and external storage mean

I will!

But I still don't get this point: Why does google not provide the ability to directly access pdf/etc files from the asset folder and start and intent??? (other than with the usage of external libraries) I mean, someone could have thought about it before, couldn't they?

EDIT: I've read the post and I have to say that this is still confusing me.. But your explanation was great, thank you very much. My goodness, I just want to open a pdf file 🤣

Berki2021 avatar May 24 '20 20:05 Berki2021

Why does google not provide the ability to directly access pdf/etc files from the asset folder and start and intent?

I imagine that they believe the purpose of assets is for internal use within the app. Google probably expects apps that want to get PDFs to users will do so by downloading those PDFs from a server, or maybe generating the PDFs in the app itself.

Personally, I have always viewed this view-a-PDF-asset capability as being primarily for app documentation. For your scenario ("Our customers need quick access to some of our pdf files"), I would lean towards downloading the PDFs from a server, rather than packaging them into the app itself. Do not have Google be a bottleneck, and Google's Play Store approval process for updates sometimes has problems. Telling your customers "sorry, we cannot get you the updated PDFs, because Google is being mean to us" is not going to make you popular. There are situations where I can see where packaging PDFs in an app might be worthwhile (e.g., the app primarily is used offline), but for ordinary apps, it is not the route that I would take.

commonsguy avatar May 24 '20 21:05 commonsguy

Hmm, okay, thank you. I have several questions when downloading the pdf file and viewing it, it would be amazing if you could answer those questions and maybe provide examples (I could not find any 😞)

Question 1: How do I make sure, that when the user clicks the button and downloads the pdf file, that the pdf file is not downloaded again when the user clicks the button again. Instead of that the button should open the pdf now. I am looking for something like

If (pdf.alreadyExists()) { openPdf() } else { downloadPDF() }

Question 2: Lets say, I've updated the pdf file and uploaded it to my server. How do I make sure, that when the user, who already downloaded the pdf file, clicks the button, the new pdf file will be downloaded and viewed? And in addition, the old pdf file should be deleted. I am looking for something like:

If(pdf.alreadyExists() && pdf.isNew() { openPDF() deleteOldPDF() } else { downloadPdf() }

I imagine that all of these could work with firebase? What do you think of firebase, would you use it? I want to write a shop as well and I have security concers writing my own server backend..

Berki2021 avatar May 25 '20 10:05 Berki2021

Question 1

The details will depend on where you are downloading the PDF to (internal storage? external storage that you can use with filesystem APIs like getExternalFilesDir()? ACTION_OPEN_DOCUMENT_TREE?).

Question 2

The details will depend on where you are downloading the PDF to, how you are downloading it, and how your app will find out about new PDF versions. So, for example, using a REST-style Web service is very different than is pulling the latest from a cloned Git repo.

What do you think of firebase, would you use it?

I try to minimize my use of commercial Google services, but that's just me.

At this point, though, the discussion has gone way beyond the scope of an issue on this library. If you have further concerns in this area, there are many resources available to you, including:

I am going to leave this issue open, in case I stumble upon what it is that the Google PDF viewers are doing.

commonsguy avatar May 25 '20 11:05 commonsguy

Thank you very much for all the help you provided! 😄

Berki2021 avatar May 25 '20 12:05 Berki2021