Linking to PDFs in Cordova apps

This post is more than 2 years old.

In today's "I wonder what happens when..." post, I decided to take a look at what happens when you try to use a PDF with a hybrid mobile application. I know that PDFs, in general, "just work" on both Android and iOS, but I was specifically curious about how you would use PDFs within a Cordova app. As usual, what I thought would be rather simple turned into anything but that.

First, I whipped up a super quick, super minimal Cordova application. Even though I'm "All Ionic, All the Time", I specifically avoided it in this case to keep my code as simple as possible. I decided on three separate tests:

  1. A simple link to a PDF.
  2. Using JavaScript to load the PDF via document.location.href
  3. Using the InAppBrowser Cordova plugin

To be clear, I expected both 1 and 2 to act the same, but I figured I might as well be complete and check it out. Here's the HTML:


<!DOCTYPE html>
<html>
	<head>
		<meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *">
		<meta name="format-detection" content="telephone=no">
		<meta name="msapplication-tap-highlight" content="no">
		<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
		<link rel="stylesheet" type="text/css" href="css/index.css">
		<title>Hello World</title>
	</head>
	<body>

		<p>
			<a href="assets/foo.pdf">Regular Ole Link</a>
		</p>
		<p>
			<button id="loadPDF1">document.location.href</button>
		</p>
		<p>
			<button id="loadPDF2">inappbrowser</button>
		</p>

		<script type="text/javascript" src="cordova.js"></script>
		<script type="text/javascript" src="js/index.js"></script>
	</body>
</html>

And here's the JavaScript behind it:


document.addEventListener('deviceready', init, false);

function init() {
    document.querySelector('#loadPDF1').addEventListener('touchend', loadPDF1,false);
    document.querySelector('#loadPDF2').addEventListener('touchend', loadPDF2,false);
}

function loadPDF1() {
    console.log('loadPDF1');
    document.location.href='assets/foo.pdf';
}

function loadPDF2() {
    console.log('loadPDF2');
    var ref = cordova.InAppBrowser.open('assets/foo.pdf', '_blank', 'location=no');
}

I assume this is simple enough that it doesn't require explanation, but let me know in the comments if anything seems off. Ok, let's test iOS!

iOS 9.3

The first two links "just worked", but not as I expected. Here's the initial page:

And here is what happened when I clicked:

