Fix for issue #141: EXIF data stripped from captured photos in android

In order to fix this issue I needed to read the EXIF data. Save it to a temporary object then after the bitmap is compressed I open the file and write the saved EXIF data.

Supports the following EXIF fields if they are set in your image:

APERTURE
DATETIME
EXPOSURE_TIME
FLASH
FOCAL_LENGTH
GPS_ALTITUDE
GPS_ALTITUDE_REF
GPS_DATESTAMP
GPS_LATITUDE
GPS_LATITUDE_REF
GPS_LONGITUDE
GPS_LONGITUDE_REF
GPS_PROCESSING_METHOD
GPS_TIMESTAMP
ISO
MAKE
MODEL
ORIENTATION
WHITE_BALANCE
This commit is contained in:
macdonst 2011-08-23 01:38:00 +08:00
parent 8a4737947b
commit a9c34e65fb
5 changed files with 1634 additions and 1458 deletions

View File

@ -35,100 +35,102 @@ import android.util.Log;
*/ */
public class CameraLauncher extends Plugin { public class CameraLauncher extends Plugin {
private static final int DATA_URL = 0; // Return base64 encoded string private static final int DATA_URL = 0; // Return base64 encoded string
private static final int FILE_URI = 1; // Return file uri (content://media/external/images/media/2 for Android) private static final int FILE_URI = 1; // Return file uri (content://media/external/images/media/2 for Android)
private static final int PHOTOLIBRARY = 0; // Choose image from picture library (same as SAVEDPHOTOALBUM for Android) private static final int PHOTOLIBRARY = 0; // Choose image from picture library (same as SAVEDPHOTOALBUM for Android)
private static final int CAMERA = 1; // Take picture from camera private static final int CAMERA = 1; // Take picture from camera
private static final int SAVEDPHOTOALBUM = 2; // Choose image from picture library (same as PHOTOLIBRARY for Android) private static final int SAVEDPHOTOALBUM = 2; // Choose image from picture library (same as PHOTOLIBRARY for Android)
private static final int JPEG = 0; // Take a picture of type JPEG private static final int JPEG = 0; // Take a picture of type JPEG
private static final int PNG = 1; // Take a picture of type PNG private static final int PNG = 1; // Take a picture of type PNG
private int mQuality; // Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality) private int mQuality; // Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality)
private int targetWidth; // desired width of the image private int targetWidth; // desired width of the image
private int targetHeight; // desired height of the image private int targetHeight; // desired height of the image
private Uri imageUri; // Uri of captured image private Uri imageUri;
public String callbackId; private int encodingType;
// Uri of captured image
public String callbackId;
/** /**
* Constructor. * Constructor.
*/ */
public CameraLauncher() { public CameraLauncher() {
} }
/** /**
* Executes the request and returns PluginResult. * Executes the request and returns PluginResult.
* *
* @param action The action to execute. * @param action The action to execute.
* @param args JSONArry of arguments for the plugin. * @param args JSONArry of arguments for the plugin.
* @param callbackId The callback id used when calling back into JavaScript. * @param callbackId The callback id used when calling back into JavaScript.
* @return A PluginResult object with a status and message. * @return A PluginResult object with a status and message.
*/ */
public PluginResult execute(String action, JSONArray args, String callbackId) { public PluginResult execute(String action, JSONArray args, String callbackId) {
PluginResult.Status status = PluginResult.Status.OK; PluginResult.Status status = PluginResult.Status.OK;
String result = ""; String result = "";
this.callbackId = callbackId; this.callbackId = callbackId;
try { try {
if (action.equals("takePicture")) { if (action.equals("takePicture")) {
int destType = DATA_URL; int destType = DATA_URL;
if (args.length() > 1) { if (args.length() > 1) {
destType = args.getInt(1); destType = args.getInt(1);
} }
int srcType = CAMERA; int srcType = CAMERA;
if (args.length() > 2) { if (args.length() > 2) {
srcType = args.getInt(2); srcType = args.getInt(2);
} }
if (args.length() > 3) { if (args.length() > 3) {
this.targetWidth = args.getInt(3); this.targetWidth = args.getInt(3);
} }
if (args.length() > 4) { if (args.length() > 4) {
this.targetHeight = args.getInt(4); this.targetHeight = args.getInt(4);
} }
int encodingType = JPEG; this.encodingType = JPEG;
if (args.length() > 5) { if (args.length() > 5) {
encodingType = args.getInt(5); this.encodingType = args.getInt(5);
} }
if (srcType == CAMERA) { if (srcType == CAMERA) {
this.takePicture(args.getInt(0), destType, encodingType); this.takePicture(args.getInt(0), destType, encodingType);
} }
else if ((srcType == PHOTOLIBRARY) || (srcType == SAVEDPHOTOALBUM)) { else if ((srcType == PHOTOLIBRARY) || (srcType == SAVEDPHOTOALBUM)) {
this.getImage(args.getInt(0), srcType, destType); this.getImage(args.getInt(0), srcType, destType);
} }
PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT); PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT);
r.setKeepCallback(true); r.setKeepCallback(true);
return r; return r;
} }
return new PluginResult(status, result); return new PluginResult(status, result);
} catch (JSONException e) { } catch (JSONException e) {
e.printStackTrace(); e.printStackTrace();
return new PluginResult(PluginResult.Status.JSON_EXCEPTION); return new PluginResult(PluginResult.Status.JSON_EXCEPTION);
} }
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// LOCAL METHODS // LOCAL METHODS
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
/** /**
* Take a picture with the camera. * Take a picture with the camera.
* When an image is captured or the camera view is cancelled, the result is returned * When an image is captured or the camera view is cancelled, the result is returned
* in PhonegapActivity.onActivityResult, which forwards the result to this.onActivityResult. * in PhonegapActivity.onActivityResult, which forwards the result to this.onActivityResult.
* *
* The image can either be returned as a base64 string or a URI that points to the file. * The image can either be returned as a base64 string or a URI that points to the file.
* To display base64 string in an img tag, set the source to: * To display base64 string in an img tag, set the source to:
* img.src="data:image/jpeg;base64,"+result; * img.src="data:image/jpeg;base64,"+result;
* or to display URI in an img tag * or to display URI in an img tag
* img.src=result; * img.src=result;
* *
* @param quality Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality) * @param quality Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality)
* @param returnType Set the type of image to return. * @param returnType Set the type of image to return.
*/ */
public void takePicture(int quality, int returnType, int encodingType) { public void takePicture(int quality, int returnType, int encodingType) {
this.mQuality = quality; this.mQuality = quality;
// Display camera // Display camera
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE"); Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
// Specify file so that large image is captured and returned // Specify file so that large image is captured and returned
@ -138,14 +140,14 @@ public class CameraLauncher extends Plugin {
this.imageUri = Uri.fromFile(photo); this.imageUri = Uri.fromFile(photo);
this.ctx.startActivityForResult((Plugin) this, intent, (CAMERA+1)*16 + returnType+1); this.ctx.startActivityForResult((Plugin) this, intent, (CAMERA+1)*16 + returnType+1);
} }
/** /**
* Create a file in the applications temporary directory based upon the supplied encoding. * Create a file in the applications temporary directory based upon the supplied encoding.
* *
* @param encodingType of the image to be taken * @param encodingType of the image to be taken
* @return a File object pointing to the temporary picture * @return a File object pointing to the temporary picture
*/ */
private File createCaptureFile(int encodingType) { private File createCaptureFile(int encodingType) {
File photo = null; File photo = null;
if (encodingType == JPEG) { if (encodingType == JPEG) {
@ -157,46 +159,46 @@ public class CameraLauncher extends Plugin {
} }
/** /**
* Get image from photo library. * Get image from photo library.
* *
* @param quality Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality) * @param quality Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality)
* @param srcType The album to get image from. * @param srcType The album to get image from.
* @param returnType Set the type of image to return. * @param returnType Set the type of image to return.
*/ */
// TODO: Images selected from SDCARD don't display correctly, but from CAMERA ALBUM do! // TODO: Images selected from SDCARD don't display correctly, but from CAMERA ALBUM do!
public void getImage(int quality, int srcType, int returnType) { public void getImage(int quality, int srcType, int returnType) {
this.mQuality = quality; this.mQuality = quality;
Intent intent = new Intent(); Intent intent = new Intent();
intent.setType("image/*"); intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT); intent.setAction(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE); intent.addCategory(Intent.CATEGORY_OPENABLE);
this.ctx.startActivityForResult((Plugin) this, Intent.createChooser(intent, this.ctx.startActivityForResult((Plugin) this, Intent.createChooser(intent,
new String("Get Picture")), (srcType+1)*16 + returnType + 1); new String("Get Picture")), (srcType+1)*16 + returnType + 1);
} }
/** /**
* Scales the bitmap according to the requested size. * Scales the bitmap according to the requested size.
* *
* @param bitmap The bitmap to scale. * @param bitmap The bitmap to scale.
* @return Bitmap A new Bitmap object of the same bitmap after scaling. * @return Bitmap A new Bitmap object of the same bitmap after scaling.
*/ */
public Bitmap scaleBitmap(Bitmap bitmap) { public Bitmap scaleBitmap(Bitmap bitmap) {
int newWidth = this.targetWidth; int newWidth = this.targetWidth;
int newHeight = this.targetHeight; int newHeight = this.targetHeight;
int origWidth = bitmap.getWidth(); int origWidth = bitmap.getWidth();
int origHeight = bitmap.getHeight(); int origHeight = bitmap.getHeight();
// If no new width or height were specified return the original bitmap // If no new width or height were specified return the original bitmap
if (newWidth <= 0 && newHeight <= 0) { if (newWidth <= 0 && newHeight <= 0) {
return bitmap; return bitmap;
} }
// Only the width was specified // Only the width was specified
else if (newWidth > 0 && newHeight <= 0) { else if (newWidth > 0 && newHeight <= 0) {
newHeight = (newWidth * origHeight) / origWidth; newHeight = (newWidth * origHeight) / origWidth;
} }
// only the height was specified // only the height was specified
else if (newWidth <= 0 && newHeight > 0) { else if (newWidth <= 0 && newHeight > 0) {
newWidth = (newHeight * origWidth) / origHeight; newWidth = (newHeight * origWidth) / origHeight;
} }
// If the user specified both a positive width and height // If the user specified both a positive width and height
@ -205,7 +207,7 @@ public class CameraLauncher extends Plugin {
// Alternatively, the specified width and height could have been // Alternatively, the specified width and height could have been
// kept and Bitmap.SCALE_TO_FIT specified when scaling, but this // kept and Bitmap.SCALE_TO_FIT specified when scaling, but this
// would result in whitespace in the new image. // would result in whitespace in the new image.
else { else {
double newRatio = newWidth / (double)newHeight; double newRatio = newWidth / (double)newHeight;
double origRatio = origWidth / (double)origHeight; double origRatio = origWidth / (double)origHeight;
@ -217,156 +219,169 @@ public class CameraLauncher extends Plugin {
} }
return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true); return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
} }
/** /**
* Called when the camera view exits. * Called when the camera view exits.
* *
* @param requestCode The request code originally supplied to startActivityForResult(), * @param requestCode The request code originally supplied to startActivityForResult(),
* allowing you to identify who this result came from. * allowing you to identify who this result came from.
* @param resultCode The integer result code returned by the child activity through its setResult(). * @param resultCode The integer result code returned by the child activity through its setResult().
* @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
*/ */
public void onActivityResult(int requestCode, int resultCode, Intent intent) { public void onActivityResult(int requestCode, int resultCode, Intent intent) {
// Get src and dest types from request code // Get src and dest types from request code
int srcType = (requestCode/16) - 1; int srcType = (requestCode/16) - 1;
int destType = (requestCode % 16) - 1; int destType = (requestCode % 16) - 1;
// If CAMERA // If CAMERA
if (srcType == CAMERA) { if (srcType == CAMERA) {
// If image available // If image available
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
try { try {
// Read in bitmap of captured image // Create an ExifHelper to save the exif data that is lost during compression
Bitmap bitmap; ExifHelper exif = new ExifHelper();
try { if (this.encodingType == JPEG) {
bitmap = android.provider.MediaStore.Images.Media.getBitmap(this.ctx.getContentResolver(), imageUri); exif.createInFile(DirectoryManager.getTempDirectoryPath(ctx) + "/Pic.jpg");
} catch (FileNotFoundException e) { exif.readExifData();
Uri uri = intent.getData(); }
android.content.ContentResolver resolver = this.ctx.getContentResolver();
bitmap = android.graphics.BitmapFactory.decodeStream(resolver.openInputStream(uri));
}
bitmap = scaleBitmap(bitmap); // Read in bitmap of captured image
Bitmap bitmap;
try {
bitmap = android.provider.MediaStore.Images.Media.getBitmap(this.ctx.getContentResolver(), imageUri);
} catch (FileNotFoundException e) {
Uri uri = intent.getData();
android.content.ContentResolver resolver = this.ctx.getContentResolver();
bitmap = android.graphics.BitmapFactory.decodeStream(resolver.openInputStream(uri));
}
// If sending base64 image back bitmap = scaleBitmap(bitmap);
if (destType == DATA_URL) {
this.processPicture(bitmap);
}
// If sending filename back // If sending base64 image back
else if (destType == FILE_URI){ if (destType == DATA_URL) {
// Create entry in media store for image this.processPicture(bitmap);
// (Don't use insertImage() because it uses default compression setting of 50 - no way to change it) }
ContentValues values = new ContentValues();
values.put(android.provider.MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
Uri uri = null;
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException e) {
System.out.println("Can't write to external media storage.");
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException ex) {
System.out.println("Can't write to internal media storage.");
this.failPicture("Error capturing image - no media storage found.");
return;
}
}
// Add compressed version of captured image to returned media store Uri // If sending filename back
OutputStream os = this.ctx.getContentResolver().openOutputStream(uri); else if (destType == FILE_URI){
bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, os); // Create entry in media store for image
os.close(); // (Don't use insertImage() because it uses default compression setting of 50 - no way to change it)
ContentValues values = new ContentValues();
values.put(android.provider.MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
Uri uri = null;
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException e) {
System.out.println("Can't write to external media storage.");
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException ex) {
System.out.println("Can't write to internal media storage.");
this.failPicture("Error capturing image - no media storage found.");
return;
}
}
// Send Uri back to JavaScript for viewing image // Add compressed version of captured image to returned media store Uri
this.success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId); OutputStream os = this.ctx.getContentResolver().openOutputStream(uri);
} bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, os);
bitmap.recycle(); os.close();
bitmap = null;
System.gc();
} catch (IOException e) {
e.printStackTrace();
this.failPicture("Error capturing image.");
}
}
// If cancelled // Restore exif data to file
else if (resultCode == Activity.RESULT_CANCELED) { if (this.encodingType == JPEG) {
this.failPicture("Camera cancelled."); exif.createOutFile(FileUtils.getRealPathFromURI(uri, this.ctx));
} exif.writeExifData();
}
// If something else // Send Uri back to JavaScript for viewing image
else { this.success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId);
this.failPicture("Did not complete!"); }
} bitmap.recycle();
} bitmap = null;
System.gc();
} catch (IOException e) {
e.printStackTrace();
this.failPicture("Error capturing image.");
}
}
// If retrieving photo from library // If cancelled
else if ((srcType == PHOTOLIBRARY) || (srcType == SAVEDPHOTOALBUM)) { else if (resultCode == Activity.RESULT_CANCELED) {
if (resultCode == Activity.RESULT_OK) { this.failPicture("Camera cancelled.");
Uri uri = intent.getData(); }
android.content.ContentResolver resolver = this.ctx.getContentResolver();
// If sending base64 image back
if (destType == DATA_URL) {
try {
Bitmap bitmap = android.graphics.BitmapFactory.decodeStream(resolver.openInputStream(uri));
bitmap = scaleBitmap(bitmap);
this.processPicture(bitmap);
bitmap.recycle();
bitmap = null;
System.gc();
} catch (FileNotFoundException e) {
e.printStackTrace();
this.failPicture("Error retrieving image.");
}
}
// If sending filename back // If something else
else if (destType == FILE_URI) { else {
this.success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId); this.failPicture("Did not complete!");
} }
} }
else if (resultCode == Activity.RESULT_CANCELED) {
this.failPicture("Selection cancelled.");
}
else {
this.failPicture("Selection did not complete!");
}
}
}
/** // If retrieving photo from library
* Compress bitmap using jpeg, convert to Base64 encoded string, and return to JavaScript. else if ((srcType == PHOTOLIBRARY) || (srcType == SAVEDPHOTOALBUM)) {
* if (resultCode == Activity.RESULT_OK) {
* @param bitmap Uri uri = intent.getData();
*/ android.content.ContentResolver resolver = this.ctx.getContentResolver();
public void processPicture(Bitmap bitmap) { // If sending base64 image back
ByteArrayOutputStream jpeg_data = new ByteArrayOutputStream(); if (destType == DATA_URL) {
try { try {
if (bitmap.compress(CompressFormat.JPEG, mQuality, jpeg_data)) { Bitmap bitmap = android.graphics.BitmapFactory.decodeStream(resolver.openInputStream(uri));
byte[] code = jpeg_data.toByteArray(); bitmap = scaleBitmap(bitmap);
byte[] output = Base64.encodeBase64(code); this.processPicture(bitmap);
String js_out = new String(output); bitmap.recycle();
this.success(new PluginResult(PluginResult.Status.OK, js_out), this.callbackId); bitmap = null;
js_out = null; System.gc();
output = null; } catch (FileNotFoundException e) {
code = null; e.printStackTrace();
} this.failPicture("Error retrieving image.");
} }
catch(Exception e) { }
this.failPicture("Error compressing image.");
}
jpeg_data = null;
}
/** // If sending filename back
* Send error message to JavaScript. else if (destType == FILE_URI) {
* this.success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId);
* @param err }
*/ }
public void failPicture(String err) { else if (resultCode == Activity.RESULT_CANCELED) {
this.error(new PluginResult(PluginResult.Status.ERROR, err), this.callbackId); this.failPicture("Selection cancelled.");
} }
else {
this.failPicture("Selection did not complete!");
}
}
}
/**
* Compress bitmap using jpeg, convert to Base64 encoded string, and return to JavaScript.
*
* @param bitmap
*/
public void processPicture(Bitmap bitmap) {
ByteArrayOutputStream jpeg_data = new ByteArrayOutputStream();
try {
if (bitmap.compress(CompressFormat.JPEG, mQuality, jpeg_data)) {
byte[] code = jpeg_data.toByteArray();
byte[] output = Base64.encodeBase64(code);
String js_out = new String(output);
this.success(new PluginResult(PluginResult.Status.OK, js_out), this.callbackId);
js_out = null;
output = null;
code = null;
}
}
catch(Exception e) {
this.failPicture("Error compressing image.");
}
jpeg_data = null;
}
/**
* Send error message to JavaScript.
*
* @param err
*/
public void failPicture(String err) {
this.error(new PluginResult(PluginResult.Status.ERROR, err), this.callbackId);
}
} }

