PDF reader from URL hybrid approach

As many StackOverflow questions and answers hint at, there is no built-in support for reading PDF files on Android, which is kind of a shame, especially since iOS has native support for the format. The vision must have been that user’s will select their own favorite PDF reader app, and then read all other apps’ PDF files through that reader app.

However, as an app developer, you cannot make assumptions on which PDF reader apps exist on a user’s device – you cannot even assume that one is installed! Several StackOverflow posts suggest redirecting the user to Google Drive’s (formerly Google Docs) online HTML PDF viewer. This solution is kind of bulky though, and doesn’t work as well as using a native app (Like Adobe Reader or other PDF reader apps).

This inspired me to come up with a hybrid solution for reading that checks if a PDF reader app is installed and does the follwing:

  • If a reader is installed, download the PDF file to the device and start a PDF reader app
  • If no reader is installed, ask the user if he wants to view the PDF file online through Google Drive

NOTE! This solution uses the Android DownloadManager class, which was introduced in API9 (Android 2.3 or Gingerbread). This means that it doesn’t work on Android 2.2 or earlier.

The main method for the code is PDFTools.showPDFUrl( Context context, String pdfUrl );

The code can be found on GitHub or just right here:

public class PDFTools {
	private static final String GOOGLE_DRIVE_PDF_READER_PREFIX = "http://drive.google.com/viewer?url=";
	private static final String PDF_MIME_TYPE = "application/pdf";
	private static final String HTML_MIME_TYPE = "text/html";
	
	/**
	 * If a PDF reader is installed, download the PDF file and open it in a reader. 
	 * Otherwise ask the user if he/she wants to view it in the Google Drive online PDF reader.<br />
	 * <br />
	 * <b>BEWARE:</b> This method
	 * @param context
	 * @param pdfUrl
	 * @return
	 */
	public static void showPDFUrl( final Context context, final String pdfUrl ) {
		if ( isPDFSupported( context ) ) {
			downloadAndOpenPDF(context, pdfUrl);
		} else {
			askToOpenPDFThroughGoogleDrive( context, pdfUrl );
		}
	}

	/**
	 * Downloads a PDF with the Android DownloadManager and opens it with an installed PDF reader app.
	 * @param context
	 * @param pdfUrl
	 */
	@TargetApi(Build.VERSION_CODES.GINGERBREAD)
	public static void downloadAndOpenPDF(final Context context, final String pdfUrl) {
		// Get filename
		final String filename = pdfUrl.substring( pdfUrl.lastIndexOf( "/" ) + 1 );
		// The place where the downloaded PDF file will be put
		final File tempFile = new File( context.getExternalFilesDir( Environment.DIRECTORY_DOWNLOADS ), filename );
		if ( tempFile.exists() ) {
			// If we have downloaded the file before, just go ahead and show it.
			openPDF( context, Uri.fromFile( tempFile ) );
			return;
		}

		// Show progress dialog while downloading
		final ProgressDialog progress = ProgressDialog.show( context, context.getString( R.string.pdf_show_local_progress_title ), context.getString( R.string.pdf_show_local_progress_content ), true );
		
		// Create the download request
		DownloadManager.Request r = new DownloadManager.Request( Uri.parse( pdfUrl ) );
		r.setDestinationInExternalFilesDir( context, Environment.DIRECTORY_DOWNLOADS, filename );
		final DownloadManager dm = (DownloadManager) context.getSystemService( Context.DOWNLOAD_SERVICE );
		BroadcastReceiver onComplete = new BroadcastReceiver() {
			@Override
			public void onReceive(Context context, Intent intent) {
				if ( !progress.isShowing() ) {
					return;
				}
				context.unregisterReceiver( this );
				
				progress.dismiss();
				long downloadId = intent.getLongExtra( DownloadManager.EXTRA_DOWNLOAD_ID, -1 );
				Cursor c = dm.query( new DownloadManager.Query().setFilterById( downloadId ) );
				
				if ( c.moveToFirst() ) {
					int status = c.getInt( c.getColumnIndex( DownloadManager.COLUMN_STATUS ) );
					if ( status == DownloadManager.STATUS_SUCCESSFUL ) {
						openPDF( context, Uri.fromFile( tempFile ) );
					}
				}
				c.close();
			}
		};
		context.registerReceiver( onComplete, new IntentFilter( DownloadManager.ACTION_DOWNLOAD_COMPLETE ) );
		
		// Enqueue the request
		dm.enqueue( r );
	}
	
