diff --git a/plugin.xml b/plugin.xml index 668954c..fe07bb6 100644 --- a/plugin.xml +++ b/plugin.xml @@ -69,16 +69,31 @@ + + + + + + + - + - + + + diff --git a/src/android/CameraLauncher.java b/src/android/CameraLauncher.java index 69cb5db..e3ee20b 100644 --- a/src/android/CameraLauncher.java +++ b/src/android/CameraLauncher.java @@ -29,8 +29,10 @@ import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.Date; +import org.apache.cordova.BuildHelper; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaResourceApi; import org.apache.cordova.LOG; import org.apache.cordova.PermissionHelper; import org.apache.cordova.PluginResult; @@ -58,6 +60,8 @@ import android.os.Bundle; import android.os.Environment; import android.provider.DocumentsContract; import android.provider.MediaStore; +import android.provider.OpenableColumns; +import android.support.v4.content.FileProvider; import android.util.Base64; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; @@ -99,7 +103,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect 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 targetHeight; // desired height of the image - private Uri imageUri; // Uri of captured image + private CordovaUri imageUri; // Uri of captured image private int encodingType; // Type of encoding to use private int mediaType; // What type of media to retrieve private int destType; // Source type (needs to be saved for the permission handling) @@ -118,6 +122,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect private Uri scanMe; // Uri of image to be added to content store private Uri croppedUri; private ExifHelper exifData; // Exif data from source + private String applicationId; /** @@ -130,6 +135,11 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect */ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { this.callbackContext = callbackContext; + //Adding an API to CoreAndroid to get the BuildConfigValue + //This allows us to not make this a breaking change to embedding + this.applicationId = (String) BuildHelper.getBuildConfigValue(cordova.getActivity(), "APPLICATION_ID"); + this.applicationId = preferences.getString("applicationId", this.applicationId); + if (action.equals("takePicture")) { this.srcType = CAMERA; @@ -232,7 +242,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect * img.src=result; * * @param returnType Set the type of image to return. - * @param encodingType JPEG or PNG + * @param encodingType Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality) */ public void callTakePicture(int returnType, int encodingType) { boolean saveAlbumPermission = PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); @@ -282,8 +292,12 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect // Specify file so that large image is captured and returned File photo = createCaptureFile(encodingType); - intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(photo)); - this.imageUri = Uri.fromFile(photo); + this.imageUri = new CordovaUri(FileProvider.getUriForFile(cordova.getActivity(), + applicationId + ".provider", + photo)); + intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, imageUri.getCorrectUri()); + //We can write to this URI, this will hopefully allow us to write files to get to the next step + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); if (this.cordova != null) { // Let's check to make sure the camera is actually installed. (Legacy Nexus 7 code) @@ -399,33 +413,37 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect */ private void performCrop(Uri picUri, int destType, Intent cameraIntent) { try { - Intent cropIntent = new Intent("com.android.camera.action.CROP"); - // indicate image type and Uri - cropIntent.setDataAndType(picUri, "image/*"); - // set crop properties - cropIntent.putExtra("crop", "true"); + Intent cropIntent = new Intent("com.android.camera.action.CROP"); + // indicate image type and Uri + cropIntent.setDataAndType(picUri, "image/*"); + // set crop properties + cropIntent.putExtra("crop", "true"); - // indicate output X and Y - if (targetWidth > 0) { + + // indicate output X and Y + if (targetWidth > 0) { cropIntent.putExtra("outputX", targetWidth); - } - if (targetHeight > 0) { + } + if (targetHeight > 0) { cropIntent.putExtra("outputY", targetHeight); - } - if (targetHeight > 0 && targetWidth > 0 && targetWidth == targetHeight) { + } + if (targetHeight > 0 && targetWidth > 0 && targetWidth == targetHeight) { cropIntent.putExtra("aspectX", 1); cropIntent.putExtra("aspectY", 1); - } - // create new file handle to get full resolution crop - croppedUri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + "")); - cropIntent.putExtra("output", croppedUri); + } + // create new file handle to get full resolution crop + croppedUri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + "")); + cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + cropIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + cropIntent.putExtra("output", croppedUri); - // start the activity - we handle returning in onActivityResult - if (this.cordova != null) { - this.cordova.startActivityForResult((CordovaPlugin) this, - cropIntent, CROP_CAMERA + destType); - } + // start the activity - we handle returning in onActivityResult + + if (this.cordova != null) { + this.cordova.startActivityForResult((CordovaPlugin) this, + cropIntent, CROP_CAMERA + destType); + } } catch (ActivityNotFoundException anfe) { LOG.e(LOG_TAG, "Crop operation not supported on this device"); try { @@ -450,9 +468,11 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect // Create an ExifHelper to save the exif data that is lost during compression ExifHelper exif = new ExifHelper(); + String sourcePath = (this.allowEdit && this.croppedUri != null) ? - FileHelper.stripFileProtocol(this.croppedUri.toString()) : - FileHelper.stripFileProtocol(this.imageUri.toString()); + FileHelper.stripFileProtocol(this.croppedUri.toString()) : + this.imageUri.getFilePath(); + if (this.encodingType == JPEG) { try { @@ -475,10 +495,11 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect if (this.saveToPhotoAlbum) { galleryUri = Uri.fromFile(new File(getPicturesPath())); - if(this.allowEdit && this.croppedUri != null) { - writeUncompressedImage(this.croppedUri, galleryUri); + if (this.allowEdit && this.croppedUri != null) { + writeUncompressedImage(croppedUri, galleryUri); } else { - writeUncompressedImage(this.imageUri, galleryUri); + Uri imageUri = this.imageUri.getFileUri(); + writeUncompressedImage(imageUri, galleryUri); } refreshGallery(galleryUri); @@ -490,7 +511,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect if (bitmap == null) { // Try to get the bitmap from intent. - bitmap = (Bitmap)intent.getExtras().get("data"); + bitmap = (Bitmap) intent.getExtras().get("data"); } // Double-check the bitmap. @@ -521,10 +542,12 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect } else { Uri uri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + "")); - if(this.allowEdit && this.croppedUri != null) { - writeUncompressedImage(this.croppedUri, uri); + if (this.allowEdit && this.croppedUri != null) { + Uri croppedUri = Uri.fromFile(new File(getFileNameFromUri(this.croppedUri))); + writeUncompressedImage(croppedUri, uri); } else { - writeUncompressedImage(this.imageUri, uri); + Uri imageUri = this.imageUri.getFileUri(); + writeUncompressedImage(imageUri, uri); } this.callbackContext.success(uri.toString()); @@ -566,12 +589,11 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect throw new IllegalStateException(); } - this.cleanup(FILE_URI, this.imageUri, galleryUri, bitmap); + this.cleanup(FILE_URI, this.imageUri.getFileUri(), galleryUri, bitmap); bitmap = null; } - private String getPicturesPath() - { + private String getPicturesPath() { String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); String imageFileName = "IMG_" + timeStamp + (this.encodingType == JPEG ? ".jpg" : ".png"); File storageDir = Environment.getExternalStoragePublicDirectory( @@ -580,8 +602,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect return galleryPath; } - private void refreshGallery(Uri contentUri) - { + private void refreshGallery(Uri contentUri) { Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); mediaScanIntent.setData(contentUri); this.cordova.getActivity().sendBroadcast(mediaScanIntent); @@ -601,9 +622,16 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect private String outputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException { + // Some content: URIs do not map to file paths (e.g. picasa). + String realPath = FileHelper.getRealPath(uri, this.cordova); + + // Get filename from uri + String fileName = realPath != null ? + realPath.substring(realPath.lastIndexOf('/') + 1) : + "modified." + (this.encodingType == JPEG ? "jpg" : "png"); String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String fileName = "IMG_" + timeStamp + (this.encodingType == JPEG ? ".jpg" : ".png"); + //String fileName = "IMG_" + timeStamp + (this.encodingType == JPEG ? ".jpg" : ".png"); String modifiedPath = getTempDirectoryPath() + "/" + fileName; OutputStream os = new FileOutputStream(modifiedPath); @@ -630,12 +658,11 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect } - /** * 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"). + * @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(); @@ -710,8 +737,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect e.printStackTrace(); this.failPicture("Error retrieving image."); } - } - else { + } else { this.callbackContext.success(fileLocation); } } @@ -727,10 +753,10 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect /** * Called when the camera view exits. * - * @param requestCode The request code originally supplied to startActivityForResult(), - * allowing you to identify who this result came from. - * @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 requestCode The request code originally supplied to startActivityForResult(), + * allowing you to identify who this result came from. + * @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"). */ public void onActivityResult(int requestCode, int resultCode, Intent intent) { @@ -767,12 +793,12 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect // If image available if (resultCode == Activity.RESULT_OK) { try { - if(this.allowEdit) - { - Uri tmpFile = Uri.fromFile(createCaptureFile(this.encodingType)); + if (this.allowEdit) { + Uri tmpFile = FileProvider.getUriForFile(cordova.getActivity(), + applicationId + ".provider", + createCaptureFile(this.encodingType)); performCrop(tmpFile, destType, intent); - } - else { + } else { this.processResultFromCamera(destType, intent); } } catch (IOException e) { @@ -801,11 +827,9 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect processResultFromGallery(finalDestType, i); } }); - } - else if (resultCode == Activity.RESULT_CANCELED) { + } else if (resultCode == Activity.RESULT_CANCELED) { this.failPicture("Selection cancelled."); - } - else { + } else { this.failPicture("Selection did not complete!"); } } @@ -847,14 +871,14 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect try { os.close(); } catch (IOException e) { - LOG.d(LOG_TAG,"Exception while closing output stream."); + LOG.d(LOG_TAG, "Exception while closing output stream."); } } if (fis != null) { try { fis.close(); } catch (IOException e) { - LOG.d(LOG_TAG,"Exception while closing file input stream."); + LOG.d(LOG_TAG, "Exception while closing file input stream."); } } } @@ -918,7 +942,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect try { fileStream.close(); } catch (IOException e) { - LOG.d(LOG_TAG,"Exception while closing file input stream."); + LOG.d(LOG_TAG, "Exception while closing file input stream."); } } } @@ -1126,8 +1150,8 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect * @return */ public static int calculateSampleSize(int srcWidth, int srcHeight, int dstWidth, int dstHeight) { - final float srcAspect = (float)srcWidth / (float)srcHeight; - final float dstAspect = (float)dstWidth / (float)dstHeight; + final float srcAspect = (float) srcWidth / (float) srcHeight; + final float dstAspect = (float) dstWidth / (float) dstHeight; if (srcAspect > dstAspect) { return srcWidth / dstWidth; @@ -1144,7 +1168,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect private Cursor queryImgDB(Uri contentStore) { return this.cordova.getActivity().getContentResolver().query( contentStore, - new String[] { MediaStore.Images.Media._ID }, + new String[]{MediaStore.Images.Media._ID}, null, null, null); @@ -1152,6 +1176,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect /** * Cleans up after picture taking. Checking for duplicates and that kind of stuff. + * * @param newImage */ private void cleanup(int imageType, Uri oldImage, Uri newImage, Bitmap bitmap) { @@ -1203,6 +1228,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect /** * Determine if we are storing the images in internal or external storage + * * @return Uri */ private Uri whichContentStore() { @@ -1251,7 +1277,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect private void scanForGallery(Uri newImage) { this.scanMe = newImage; - if(this.conn != null) { + if (this.conn != null) { this.conn.disconnect(); } this.conn = new MediaScannerConnection(this.cordova.getActivity().getApplicationContext(), this); @@ -1259,9 +1285,9 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect } public void onMediaScannerConnected() { - try{ + try { this.conn.scanFile(this.scanMe.toString(), "image/*"); - } catch (java.lang.IllegalStateException e){ + } catch (java.lang.IllegalStateException e) { LOG.e(LOG_TAG, "Can't scan file in MediaScanner after taking picture"); } @@ -1273,18 +1299,14 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect public void onRequestPermissionResult(int requestCode, String[] permissions, - int[] grantResults) throws JSONException - { - for(int r:grantResults) - { - if(r == PackageManager.PERMISSION_DENIED) - { + int[] grantResults) throws JSONException { + for (int r : grantResults) { + if (r == PackageManager.PERMISSION_DENIED) { this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR)); return; } } - switch(requestCode) - { + switch (requestCode) { case TAKE_PIC_SEC: takePicture(this.destType, this.encodingType); break; @@ -1313,12 +1335,12 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect state.putBoolean("correctOrientation", this.correctOrientation); state.putBoolean("saveToPhotoAlbum", this.saveToPhotoAlbum); - if(this.croppedUri != null) { + if (this.croppedUri != null) { state.putString("croppedUri", this.croppedUri.toString()); } - if(this.imageUri != null) { - state.putString("imageUri", this.imageUri.toString()); + if (this.imageUri != null) { + state.putString("imageUri", this.imageUri.getFileUri().toString()); } return state; @@ -1337,14 +1359,38 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect this.correctOrientation = state.getBoolean("correctOrientation"); this.saveToPhotoAlbum = state.getBoolean("saveToPhotoAlbum"); - if(state.containsKey("croppedUri")) { + if (state.containsKey("croppedUri")) { this.croppedUri = Uri.parse(state.getString("croppedUri")); } - if(state.containsKey("imageUri")) { - this.imageUri = Uri.parse(state.getString("imageUri")); + if (state.containsKey("imageUri")) { + //I have no idea what type of URI is being passed in + this.imageUri = new CordovaUri(Uri.parse(state.getString("imageUri"))); } this.callbackContext = callbackContext; } -} \ No newline at end of file + + /* + * This is dirty, but it does the job. + * + * Since the FilesProvider doesn't really provide you a way of getting a URL from the file, + * and since we actually need the Camera to create the file for us most of the time, we don't + * actually write the file, just generate the location based on a timestamp, we need to get it + * back from the Intent. + * + * However, the FilesProvider preserves the path, so we can at least write to it from here, since + * we own the context in this case. + */ + + private String getFileNameFromUri(Uri uri) { + String fullUri = uri.toString(); + String partial_path = fullUri.split("external_files")[1]; + File external_storage = Environment.getExternalStorageDirectory(); + String path = external_storage.getAbsolutePath() + partial_path; + return path; + + } + + +} diff --git a/src/android/CordovaUri.java b/src/android/CordovaUri.java new file mode 100644 index 0000000..65ba878 --- /dev/null +++ b/src/android/CordovaUri.java @@ -0,0 +1,85 @@ +package org.apache.cordova.camera; + +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.support.v4.content.FileProvider; + +import java.io.File; + +/* + * This class exists because Andorid FilesProvider doesn't work on Android 4.4.4 and below and throws + * weird errors. I'm not sure why writing to shared cache directories is somehow verboten, but it is + * and this error is irritating for a Compatibility library to have. + * + */ + +public class CordovaUri { + + private Uri androidUri; + private String fileName; + private Uri fileUri; + + /* + * We always expect a FileProvider string to be passed in for the file that we create + * + */ + CordovaUri (Uri inputUri) + { + //Determine whether the file is a content or file URI + if(inputUri.getScheme().equals("content")) + { + androidUri = inputUri; + fileName = getFileNameFromUri(androidUri); + fileUri = Uri.parse("file://" + fileName); + } + else + { + fileUri = inputUri; + fileName = FileHelper.stripFileProtocol(inputUri.toString()); + } + } + + public Uri getFileUri() + { + return fileUri; + } + + public String getFilePath() + { + return fileName; + } + + /* + * This only gets called by takePicture + */ + + public Uri getCorrectUri() + { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + return androidUri; + else + return fileUri; + } + + /* + * This is dirty, but it does the job. + * + * Since the FilesProvider doesn't really provide you a way of getting a URL from the file, + * and since we actually need the Camera to create the file for us most of the time, we don't + * actually write the file, just generate the location based on a timestamp, we need to get it + * back from the Intent. + * + * However, the FilesProvider preserves the path, so we can at least write to it from here, since + * we own the context in this case. + */ + + private String getFileNameFromUri(Uri uri) { + String fullUri = uri.toString(); + String partial_path = fullUri.split("external_files")[1]; + File external_storage = Environment.getExternalStorageDirectory(); + String path = external_storage.getAbsolutePath() + partial_path; + return path; + + } +} diff --git a/src/android/xml/provider_paths.xml b/src/android/xml/provider_paths.xml new file mode 100644 index 0000000..ffa74ab --- /dev/null +++ b/src/android/xml/provider_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file