View File

@ -18,12 +18,10 @@ import org.json.JSONObject;
import android.app.Activity; import android.app.Activity;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.media.MediaPlayer; import android.media.MediaPlayer;
import android.net.Uri; import android.net.Uri;
import android.os.Environment;
import android.util.Log; import android.util.Log;
import com.phonegap.api.Plugin; import com.phonegap.api.Plugin;
@ -31,146 +29,145 @@ import com.phonegap.api.PluginResult;
public class Capture extends Plugin { public class Capture extends Plugin {
private static final String _DATA = "_data"; // The column name where the file path is stored private static final int CAPTURE_AUDIO = 0; // Constant for capture audio
private static final int CAPTURE_AUDIO = 0; // Constant for capture audio private static final int CAPTURE_IMAGE = 1; // Constant for capture image
private static final int CAPTURE_IMAGE = 1; // Constant for capture image private static final int CAPTURE_VIDEO = 2; // Constant for capture video
private static final int CAPTURE_VIDEO = 2; // Constant for capture video private static final String LOG_TAG = "Capture";
private static final String LOG_TAG = "Capture"; private String callbackId; // The ID of the callback to be invoked with our result
private String callbackId; // The ID of the callback to be invoked with our result private long limit; // the number of pics/vids/clips to take
private long limit; // the number of pics/vids/clips to take private double duration; // optional duration parameter for video recording
private double duration; // optional duration parameter for video recording private JSONArray results; // The array of results to be returned to the user
private JSONArray results; // The array of results to be returned to the user private Uri imageUri; // Uri of captured image
private Uri imageUri; // Uri of captured image
@Override @Override
public PluginResult execute(String action, JSONArray args, String callbackId) { public PluginResult execute(String action, JSONArray args, String callbackId) {
this.callbackId = callbackId; this.callbackId = callbackId;
this.limit = 1; this.limit = 1;
this.duration = 0.0f; this.duration = 0.0f;
this.results = new JSONArray(); this.results = new JSONArray();
JSONObject options = args.optJSONObject(0); JSONObject options = args.optJSONObject(0);
if (options != null) { if (options != null) {
limit = options.optLong("limit", 1); limit = options.optLong("limit", 1);
duration = options.optDouble("duration", 0.0f); duration = options.optDouble("duration", 0.0f);
} }
if (action.equals("getFormatData")) { if (action.equals("getFormatData")) {
try { try {
JSONObject obj = getFormatData(args.getString(0), args.getString(1)); JSONObject obj = getFormatData(args.getString(0), args.getString(1));
return new PluginResult(PluginResult.Status.OK, obj); return new PluginResult(PluginResult.Status.OK, obj);
} catch (JSONException e) { } catch (JSONException e) {
return new PluginResult(PluginResult.Status.ERROR); return new PluginResult(PluginResult.Status.ERROR);
} }
} }
else if (action.equals("captureAudio")) { else if (action.equals("captureAudio")) {
this.captureAudio(); this.captureAudio();
} }
else if (action.equals("captureImage")) { else if (action.equals("captureImage")) {
this.captureImage(); this.captureImage();
} }
else if (action.equals("captureVideo")) { else if (action.equals("captureVideo")) {
this.captureVideo(duration); this.captureVideo(duration);
} }
PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT); PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT);
r.setKeepCallback(true); r.setKeepCallback(true);
return r; return r;
} }
/** /**
* Provides the media data file data depending on it's mime type * Provides the media data file data depending on it's mime type
* *
* @param filePath path to the file * @param filePath path to the file
* @param mimeType of the file * @param mimeType of the file
* @return a MediaFileData object * @return a MediaFileData object
*/ */
private JSONObject getFormatData(String filePath, String mimeType) { private JSONObject getFormatData(String filePath, String mimeType) {
JSONObject obj = new JSONObject(); JSONObject obj = new JSONObject();
try { try {
// setup defaults // setup defaults
obj.put("height", 0); obj.put("height", 0);
obj.put("width", 0); obj.put("width", 0);
obj.put("bitrate", 0); obj.put("bitrate", 0);
obj.put("duration", 0); obj.put("duration", 0);
obj.put("codecs", ""); obj.put("codecs", "");
// If the mimeType isn't set the rest will fail // If the mimeType isn't set the rest will fail
// so let's see if we can determine it. // so let's see if we can determine it.
if (mimeType == null || mimeType.equals("")) { if (mimeType == null || mimeType.equals("")) {
mimeType = FileUtils.getMimeType(filePath); mimeType = FileUtils.getMimeType(filePath);
} }
if (mimeType.equals("image/jpeg") || filePath.endsWith(".jpg")) { if (mimeType.equals("image/jpeg") || filePath.endsWith(".jpg")) {
obj = getImageData(filePath, obj); obj = getImageData(filePath, obj);
} }
else if (filePath.endsWith("audio/3gpp")) { else if (filePath.endsWith("audio/3gpp")) {
obj = getAudioVideoData(filePath, obj, false); obj = getAudioVideoData(filePath, obj, false);
} }
else if (mimeType.equals("video/3gpp")) { else if (mimeType.equals("video/3gpp")) {
obj = getAudioVideoData(filePath, obj, true); obj = getAudioVideoData(filePath, obj, true);
} }
} }
catch (JSONException e) { catch (JSONException e) {
Log.d(LOG_TAG, "Error: setting media file data object"); Log.d(LOG_TAG, "Error: setting media file data object");
} }
return obj; return obj;
} }
/** /**
* Get the Image specific attributes * Get the Image specific attributes
* *
* @param filePath path to the file * @param filePath path to the file
* @param obj represents the Media File Data * @param obj represents the Media File Data
* @return a JSONObject that represents the Media File Data * @return a JSONObject that represents the Media File Data
* @throws JSONException * @throws JSONException
*/ */
private JSONObject getImageData(String filePath, JSONObject obj) throws JSONException { private JSONObject getImageData(String filePath, JSONObject obj) throws JSONException {
Bitmap bitmap = BitmapFactory.decodeFile(filePath); Bitmap bitmap = BitmapFactory.decodeFile(filePath);
obj.put("height", bitmap.getHeight()); obj.put("height", bitmap.getHeight());
obj.put("width", bitmap.getWidth()); obj.put("width", bitmap.getWidth());
return obj; return obj;
} }
/** /**
* Get the Image specific attributes * Get the Image specific attributes
* *
* @param filePath path to the file * @param filePath path to the file
* @param obj represents the Media File Data * @param obj represents the Media File Data
* @param video if true get video attributes as well * @param video if true get video attributes as well
* @return a JSONObject that represents the Media File Data * @return a JSONObject that represents the Media File Data
* @throws JSONException * @throws JSONException
*/ */
private JSONObject getAudioVideoData(String filePath, JSONObject obj, boolean video) throws JSONException { private JSONObject getAudioVideoData(String filePath, JSONObject obj, boolean video) throws JSONException {
MediaPlayer player = new MediaPlayer(); MediaPlayer player = new MediaPlayer();
try { try {
player.setDataSource(filePath); player.setDataSource(filePath);
player.prepare(); player.prepare();
obj.put("duration", player.getDuration()); obj.put("duration", player.getDuration());
if (video) { if (video) {
obj.put("height", player.getVideoHeight()); obj.put("height", player.getVideoHeight());
obj.put("width", player.getVideoWidth()); obj.put("width", player.getVideoWidth());
} }
} }
catch (IOException e) { catch (IOException e) {
Log.d(LOG_TAG, "Error: loading video file"); Log.d(LOG_TAG, "Error: loading video file");
} }
return obj; return obj;
} }
/** /**
* Sets up an intent to capture audio. Result handled by onActivityResult() * Sets up an intent to capture audio. Result handled by onActivityResult()
*/ */
private void captureAudio() { private void captureAudio() {
Intent intent = new Intent(android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION); Intent intent = new Intent(android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION);
this.ctx.startActivityForResult((Plugin) this, intent, CAPTURE_AUDIO); this.ctx.startActivityForResult((Plugin) this, intent, CAPTURE_AUDIO);
} }
/** /**
* Sets up an intent to capture images. Result handled by onActivityResult() * Sets up an intent to capture images. Result handled by onActivityResult()
*/ */
private void captureImage() { private void captureImage() {
Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
// Specify file so that large image is captured and returned // Specify file so that large image is captured and returned
@ -181,10 +178,10 @@ public class Capture extends Plugin {
this.ctx.startActivityForResult((Plugin) this, intent, CAPTURE_IMAGE); this.ctx.startActivityForResult((Plugin) this, intent, CAPTURE_IMAGE);
} }
/** /**
* Sets up an intent to capture video. Result handled by onActivityResult() * Sets up an intent to capture video. Result handled by onActivityResult()
*/ */
private void captureVideo(double duration) { private void captureVideo(double duration) {
Intent intent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE); Intent intent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE);
// Introduced in API 8 // Introduced in API 8
//intent.putExtra(android.provider.MediaStore.EXTRA_DURATION_LIMIT, duration); //intent.putExtra(android.provider.MediaStore.EXTRA_DURATION_LIMIT, duration);
@ -195,165 +192,160 @@ public class Capture extends Plugin {
/** /**
* Called when the video view exits. * Called when the video view exits.
* *
* @param requestCode The request code originally supplied to startActivityForResult(), * @param requestCode The request code originally supplied to startActivityForResult(),
* allowing you to identify who this result came from. * allowing you to identify who this result came from.
* @param resultCode The integer result code returned by the child activity through its setResult(). * @param resultCode The integer result code returned by the child activity through its setResult().
* @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
* @throws JSONException * @throws JSONException
*/ */
public void onActivityResult(int requestCode, int resultCode, Intent intent) { public void onActivityResult(int requestCode, int resultCode, Intent intent) {
// Result received okay // Result received okay
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
// An audio clip was requested // An audio clip was requested
if (requestCode == CAPTURE_AUDIO) { if (requestCode == CAPTURE_AUDIO) {
// Get the uri of the audio clip // Get the uri of the audio clip
Uri data = intent.getData(); Uri data = intent.getData();
// create a file object from the uri // create a file object from the uri
results.put(createMediaFile(data)); results.put(createMediaFile(data));
if (results.length() >= limit) { if (results.length() >= limit) {
// Send Uri back to JavaScript for listening to audio // Send Uri back to JavaScript for listening to audio
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId); this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
} else { } else {
// still need to capture more audio clips // still need to capture more audio clips
captureAudio(); captureAudio();
} }
} else if (requestCode == CAPTURE_IMAGE) { } else if (requestCode == CAPTURE_IMAGE) {
// For some reason if I try to do: // For some reason if I try to do:
// Uri data = intent.getData(); // Uri data = intent.getData();
// It crashes in the emulator and on my phone with a null pointer exception // It crashes in the emulator and on my phone with a null pointer exception
// To work around it I had to grab the code from CameraLauncher.java // To work around it I had to grab the code from CameraLauncher.java
try { try {
// Read in bitmap of captured image // Create an ExifHelper to save the exif data that is lost during compression
Bitmap bitmap = android.provider.MediaStore.Images.Media.getBitmap(this.ctx.getContentResolver(), imageUri); ExifHelper exif = new ExifHelper();
exif.createInFile(DirectoryManager.getTempDirectoryPath(ctx) + "/Capture.jpg");
exif.readExifData();
// Create entry in media store for image // Read in bitmap of captured image
// (Don't use insertImage() because it uses default compression setting of 50 - no way to change it) Bitmap bitmap = android.provider.MediaStore.Images.Media.getBitmap(this.ctx.getContentResolver(), imageUri);
ContentValues values = new ContentValues();
values.put(android.provider.MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
Uri uri = null;
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException e) {
System.out.println("Can't write to external media storage.");
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException ex) {
System.out.println("Can't write to internal media storage.");
this.fail("Error capturing image - no media storage found.");
return;
}
}
// Add compressed version of captured image to returned media store Uri // Create entry in media store for image
OutputStream os = this.ctx.getContentResolver().openOutputStream(uri); // (Don't use insertImage() because it uses default compression setting of 50 - no way to change it)
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os); ContentValues values = new ContentValues();
os.close(); values.put(android.provider.MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
Uri uri = null;
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException e) {
System.out.println("Can't write to external media storage.");
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException ex) {
System.out.println("Can't write to internal media storage.");
this.fail("Error capturing image - no media storage found.");
return;
}
}
bitmap.recycle(); // Add compressed version of captured image to returned media store Uri
bitmap = null; OutputStream os = this.ctx.getContentResolver().openOutputStream(uri);
System.gc(); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
os.close();
// Add image to results bitmap.recycle();
results.put(createMediaFile(uri)); bitmap = null;
System.gc();
if (results.length() >= limit) { // Restore exif data to file
// Send Uri back to JavaScript for viewing image exif.createOutFile(FileUtils.getRealPathFromURI(uri, this.ctx));
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId); exif.writeExifData();
} else {
// still need to capture more images
captureImage();
}
} catch (IOException e) {
e.printStackTrace();
this.fail("Error capturing image.");
}
} else if (requestCode == CAPTURE_VIDEO) {
// Get the uri of the video clip
Uri data = intent.getData();
// create a file object from the uri
results.put(createMediaFile(data));
if (results.length() >= limit) { // Add image to results
// Send Uri back to JavaScript for viewing video results.put(createMediaFile(uri));
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
} else {
// still need to capture more video clips
captureVideo(duration);
}
}
}
// If canceled
else if (resultCode == Activity.RESULT_CANCELED) {
// If we have partial results send them back to the user
if (results.length() > 0) {
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
}
// user canceled the action
else {
this.fail("Canceled.");
}
}
// If something else
else {
// If we have partial results send them back to the user
if (results.length() > 0) {
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
}
// something bad happened
else {
this.fail("Did not complete!");
}
}
}
/** if (results.length() >= limit) {
* Creates a JSONObject that represents a File from the Uri // Send Uri back to JavaScript for viewing image
* this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
* @param data the Uri of the audio/image/video } else {
* @return a JSONObject that represents a File // still need to capture more images
*/ captureImage();
private JSONObject createMediaFile(Uri data) { }
File fp = new File(getRealPathFromURI(data)); } catch (IOException e) {
e.printStackTrace();
this.fail("Error capturing image.");
}
} else if (requestCode == CAPTURE_VIDEO) {
// Get the uri of the video clip
Uri data = intent.getData();
// create a file object from the uri
results.put(createMediaFile(data));
JSONObject obj = new JSONObject(); if (results.length() >= limit) {
// Send Uri back to JavaScript for viewing video
try { this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
// File properties } else {
obj.put("name", fp.getName()); // still need to capture more video clips
obj.put("fullPath", fp.getAbsolutePath()); captureVideo(duration);
obj.put("type", FileUtils.getMimeType(fp.getAbsolutePath())); }
obj.put("lastModifiedDate", fp.lastModified()); }
obj.put("size", fp.length()); }
} catch (JSONException e) { // If canceled
// this will never happen else if (resultCode == Activity.RESULT_CANCELED) {
e.printStackTrace(); // If we have partial results send them back to the user
} if (results.length() > 0) {
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
return obj; }
} // user canceled the action
else {
/** this.fail("Canceled.");
* Queries the media store to find out what the file path is for the Uri we supply }
* }
* @param contentUri the Uri of the audio/image/video // If something else
* @return the full path to the file else {
*/ // If we have partial results send them back to the user
private String getRealPathFromURI(Uri contentUri) { if (results.length() > 0) {
String[] proj = { _DATA }; this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
Cursor cursor = this.ctx.managedQuery(contentUri, proj, null, null, null); }
int column_index = cursor.getColumnIndexOrThrow(_DATA); // something bad happened
cursor.moveToFirst(); else {
return cursor.getString(column_index); this.fail("Did not complete!");
}
}
} }
/** /**
* Send error message to JavaScript. * Creates a JSONObject that represents a File from the Uri
* *
* @param err * @param data the Uri of the audio/image/video
*/ * @return a JSONObject that represents a File
public void fail(String err) { * @throws IOException
this.error(new PluginResult(PluginResult.Status.ERROR, err), this.callbackId); */
} private JSONObject createMediaFile(Uri data){
File fp = new File(FileUtils.getRealPathFromURI(data, this.ctx));
JSONObject obj = new JSONObject();
try {
// File properties
obj.put("name", fp.getName());
obj.put("fullPath", fp.getAbsolutePath());
obj.put("type", FileUtils.getMimeType(fp.getAbsolutePath()));
obj.put("lastModifiedDate", fp.lastModified());
obj.put("size", fp.length());
} catch (JSONException e) {
// this will never happen
e.printStackTrace();
}
return obj;
}
/**
* Send error message to JavaScript.
*
* @param err
*/
public void fail(String err) {
this.error(new PluginResult(PluginResult.Status.ERROR, err), this.callbackId);
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,153 @@
/*
* PhoneGap is available under *either* the terms of the modified BSD license *or* the
* MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
*
* Copyright (c) 2011, IBM Corporation
*/
package com.phonegap;
import java.io.IOException;
import android.media.ExifInterface;
public class ExifHelper {
private String aperature = null;
private String datetime = null;
private String exposureTime = null;
private String flash = null;
private String focalLength = null;
private String gpsAltitude = null;
private String gpsAltitudeRef = null;
private String gpsDateStamp = null;
private String gpsLatitude = null;
private String gpsLatitudeRef = null;
private String gpsLongitude = null;
private String gpsLongitudeRef = null;
private String gpsProcessingMethod = null;
private String gpsTimestamp = null;
private String iso = null;
private String make = null;
private String model = null;
private String orientation = null;
private String whiteBalance = null;
private ExifInterface inFile = null;
private ExifInterface outFile = null;
/**
* The file before it is compressed
*
* @param filePath
* @throws IOException
*/
public void createInFile(String filePath) throws IOException {
this.inFile = new ExifInterface(filePath);
}
/**
* The file after it has been compressed
*
* @param filePath
* @throws IOException
*/
public void createOutFile(String filePath) throws IOException {
this.outFile = new ExifInterface(filePath);
}
/**
* Reads all the EXIF data from the input file.
*/
public void readExifData() {
this.aperature = inFile.getAttribute(ExifInterface.TAG_APERTURE);
this.datetime = inFile.getAttribute(ExifInterface.TAG_DATETIME);
this.exposureTime = inFile.getAttribute(ExifInterface.TAG_EXPOSURE_TIME);
this.flash = inFile.getAttribute(ExifInterface.TAG_FLASH);
this.focalLength = inFile.getAttribute(ExifInterface.TAG_FOCAL_LENGTH);
this.gpsAltitude = inFile.getAttribute(ExifInterface.TAG_GPS_ALTITUDE);
this.gpsAltitudeRef = inFile.getAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF);
this.gpsDateStamp = inFile.getAttribute(ExifInterface.TAG_GPS_DATESTAMP);
this.gpsLatitude = inFile.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
this.gpsLatitudeRef = inFile.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
this.gpsLongitude = inFile.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
this.gpsLongitudeRef = inFile.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
this.gpsProcessingMethod = inFile.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD);
this.gpsTimestamp = inFile.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP);
this.iso = inFile.getAttribute(ExifInterface.TAG_ISO);
this.make = inFile.getAttribute(ExifInterface.TAG_MAKE);
this.model = inFile.getAttribute(ExifInterface.TAG_MODEL);
this.orientation = inFile.getAttribute(ExifInterface.TAG_ORIENTATION);
this.whiteBalance = inFile.getAttribute(ExifInterface.TAG_WHITE_BALANCE);
}
/**
* Writes the previously stored EXIF data to the output file.
*
* @throws IOException
*/
public void writeExifData() throws IOException {
// Don't try to write to a null file
if (this.outFile == null) {
return;
}
if (this.aperature != null) {
this.outFile.setAttribute(ExifInterface.TAG_APERTURE, this.aperature);
}
if (this.datetime != null) {
this.outFile.setAttribute(ExifInterface.TAG_DATETIME, this.datetime);
}
if (this.exposureTime != null) {
this.outFile.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, this.exposureTime);
}
if (this.flash != null) {
this.outFile.setAttribute(ExifInterface.TAG_FLASH, this.flash);
}
if (this.focalLength != null) {
this.outFile.setAttribute(ExifInterface.TAG_FOCAL_LENGTH, this.focalLength);
}
if (this.gpsAltitude != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, this.gpsAltitude);
}
if (this.gpsAltitudeRef != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, this.gpsAltitudeRef);
}
if (this.gpsDateStamp != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, this.gpsDateStamp);
}
if (this.gpsLatitude != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_LATITUDE, this.gpsLatitude);
}
if (this.gpsLatitudeRef != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, this.gpsLatitudeRef);
}
if (this.gpsLongitude != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, this.gpsLongitude);
}
if (this.gpsLongitudeRef != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, this.gpsLongitudeRef);
}
if (this.gpsProcessingMethod != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, this.gpsProcessingMethod);
}
if (this.gpsTimestamp != null) {
this.outFile.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, this.gpsTimestamp);
}
if (this.iso != null) {
this.outFile.setAttribute(ExifInterface.TAG_ISO, this.iso);
}
if (this.make != null) {
this.outFile.setAttribute(ExifInterface.TAG_MAKE, this.make);
}
if (this.model != null) {
this.outFile.setAttribute(ExifInterface.TAG_MODEL, this.model);
}
if (this.orientation != null) {
this.outFile.setAttribute(ExifInterface.TAG_ORIENTATION, this.orientation);
}
if (this.whiteBalance != null) {
this.outFile.setAttribute(ExifInterface.TAG_WHITE_BALANCE, this.whiteBalance);
}
this.outFile.saveAttributes();
}
}

