diff --git a/plugin.xml b/plugin.xml index 0911fb8..d63cfb4 100644 --- a/plugin.xml +++ b/plugin.xml @@ -69,16 +69,30 @@ <config-file target="AndroidManifest.xml" parent="/*"> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> </config-file> + <config-file target="AndroidManifest.xml" parent="application"> + <provider + android:name="android.support.v4.content.FileProvider" + android:authorities="${applicationId}.provider" + android:exported="false" + android:grantUriPermissions="true" > + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/provider_paths"/> + </provider> + </config-file> <source-file src="src/android/CameraLauncher.java" target-dir="src/org/apache/cordova/camera" /> <source-file src="src/android/FileHelper.java" target-dir="src/org/apache/cordova/camera" /> <source-file src="src/android/ExifHelper.java" target-dir="src/org/apache/cordova/camera" /> + <source-file src="src/android/xml/provider_paths.xml" target-dir="res/xml" /> <js-module src="www/CameraPopoverHandle.js" name="CameraPopoverHandle"> <clobbers target="CameraPopoverHandle" /> - </js-module> + </js-module> - </platform> + <framework src="com.android.support:support-v4:24.1.1+" /> + + </platform> <!-- amazon-fireos --> <platform name="amazon-fireos"> diff --git a/src/android/CameraLauncher.java b/src/android/CameraLauncher.java index 69cb5db..c68462d 100644 --- a/src/android/CameraLauncher.java +++ b/src/android/CameraLauncher.java @@ -31,6 +31,8 @@ import java.util.Date; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaResourceApi; +import org.apache.cordova.CoreAndroid; 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; @@ -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,10 @@ 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) CoreAndroid.getBuildConfigValue(cordova.getActivity(), "APPLICATION_ID"); + if (action.equals("takePicture")) { this.srcType = CAMERA; @@ -232,7 +241,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 +291,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 = FileProvider.getUriForFile(cordova.getActivity(), + applicationId + ".provider", + photo); + intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, this.imageUri); + //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 +412,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 +467,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()) : + getFileNameFromUri(this.imageUri); + if (this.encodingType == JPEG) { try { @@ -475,10 +494,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 = Uri.fromFile(new File(getFileNameFromUri(this.imageUri))); + writeUncompressedImage(imageUri, galleryUri); } refreshGallery(galleryUri); @@ -490,7 +510,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 +541,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 = Uri.fromFile(new File(getFileNameFromUri(this.imageUri))); + writeUncompressedImage(imageUri, uri); } this.callbackContext.success(uri.toString()); @@ -570,8 +592,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect 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 +601,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 +621,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 +657,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 +736,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect e.printStackTrace(); this.failPicture("Error retrieving image."); } - } - else { + } else { this.callbackContext.success(fileLocation); } } @@ -727,10 +752,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 +792,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 +826,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 +870,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 +941,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 +1149,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 +1167,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 +1175,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 +1227,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 +1276,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 +1284,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 +1298,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,11 +1334,11 @@ 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) { + if (this.imageUri != null) { state.putString("imageUri", this.imageUri.toString()); } @@ -1337,14 +1358,36 @@ 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")) { + if (state.containsKey("imageUri")) { this.imageUri = 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/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 @@ +<?xml version="1.0" encoding="utf-8"?> +<paths xmlns:android="http://schemas.android.com/apk/res/android"> + <external-path name="external_files" path="."/> +</paths> \ No newline at end of file