Downloading an entire file via the Storage Access Framework

I’ve been working on an app that works with image files that the user selects using the Storage Access Framework (SAF) that was introduced with the release of Android 4.4 (API level 19). Whilst the new selector UI provides the user with an easy way to access files stored on their device and in the cloud seamlessly, it doesn’t necessarily give developers the same ease of use.

It was straightforward to access the images and display them in an ImageView, but I found that it wasn’t the file that I was working with but a cached version of that file in memory. Whilst that’s great for memory management and responsiveness, I was after the Exif data that was stored within those files; that meant I needed to pull the file from its current location and reconstitute it on the device itself.

This part was lifted straight from the developer documentation. It’s provided here without comments in case you want to do a quick rip-n-run to try it out.

private static final int READ_REQUEST_CODE = 42;

public void performFileSearch() {

	Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
	intent.addCategory(Intent.CATEGORY_OPENABLE);
	intent.setType("image/*");

	startActivityForResult(intent, READ_REQUEST_CODE);
}

This is the juicy bit that writes the item that the user selects to a local file on the device.


/** We're overriding this function, which is called when an Activity result is
 * returned to this class. */
@Override
public void onActivityResult(int requestCode, int resultCode,
	Intent resultData) {

	/** 	This conditional statement ensures that we only run when the SAF has
	 *	returned us a result successfully (Activity.RESULT_OK) and that the
	 *	request code matches the one we used to request a file via the SAF
	 *	Activity (resultCode == READ_REQUEST_CODE). */
	if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {

		Uri uri = null;		// Instantiate a Uri object to hold the image's Uri.

		/**	We need to make sure that the data returned isn't null, to prevent
		 *	any nasty null pointer exceptions. */
		 if (resultData != null) {

		 	try{
			 	/** Here we keep it all in a try-catch in case, so we don't
			 	 *	force-close if something doesn't go to plan.
			 	 *
			 	 *	This finds the location of the device's local storage (don't
			 	 *	assume that this will be /sdcard/!), and appends a hard-
			 	 *  coded string with a new subfolder, and gives the file that
			 	 *  we are going to create a name.
			 	 *
			 	 *  Note: You'll want to replace 'gdrive_image.jpg' with the
			 	 *  filename that you fetch from Drive if you want to preserve
			 	 *  the filename. That's out of the scope of this post. */
			 	String output_path = Environment.getExternalStorageDirectory()
			 							+ "/MyNewFolder/gdrive_image.jpg";

			 	// Create the file in the location that we just defined.
			 	File oFile = new File(output_path);

			 	/**	Create the file if it doesn't exist; be aware that if it
			 	 *	does, we'll be overwriting it further down. */
			 	if (!oFile.exists()) {
				 	/**	Note that this isn't just mkdirs; that would make our
				 	 *	file into a directory! The 'getParentFile()' bit ensures
				 	 *	that the tail end remains a File. */
			 		oFile.getParentFile().mkdirs();
			 		oFile.createNewFile();
			 	}

			 	/**	The 'getActivity()' bit assumes that this is being run from
			 	 *	within a Fragment, which it is of course. You wouldn't be
			 	 *	working outside of current Android good practice would
			 	 *	you?... */
			 	InputStream iStream = getActivity()
			 							.getContentResolver()
			 							.openInputStream(uri);

			 	/**	Create a byte array to hold the content that exists at the
			 	 *	Uri we're interested in; this preserves all of the data that
			 	 *	exists within the file, including any JPEG meta data. If
			 	 *	you punt this straight to a Bitmap object, you'll lose all
			 	 *	of that.
			 	 *
			 	 *	Note: This is reallt the main point of this entire post, as
			 	 *	you're getting ALL OF THE DATA from the source file, as
			 	 *	is. */
			 	byte[] inputData = getBytes(iStream);

			 	writeFile(inputData,output_path);

			} catch (Exception e){
				/** You'll have to forgive the lazy exception handling here...
				 * I'm keeping it clean for the sake of the post length! */
				e.printStackTrace();
			}
		 }

	}

}

/** This function puts everything in the provided InputStream into a byte array
 *	and returns it to the calling function. */
public byte[] getBytes(InputStream inputStream) throws IOException {

	ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
	int bufferSize = 1024;
	byte[] buffer = new byte[bufferSize];

	int len = 0;

	while ((len = inputStream.read(buffer)) != -1) {
		byteBuffer.write(buffer, 0, len);
	}

	return byteBuffer.toByteArray();
}

/**	This function rewrites the byte array to the provided filename.
 *
 *	Note: A String, NOT a file object, though you could easily tweak it to do
 *	that. */
public void writeFile(byte[] data, String fileName) throws IOException{
	FileOutputStream out = new FileOutputStream(fileName);
	out.write(data);
	out.close();
}

Here’s a screencast showing how it should look once you’ve got the example running.

The code is basic and outnumbered by the comments, so if you’ve worked with Android’s flavour of Java project before you shouldn’t have any problem understanding what’s going on. You can pull the full working example from the repo below; use it any way that you like.