Monday 15 July 2013

How to get Picasa images using the Image Picker on Android devices running any OS version

Using the Image Picker to get a local image from the Gallery is pretty easy and trivial task. But things get a lot more interesting if the user has a Picasa account and he/she happens to select an image from one of their Picasa albums. If you do not handle this scenario, your app will crash! And there is no way for you to tell the Image Picker to show just local files. So, you have to handle it, or you will be releasing a buggy application!
Things got even more interesting after the release of Honeycomb! All of a sudden the code that was fetching Picasa images and was working flawlessly started failing on devices running OS 3.0 and up. After some investigation I found the culprit- Google changed the URI returned when the user was selecting a Picasa image. This change was completely undocumented, or at least I could not find any documentation on this! So, on devices running Android OS prior to 3.0 the URI returned was an actual URL and now on devices running OS 3.0 and higher, the URI returned had a different format. For example:
1. https://lh4.googleusercontent.com/… (URI returned on devices running OS prior to 3.0)
2. content://com.google.android.gallery3d (URI returned on devices running OS 3.0 and higher)
I posted a brief solution on the official Android bug report site, but I will expand on it here.
In order to properly handle fetching an image from the Gallery you need to handle three scenarios:
1. The user selected a local image file
2. The user selected a Picasa image and the device is running Android version prior to 3.0
3. The user selected a Picasa image and the device is running Android version 3.0 and higher
Let’s delve straight into the code:
1. Start the Image Picker:

private static final int PICTURE_GALLERY = 1;
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(photoPickerIntent, PICTURE_GALLERY);
In the first line above I am passing a URI to the intent because I would like to fetch the full image, not just get a Bitmap with the thumbnail. The Android team did a good job of acknowledging that if you just wanted a thumbnail of the image then you can get back a Bitmap object containing it, since a thumbnail would not be that big in size. But if you wanted the full image, it would be foolish to do the same, since that would take up a big chunk of the heap! So, what you get instead is some data about the image file, among which is the path to it.
2. We need to implement the onActivityResult method that will be called when the user closes (picks the image) the Image Gallery application:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
 super.onActivityResult(requestCode, resultCode, intent);
 switch (requestCode) {
  case PICTURE_GALLERY:
  if (resultCode == RESULT_OK && intent != null) {
   Uri selectedImage = intent.getData();
   final String[] filePathColumn = { MediaColumns.DATA, MediaColumns.DISPLAY_NAME };
   Cursor cursor = getContentResolver().query(selectedImage, filePathColumn, null, null, null);
   // some devices (OS versions return an URI of com.android instead of com.google.android
   if (selectedImage.toString().startsWith("content://com.android.gallery3d.provider"))  {
    // use the com.google provider, not the com.android provider.
    selectedImage = Uri.parse(selectedImage.toString().replace("com.android.gallery3d","com.google.android.gallery3d"));
   }
   if (cursor != null) {
    cursor.moveToFirst();
    int columnIndex = cursor.getColumnIndex(MediaColumns.DATA);
    // if it is a picasa image on newer devices with OS 3.0 and up
    if (selectedImage.toString().startsWith("content://com.google.android.gallery3d")){
     columnIndex = cursor.getColumnIndex(MediaColumns.DISPLAY_NAME);
     if (columnIndex != -1) {
      progress_bar.setVisibility(View.VISIBLE);
      final Uri uriurl = selectedImage;
      // Do this in a background thread, since we are fetching a large image from the web
      new Thread(new Runnable() {
       public void run() {
        Bitmap the_image = getBitmap("image_file_name.jpg", uriurl);
       }
      }).start();
     }
    } else { // it is a regular local image file
     String filePath = cursor.getString(columnIndex);
     cursor.close();
     Bitmap the_image = decodeFile(new File(filePath));
    }
   }
   // If it is a picasa image on devices running OS prior to 3.0
   else if (selectedImage != null && selectedImage.toString().length() > 0) {
    progress_bar.setVisibility(View.VISIBLE);
    final Uri uriurl = selectedImage;
    // Do this in a background thread, since we are fetching a large image from the web
    new Thread(new Runnable() {
     public void run() {
      Bitmap the_image = getBitmap("image_file_name.jpg", uriurl);
     }
    }).start();
   }
  }
  break;
 }
}
3. Implement the getBitmap method, which will download the image from the web if the user selected a Picasa image:

private Bitmap getBitmap(String tag, Uri url)
{
 File cacheDir;
 // if the device has an SD card
 if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) {
  cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),".OCFL311");
 } else {
  // it does not have an SD card
     cacheDir=ActivityPicture.this.getCacheDir();
 }
 if(!cacheDir.exists())
      cacheDir.mkdirs();

 File f=new File(cacheDir, tag);

 try {
  Bitmap bitmap=null;
  InputStream is = null;
  if (url.toString().startsWith("content://com.google.android.gallery3d")) {
   is=getContentResolver().openInputStream(url);
  } else {
   is=new URL(url.toString()).openStream();
  }
  OutputStream os = new FileOutputStream(f);
  Utils.CopyStream(is, os);
  os.close();
  return decodeFile(f);
 } catch (Exception ex) {
  Log.d(Utils.DEBUG_TAG, "Exception: " + ex.getMessage());
  // something went wrong
  ex.printStackTrace();
  return null;
 }
}
The decodeFile method takes a reference to a file and returns a Bitmap. This is pretty trivial. In my case I also scale down the image as I am reading it from the file, so it does not take much memory.
I think Google has some work to do about all this and if they want to seamlessly integrate Picasa with the Android Gallery application, they should have done a better job with the Image Picker functionality. The developer should not be doing all this heavy lifting and be concerned with where the actual image resides. The returned URI should be exactly the same no matter if the image is local or not. And if it is not local, the Gallery app should be fetching it for us. That way, we will have a consistent and predictable functionality. Right now you will be surprised how many applications out there do not handle Picasa images and crash with an ugly error.

No comments:

Post a Comment

Disqus for Android