From 48c4cdd47fd0e3830ee1229bdeb58c5af7e2de51 Mon Sep 17 00:00:00 2001 From: Norman Breau Date: Mon, 28 Oct 2024 15:21:37 -0300 Subject: [PATCH] refactor(android): Make WRITE_EXTERNAL_STORAGE optional (#909) * refactor(android): Rework permission management to make WRITE_EXTERNAL_STORAGE optional * removed unused getPermissions API * Proper error if WRITE_EXTERNAL_STORAGE is required but missing the declaration * removed obsolete hasPermissions API --- README.md | 14 +++- src/android/CameraLauncher.java | 117 +++++++++++++++----------------- 2 files changed, 68 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 0639ce412..281b12240 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,18 @@ quality, even if a `quality` parameter is specified. To avoid common memory problems, set `Camera.destinationType` to `FILE_URI` rather than `DATA_URL`. +__NOTE__: To use `saveToPhotoAlbum` option on Android 9 (API 28) and lower, the `WRITE_EXTERNAL_STORAGE` permission must be declared. + +To do this, add the following in your `config.xml`: + +```xml + + + +``` + +Android 10 (API 29) and later devices does not require `WRITE_EXTERNAL_STORAGE` permission. If your application only supports Android 10 or later, then this step is not necessary. + #### FILE_URI Usage When `FILE_URI` is used, the returned path is not directly usable. The file path needs to be resolved into @@ -301,7 +313,7 @@ Optional parameters to customize the camera settings. | targetHeight | number | | Height in pixels to scale image. Must be used with `targetWidth`. Aspect ratio remains constant. | | mediaType | [MediaType](#module_Camera.MediaType) | PICTURE | Set the type of media to select from. Only works when `PictureSourceType` is `PHOTOLIBRARY` or `SAVEDPHOTOALBUM`. | | correctOrientation | Boolean | | Rotate the image to correct for the orientation of the device during capture. | -| saveToPhotoAlbum | Boolean | | Save the image to the photo album on the device after capture. | +| saveToPhotoAlbum | Boolean | | Save the image to the photo album on the device after capture.
See [Android Quirks](#cameragetpicturesuccesscallback-errorcallback-options). | | popoverOptions | [CameraPopoverOptions](#module_CameraPopoverOptions) | | iOS-only options that specify popover location in iPad. | | cameraDirection | [Direction](#module_Camera.Direction) | BACK | Choose the camera to use (front- or back-facing). | diff --git a/src/android/CameraLauncher.java b/src/android/CameraLauncher.java index 881730113..aea896e43 100644 --- a/src/android/CameraLauncher.java +++ b/src/android/CameraLauncher.java @@ -193,15 +193,16 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo this.callTakePicture(destType, encodingType); } else if ((this.srcType == PHOTOLIBRARY) || (this.srcType == SAVEDPHOTOALBUM)) { - // FIXME: Stop always requesting the permission - String[] permissions = getPermissions(true, mediaType); - if(!hasPermissions(permissions)) { - PermissionHelper.requestPermissions(this, SAVE_TO_ALBUM_SEC, permissions); - } else { - this.getImage(this.srcType, destType); - } + this.getImage(this.srcType, destType); } } + catch (IllegalStateException e) + { + callbackContext.error(e.getLocalizedMessage()); + PluginResult r = new PluginResult(PluginResult.Status.ERROR); + callbackContext.sendPluginResult(r); + return true; + } catch (IllegalArgumentException e) { callbackContext.error("Illegal Argument Exception"); @@ -223,22 +224,6 @@ else if ((this.srcType == PHOTOLIBRARY) || (this.srcType == SAVEDPHOTOALBUM)) { // LOCAL METHODS //-------------------------------------------------------------------------- - private String[] getPermissions(boolean storageOnly, int mediaType) { - ArrayList permissions = new ArrayList<>(); - - if (android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { - // Android API 30 or lower - permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE); - permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); - } - if (!storageOnly) { - // Add camera permission when not storage. - permissions.add(Manifest.permission.CAMERA); - } - - return permissions.toArray(new String[0]); - } - private String getTempDirectoryPath() { File cache = cordova.getActivity().getCacheDir(); // Create the cache directory if it doesn't exist @@ -260,47 +245,64 @@ private String getTempDirectoryPath() { * @param returnType Set the type of image to return. * @param encodingType Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality) */ - public void callTakePicture(int returnType, int encodingType) { - String[] storagePermissions = getPermissions(true, mediaType); - boolean saveAlbumPermission; - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - saveAlbumPermission = this.saveToPhotoAlbum ? hasPermissions(storagePermissions) : true; - } else { - saveAlbumPermission = hasPermissions(storagePermissions); - } - boolean takePicturePermission = PermissionHelper.hasPermission(this, Manifest.permission.CAMERA); + public void callTakePicture(int returnType, int encodingType) throws IllegalStateException { // CB-10120: The CAMERA permission does not need to be requested unless it is declared // in AndroidManifest.xml. This plugin does not declare it, but others may and so we must // check the package info to determine if the permission is present. + boolean manifestContainsCameraPermission = false; - if (!takePicturePermission) { - takePicturePermission = true; - try { - PackageManager packageManager = this.cordova.getActivity().getPackageManager(); - String[] permissionsInPackage = packageManager.getPackageInfo(this.cordova.getActivity().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions; - if (permissionsInPackage != null) { - for (String permission : permissionsInPackage) { - if (permission.equals(Manifest.permission.CAMERA)) { - takePicturePermission = false; - break; - } + // write permission is not necessary, unless if we are saving to photo album + // On API 29+ devices, write permission is completely obsolete and not required. + boolean manifestContainsWriteExternalPermission = false; + + boolean cameraPermissionGranted = PermissionHelper.hasPermission(this, Manifest.permission.CAMERA); + boolean writeExternalPermissionGranted = false; + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + writeExternalPermissionGranted = PermissionHelper.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); + } + else { + writeExternalPermissionGranted = true; + } + + try { + PackageManager packageManager = this.cordova.getActivity().getPackageManager(); + String[] permissionsInPackage = packageManager.getPackageInfo(this.cordova.getActivity().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions; + if (permissionsInPackage != null) { + for (String permission : permissionsInPackage) { + if (permission.equals(Manifest.permission.CAMERA)) { + manifestContainsCameraPermission = true; + } + else if (permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + manifestContainsWriteExternalPermission = true; } } - } catch (NameNotFoundException e) { - // We are requesting the info for our package, so this should - // never be caught } + } catch (NameNotFoundException e) { + // We are requesting the info for our package, so this should + // never be caught + } + + ArrayList requiredPermissions = new ArrayList<>(); + if (manifestContainsCameraPermission && !cameraPermissionGranted) { + requiredPermissions.add(Manifest.permission.CAMERA); } - if (takePicturePermission && saveAlbumPermission) { + if (saveToPhotoAlbum && !writeExternalPermissionGranted) { + // This block only applies for API 24-28 + // because writeExternalPermissionGranted is always true on API 29+ + if (!manifestContainsWriteExternalPermission) { + throw new IllegalStateException("WRITE_EXTERNAL_STORAGE permission not declared in AndroidManifest"); + } + + requiredPermissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); + } + + if (!requiredPermissions.isEmpty()) { + PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, requiredPermissions.toArray(new String[0])); + } + else { takePicture(returnType, encodingType); - } else if (saveAlbumPermission) { - PermissionHelper.requestPermission(this, TAKE_PIC_SEC, Manifest.permission.CAMERA); - } else if (takePicturePermission) { - PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, storagePermissions); - } else { - PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, getPermissions(false, mediaType)); } } @@ -1358,15 +1360,6 @@ 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; - } - /** * Gets the ideal buffer size for processing streams of data. *