Grokking Android

Getting Down to the Nitty Gritty of Android Development

Handling Binary Data with ContentProviders

By

To share binary data across app boundaries Android's programming model expects you to use content providers. And while I have covered how to access content providers and how to code one yourself, I haven't covered how to deal with binary data. I am going to correct that in this post.

How to access binary data of existing content providers

The ContentResolver class offers two methods to access binary data of content providers: openInputStream() and openOutputStream().

Obvioulsly you use the openInputStream() method to read data and the openOutputStream() method to write data. Both methods expect an id-based URI as parameter. It's up to you to close the stream later on.


Uri thisPhotoUri = 
      ContentUris.withAppendedId(Photos.CONTENT_URI, photoId);
InputStream inStream = null;
try {
   inStream = resolver.openInputStream(thisPhotoUri);

   // what to do with the stream is up to you
   // I simply create a bitmap to display it
   Bitmap bm = BitmapFactory.decodeStream(inStream);
   FrameLayout frame = 
         (FrameLayout)findViewById(R.id.picture_frame);
   ImageView view = new ImageView(getApplicationContext());
   view.setImageBitmap(bm);
   frame.addView(view);
} catch (FileNotFoundException e) {
   Log.e("cpsample", "file not found " + thisPhotoUri, e);
}
finally {
   if (inStream != null) {
      try {
         inStream.close();
      } catch (IOException e) {
         Log.e("cpsample", "could not close stream", e);
      }
   }
}

How to make your binary data available to others

Offering binary data from within your content provider to others is also easy - in principle.

The contract of content providers requires you to implement the openFile() method. The method of the base class will always throw a FileNotFoundException. The message is a tad confusing though (the URI might differ, of course):


No files supported by provider at 
      content://de.openminds.cpsample.provider.lentitems/photos/10

I think an IllegalStateException with a message text of "method not supported" would be better.

Android provides the helper method openFileHelper() that makes implementing the openFile() method very easy. All you have to do, to use this method, is to provide the location of the file in a column named "_data".

The openFileHelper() method queries your content provider for the value of the _data column and then creates a ParceFileDescriptor for you.

As you can see in the next snippet, implementing the openFile() method get's pretty simple:


@Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
      throws FileNotFoundException {
   if (URI_MATCHER.match(uri) != PHOTO_ID) {
      throw new IllegalArgumentException
            ("URI invalid. Use an id-based URI only.");
   }
   return openFileHelper(uri, mode);
}

If you violate the preconditions Android expects, you will see a FileNotFoundException. Three messages are possible:

Message texts of FileNotFoundExceptions thrown by openFileHelper()
Message Meaning
No entry for someUri Your _data column is empty for this record
Multiple items at someUri You used a dir-based path instead of an id-based one
Column _data not found. Your provider doesn't support the _data column

If you want to use another column for your filename or if you can compute it in some way, you cannot use openFileHelper(). In this case you have to implement the openFile() method yourself. Have a look at the source code of ContentProvider's openFileHelper() method to get you started.

One problem I ignored so far

What I haven't covered so far is when and how to create the file. And there is a reason for that. The problem with binary data and content providers is, that you cannot create them whenever you feel like it. Instead for all this to function, you have to create a filename at the same time as the record is created. But the file will be added later on using the openOutputStream() method.

It get's complicated when you have a model in which binary data are optional. Because now you cannot create a filename when the record is created since you do not know whether the client intends to add binary data at all.

The solution to this dilemma is to use indirection. The sample project deals with keeping track of lent items - and the user can add a snapshot of the item to the record but is not required to do so. Thus I have added a table solely for those snapshots. This table has an _id, an item_id that refers to the item the snapshot belongs to and the _data column. And of course I use a different CONTENT_URI to access it.

This way I am able to query for the existence of a picture first, before trying to load it:


// try to get a photo
Cursor photoIdCursor = resolver.query(Photos.CONTENT_URI,
         Photos.PROJECTION, 
         Photos.ITEMS_ID + " = ? ",
         new String[] { Long.toString(id) }, 
         null);
