Sunday, February 26, 2012

Loading Bitmaps from the Gallery

A common intent to use is requesting a photo from the Gallery. The request itself is pretty easy. Here's the code.
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
activity.startActivityForResult(intent, requestCode);
Getting the data back in Android 4.0 is almost just as simple, thanks to ContentProviders.
final InputStream is = context.getContentResolver().openInputStream(intent.getData());
final Bitmap imageData = BitmapFactory.decodeStream(is, null, options);
is.close();
This is kinda amazing considering that the Gallery does all sorts of complicated stuff to provide a good user experience as well as handling the management of the content itself. Remember, the gallery doesn't just handle Camera pictures, it pictures from other folders of the phone, albums from Picasa/Google+ and other pluggable data sources.

Unfortunately, if you support earlier versions you'll need to do more work because these abstractions weren't covered up as well back then. You'll need code for different Uri schemes that are returned from intent.getData().

  1. http:// or https:// - Download the file from the internet and read the file locally.
  2. file:// - Load the file via FileInputStream rather than the suggested getContentResolver.openInputStream.
  3. Complete garbage - Nothing you can do. The file failed to load, this can happen when the phone is low on memory, acting weird, data corruption, picture is too ugly, etc.
For a sample app doing the minimum is fine. For apps posted on the market there's a lot more things to handle before you can safely load the picture into memory...

Considerations for Robust Bitmap Loading:
  1. Code very defensively. You're reading user data which can be error prone. Catch exceptions when appropriate. Handle bad read permissions, handle network errors, and OutOfMemoryErrors. Lots of things can go wrong.
  2. Never load on the UI thread. It can take over 10 seconds to get the data if it isn't stored on the phone yet. Use AsyncTask. Tell the user that stuff is loading.
  3. Pictures can be large. Always check the size metrics before loading a graphic into memory. Use BitmapFactory.Options.inJustDecodeBounds. If the image file's metrics are too large then downsample the image using BitmapFactory.Options.inSampleSize.
  4. Bitmaps that have non-powers of 2 (NPOT) sizes are a bit troublesome for hardware accelerated views (relevant to Android 3.0 and higher). NPOT images can take up a lot more memory and the errors are subtle. Check Logcat if images aren't appearing. Try downsampling, resizing, or cropping the bitmap.
  5. In Android 3.0, images are loaded into Java-native byte arrays which means they count towards your VM budget. Android 2.3 and lower used native code memory which isn't tracked. This masked some memory load problems. Use Eclipse Memory Analyzer (MAT) to detect large bitmaps.
The sample code in this article is not enough for a robust implementation of loading bitmaps. It doesn't take into account all the considerations listed above. There's some code in the android-beryl project that does this for you. You'll want to checkout BitmapWrapper.create().

If you want a start-to-finish for handling the intent result of the gallery then look into, Gallery.java.

Beryl Gallery Intent Handler Code
// Sample code to call Beryl's async intent launch system to load a bitmap from the gallery.
// It's kinda complicated but it does a lot of stuff in the background for you.
// Just drop this code in whatever Fragment or Activity you want to get picture data from
// and implement acquirePicture() callback method and everything should work.

private static final int ACTIVITYRESULT_CHOOSEPICTURE = 100;

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);

  IActivityResultHandler handler = null;
  
  // If receiving result from Gallery.
  if(requestCode == ACTIVITYRESULT_CHOOSEPICTURE) {
    handler = new Gallery.GetImageResult() {
    
      // These callbacks are run on the UI thread.
      // They handle the different results of loading the bitmap.
      public void onResultCompleted() {
        if(this.bitmapResult.isAvailable()) {
          acquirePicture(this.bitmapResult.get());
        }
        
        this.bitmapResult = null;
      }

      public void onResultCanceled() {
        this.bitmapResult = null;
      }

      public void onResultCustomCode(int resultCode) {
        this.bitmapResult = null;
      }
    };
  }
  
  if(handler != null) {
    final ActivityResultTask resultTask = 
       new ActivityResultTask(intentLauncher, handler, resultCode, data);
    resultTask.execute();
  }
}

// Request Gallery for a picture.
public void selectPictureFromGallery() {
    if(checkStorageState()) {
      intentLauncher.beginStartActivity(new Gallery.GetImage(), ACTIVITYRESULT_CHOOSEPICTURE);
    }
  }

// Proxy for launching the gallery.
// Needed since we can be launching from a Fragment or an Activity.
final ActivityIntentLauncher.IActivityLauncherProxy launcherProxy = 
  new ActivityIntentLauncher.IActivityLauncherProxy() {
  public Activity getContext() {
    return WorkerFragment.this.getActivity();
  }

  public void startActivityForResult(Intent intent, int requestCode) {
    WorkerFragment.this.startActivityForResult(intent, requestCode);
  }

  public void startActivity(Intent intent) {
    WorkerFragment.this.startActivity(intent);
  }

  public void onStartActivityFailed(Intent intent) {
    WorkerFragment.this.showToast(R.string.nolocal_generic_error);
  }

  public void onStartActivityForResultFailed(Intent intent, int requestCode) {
    WorkerFragment.this.showToast(R.string.nolocal_generic_error);
  }
};

2 comments:

  1. Could you please edit your example to include the definition for intentLauncher and checkStorageState()? I am having issues on the callback from the gallery and my guess is that it is around my instantiation of intentLauncher.

    ReplyDelete
  2. Figured out that it is not that there is nothing special about the intentLauncher just that its constructor does not reveal what it actually needs to be used in this context. At the beginning of onActivityResult method after the call to super I added: intentLauncher.setActivityLauncher(launcherProxy);

    This fixed my issue, but did not fix the issue where I have no idea beside returning a boolean value what checkStorageState() should be doing.

    ReplyDelete