How to Call the Camera in Android Apps Part 2: Capture and Store Photos

683

In the last Android tutorial, we started building a basic Camera activity on Android. This will allow us to control and operate the device camera directly, rather than just firing off an Intent to the default camera activity. The code in the last tutorial set up the camera and the camera preview. Now we’re going to capture and store a photo.

Setup

We already set up the manifest to allow the use of the camera. To store a photo, we also have to add permission to use the external storage, by editing AndroidManifest.xml:

android camera saved image

<manifest .... >
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
/>

Next, we need to add a ‘take photo’ button to the camera preview. Edit main.xml to include a button:

<LinearLayout ... >
  <FrameLayout ... />
  <Button
      android:id="@+id/button_photo"
      android:text="Take Photo"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      />
</LinearLayout>

(It’s preferable to use strings.xml to store the button text rather than hard-coding it as it is here.)

Capturing and storing a photo

If you run the code now, you should see a Take Photo button; but as yet it doesn’t do anything. We need to write the code that handles it. Add this to setUpLayout() in MyCameraActivity:

Button captureBtn = (Button) findViewById(R.id.button_photo); 
captureBtn.setOnClickListener(
  new View.OnClickListener() {
    public void onClick(View v) {
      takePhoto();
    }
  }
);

takePhoto() does the work of grabbing an image from the camera preview:

protected static final int MEDIA_TYPE_IMAGE = 0; 
private void takePhoto() {
  PictureCallback pictureCB = new PictureCallback() {
    public void onPictureTaken(byte[] data, Camera cam) {
      File picFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
      if (picFile == null) {
        Log.e(TAG, "Couldn't create media file; check storage permissions?");
        return;
      }
  
      try {
        FileOutputStream fos = new FileOutputStream(picFile);
        fos.write(data);
        fos.close();
      } catch (FileNotFoundException e) {
        Log.e(TAG, "File not found: " + e.getMessage());
        e.getStackTrace();
      } catch (IOException e) {
        Log.e(TAG, "I/O error writing file: " + e.getMessage());
        e.getStackTrace();
      }
    }
  };
  camera.takePicture(null, null, pictureCB);
}

Let’s look at that in more detail. Android provides the PictureCallback interface to get a hold of the image data from a camera. As you can see, most of the code in takePhoto() sets up the PictureCallback. The final line is the one that hooks this new callback into the camera by passing it into takePicture() from the Camera API. So when the user hits the Capture button, the method uses takePicture() to tell the camera to take a photo, then passes the data into the PictureCallback.

In the PictureCallback, we get a file to write to, write to it, and deal with the various possible errors. (If you were writing a different sort of app, you might do something entirely different with this photo data, such as displaying it or storing it in a database.) To get a specific output file, we need a helper method:

private File getOutputMediaFile(int type) {
  File dir = new File(Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_PICTURES), getPackageName());
  if (!dir.exists()) {
    if (!dir.mkdirs()) {
      Log.e(TAG, "Failed to create storage directory.");
      return null;
    }
  }
  String timeStamp = 
      new SimpleDateFormat("yyyMMdd_HHmmss", Locale.UK).format(new Date());
  if (type == MEDIA_TYPE_IMAGE) {
    return new File(directory.getPath() + File.separator + "IMG_"  
                    + timeStamp + ".jpg");
  } else {
    return null;
  }
}

There are two options for getting a storage directory on the SD card:

  1. Environment.getExternalStoragePublicDirectory(): this uses a public directory. If the app is uninstalled, the photos will remain.
  2. Context.getExternalFilesDir(): this uses a directory which is private to the app. If the app is uninstalled, the photos will also be deleted.

When deciding which of these to use, you should think carefully about the behavior your users will expect if, or when, they delete your app. In most cases, users might be quite disappointed to find photos unexpectedly deleted, but this will depend on the details of your app.

If using the public directory, as here, make sure you create a subdirectory for storage (as here, one possible directory name is whatever is returned by getPackageName()). Otherwise your user winds up with a lot of photos polluting their root external storage. Once the directory is chosen — and if necessary, created — we also create a filename; here using date/time, as is fairly standard. Here we’re only dealing with JPG media files. If you wish to handle video as well, you can add code to deal with that and save it appropriately. However, note that video is a lot more complicated to manage.

Background code

Run the code and try it out. You can now save a file (as you’ll see if you use a file manager app to navigate to the storage directory).

However, you’ll find that the preview freezes as you take the photo. You can restart it easily, by adding this line to takePhoto():

camera.takePicture(null, null, pictureCB);
camera.startPreview();

Even with this line, though, the preview freezes while the file is being stored. This is because as our code stands, everything is being done on the same thread. We looked at threading in the Canvas tutorial, but instead of writing your own thread, AsyncTask provides a quick way of handing something out to a background helper thread with minimal code. (Remember, the Android code isn’t threadsafe: only use a background thread for something that doesn’t involve the UI. Here, we’re only saving the photo in the background.) We’ll look at AsyncTask in detail in another tutorial, but here’s the code to fix the problem:

private void takePhoto() {
  PictureCallback pictureCB = new PictureCallback() {
  	public void onPictureTaken(byte[] data, Camera cam) {
   	  new SavePhotoTask().execute(data);
      cam.startPreview();
	}
  };
  camera.takePicture(null, null, pictureCB);
}
class SavePhotoTask extends AsyncTask<byte[], String, String> {
  @Override
  protected String doInBackground(byte[]... data) {
    File picFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
    if (picFile == null) {
	  Log.e(TAG, "Error creating media file; are storage permissions correct?");
	  return null;
    }    
  android camera capture  byte[0] photoData = data[0];
    try {
        FileOutputStream fos = new FileOutputStream(picFile);
        fos.write(photoData);
        fos.close();
      } catch (FileNotFoundException e) {
        Log.e(TAG, "File not found: " + e.getMessage());
        e.getStackTrace();
      } catch (IOException e) {
        Log.e(TAG, "I/O error with file: " + e.getMessage());
        e.getStackTrace();
      }
    return null;
  }
}

Note that you need to get the single array photoData out of the multi-dimensional array data before you can write it to the file. Otherwise, it’s the same code just moved into a separate class and thread. Run it, then check out your saved photo with your file manager.  

There are a lot of improvements you can make to this app; cameras have lots of different capabilities, for example. In further tutorials we’ll look at taking video and at adding options for flash and other camera capabilities.