if (photoIdCursor.moveToFirst()) {
   long photoId = photoIdCursor.getLong(0);
   Uri thisPhotoUri = 
         ContentUris.withAppendedId(Photos.CONTENT_URI, photoId);
   try {
      InputStream inStream = 
            resolver.openInputStream(thisPhotoUri);
      Bitmap bm = BitmapFactory.decodeStream(inStream);
      FrameLayout frame = 
            (FrameLayout)findViewById(R.id.picture_frame);
      ImageView view = new ImageView(getApplicationContext());
      view.setScaleType(ScaleType.CENTER_INSIDE);
      view.setImageBitmap(bm);
      frame.addView(view);
   } catch (FileNotFoundException e) {
      Log.e("cpsample", "file not found " + thisPhotoUri, e);
   }
}

And I create a record for this table only when the user made a snapshot of the item.


Uri uri = resolver.insert(LentItemsProvider.LentItems.CONTENT_URI, values);
if (withPhoto) {
   // add the photo to the ContentProvider
   long id = ContentUris.parseId(uri);
   ContentValues photoValues = new ContentValues();
   photoValues.put(DBSchema.COL_ITEMS_ID, id);
   Uri cpPhotoUri = 
         resolver.insert(LentItemsProvider.Photos.CONTENT_URI, photoValues);
   OutputStream out = null;
   BufferedInputStream inStream = null;
   try {
      // "w" is the mode used: write only
      out = resolver.openOutputStream(cpPhotoUri, "w");
      
      // read the file from the temp path 
      // and copy the content
      File file = new File(this.photoUri.getPath());
      inStream = new BufferedInputStream(new FileInputStream(file));
      int available = inStream.available();
      byte[] buffer;
      while (available > 0) {
         buffer = new byte[available];
         int i = inStream.read(buffer, 0, available);
         out.write(buffer, 0, i);
         available = inStream.available();
      }
      out.flush();
   }
   catch (FileNotFoundException e) {
      Log.e("cpsample", "file not found", e);
   } catch (IOException e) {
      Log.e("cpsample", "IOException while using OutputStream", e);
   } finally {
      // you have to close the stream
      // the content provider does not do that
      try {
         if (out != null) {
            out.close();
         }
         if (inStream != null) {
            inStream.close();
         }
      } catch (IOException e) {
         Log.e("cpsample", "IOException while closing stream", e);
      }
   }
}

Your clients might break your provider

The big downside of adding binary data with content providers is that you rely on a well-behaving client. You create the _data column before the file has been created. Of course normally it should be the other way round.

The client could just insert a record into your content provider but never actually add the file. In this case your _data column contains a value that points to a non-existing file. Should another client try to use it, your provider would throw an exception. Not nice!

This probably is the reason why the MediaStore content provider offers methods to insert images in its nested MediaStore.Image.Media class and why for other media the media scanner is the preferred solution. You really should think about using the same approach.

Write a class that defines the contract of your provider, exports the URIs as constants, defines the columns and that offers helper methods to create whatever binary data you need. Within these methods you do what I have described above - but now you are no longer at the mercy of your clients.

What to do, if you there are multiple binary objects?

Sometimes you might have multiple binary data entries that belong to one object. Say you are developing a birdwatcher app. Now you might want to show videos of birds, as well as photos and maybe even the sound they are making. At least three items, maybe even multiple items per media type.

I think there are two approaches you could use in this case. If the meta data for these types differ, use a distinct table for each of the binary data with different CONTENT_URIs for all of them. For example separate URIs for bird videos, bird twitterings and bird photos.

But if the meta data are the same for all types (e.g. "created at", "created by", "display name") use one table and one URI only but use a content type column to distinguish between the media types.

Wolfram Rittmeyer lives in Germany and has been developing with Java for many years.

He has been interested in Android for quite a while and has been blogging about all kind of topics around Android.

You can find him on Google+ and Twitter.