Yes - the PDF is rendering beautifully. (And it's easy to read, but to be fair, I use a Keynote slidedeck as my source.) However... notice something missing?

Yep - there's no way to get back to the app. Technically you're still in the app, but the entire webview is the PDF, and since iOS doesn't support a Back button, you're screwed. I had to kill the application to get it back to normal.

Of course, the InAppBrowser makes this easy enough to handle:

In case you can't see it, there is a bar at the bottom with a "Close" link that will bring you back to the app.

Ok, so that's easy - Android will probably respond the same. Let's take a look!

Android 6.0.0

Alright - so going into my test, I expected the exact same results - except that Android would let me go back from the PDF using the first two tests. I loaded it up in my emulator, and...

Nothing.

Zip. No responses. At first I thought maybe it was a CSP issue, but when I opened up the console I saw this:

Resource interpreted as Document but transferred with MIME type application/pdf: "file:///android_asset/www/assets/foo.pdf".

Weird. I've seen issues with dynamic apps (ColdFusion) outputting binary date without the right content type, but if my memory serves me right, normally the browser just tries to handle it as best it can. In the past (far past) I can remember browsers trying to render the binary data as text, but it seems like I've not seen that in quite some time.

Correction - I decided to actually test that hypothesis and desktop Chrome barfed on ColdFusion outputting a PDF without the right header. Chrome, Safari, and Firefox all crapped the bed trying to load it.

Anyway - that's with the first two links. The third link, the InAppBrowser one? Returns nothing. I kid you not. The new window opens, and nothing loads. I get zip in the console as well. Or so I thought. Returning back to Chrome's device window shows that it did load as a new web view:

But that console has nothing in it. I can't even execute JavaScript in the console. It's like the Phantom Zone of debugging.

Ok - so after a bit of searching, I found someone recommending using the download attribute. This is a newish HTML5 feature that tells the browser that it should download the asset instead of trying to render it. This also did nothing. No error, zip. (In case you're curious, iOS ignored the download attribute and just responded like it did with the first link.)

It turns out that the Android web view simply doesn't support PDFs. That seems... crazy - especially considering how many PDFs are out there. One could argue that they probably don't fit the mobile form factor very well, but I'd have assumed that showing something would be better than nothing. And heck - I'm sure Google could license a PDF viewer from Adobe. They probably have enough money for that.

Now what?

So - I did some Googling around, and asking on Slack, and Simon Prickett shared some things that worked for him. One of them in particular looked interesting, cordova-plugin-file-opener2. This plugin tries to open a file in a local viewer. It seemed easy enough so I decided to try iOS again. I added a new button and used this code (after adding the plugin and the File plugin):


function loadPDF3() {
    console.log('loadPDF3');
    console.log(cordova.file.applicationDirectory);
    cordova.plugins.fileOpener2.open(
        cordova.file.applicationDirectory+'www/assets/foo.pdf',
        'application/pdf', 
        { 
            error : function(e) { 
                console.log('Error status: ' + e.status + ' - Error message: ' + e.message);
            },
            success : function () {
                console.log('file opened successfully'); 				
            }
        }
    );
}

And it seemed to work fine. I had the Adobe PDF viewer installed on my iPhone and it was suggested:

and it viewed just fine - and I absolutely love the new iOS feature that provides links back to previous apps:

Ok - let's try Android. It's going to work. I bet.

Except no, of course it doesn't. Android reports this (via the plugin's error handler):

Error status: 9 - Error message: File not found

Sigh. I did some digging on the plugin's GitHub issues though and ran across this report: Opening local file (pdf) : "not found".

If you read down the thread a bit, you run into a really nice solution by japostigo-atsistemas. I modified his code a bit to work with my solution and came up with this:


window.resolveLocalFileSystemURL(cordova.file.applicationDirectory +  'www/assets/foo.pdf', function(fileEntry) {
	window.resolveLocalFileSystemURL(cordova.file.externalDataDirectory, function(dirEntry) {
		fileEntry.copyTo(dirEntry, 'file.pdf', function(newFileEntry) {
			cordova.plugins.fileOpener2.open(newFileEntry.nativeURL,'application/pdf',
			{ 
				error : function(e) { 
					console.log('Error status: ' + e.status + ' - Error message: ' + e.message);
				},
				success : function () {
					console.log('file opened successfully'); 				
				}
			}
			);
		});
	});
});

As you can see, he is using the FileSystem to copy to an external data directory and then uses the plugin to load it from there. A royal pain in the ass, but it works... I believe. I ended up with:

Error status: 9 - Error message: Activity not found: No Activity found to handle Intent { act=android.intent.action.VIEW dat=file:///storage/emulated/0/Android/data/io.cordova.hellocordova/files/file.pdf typ=application/pdf flg=0x4000000 }

Which implies that it simply couldn't find a PDF viewer on my Android simulator. I've got a device, but I'm at an airport currently and the device is back home. Considering multiple people up voted the idea, it seems like a good solution. Of course it doesn't work on iOS because, reasons, but at this point I'd simply consider using an IF/ELSE with the device plugin.

At this point - I consider the issue solved - roughly - and hopefully this will be of help to others. Thanks again to Simon for his help with this plugin!

Raymond Camden's Picture

About Raymond Camden

Raymond is a developer advocate for HERE Technologies. He focuses on JavaScript, serverless and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Jacob Hargrave posted on 6/27/2016 at 10:11 PM

We ended up using PDF.js to render PDFs on Android, and the default renderer on iOS. Works well enough.

Comment 2 by Steve Husting posted on 6/27/2016 at 10:20 PM

Raymond, why not include PDF.js in this article as well?

Comment 3 by Raymond Camden posted on 6/27/2016 at 11:46 PM

Hey Steven/Jacob: I knew of PDF.js, but it just felt like it wasn't appropriate for what I was trying to do. I know that sounds a bit arbitrary, but I thought rendering it *in* the app, with other UI around it, just wouldn't make sense on a mobile device. I am *very* open to be proven wrong - and would love to see some screen shots of it in action. (You can share pics on Disqus btw.)

Comment 4 (In reply to #1) by Simon Prickett posted on 6/28/2016 at 4:11 PM

Did you get pinch zooming working with PDF.js?

Comment 5 (In reply to #2) by Simon Prickett posted on 6/28/2016 at 4:15 PM

I put together an example Cordova app that tries to render PDF using external app with the file opener plugin, PDF.js inline and also using the InAppBrowser plugin (which doesn't work on Android). You can see the code for that here (I should also add a gif of it working in the README) https://github.com/simonpri...

Comment 6 by Simon MacDonald posted on 6/28/2016 at 6:51 PM

Hey Ray, yeah you can't use the plugin to open a file in your assets directory in another app as your app is running as a different user ID than the PDF viewing app. So the PDF viewer does not have the right privs to open the file.

The solution you have posted about copying the file to the external data directory should work but the emulator may be screwing you up. Try replacing `storage/emulated/0` with `sdcard`.

Comment 7 (In reply to #6) by Raymond Camden posted on 6/28/2016 at 11:05 PM

Oh fascinating - that makes sense. Too bad there isn't a way for App A to say that App B has permission to open one of its own resources.

Comment 8 (In reply to #7) by Simon MacDonald posted on 6/28/2016 at 11:10 PM

There are, they are called [ContentProviders](https://developer.android.c.... Basically you would create a ContentProvider to give other apps access to your apps data. The assets file system is not really meant for this type of access though.

Comment 9 (In reply to #8) by Raymond Camden posted on 6/28/2016 at 11:16 PM

So given your last sentence, if I were to ship an app and for whatever reason, I thought PDFs were good (maybe docs?), would you imagine the app copying the assets out on first launch to make it easier to use?

Comment 10 (In reply to #9) by Simon MacDonald posted on 6/28/2016 at 11:20 PM

Yeah, I would probably copy the docs to the local file system on first launch. You can use the content-sync plugin to do that. I fact on Android you can specify a manifest that only copies the PDF files if you want. Ping me on email we'll go over it and you have another blog post :)

Comment 11 (In reply to #10) by Raymond Camden posted on 6/28/2016 at 11:24 PM

Ok, I know you said 'ping me on email', but I want to do a bit more here. ;) Doesn't the content-sync plugin assume you have internet connectivity, at least once? I'm not saying that makes it a bad solution per se, but it does mean you can't ship the assets with the initial app download.

Comment 12 (In reply to #11) by Simon MacDonald posted on 6/28/2016 at 11:30 PM

Whoops, I forgot content-sync copies to a directory internal to the app so it would not be accessible buy outside apps on Android. What we'd need to do is something similar to what content-sync does when you set `copyRootApp` to true. So yeah, on startup copy the PDF's to external storage on Android.

Note: the asset file system is super slow on Android so you want this to be a non-blocking call.

Comment 13 (In reply to #4) by Jacob Hargrave posted on 6/29/2016 at 7:38 PM

Yes. I think we had to change the config <preference name="EnableViewportScale" value="true"/>

Comment 14 by maxx0r posted on 7/19/2016 at 2:33 PM

I once solved this by opening the PDF in the google docs viewer in the inAppBrowser. But I used a weblink for the PDF. Which is a better solution in more than one way (control over updates to the PDF, smaller package size)

Comment 15 by Roy posted on 10/17/2016 at 10:27 AM

Hey Raymond, nice article, I'm a web developer that is trying to make an app that can cache and view PDFs using cordova for the first time.

I had success using cordova-plugin-file-opener2 when tested it on my htc desire 728 but then fails when a remote team tested it with samsung s6 edge plus.

By using avd to simulate it, it seems that cordova.file.externalDataDirectory is missing while cordova.file.applicationDirectory is present, but I'm not sure if I'm simulating it the right way.

I tried to use PDF.js afterward and again it worked nice on my device but failed on samsung too, I'm still trying to find the reason but do you have any suggestion on what should I do to solve this phenomenon or what kind of knowledge/skill I should be after? I feel like I'm lacking something really important but I can't really tell what is.

Comment 16 (In reply to #15) by Raymond Camden posted on 10/17/2016 at 2:21 PM

I'm not sure the simulator will have externalDataDirectory. Is there anyway you can get the device to test?

Comment 17 by dsmithhfx posted on 10/17/2016 at 4:16 PM

Hi,

I'm trying to get this working in an Intel XDK project apk, but it's throwing an "Uncaught TypeError: Cannot read property 'applicationDirectory' of undefined", when pasting the code sample (with appropriate path/filename changes) that follows "nice solution by japostigo-atsistemas [...]" above into my custom script (so it's not one of the default cordova scripts). (-- note I'm also got jquery onboard).

I'm testing it on a 2013 Nexus 7 with Android 5.1.1 and chrome usb debugging

I've installed the cordova-plugin-file-opener2 into the project using the built-in XDK plugin manager, I've just got a feeling the syntax isn't quite right, or I've put it in the wrong script. Hope it's nothing terribly obvious!

Appreciate any pointers.

Thanks

Edit: ok, it also needs the file plugin, which I had wrongly assumed would be redundant and so deleted from my project. Got it working now.

Comment 18 (In reply to #16) by Roy posted on 10/18/2016 at 1:57 AM

That's not quite possible at moment, we are just a starter company without many resource to spend on :/, is there any way beside real device for testing? I saw some options like Xamarin Test Cloud, but I haven't look deeper into it yet so I'm not sure if they can provide the necessary test environment, what normally mobile developers do in this case? Buy the actual device?

Comment 19 (In reply to #18) by Raymond Camden posted on 10/18/2016 at 2:27 PM

Unfortunately, I'd say this is part of the price you have to pay for developing on the Android platform. There are virtual test beds (I can't comment on the Xamarin one, I haven't used it), but in the end, I think it's better to actually have the device on hand.

Just my opinion.

Comment 20 (In reply to #19) by Roy posted on 10/19/2016 at 2:45 AM

Thanks Raymond, now I know better what we'll be facing down this road, we'll keep searching for a solution that best fit for our situation, if we do find some very interesting solution/situation, I'll report it back here, once again, thanks for your attention :)

Comment 21 by Tony Davidson posted on 1/10/2017 at 4:29 PM

Hi Raymond,
I have used one of your posts as a basis for a Cordova app I am building using the cordova-plugin-file and cordova-plugin-file-transfer. My app is a cross platform for ios and android. One of the functions of the app is to download and edit pdf documents. This is where I have come unstuck on the ios side of things. Basically I can download and save easily enough and using cordova-plugin-fileopener2 I can open the document using a pdf editor. However the editors I have tried seem to be making copies of the original document and any changes I make are not saved to the original download which is supposed to be uploaded with changes from the app.

I suspect the problem lies not with my code on the app but with the way iOS locks everything down. I was wondering if you had come across anything like this and what solutions you used to overcome this problem.
Thanks
Tony Davidson

Comment 22 (In reply to #21) by Raymond Camden posted on 1/11/2017 at 3:41 PM

Sorry no. You may have to use a warning in iOS to tell users that they can only work on a copy.

Comment 23 (In reply to #22) by Tony Davidson posted on 1/11/2017 at 4:09 PM

I feared as much. Thank you for getting back to me, I appreciate it.

Comment 24 by Kashban posted on 3/9/2017 at 12:31 PM

I came across a much simpler solution:

this.showDocument = function(path) {
var platform = WL.Client.getEnvironment();
if (path && path.length > 0){
var ref = window.open(decodeURI(path), platform === WL.Environment.ANDROID ? '_system' : '_blank', 'location=yes');
});
}
}

WL.Client is from the IBM Mobile First Client Framework but any platform detection plugin would do.

This starts the default system browser on Android which takes the pdf behind the path and issues an intent for a pdf viewer. Path is a file:/// url pointing to the local filesystem. Tested on Android 6.0.1 (Galaxy Tab S2).

Comment 25 (In reply to #24) by Avijit Sarkar posted on 3/22/2017 at 6:17 PM

@Kashban, Is it showing any back button to close the viewer and go back to the app? Do we need to use only hardware back button to close it?

Comment 26 (In reply to #24) by Max Shell posted on 7/7/2017 at 6:27 PM

great, man!! thank you!!

Comment 27 (In reply to #25) by Kashban posted on 9/7/2017 at 12:29 PM

Avijit Sarkar
Sorry for the late answer... Yes, you need to use the back button on the device to get back to your app. Since the PDF viewer is a whole different activity there is no way to show any additional controls there.

Which are not neede btw because the back button is the standard android way to go back ;-)

Comment 28 (In reply to #26) by Kashban posted on 9/7/2017 at 12:29 PM

You're welcome :-)

Comment 29 by Liliana Stanescu posted on 1/22/2018 at 11:33 AM

https://uploads.disquscdn.c...

Hello! I would like to call a variable instead of foo.pdf How can I do that?
Thank you!

Comment 30 (In reply to #29) by Liliana Stanescu posted on 1/22/2018 at 1:21 PM

I found the solution!

Comment 31 (In reply to #30) by Raymond Camden posted on 1/22/2018 at 1:51 PM

Cool beans. :)

Comment 32 by huseyin posted on 1/31/2018 at 1:29 PM

thanks dude :)

Comment 33 by John Morgan posted on 4/19/2018 at 11:00 AM

This page was a lifesaver, thanks! Yet another thing I expected to "just work" on Android :(

Comment 34 by Sher Salafi posted on 7/9/2018 at 12:40 AM

very good...please can you explain or give me a link about how i can place mp3 files so client can download it from internet using our hybrid app then open locally in the app...when internet is not available...i mean audio files should be available for offline.

Comment 35 (In reply to #34) by Raymond Camden posted on 7/14/2018 at 3:56 PM

That really isn't on topic for this post at all. Please post general questions like this to StackOverflow.

Comment 36 by sushma rani posted on 8/17/2018 at 12:41 PM

const browser = this.inAppBrowser.create(data, '_self', 'hideurlbar=yes');
browser.show();

i used this code in the ionic3 but it does not open the pdf,doc links only blank .
also tried with _blank but it only work with _system .

Comment 37 (In reply to #36) by Raymond Camden posted on 8/17/2018 at 7:25 PM

So is this a question or.... ?

Comment 38 (In reply to #37) by sushma rani posted on 8/18/2018 at 8:00 AM

Please provide me solution for this its not working for me .

Comment 39 (In reply to #38) by sushma rani posted on 8/19/2018 at 8:49 AM

Hi raymond this is not working in the ionic 3 native in app browser plugin. Actually i have created a app in which first home page after login open in the in app browser using the _self and on home i have the many links that are downloadable links like pdf files docx when i click on them now happens nothing.If you provide me any solution so that if i click on the links then they will be start downloading i appreciate it .

Comment 40 (In reply to #39) by Raymond Camden posted on 8/20/2018 at 1:46 PM

The only solution I have is the one here - sorry. I'm not doing much Cordova or Ionic these days.

Comment 41 by Derek Baker posted on 6/20/2019 at 9:54 PM

Raymond Camden, you're a lifesaver. Thanks for this!

Comment 42 (In reply to #41) by Raymond Camden posted on 6/21/2019 at 1:45 PM

You are most welcome.