rpanadero closed pull request #298: Fixed FileProvider NullPointerException and pictures edition by Google+ URL: https://github.com/apache/cordova-plugin-camera/pull/298
This is a PR merged from a forked repository. As GitHub hides the original diff on merge, it is displayed below for the sake of provenance: As this is a foreign pull request (from a fork), the diff is supplied below (as it won't show otherwise due to GitHub magic): diff --git a/src/android/CameraLauncher.java b/src/android/CameraLauncher.java index 0f9b4b0c..9654fd4f 100644 --- a/src/android/CameraLauncher.java +++ b/src/android/CameraLauncher.java @@ -18,34 +18,14 @@ Licensed to the Apache Software Foundation (ASF) under one */ package org.apache.cordova.camera; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -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; -import org.json.JSONArray; -import org.json.JSONException; - import android.Manifest; -import android.annotation.TargetApi; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ContentValues; -import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; @@ -55,16 +35,35 @@ Licensed to the Apache Software Foundation (ASF) under one import android.media.MediaScannerConnection; import android.media.MediaScannerConnection.MediaScannerConnectionClient; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.os.Environment; -import android.provider.DocumentsContract; +import android.os.Parcelable; 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; +import android.util.Log; + +import org.apache.cordova.BuildHelper; +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.LOG; +import org.apache.cordova.PermissionHelper; +import org.apache.cordova.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; /** * This class launches the camera view, allows the user to take a picture, closes the camera view, @@ -95,6 +94,10 @@ Licensed to the Apache Software Foundation (ASF) under one public static final int TAKE_PIC_SEC = 0; public static final int SAVE_TO_ALBUM_SEC = 1; + private static final String DEFAULT_EDIT_CHOOSER_TITLE = "Complete action using"; + private static final int FILTER_INTENT_FLAG = 0; // Filter intent metadata (0=none) + private static final String INTENT_ACTION_CROP = "com.android.camera.action.CROP"; + private static final String LOG_TAG = "CameraLauncher"; //Where did this come from? @@ -112,8 +115,11 @@ Licensed to the Apache Software Foundation (ASF) under one 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. + private String editChooserTitle; // Text shown after taking a picture and picture edition is enabled - protected final static String[] permissions = { Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE }; + protected final static String[] permissions = { Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }; + // Forbidden picture editors they have a strange behaviour editing pictures (returning old photos, etc) + protected final List<String> forbiddenPicEditors = new ArrayList<String>(); public CallbackContext callbackContext; private int numPics; @@ -162,7 +168,9 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo this.allowEdit = args.getBoolean(7); this.correctOrientation = args.getBoolean(8); this.saveToPhotoAlbum = args.getBoolean(9); - + // Look at Camera javascript file to know the right index values for every option + this.setForbiddenPicEditors(args.getJSONArray(12)); + this.setEditChooserTitle(args.getString(13)); // If the user specifies a 0 or smaller width/height // make it -1 so later comparisons succeed if (this.targetWidth < 1) { @@ -213,6 +221,36 @@ else if ((this.srcType == PHOTOLIBRARY) || (this.srcType == SAVEDPHOTOALBUM)) { // LOCAL METHODS //-------------------------------------------------------------------------- + private void setForbiddenPicEditors(JSONArray forbiddenEditors) { + this.forbiddenPicEditors.clear(); + if (forbiddenEditors != null) { + for (int i = 0; i < forbiddenEditors.length(); i++) { + try { + String forbiddenPicEditor = forbiddenEditors.getString(i); + this.forbiddenPicEditors.add(forbiddenPicEditor); + } catch (JSONException e) { + Log.e(LOG_TAG, "Error processing forbidden pic editor. Error: " + e); + } + } + } + } + + private void setEditChooserTitle(String title) { + this.editChooserTitle = title != null ? title : DEFAULT_EDIT_CHOOSER_TITLE; + } + + private String getForbiddenPicEditors() { + final String separator = ","; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < this.forbiddenPicEditors.size(); i++) { + sb.append(this.forbiddenPicEditors.get(i)); + if (i < this.forbiddenPicEditors.size() - 1) { + sb.append(separator); + } + } + return sb.toString(); + } + private String getTempDirectoryPath() { File cache = null; @@ -245,7 +283,8 @@ private String getTempDirectoryPath() { * @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); + boolean saveAlbumPermission = PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + && PermissionHelper.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); boolean takePicturePermission = PermissionHelper.hasPermission(this, Manifest.permission.CAMERA); // CB-10120: The CAMERA permission does not need to be requested unless it is declared @@ -276,7 +315,8 @@ public void callTakePicture(int returnType, int encodingType) { } else if (saveAlbumPermission && !takePicturePermission) { PermissionHelper.requestPermission(this, TAKE_PIC_SEC, Manifest.permission.CAMERA); } else if (!saveAlbumPermission && takePicturePermission) { - PermissionHelper.requestPermission(this, TAKE_PIC_SEC, Manifest.permission.READ_EXTERNAL_STORAGE); + PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}); } else { PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, permissions); } @@ -406,56 +446,119 @@ public void getImage(int srcType, int returnType, int encodingType) { } - /** - * Brings up the UI to perform crop on passed image URI - * - * @param picUri - */ - 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"); + /** + * Brings up the UI to perform crop on passed image URI + * + * @param picUri + */ + private void performCrop(Uri picUri, int destType, Intent cameraIntent) { + try { + final List<Intent> cropIntents = this.getIntentsForEdition(picUri, destType); + if(cropIntents.isEmpty()) { + Log.w(LOG_TAG, "The mobile has no applications to edit a picture"); + this.allowEdit = false; + this.processCamareResultSafely(cameraIntent); + return; + } - // indicate output X and Y - if (targetWidth > 0) { - cropIntent.putExtra("outputX", targetWidth); + Intent chooserIntent = Intent.createChooser(cropIntents.remove(0), this.editChooserTitle); + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, + cropIntents.toArray(new Parcelable[]{})); + + + // start the activity - we handle returning in onActivityResult + + if (this.cordova != null) { + this.cordova.startActivityForResult((CordovaPlugin) this, + chooserIntent, CROP_CAMERA + destType); + } + } catch (ActivityNotFoundException anfe) { + LOG.e(LOG_TAG, "Crop operation not supported on this device"); + this.allowEdit = false; + this.processCamareResultSafely(cameraIntent); } - if (targetHeight > 0) { - cropIntent.putExtra("outputY", targetHeight); + } + + /** + * Tries to safely process the camera result, catching IOExcepction, in order to avoid + * that app crashes + * @param cameraIntent + */ + private void processCamareResultSafely(Intent cameraIntent) { + try { + processResultFromCamera(destType, cameraIntent); } - if (targetHeight > 0 && targetWidth > 0 && targetWidth == targetHeight) { - cropIntent.putExtra("aspectX", 1); - cropIntent.putExtra("aspectY", 1); + catch (IOException e) + { + e.printStackTrace(); + LOG.e(LOG_TAG, "Unable to write to file"); } + } + + /** + * Returns the list of available picture editors on the mobile phone but ignoring + * those declared as forbidden {@link #forbiddenPicEditors} + * @param picUri + * @param destType + * @return + */ + private List<Intent> getIntentsForEdition(Uri picUri, int destType) { + Log.d(LOG_TAG, "Getting intents to edit a picture"); + final List<Intent> targetedIntents = new ArrayList<Intent>(); + Intent cropIntent = new Intent(INTENT_ACTION_CROP); // 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); + this.addExtraDataToCropIntent(cropIntent, picUri, croppedUri); + // Get the applications which can handle the Intent + List<ResolveInfo> resolveInfos = this.cordova.getActivity().getPackageManager().queryIntentActivities(cropIntent, FILTER_INTENT_FLAG); + Log.d(LOG_TAG, "Getting applications that are capable of edit a picture"); + // Loop on your Activities, filter and construct your own list here + if (!resolveInfos.isEmpty()){ + Log.d(LOG_TAG, "Application list ignoring those on the black list: " + this.getForbiddenPicEditors()); + for (ResolveInfo resolveInfo : resolveInfos) { + Intent targetedCropIntent = new Intent("com.android.camera.action.CROP"); + String packageName = resolveInfo.activityInfo.packageName; + if(!this.forbiddenPicEditors.contains(packageName)) { + Log.d(LOG_TAG, String.format("Pic editor (application) package name: %s", packageName)); + targetedCropIntent.setPackage(packageName); + this.addExtraDataToCropIntent(targetedCropIntent, picUri, croppedUri); + targetedIntents.add(targetedCropIntent); + } + } + } + return new ArrayList<Intent>(targetedIntents); + } - // start the activity - we handle returning in onActivityResult + /** + * Modifies the intent received as parameter adding extra data needed to edit the picture + * , also received as parameter (URI). + * @param intent + * @param picUri + * @param croppedUri + */ + private void addExtraDataToCropIntent(Intent intent, Uri picUri, Uri croppedUri) { + // indicate image type and Uri + intent.setDataAndType(picUri, "image/*"); + // set crop properties + intent.putExtra("crop", "true"); - if (this.cordova != null) { - this.cordova.startActivityForResult((CordovaPlugin) this, - cropIntent, CROP_CAMERA + destType); + // indicate output X and Y + if (targetWidth > 0) { + intent.putExtra("outputX", targetWidth); } - } catch (ActivityNotFoundException anfe) { - LOG.e(LOG_TAG, "Crop operation not supported on this device"); - try { - processResultFromCamera(destType, cameraIntent); - } - catch (IOException e) - { - e.printStackTrace(); - LOG.e(LOG_TAG, "Unable to write to file"); - } + if (targetHeight > 0) { + intent.putExtra("outputY", targetHeight); + } + if (targetHeight > 0 && targetWidth > 0 && targetWidth == targetHeight) { + intent.putExtra("aspectX", 1); + intent.putExtra("aspectY", 1); + } + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + intent.putExtra("output", croppedUri); } - } /** * Applies all needed transformation to the image received from the camera. @@ -764,6 +867,18 @@ else if (destType == FILE_URI || destType == NATIVE_URI) { */ public void onActivityResult(int requestCode, int resultCode, Intent intent) { + /* Interrupting the plugin execution if the cordova application was restarted by OS. + * We detect that the application was restarted because class variables are cleaned up (not initialized) + * This check avoids a frequent crash with {@see android.support.v4.content.FileProvider} which + * happens because the plugin invoke the 'getUriForFile' method (FileProvider method) with a NULL authority (=applicationId). + * + * Stacktrace: + * java.lang.NullPointerException: Attempt to invoke virtual method ?android.content.res.XmlResourceParser + * android.content.pm.ProviderInfo.loadXmlMetaData(android.content.pm.PackageManager, java.lang.String)? on a null object reference + * + */ + if (this.applicationId == null) return; + // Get src and dest types from request code for a Camera Activity int srcType = (requestCode / 16) - 1; int destType = (requestCode % 16) - 1; diff --git a/www/Camera.js b/www/Camera.js index c14e3f30..10252463 100644 --- a/www/Camera.js +++ b/www/Camera.js @@ -148,9 +148,11 @@ cameraExport.getPicture = function (successCallback, errorCallback, options) { var saveToPhotoAlbum = !!options.saveToPhotoAlbum; var popoverOptions = getValue(options.popoverOptions, null); var cameraDirection = getValue(options.cameraDirection, Camera.Direction.BACK); + var forbiddenPicEditors = getValue(options.forbiddenPicEditors, []); + var editChooserTitle = getValue(options.editChooserTitle, 'Complete action using'); var args = [quality, destinationType, sourceType, targetWidth, targetHeight, encodingType, - mediaType, allowEdit, correctOrientation, saveToPhotoAlbum, popoverOptions, cameraDirection]; + mediaType, allowEdit, correctOrientation, saveToPhotoAlbum, popoverOptions, cameraDirection, forbiddenPicEditors, editChooserTitle]; exec(successCallback, errorCallback, 'Camera', 'takePicture', args); // XXX: commented out ---------------------------------------------------------------- This is an automated message from the Apache Git Service. To respond to the message, please log on GitHub and use the URL above to go to the specific comment. For queries about this service, please contact Infrastructure at: us...@infra.apache.org With regards, Apache Git Services --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@cordova.apache.org For additional commands, e-mail: commits-h...@cordova.apache.org