	/**
	 * Show a dialog asking the user if he wants to open the PDF through Google Drive
	 * @param context
	 * @param pdfUrl
	 */
	public static void askToOpenPDFThroughGoogleDrive( final Context context, final String pdfUrl ) {
		new AlertDialog.Builder( context )
			.setTitle( R.string.pdf_show_online_dialog_title )
			.setMessage( R.string.pdf_show_online_dialog_question )
			.setNegativeButton( R.string.pdf_show_online_dialog_button_no, null )
			.setPositiveButton( R.string.pdf_show_online_dialog_button_yes, new OnClickListener() {
				@Override
				public void onClick(DialogInterface dialog, int which) {
					openPDFThroughGoogleDrive(context, pdfUrl);	
				}
			})
			.show();
	}
	
	/**
	 * Launches a browser to view the PDF through Google Drive
	 * @param context
	 * @param pdfUrl
	 */
	public static void openPDFThroughGoogleDrive(final Context context, final String pdfUrl) {
		Intent i = new Intent( Intent.ACTION_VIEW );
		i.setDataAndType(Uri.parse(GOOGLE_DRIVE_PDF_READER_PREFIX + pdfUrl ), HTML_MIME_TYPE );
		context.startActivity( i );
	}
	/**
	 * Open a local PDF file with an installed reader
	 * @param context
	 * @param localUri
	 */
	public static final void openPDF(Context context, Uri localUri ) {
		Intent i = new Intent( Intent.ACTION_VIEW );
		i.setDataAndType( localUri, PDF_MIME_TYPE );
		context.startActivity( i );
	}
	/**
	 * Checks if any apps are installed that supports reading of PDF files.
	 * @param context
	 * @return
	 */
	public static boolean isPDFSupported( Context context ) {
		Intent i = new Intent( Intent.ACTION_VIEW );
		final File tempFile = new File( context.getExternalFilesDir( Environment.DIRECTORY_DOWNLOADS ), "test.pdf" );
		i.setDataAndType( Uri.fromFile( tempFile ), PDF_MIME_TYPE );
		return context.getPackageManager().queryIntentActivities( i, PackageManager.MATCH_DEFAULT_ONLY ).size() > 0;
	}

}

About the author

Jesper Borgstrup Jesper is a Masters student of computer science at the University of Copenhagen with many years of experience in writing applications for Android.

17 thoughts on “PDF reader from URL hybrid approach

  1. Joe

    Hi, this doesn’t work for me.
    It works when I don’t have a pdf-reader installed. But if I have a pdf-reader installed it stops on this line

    r.setDestinationInExternalFilesDir( context, Environment.DIRECTORY_DOWNLOADS, filename );

    I use an emulator with android 4.2.2 and have installed adobe reader manually on the emulator.
    Hope you know what the problem is.

    Reply
    1. Jesper Borgstrup Post author

      What is the error? Have you set up your emulator to have external storage (SD Card)?

      Reply
      1. Franz

        Do you really have to have SD-storage? I’m getting the same error as I don’t have one. It would be nive if you wouldn’t take SD-storage as granted!

        Reply
        1. Jesper Borgstrup Post author

          I’m not sure, but it looks like the DownloadManager doesn’t need to use SD storage:

          “This class contains all the information necessary to request a new download. The URI is the only required parameter. Note that the default download destination is a shared volume where the system might delete your file if it needs to reclaim space for system use. If this is a problem, use a location on external storage (see setDestinationUri(Uri)).” [https://developer.android.com/reference/android/app/DownloadManager.Request.html]

          Reply
        2. Franz

          ok, i now realized, that the external storage doesn’t necessaryly has to be USB-storage… But I still keep getting the “IllegalStateException” which means, the the storage cannot be found or created.

          I even granted the according permission “android.permission.WRITE_EXTERNAL_STORAGE” in the manifest, but still keep getting this error..

          Reply
          1. Jesper Borgstrup Post author

            Hmmm… Looking at this StackOverflow answer, it could seem that to have DownloadManager save on the internal storage, you would have to go through a custom made ContentProvider in your app.

            If you manage to pull it off, please let me know, or post an answer to the StackOverflow question (or both ;-)

          2. Jesper Borgstrup Post author

            I can’t tell you, because I’m writing my master’s thesis at the moment, and haven’t developed for Android for half a year.

          3. Franz

            Ok, I figured out, that there was only an additional permission missing, namely android.permission.INTERNET

            That’s it, that’s all. Thanks for this great solution! Good luck for your thesis!

  2. Pingback: Display PDF within app on Android? - Android Questions - Developers Q & A

  3. jojo

    hej Jesper,
    your solution looks interesting, but since i m a beginner with android i dont know where to put the PDF’s URL. could you help me please?
    Thanks a lot ;)

    Reply
  4. Genelia

    I was searching for a library from where i could find a code for reading PDF files in android app and i have found one library from Aspose known as Aspose.PDF for Android

    It offers many features that developers can use in their app. You should also check it out.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>