CB-11625: Working on fix to API 24 no longer allowing File URIs to be passed across intents

This commit is contained in:
Joe Bowser 2016-07-27 14:06:07 -07:00
parent b695717240
commit 3d26986bfd
2 changed files with 126 additions and 88 deletions

View File

@ -69,6 +69,17 @@
<config-file target="AndroidManifest.xml" parent="/*"> <config-file target="AndroidManifest.xml" parent="/*">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</config-file> </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>
<source-file src="src/android/CameraLauncher.java" target-dir="src/org/apache/cordova/camera" /> <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/FileHelper.java" target-dir="src/org/apache/cordova/camera" />
@ -78,6 +89,8 @@
<clobbers target="CameraPopoverHandle" /> <clobbers target="CameraPopoverHandle" />
</js-module> </js-module>
<framework src="com.android.support:support-v4:24.1.1+" />
</platform> </platform>
<!-- amazon-fireos --> <!-- amazon-fireos -->

View File

@ -36,6 +36,7 @@ import org.apache.cordova.CordovaResourceApi;
import org.apache.cordova.LOG; import org.apache.cordova.LOG;
import org.apache.cordova.PermissionHelper; import org.apache.cordova.PermissionHelper;
import org.apache.cordova.PluginResult; import org.apache.cordova.PluginResult;
import org.apache.mobilespec.BuildConfig;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
@ -56,6 +57,8 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.support.v4.content.FileProvider;
import android.util.Base64; import android.util.Base64;
import android.util.Log; import android.util.Log;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
@ -281,8 +284,12 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
// Specify file so that large image is captured and returned // Specify file so that large image is captured and returned
File photo = createCaptureFile(encodingType); File photo = createCaptureFile(encodingType);
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(photo)); this.imageUri = FileProvider.getUriForFile(cordova.getActivity(),
this.imageUri = Uri.fromFile(photo); BuildConfig.APPLICATION_ID + ".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) { if (this.cordova != null) {
// Let's check to make sure the camera is actually installed. (Legacy Nexus 7 code) // Let's check to make sure the camera is actually installed. (Legacy Nexus 7 code)
@ -416,7 +423,9 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
cropIntent.putExtra("aspectY", 1); cropIntent.putExtra("aspectY", 1);
} }
// create new file handle to get full resolution crop // create new file handle to get full resolution crop
croppedUri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + "")); croppedUri = FileProvider.getUriForFile(cordova.getActivity(),
BuildConfig.APPLICATION_ID + ".provider",
createCaptureFile(this.encodingType, System.currentTimeMillis() + ""));
cropIntent.putExtra("output", croppedUri); cropIntent.putExtra("output", croppedUri);
// start the activity - we handle returning in onActivityResult // start the activity - we handle returning in onActivityResult
@ -449,9 +458,11 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
// Create an ExifHelper to save the exif data that is lost during compression // Create an ExifHelper to save the exif data that is lost during compression
ExifHelper exif = new ExifHelper(); ExifHelper exif = new ExifHelper();
String sourcePath = (this.allowEdit && this.croppedUri != null) ? String sourcePath = (this.allowEdit && this.croppedUri != null) ?
FileHelper.stripFileProtocol(this.croppedUri.toString()) : getFileNameFromUri(this.croppedUri) :
FileHelper.stripFileProtocol(this.imageUri.toString()); getFileNameFromUri(this.imageUri);
if (this.encodingType == JPEG) { if (this.encodingType == JPEG) {
try { try {
@ -472,12 +483,15 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
// in the gallery and the modified image is saved in the temporary // in the gallery and the modified image is saved in the temporary
// directory // directory
if (this.saveToPhotoAlbum) { if (this.saveToPhotoAlbum) {
galleryUri = Uri.fromFile(new File(getPicutresPath())); galleryUri = Uri.fromFile(new File(getPicturesPath()));
if(this.allowEdit && this.croppedUri != null) {
writeUncompressedImage(this.croppedUri, galleryUri); if (this.allowEdit && this.croppedUri != null) {
Uri croppedUri = Uri.fromFile(new File(getFileNameFromUri(this.croppedUri)));
writeUncompressedImage(croppedUri, galleryUri);
} else { } else {
writeUncompressedImage(this.imageUri, galleryUri); Uri imageUri = Uri.fromFile(new File(getFileNameFromUri(this.imageUri)));
writeUncompressedImage(imageUri, galleryUri);
} }
refreshGallery(galleryUri); refreshGallery(galleryUri);
@ -489,7 +503,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
if (bitmap == null) { if (bitmap == null) {
// Try to get the bitmap from intent. // Try to get the bitmap from intent.
bitmap = (Bitmap)intent.getExtras().get("data"); bitmap = (Bitmap) intent.getExtras().get("data");
} }
// Double-check the bitmap. // Double-check the bitmap.
@ -523,10 +537,12 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
} else { } else {
Uri uri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + "")); Uri uri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + ""));
if(this.allowEdit && this.croppedUri != null) { if (this.allowEdit && this.croppedUri != null) {
writeUncompressedImage(this.croppedUri, uri); Uri croppedUri = Uri.fromFile(new File(getFileNameFromUri(this.croppedUri)));
writeUncompressedImage(croppedUri, uri);
} else { } else {
writeUncompressedImage(this.imageUri, uri); Uri imageUri = Uri.fromFile(new File(getFileNameFromUri(this.imageUri)));
writeUncompressedImage(imageUri, uri);
} }
this.callbackContext.success(uri.toString()); this.callbackContext.success(uri.toString());
@ -575,25 +591,23 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
bitmap = null; bitmap = null;
} }
private String getPicutresPath() private String getPicturesPath() {
{
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "IMG_" + timeStamp + (this.encodingType == JPEG ? ".jpg" : ".png"); String imageFileName = "IMG_" + timeStamp + (this.encodingType == JPEG ? ".jpg" : ".png");
File storageDir = Environment.getExternalStoragePublicDirectory( File storageDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES); Environment.DIRECTORY_PICTURES);
String galleryPath = storageDir.getAbsolutePath() + "/" + imageFileName; String galleryPath = storageDir.getAbsolutePath() + "/" + imageFileName;
return galleryPath; return galleryPath;
} }
private void refreshGallery(Uri contentUri) private void refreshGallery(Uri contentUri) {
{
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(contentUri); mediaScanIntent.setData(contentUri);
this.cordova.getActivity().sendBroadcast(mediaScanIntent); this.cordova.getActivity().sendBroadcast(mediaScanIntent);
} }
private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException { private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
// Some content: URIs do not map to file paths (e.g. picasa). // Some content: URIs do not map to file paths (e.g. picasa).
String realPath = FileHelper.getRealPath(uri, this.cordova); String realPath = FileHelper.getRealPath(uri, this.cordova);
@ -632,8 +646,7 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
} }
/**
/**
* Applies all needed transformation to the image received from the gallery. * Applies all needed transformation to the image received from the gallery.
* *
* @param destType In which form should we return the image * @param destType In which form should we return the image
@ -658,8 +671,7 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
// and there will be no attempt to resize any returned data // and there will be no attempt to resize any returned data
if (this.mediaType != PICTURE) { if (this.mediaType != PICTURE) {
this.callbackContext.success(fileLocation); this.callbackContext.success(fileLocation);
} } else {
else {
// This is a special case to just return the path as no scaling, // This is a special case to just return the path as no scaling,
// rotating, nor compressing needs to be done // rotating, nor compressing needs to be done
if (this.targetHeight == -1 && this.targetWidth == -1 && if (this.targetHeight == -1 && this.targetWidth == -1 &&
@ -709,8 +721,8 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
// If sending filename back // If sending filename back
else if (destType == FILE_URI || destType == NATIVE_URI) { else if (destType == FILE_URI || destType == NATIVE_URI) {
// Did we modify the image? // Did we modify the image?
if ( (this.targetHeight > 0 && this.targetWidth > 0) || if ((this.targetHeight > 0 && this.targetWidth > 0) ||
(this.correctOrientation && this.orientationCorrected) ) { (this.correctOrientation && this.orientationCorrected)) {
try { try {
String modifiedPath = this.ouputModifiedBitmap(bitmap, uri); 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 // The modified image is cached by the app in order to get around this and not have to delete you
@ -721,8 +733,7 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
e.printStackTrace(); e.printStackTrace();
this.failPicture("Error retrieving image."); this.failPicture("Error retrieving image.");
} }
} } else {
else {
this.callbackContext.success(fileLocation); this.callbackContext.success(fileLocation);
} }
} }
@ -778,12 +789,12 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
// If image available // If image available
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
try { try {
if(this.allowEdit) if (this.allowEdit) {
{ Uri tmpFile = FileProvider.getUriForFile(cordova.getActivity(),
Uri tmpFile = Uri.fromFile(createCaptureFile(this.encodingType)); BuildConfig.APPLICATION_ID + ".provider",
createCaptureFile(this.encodingType));
performCrop(tmpFile, destType, intent); performCrop(tmpFile, destType, intent);
} } else {
else {
this.processResultFromCamera(destType, intent); this.processResultFromCamera(destType, intent);
} }
} catch (IOException e) { } catch (IOException e) {
@ -812,11 +823,9 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
processResultFromGallery(finalDestType, i); processResultFromGallery(finalDestType, i);
} }
}); });
} } else if (resultCode == Activity.RESULT_CANCELED) {
else if (resultCode == Activity.RESULT_CANCELED) {
this.failPicture("Selection cancelled."); this.failPicture("Selection cancelled.");
} } else {
else {
this.failPicture("Selection did not complete!"); this.failPicture("Selection did not complete!");
} }
} }
@ -824,7 +833,7 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
private int getImageOrientation(Uri uri) { private int getImageOrientation(Uri uri) {
int rotate = 0; int rotate = 0;
String[] cols = { MediaStore.Images.Media.ORIENTATION }; String[] cols = {MediaStore.Images.Media.ORIENTATION};
try { try {
Cursor cursor = cordova.getActivity().getContentResolver().query(uri, Cursor cursor = cordova.getActivity().getContentResolver().query(uri,
cols, null, null, null); cols, null, null, null);
@ -855,13 +864,10 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
matrix.setRotate(rotate, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2); matrix.setRotate(rotate, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2);
} }
try try {
{
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
exif.resetOrientation(); exif.resetOrientation();
} } catch (OutOfMemoryError oom) {
catch (OutOfMemoryError oom)
{
// You can run out of memory if the image is very large: // You can run out of memory if the image is very large:
// http://simonmacdonald.blogspot.ca/2012/07/change-to-camera-code-in-phonegap-190.html // http://simonmacdonald.blogspot.ca/2012/07/change-to-camera-code-in-phonegap-190.html
// If this happens, simply do not rotate the image and return it unmodified. // If this happens, simply do not rotate the image and return it unmodified.
@ -897,14 +903,14 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
try { try {
os.close(); os.close();
} catch (IOException e) { } catch (IOException e) {
LOG.d(LOG_TAG,"Exception while closing output stream."); LOG.d(LOG_TAG, "Exception while closing output stream.");
} }
} }
if (fis != null) { if (fis != null) {
try { try {
fis.close(); fis.close();
} catch (IOException e) { } catch (IOException e) {
LOG.d(LOG_TAG,"Exception while closing file input stream."); LOG.d(LOG_TAG, "Exception while closing file input stream.");
} }
} }
} }
@ -953,7 +959,7 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
try { try {
fileStream.close(); fileStream.close();
} catch (IOException e) { } catch (IOException e) {
LOG.d(LOG_TAG,"Exception while closing file input stream."); LOG.d(LOG_TAG, "Exception while closing file input stream.");
} }
} }
} }
@ -972,14 +978,13 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
try { try {
fileStream.close(); fileStream.close();
} catch (IOException e) { } catch (IOException e) {
LOG.d(LOG_TAG,"Exception while closing file input stream."); LOG.d(LOG_TAG, "Exception while closing file input stream.");
} }
} }
} }
//CB-2292: WTF? Why is the width null? //CB-2292: WTF? Why is the width null?
if(options.outWidth == 0 || options.outHeight == 0) if (options.outWidth == 0 || options.outHeight == 0) {
{
return null; return null;
} }
@ -998,7 +1003,7 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
try { try {
fileStream.close(); fileStream.close();
} catch (IOException e) { } catch (IOException e) {
LOG.d(LOG_TAG,"Exception while closing file input stream."); LOG.d(LOG_TAG, "Exception while closing file input stream.");
} }
} }
} }
@ -1067,8 +1072,8 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
* @return * @return
*/ */
public static int calculateSampleSize(int srcWidth, int srcHeight, int dstWidth, int dstHeight) { public static int calculateSampleSize(int srcWidth, int srcHeight, int dstWidth, int dstHeight) {
final float srcAspect = (float)srcWidth / (float)srcHeight; final float srcAspect = (float) srcWidth / (float) srcHeight;
final float dstAspect = (float)dstWidth / (float)dstHeight; final float dstAspect = (float) dstWidth / (float) dstHeight;
if (srcAspect > dstAspect) { if (srcAspect > dstAspect) {
return srcWidth / dstWidth; return srcWidth / dstWidth;
@ -1085,7 +1090,7 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
private Cursor queryImgDB(Uri contentStore) { private Cursor queryImgDB(Uri contentStore) {
return this.cordova.getActivity().getContentResolver().query( return this.cordova.getActivity().getContentResolver().query(
contentStore, contentStore,
new String[] { MediaStore.Images.Media._ID }, new String[]{MediaStore.Images.Media._ID},
null, null,
null, null,
null); null);
@ -1093,6 +1098,7 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
/** /**
* Cleans up after picture taking. Checking for duplicates and that kind of stuff. * Cleans up after picture taking. Checking for duplicates and that kind of stuff.
*
* @param newImage * @param newImage
*/ */
private void cleanup(int imageType, Uri oldImage, Uri newImage, Bitmap bitmap) { private void cleanup(int imageType, Uri oldImage, Uri newImage, Bitmap bitmap) {
@ -1144,6 +1150,7 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
/** /**
* Determine if we are storing the images in internal or external storage * Determine if we are storing the images in internal or external storage
*
* @return Uri * @return Uri
*/ */
private Uri whichContentStore() { private Uri whichContentStore() {
@ -1192,7 +1199,7 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
private void scanForGallery(Uri newImage) { private void scanForGallery(Uri newImage) {
this.scanMe = newImage; this.scanMe = newImage;
if(this.conn != null) { if (this.conn != null) {
this.conn.disconnect(); this.conn.disconnect();
} }
this.conn = new MediaScannerConnection(this.cordova.getActivity().getApplicationContext(), this); this.conn = new MediaScannerConnection(this.cordova.getActivity().getApplicationContext(), this);
@ -1200,9 +1207,9 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
} }
public void onMediaScannerConnected() { public void onMediaScannerConnected() {
try{ try {
this.conn.scanFile(this.scanMe.toString(), "image/*"); 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"); LOG.e(LOG_TAG, "Can't scan file in MediaScanner after taking picture");
} }
@ -1214,18 +1221,14 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
public void onRequestPermissionResult(int requestCode, String[] permissions, public void onRequestPermissionResult(int requestCode, String[] permissions,
int[] grantResults) throws JSONException int[] grantResults) throws JSONException {
{ for (int r : grantResults) {
for(int r:grantResults) if (r == PackageManager.PERMISSION_DENIED) {
{
if(r == PackageManager.PERMISSION_DENIED)
{
this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR)); this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR));
return; return;
} }
} }
switch(requestCode) switch (requestCode) {
{
case TAKE_PIC_SEC: case TAKE_PIC_SEC:
takePicture(this.destType, this.encodingType); takePicture(this.destType, this.encodingType);
break; break;
@ -1254,11 +1257,11 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
state.putBoolean("correctOrientation", this.correctOrientation); state.putBoolean("correctOrientation", this.correctOrientation);
state.putBoolean("saveToPhotoAlbum", this.saveToPhotoAlbum); state.putBoolean("saveToPhotoAlbum", this.saveToPhotoAlbum);
if(this.croppedUri != null) { if (this.croppedUri != null) {
state.putString("croppedUri", this.croppedUri.toString()); state.putString("croppedUri", this.croppedUri.toString());
} }
if(this.imageUri != null) { if (this.imageUri != null) {
state.putString("imageUri", this.imageUri.toString()); state.putString("imageUri", this.imageUri.toString());
} }
@ -1278,14 +1281,36 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
this.correctOrientation = state.getBoolean("correctOrientation"); this.correctOrientation = state.getBoolean("correctOrientation");
this.saveToPhotoAlbum = state.getBoolean("saveToPhotoAlbum"); this.saveToPhotoAlbum = state.getBoolean("saveToPhotoAlbum");
if(state.containsKey("croppedUri")) { if (state.containsKey("croppedUri")) {
this.croppedUri = Uri.parse(state.getString("croppedUri")); this.croppedUri = Uri.parse(state.getString("croppedUri"));
} }
if(state.containsKey("imageUri")) { if (state.containsKey("imageUri")) {
this.imageUri = Uri.parse(state.getString("imageUri")); this.imageUri = Uri.parse(state.getString("imageUri"));
} }
this.callbackContext = callbackContext; this.callbackContext = callbackContext;
} }
/*
* 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;
}
} }