View File

@ -25,6 +25,7 @@ import android.provider.MediaStore;
import android.util.Log; import android.util.Log;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import com.phonegap.api.PhonegapActivity;
import com.phonegap.api.Plugin; import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult; import com.phonegap.api.PluginResult;
import com.phonegap.file.EncodingException; import com.phonegap.file.EncodingException;
@ -39,6 +40,7 @@ import com.phonegap.file.TypeMismatchException;
*/ */
public class FileUtils extends Plugin { public class FileUtils extends Plugin {
private static final String LOG_TAG = "FileUtils"; private static final String LOG_TAG = "FileUtils";
private static final String _DATA = "_data"; // The column name where the file path is stored
public static int NOT_FOUND_ERR = 1; public static int NOT_FOUND_ERR = 1;
public static int SECURITY_ERR = 2; public static int SECURITY_ERR = 2;
@ -988,5 +990,19 @@ public class FileUtils extends Plugin {
return new FileInputStream(path); return new FileInputStream(path);
} }
} }
}
/**
* Queries the media store to find out what the file path is for the Uri we supply
*
* @param contentUri the Uri of the audio/image/video
* @param ctx the current applicaiton context
* @return the full path to the file
*/
protected static String getRealPathFromURI(Uri contentUri, PhonegapActivity ctx) {
String[] proj = { _DATA };
Cursor cursor = ctx.managedQuery(contentUri, proj, null, null, null);
int column_index = cursor.getColumnIndexOrThrow(_DATA);
cursor.moveToFirst();
return cursor.getString(column_index);
}
}