diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..8ec56a5 --- /dev/null +++ b/NOTICE @@ -0,0 +1,5 @@ +Apache Cordova +Copyright 2012 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 91cabf2..531019e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -68,3 +68,6 @@ * Documented quirk for CB-5335 + CB-5206 for WP7+8 * reference the correct firefoxos implementation * [BlackBerry10] Add permission to access_shared + +### 0.2.8 (Feb 26, 2014) +* CB-1826 Catch OOM on gallery image resize diff --git a/plugin.xml b/plugin.xml index 82fc073..b2db93e 100644 --- a/plugin.xml +++ b/plugin.xml @@ -3,7 +3,7 @@ + version="0.2.8"> Camera Cordova Camera Plugin Apache 2.0 diff --git a/src/android/CameraLauncher.java b/src/android/CameraLauncher.java index 0dd247b..57878ab 100755 --- a/src/android/CameraLauncher.java +++ b/src/android/CameraLauncher.java @@ -58,7 +58,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect 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 NATIVE_URI = 2; // On Android, this is the same as FILE_URI + private static final int NATIVE_URI = 2; // On Android, this is the same as FILE_URI 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 @@ -84,6 +84,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect private int mediaType; // What type of media to retrieve private boolean saveToPhotoAlbum; // Should the picture be saved to the device's photo album private boolean correctOrientation; // Should the pictures orientation be corrected + private boolean orientationCorrected; // Has the picture's orientation been corrected //private boolean allowEdit; // Should we allow the user to crop the image. UNUSED. public CallbackContext callbackContext; @@ -95,10 +96,10 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect /** * Executes the request and returns PluginResult. * - * @param action The action to execute. - * @param args JSONArry of arguments for the plugin. + * @param action The action to execute. + * @param args JSONArry of arguments for the plugin. * @param callbackContext 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 boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { this.callbackContext = callbackContext; @@ -264,6 +265,223 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect } } + /** + * Applies all needed transformation to the image received from the camera. + * + * @param destType In which form should we return the image + * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). + */ + private void processResultFromCamera(int destType, Intent intent) throws IOException { + int rotate = 0; + + // Create an ExifHelper to save the exif data that is lost during compression + ExifHelper exif = new ExifHelper(); + try { + if (this.encodingType == JPEG) { + exif.createInFile(getTempDirectoryPath() + "/.Pic.jpg"); + exif.readExifData(); + rotate = exif.getOrientation(); + } + } catch (IOException e) { + e.printStackTrace(); + } + + Bitmap bitmap = null; + Uri uri = null; + + // If sending base64 image back + if (destType == DATA_URL) { + bitmap = getScaledBitmap(FileHelper.stripFileProtocol(imageUri.toString())); + if (bitmap == null) { + // Try to get the bitmap from intent. + bitmap = (Bitmap)intent.getExtras().get("data"); + } + + // Double-check the bitmap. + if (bitmap == null) { + Log.d(LOG_TAG, "I either have a null image path or bitmap"); + this.failPicture("Unable to create bitmap!"); + return; + } + + if (rotate != 0 && this.correctOrientation) { + bitmap = getRotatedBitmap(rotate, bitmap, exif); + } + + this.processPicture(bitmap); + checkForDuplicateImage(DATA_URL); + } + + // If sending filename back + else if (destType == FILE_URI || destType == NATIVE_URI) { + if (this.saveToPhotoAlbum) { + Uri inputUri = getUriFromMediaStore(); + //Just because we have a media URI doesn't mean we have a real file, we need to make it + uri = Uri.fromFile(new File(FileHelper.getRealPath(inputUri, this.cordova))); + } else { + uri = Uri.fromFile(new File(getTempDirectoryPath(), System.currentTimeMillis() + ".jpg")); + } + + if (uri == null) { + this.failPicture("Error capturing image - no media storage found."); + } + + // If all this is true we shouldn't compress the image. + if (this.targetHeight == -1 && this.targetWidth == -1 && this.mQuality == 100 && + !this.correctOrientation) { + writeUncompressedImage(uri); + + this.callbackContext.success(uri.toString()); + } else { + bitmap = getScaledBitmap(FileHelper.stripFileProtocol(imageUri.toString())); + + if (rotate != 0 && this.correctOrientation) { + bitmap = getRotatedBitmap(rotate, bitmap, exif); + } + + // Add compressed version of captured image to returned media store Uri + OutputStream os = this.cordova.getActivity().getContentResolver().openOutputStream(uri); + bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, os); + os.close(); + + // Restore exif data to file + if (this.encodingType == JPEG) { + String exifPath; + if (this.saveToPhotoAlbum) { + exifPath = FileHelper.getRealPath(uri, this.cordova); + } else { + exifPath = uri.getPath(); + } + exif.createOutFile(exifPath); + exif.writeExifData(); + } + + } + // Send Uri back to JavaScript for viewing image + this.callbackContext.success(uri.toString()); + } + + this.cleanup(FILE_URI, this.imageUri, uri, bitmap); + bitmap = null; + } + + private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException { + // Create an ExifHelper to save the exif data that is lost during compression + String modifiedPath = getTempDirectoryPath() + "/modified.jpg"; + + OutputStream os = new FileOutputStream(modifiedPath); + bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, os); + os.close(); + + // Some content: URIs do not map to file paths (e.g. picasa). + String realPath = FileHelper.getRealPath(uri, this.cordova); + ExifHelper exif = new ExifHelper(); + if (realPath != null && this.encodingType == JPEG) { + try { + exif.createInFile(realPath); + exif.readExifData(); + if (this.correctOrientation && this.orientationCorrected) { + exif.resetOrientation(); + } + exif.createOutFile(modifiedPath); + exif.writeExifData(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return modifiedPath; + } + + /** + * Applies all needed transformation to the image received from the gallery. + * + * @param destType In which form should we return the image + * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). + */ + private void processResultFromGallery(int destType, Intent intent) { + Uri uri = intent.getData(); + int rotate = 0; + + // If you ask for video or all media type you will automatically get back a file URI + // and there will be no attempt to resize any returned data + if (this.mediaType != PICTURE) { + this.callbackContext.success(uri.toString()); + } + else { + // This is a special case to just return the path as no scaling, + // rotating, nor compressing needs to be done + if (this.targetHeight == -1 && this.targetWidth == -1 && + (destType == FILE_URI || destType == NATIVE_URI) && !this.correctOrientation) { + this.callbackContext.success(uri.toString()); + } else { + String uriString = uri.toString(); + // Get the path to the image. Makes loading so much easier. + String mimeType = FileHelper.getMimeType(uriString, this.cordova); + // If we don't have a valid image so quit. + if (!("image/jpeg".equalsIgnoreCase(mimeType) || "image/png".equalsIgnoreCase(mimeType))) { + Log.d(LOG_TAG, "I either have a null image path or bitmap"); + this.failPicture("Unable to retrieve path to picture!"); + return; + } + Bitmap bitmap = null; + try { + bitmap = getScaledBitmap(uriString); + } catch (IOException e) { + e.printStackTrace(); + } + if (bitmap == null) { + Log.d(LOG_TAG, "I either have a null image path or bitmap"); + this.failPicture("Unable to create bitmap!"); + return; + } + + if (this.correctOrientation) { + rotate = getImageOrientation(uri); + if (rotate != 0) { + Matrix matrix = new Matrix(); + matrix.setRotate(rotate); + try { + bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + this.orientationCorrected = true; + } catch (OutOfMemoryError oom) { + this.orientationCorrected = false; + } + } + } + + // If sending base64 image back + if (destType == DATA_URL) { + this.processPicture(bitmap); + } + + // If sending filename back + else if (destType == FILE_URI || destType == NATIVE_URI) { + // Did we modify the image? + if ( (this.targetHeight > 0 && this.targetWidth > 0) || + (this.correctOrientation && this.orientationCorrected) ) { + try { + String modifiedPath = this.ouputModifiedBitmap(bitmap, uri); + // The modified image is cached by the app in order to get around this and not have to delete you + // application cache I'm adding the current system time to the end of the file url. + this.callbackContext.success("file://" + modifiedPath + "?" + System.currentTimeMillis()); + } catch (Exception e) { + e.printStackTrace(); + this.failPicture("Error retrieving image."); + } + } + else { + this.callbackContext.success(uri.toString()); + } + } + if (bitmap != null) { + bitmap.recycle(); + bitmap = null; + } + System.gc(); + } + } + } + /** * Called when the camera view exits. * @@ -277,103 +495,13 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect // Get src and dest types from request code int srcType = (requestCode / 16) - 1; int destType = (requestCode % 16) - 1; - int rotate = 0; // If CAMERA if (srcType == CAMERA) { // If image available if (resultCode == Activity.RESULT_OK) { try { - // Create an ExifHelper to save the exif data that is lost during compression - ExifHelper exif = new ExifHelper(); - try { - if (this.encodingType == JPEG) { - exif.createInFile(getTempDirectoryPath() + "/.Pic.jpg"); - exif.readExifData(); - rotate = exif.getOrientation(); - } - } catch (IOException e) { - e.printStackTrace(); - } - - Bitmap bitmap = null; - Uri uri = null; - - // If sending base64 image back - if (destType == DATA_URL) { - bitmap = getScaledBitmap(FileHelper.stripFileProtocol(imageUri.toString())); - if (bitmap == null) { - // Try to get the bitmap from intent. - bitmap = (Bitmap)intent.getExtras().get("data"); - } - - // Double-check the bitmap. - if (bitmap == null) { - Log.d(LOG_TAG, "I either have a null image path or bitmap"); - this.failPicture("Unable to create bitmap!"); - return; - } - - if (rotate != 0 && this.correctOrientation) { - bitmap = getRotatedBitmap(rotate, bitmap, exif); - } - - this.processPicture(bitmap); - checkForDuplicateImage(DATA_URL); - } - - // If sending filename back - else if (destType == FILE_URI || destType == NATIVE_URI) { - if (this.saveToPhotoAlbum) { - Uri inputUri = getUriFromMediaStore(); - //Just because we have a media URI doesn't mean we have a real file, we need to make it - uri = Uri.fromFile(new File(FileHelper.getRealPath(inputUri, this.cordova))); - } else { - uri = Uri.fromFile(new File(getTempDirectoryPath(), System.currentTimeMillis() + ".jpg")); - } - - if (uri == null) { - this.failPicture("Error capturing image - no media storage found."); - } - - // If all this is true we shouldn't compress the image. - if (this.targetHeight == -1 && this.targetWidth == -1 && this.mQuality == 100 && - !this.correctOrientation) { - writeUncompressedImage(uri); - - this.callbackContext.success(uri.toString()); - } else { - bitmap = getScaledBitmap(FileHelper.stripFileProtocol(imageUri.toString())); - - if (rotate != 0 && this.correctOrientation) { - bitmap = getRotatedBitmap(rotate, bitmap, exif); - } - - // Add compressed version of captured image to returned media store Uri - OutputStream os = this.cordova.getActivity().getContentResolver().openOutputStream(uri); - bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, os); - os.close(); - - // Restore exif data to file - if (this.encodingType == JPEG) { - String exifPath; - if (this.saveToPhotoAlbum) { - exifPath = FileHelper.getRealPath(uri, this.cordova); - } else { - exifPath = uri.getPath(); - } - exif.createOutFile(exifPath); - exif.writeExifData(); - } - - } - // Send Uri back to JavaScript for viewing image - this.callbackContext.success(uri.toString()); - } - - this.cleanup(FILE_URI, this.imageUri, uri, bitmap); - bitmap = null; - + this.processResultFromCamera(destType, intent); } catch (IOException e) { e.printStackTrace(); this.failPicture("Error capturing image."); @@ -394,104 +522,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect // If retrieving photo from library else if ((srcType == PHOTOLIBRARY) || (srcType == SAVEDPHOTOALBUM)) { if (resultCode == Activity.RESULT_OK) { - Uri uri = intent.getData(); - - // If you ask for video or all media type you will automatically get back a file URI - // and there will be no attempt to resize any returned data - if (this.mediaType != PICTURE) { - this.callbackContext.success(uri.toString()); - } - else { - // This is a special case to just return the path as no scaling, - // rotating, nor compressing needs to be done - if (this.targetHeight == -1 && this.targetWidth == -1 && - (destType == FILE_URI || destType == NATIVE_URI) && !this.correctOrientation) { - this.callbackContext.success(uri.toString()); - } else { - String uriString = uri.toString(); - // Get the path to the image. Makes loading so much easier. - String mimeType = FileHelper.getMimeType(uriString, this.cordova); - // If we don't have a valid image so quit. - if (!("image/jpeg".equalsIgnoreCase(mimeType) || "image/png".equalsIgnoreCase(mimeType))) { - Log.d(LOG_TAG, "I either have a null image path or bitmap"); - this.failPicture("Unable to retrieve path to picture!"); - return; - } - Bitmap bitmap = null; - try { - bitmap = getScaledBitmap(uriString); - } catch (IOException e) { - e.printStackTrace(); - } - if (bitmap == null) { - Log.d(LOG_TAG, "I either have a null image path or bitmap"); - this.failPicture("Unable to create bitmap!"); - return; - } - - if (this.correctOrientation) { - rotate = getImageOrientation(uri); - if (rotate != 0) { - Matrix matrix = new Matrix(); - matrix.setRotate(rotate); - bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); - } - } - - // If sending base64 image back - if (destType == DATA_URL) { - this.processPicture(bitmap); - } - - // If sending filename back - else if (destType == FILE_URI || destType == NATIVE_URI) { - // Do we need to scale the returned file - if (this.targetHeight > 0 && this.targetWidth > 0) { - try { - // Create an ExifHelper to save the exif data that is lost during compression - String resizePath = getTempDirectoryPath() + "/resize.jpg"; - // Some content: URIs do not map to file paths (e.g. picasa). - String realPath = FileHelper.getRealPath(uri, this.cordova); - ExifHelper exif = new ExifHelper(); - if (realPath != null && this.encodingType == JPEG) { - try { - exif.createInFile(realPath); - exif.readExifData(); - rotate = exif.getOrientation(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - OutputStream os = new FileOutputStream(resizePath); - bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, os); - os.close(); - - // Restore exif data to file - if (realPath != null && this.encodingType == JPEG) { - exif.createOutFile(resizePath); - exif.writeExifData(); - } - - // The resized image is cached by the app in order to get around this and not have to delete you - // application cache I'm adding the current system time to the end of the file url. - this.callbackContext.success("file://" + resizePath + "?" + System.currentTimeMillis()); - } catch (Exception e) { - e.printStackTrace(); - this.failPicture("Error retrieving image."); - } - } - else { - this.callbackContext.success(uri.toString()); - } - } - if (bitmap != null) { - bitmap.recycle(); - bitmap = null; - } - System.gc(); - } - } + this.processResultFromGallery(destType, intent); } else if (resultCode == Activity.RESULT_CANCELED) { this.failPicture("Selection cancelled."); @@ -503,14 +534,18 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect } private int getImageOrientation(Uri uri) { - String[] cols = { MediaStore.Images.Media.ORIENTATION }; - Cursor cursor = cordova.getActivity().getContentResolver().query(uri, - cols, null, null, null); int rotate = 0; - if (cursor != null) { - cursor.moveToPosition(0); - rotate = cursor.getInt(0); - cursor.close(); + String[] cols = { MediaStore.Images.Media.ORIENTATION }; + try { + Cursor cursor = cordova.getActivity().getContentResolver().query(uri, + cols, null, null, null); + if (cursor != null) { + cursor.moveToPosition(0); + rotate = cursor.getInt(0); + cursor.close(); + } + } catch (Exception e) { + // You can get an IllegalArgumentException if ContentProvider doesn't support querying for orientation. } return rotate; }