diff --git a/src/android/CameraLauncher.java b/src/android/CameraLauncher.java index 5fd38b238..365385fc7 100644 --- a/src/android/CameraLauncher.java +++ b/src/android/CameraLauncher.java @@ -19,11 +19,15 @@ Licensed to the Apache Software Foundation (ASF) under one package org.apache.cordova.camera; import android.Manifest; +import android.annotation.SuppressLint; import android.app.Activity; +import android.app.PendingIntent; +import android.app.RecoverableSecurityException; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Intent; +import android.content.IntentSender; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.database.Cursor; @@ -78,6 +82,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect private static final int PICTURE = 0; // allow selection of still pictures only. DEFAULT. Will return format specified via DestinationType private static final int VIDEO = 1; // allow selection of video only, ONLY RETURNS URL private static final int ALLMEDIA = 2; // allow selection from all media types + private static final int RECOVERABLE_DELETE_REQUEST = 3; // Result of Recoverable Security Exception private static final int JPEG = 0; // Take a picture of type JPEG private static final int PNG = 1; // Take a picture of type PNG @@ -122,8 +127,6 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect private boolean orientationCorrected; // Has the picture's orientation been corrected private boolean allowEdit; // Should we allow the user to crop the image. - protected final static String[] permissions = { Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }; - public CallbackContext callbackContext; private int numPics; @@ -133,6 +136,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect private String croppedFilePath; private ExifHelper exifData; // Exif data from source private String applicationId; + private Uri pendingDeleteMediaUri; /** @@ -145,10 +149,12 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect */ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { this.callbackContext = callbackContext; - - this.applicationId = cordova.getContext().getPackageName(); + //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(TAKE_PICTURE_ACTION)) { this.srcType = CAMERA; this.destType = FILE_URI; @@ -193,8 +199,9 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo } else if ((this.srcType == PHOTOLIBRARY) || (this.srcType == SAVEDPHOTOALBUM)) { // FIXME: Stop always requesting the permission - if(!PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)) { - PermissionHelper.requestPermission(this, SAVE_TO_ALBUM_SEC, Manifest.permission.READ_EXTERNAL_STORAGE); + String[] permissions = getPermissions(true, mediaType); + if(!hasPermissions(permissions)) { + PermissionHelper.requestPermissions(this, SAVE_TO_ALBUM_SEC, permissions); } else { this.getImage(this.srcType, destType); } @@ -221,6 +228,37 @@ else if ((this.srcType == PHOTOLIBRARY) || (this.srcType == SAVEDPHOTOALBUM)) { // LOCAL METHODS //-------------------------------------------------------------------------- + private String[] getPermissions(boolean storageOnly, int mediaType) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (storageOnly) { + switch (mediaType) { + case PICTURE: + return new String[]{ Manifest.permission.READ_MEDIA_IMAGES }; + case VIDEO: + return new String[]{ Manifest.permission.READ_MEDIA_VIDEO }; + default: + return new String[]{ Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO }; + } + } + else { + switch (mediaType) { + case PICTURE: + return new String[]{ Manifest.permission.CAMERA, Manifest.permission.CAMERA, Manifest.permission.READ_MEDIA_IMAGES }; + case VIDEO: + return new String[]{ Manifest.permission.CAMERA, Manifest.permission.READ_MEDIA_VIDEO }; + default: + return new String[]{ Manifest.permission.CAMERA, Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO }; + } + } + } else { + if (storageOnly) { + return new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}; + } else { + return new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}; + } + } + } + private String getTempDirectoryPath() { File cache = cordova.getActivity().getCacheDir(); // Create the cache directory if it doesn't exist @@ -243,8 +281,13 @@ 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) - && PermissionHelper.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); + String[] storagePermissions = getPermissions(true, mediaType); + boolean saveAlbumPermission; + if (this.saveToPhotoAlbum) { + saveAlbumPermission = hasPermissions(storagePermissions); + } else { + saveAlbumPermission = true; + } boolean takePicturePermission = PermissionHelper.hasPermission(this, Manifest.permission.CAMERA); // CB-10120: The CAMERA permission does not need to be requested unless it is declared @@ -275,10 +318,9 @@ public void callTakePicture(int returnType, int encodingType) { } else if (saveAlbumPermission) { PermissionHelper.requestPermission(this, TAKE_PIC_SEC, Manifest.permission.CAMERA); } else if (takePicturePermission) { - PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, - new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}); + PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, storagePermissions); } else { - PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, permissions); + PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, getPermissions(false, mediaType)); } } @@ -871,6 +913,20 @@ public void run() { } else { this.failPicture("Selection did not complete!"); } + } + // retry media store deletion ... + else if(requestCode == RECOVERABLE_DELETE_REQUEST){ + if (resultCode == Activity.RESULT_OK) { + ContentResolver contentResolver = this.cordova.getActivity().getContentResolver(); + try { + contentResolver.delete(this.pendingDeleteMediaUri, null, null); + } catch (Exception e) { + LOG.e(LOG_TAG, "Unable to delete media store file after permission was granted"); + } + this.pendingDeleteMediaUri = null; + } else { + LOG.e(LOG_TAG, "Failed to delete file. Permission not granted."); + } } } @@ -976,7 +1032,7 @@ private Bitmap getScaledAndRotatedBitmap(String imageUrl) throws IOException { This is the only way to determine the orientation of the photo coming from 3rd party providers (Google Drive, Dropbox,etc) This also ensures we create a scaled bitmap with the correct orientation - We delete the temporary file once we are done + We delete the temporary file once we are done */ File localFile = null; Uri galleryUri = null; @@ -1193,7 +1249,7 @@ private Cursor queryImgDB(Uri contentStore) { /** * 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) { if (bitmap != null) { @@ -1232,13 +1288,36 @@ private void checkForDuplicateImage(int type) { // delete the duplicate file if the difference is 2 for file URI or 1 for Data URL if ((currentNumOfImages - numPics) == diff) { cursor.moveToLast(); + @SuppressLint("Range") int id = Integer.valueOf(cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media._ID))); if (diff == 2) { id--; } Uri uri = Uri.parse(contentStore + "/" + id); - this.cordova.getActivity().getContentResolver().delete(uri, null, null); - cursor.close(); + try { + this.cordova.getActivity().getContentResolver().delete(uri, null, null); + } catch (SecurityException securityException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + RecoverableSecurityException recoverableSecurityException; + if (securityException instanceof RecoverableSecurityException) { + recoverableSecurityException = (RecoverableSecurityException) securityException; + } else { + throw new RuntimeException(securityException.getMessage(), securityException); + } + IntentSender requestPermission = recoverableSecurityException.getUserAction().getActionIntent().getIntentSender(); + try { + this.cordova.setActivityResultCallback(this); + this.cordova.getActivity().startIntentSenderForResult(requestPermission, + RECOVERABLE_DELETE_REQUEST, null, 0, 0, 0, null); + } catch (IntentSender.SendIntentException e) { + e.printStackTrace(); + } + } else { + throw new RuntimeException(securityException.getMessage(), securityException); + } + } finally { + cursor.close(); + } } } @@ -1312,20 +1391,39 @@ public void onScanCompleted(String path, Uri uri) { } public void onRequestPermissionResult(int requestCode, String[] permissions, - int[] grantResults) { - for (int r : grantResults) { - if (r == PackageManager.PERMISSION_DENIED) { - this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR)); - return; + int[] grantResults) throws JSONException { + String version = android.os.Build.VERSION.RELEASE; + if(Integer.parseInt(version) == 13) + { + if(PermissionHelper.hasPermission(this, Manifest.permission.CAMERA)) { + switch (requestCode) { + case TAKE_PIC_SEC: + takePicture(this.destType, this.encodingType); + break; + case SAVE_TO_ALBUM_SEC: + this.getImage(this.srcType, this.destType, this.encodingType); + break; + } + } + else { + PermissionHelper.hasPermission(this, Manifest.permission.CAMERA); } } - switch (requestCode) { - case TAKE_PIC_SEC: - takePicture(this.destType, this.encodingType); - break; - case SAVE_TO_ALBUM_SEC: - this.getImage(this.srcType, this.destType); - break; + else{ + for (int r : grantResults) { + if (r == PackageManager.PERMISSION_DENIED) { + this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR)); + return; + } + } + switch (requestCode) { + case TAKE_PIC_SEC: + takePicture(this.destType, this.encodingType); + break; + case SAVE_TO_ALBUM_SEC: + this.getImage(this.srcType, this.destType, this.encodingType); + break; + } } } @@ -1391,4 +1489,13 @@ public void onRestoreStateForActivityResult(Bundle state, CallbackContext callba this.callbackContext = callbackContext; } + + private boolean hasPermissions(String[] permissions) { + for (String permission: permissions) { + if (!PermissionHelper.hasPermission(this, permission)) { + return false; + } + } + return true; + } }