Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(android): Make WRITE_EXTERNAL_STORAGE optional #909

Merged
merged 4 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<config-file target="AndroidManifest.xml" parent="/*" xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
</config-file>
```

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
Expand Down Expand Up @@ -301,7 +313,7 @@ Optional parameters to customize the camera settings.
| targetHeight | <code>number</code> | | Height in pixels to scale image. Must be used with `targetWidth`. Aspect ratio remains constant. |
| mediaType | <code>[MediaType](#module_Camera.MediaType)</code> | <code>PICTURE</code> | Set the type of media to select from. Only works when `PictureSourceType` is `PHOTOLIBRARY` or `SAVEDPHOTOALBUM`. |
| correctOrientation | <code>Boolean</code> | | Rotate the image to correct for the orientation of the device during capture. |
| saveToPhotoAlbum | <code>Boolean</code> | | Save the image to the photo album on the device after capture. |
| saveToPhotoAlbum | <code>Boolean</code> | | Save the image to the photo album on the device after capture.<br />See [Android Quirks](#cameragetpicturesuccesscallback-errorcallback-options). |
| popoverOptions | <code>[CameraPopoverOptions](#module_CameraPopoverOptions)</code> | | iOS-only options that specify popover location in iPad. |
| cameraDirection | <code>[Direction](#module_Camera.Direction)</code> | <code>BACK</code> | Choose the camera to use (front- or back-facing). |

Expand Down
117 changes: 55 additions & 62 deletions src/android/CameraLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -223,22 +224,6 @@ else if ((this.srcType == PHOTOLIBRARY) || (this.srcType == SAVEDPHOTOALBUM)) {
// LOCAL METHODS
//--------------------------------------------------------------------------

private String[] getPermissions(boolean storageOnly, int mediaType) {
ArrayList<String> 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
Expand All @@ -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<String> 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));
}
}

Expand Down Expand Up @@ -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.
*
Expand Down
Loading