Skip to content

Commit 48c4cdd

Browse files
authored
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
1 parent 7d159cf commit 48c4cdd

File tree

2 files changed

+68
-63
lines changed

2 files changed

+68
-63
lines changed

README.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,18 @@ quality, even if a `quality` parameter is specified. To avoid common
184184
memory problems, set `Camera.destinationType` to `FILE_URI` rather
185185
than `DATA_URL`.
186186

187+
__NOTE__: To use `saveToPhotoAlbum` option on Android 9 (API 28) and lower, the `WRITE_EXTERNAL_STORAGE` permission must be declared.
188+
189+
To do this, add the following in your `config.xml`:
190+
191+
```xml
192+
<config-file target="AndroidManifest.xml" parent="/*" xmlns:android="http://schemas.android.com/apk/res/android">
193+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
194+
</config-file>
195+
```
196+
197+
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.
198+
187199
#### FILE_URI Usage
188200

189201
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.
301313
| targetHeight | <code>number</code> | | Height in pixels to scale image. Must be used with `targetWidth`. Aspect ratio remains constant. |
302314
| 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`. |
303315
| correctOrientation | <code>Boolean</code> | | Rotate the image to correct for the orientation of the device during capture. |
304-
| saveToPhotoAlbum | <code>Boolean</code> | | Save the image to the photo album on the device after capture. |
316+
| saveToPhotoAlbum | <code>Boolean</code> | | Save the image to the photo album on the device after capture.<br />See [Android Quirks](#cameragetpicturesuccesscallback-errorcallback-options). |
305317
| popoverOptions | <code>[CameraPopoverOptions](#module_CameraPopoverOptions)</code> | | iOS-only options that specify popover location in iPad. |
306318
| cameraDirection | <code>[Direction](#module_Camera.Direction)</code> | <code>BACK</code> | Choose the camera to use (front- or back-facing). |
307319

src/android/CameraLauncher.java

+55-62
Original file line numberDiff line numberDiff line change
@@ -193,15 +193,16 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
193193
this.callTakePicture(destType, encodingType);
194194
}
195195
else if ((this.srcType == PHOTOLIBRARY) || (this.srcType == SAVEDPHOTOALBUM)) {
196-
// FIXME: Stop always requesting the permission
197-
String[] permissions = getPermissions(true, mediaType);
198-
if(!hasPermissions(permissions)) {
199-
PermissionHelper.requestPermissions(this, SAVE_TO_ALBUM_SEC, permissions);
200-
} else {
201-
this.getImage(this.srcType, destType);
202-
}
196+
this.getImage(this.srcType, destType);
203197
}
204198
}
199+
catch (IllegalStateException e)
200+
{
201+
callbackContext.error(e.getLocalizedMessage());
202+
PluginResult r = new PluginResult(PluginResult.Status.ERROR);
203+
callbackContext.sendPluginResult(r);
204+
return true;
205+
}
205206
catch (IllegalArgumentException e)
206207
{
207208
callbackContext.error("Illegal Argument Exception");
@@ -223,22 +224,6 @@ else if ((this.srcType == PHOTOLIBRARY) || (this.srcType == SAVEDPHOTOALBUM)) {
223224
// LOCAL METHODS
224225
//--------------------------------------------------------------------------
225226

226-
private String[] getPermissions(boolean storageOnly, int mediaType) {
227-
ArrayList<String> permissions = new ArrayList<>();
228-
229-
if (android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
230-
// Android API 30 or lower
231-
permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE);
232-
permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
233-
}
234-
if (!storageOnly) {
235-
// Add camera permission when not storage.
236-
permissions.add(Manifest.permission.CAMERA);
237-
}
238-
239-
return permissions.toArray(new String[0]);
240-
}
241-
242227
private String getTempDirectoryPath() {
243228
File cache = cordova.getActivity().getCacheDir();
244229
// Create the cache directory if it doesn't exist
@@ -260,47 +245,64 @@ private String getTempDirectoryPath() {
260245
* @param returnType Set the type of image to return.
261246
* @param encodingType Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality)
262247
*/
263-
public void callTakePicture(int returnType, int encodingType) {
264-
String[] storagePermissions = getPermissions(true, mediaType);
265-
boolean saveAlbumPermission;
266-
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
267-
saveAlbumPermission = this.saveToPhotoAlbum ? hasPermissions(storagePermissions) : true;
268-
} else {
269-
saveAlbumPermission = hasPermissions(storagePermissions);
270-
}
271-
boolean takePicturePermission = PermissionHelper.hasPermission(this, Manifest.permission.CAMERA);
248+
public void callTakePicture(int returnType, int encodingType) throws IllegalStateException {
272249

273250
// CB-10120: The CAMERA permission does not need to be requested unless it is declared
274251
// in AndroidManifest.xml. This plugin does not declare it, but others may and so we must
275252
// check the package info to determine if the permission is present.
253+
boolean manifestContainsCameraPermission = false;
276254

277-
if (!takePicturePermission) {
278-
takePicturePermission = true;
279-
try {
280-
PackageManager packageManager = this.cordova.getActivity().getPackageManager();
281-
String[] permissionsInPackage = packageManager.getPackageInfo(this.cordova.getActivity().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions;
282-
if (permissionsInPackage != null) {
283-
for (String permission : permissionsInPackage) {
284-
if (permission.equals(Manifest.permission.CAMERA)) {
285-
takePicturePermission = false;
286-
break;
287-
}
255+
// write permission is not necessary, unless if we are saving to photo album
256+
// On API 29+ devices, write permission is completely obsolete and not required.
257+
boolean manifestContainsWriteExternalPermission = false;
258+
259+
boolean cameraPermissionGranted = PermissionHelper.hasPermission(this, Manifest.permission.CAMERA);
260+
boolean writeExternalPermissionGranted = false;
261+
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
262+
writeExternalPermissionGranted = PermissionHelper.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
263+
}
264+
else {
265+
writeExternalPermissionGranted = true;
266+
}
267+
268+
try {
269+
PackageManager packageManager = this.cordova.getActivity().getPackageManager();
270+
String[] permissionsInPackage = packageManager.getPackageInfo(this.cordova.getActivity().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions;
271+
if (permissionsInPackage != null) {
272+
for (String permission : permissionsInPackage) {
273+
if (permission.equals(Manifest.permission.CAMERA)) {
274+
manifestContainsCameraPermission = true;
275+
}
276+
else if (permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
277+
manifestContainsWriteExternalPermission = true;
288278
}
289279
}
290-
} catch (NameNotFoundException e) {
291-
// We are requesting the info for our package, so this should
292-
// never be caught
293280
}
281+
} catch (NameNotFoundException e) {
282+
// We are requesting the info for our package, so this should
283+
// never be caught
284+
}
285+
286+
ArrayList<String> requiredPermissions = new ArrayList<>();
287+
if (manifestContainsCameraPermission && !cameraPermissionGranted) {
288+
requiredPermissions.add(Manifest.permission.CAMERA);
294289
}
295290

296-
if (takePicturePermission && saveAlbumPermission) {
291+
if (saveToPhotoAlbum && !writeExternalPermissionGranted) {
292+
// This block only applies for API 24-28
293+
// because writeExternalPermissionGranted is always true on API 29+
294+
if (!manifestContainsWriteExternalPermission) {
295+
throw new IllegalStateException("WRITE_EXTERNAL_STORAGE permission not declared in AndroidManifest");
296+
}
297+
298+
requiredPermissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
299+
}
300+
301+
if (!requiredPermissions.isEmpty()) {
302+
PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, requiredPermissions.toArray(new String[0]));
303+
}
304+
else {
297305
takePicture(returnType, encodingType);
298-
} else if (saveAlbumPermission) {
299-
PermissionHelper.requestPermission(this, TAKE_PIC_SEC, Manifest.permission.CAMERA);
300-
} else if (takePicturePermission) {
301-
PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, storagePermissions);
302-
} else {
303-
PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, getPermissions(false, mediaType));
304306
}
305307
}
306308

@@ -1358,15 +1360,6 @@ public void onRestoreStateForActivityResult(Bundle state, CallbackContext callba
13581360
this.callbackContext = callbackContext;
13591361
}
13601362

1361-
private boolean hasPermissions(String[] permissions) {
1362-
for (String permission: permissions) {
1363-
if (!PermissionHelper.hasPermission(this, permission)) {
1364-
return false;
1365-
}
1366-
}
1367-
return true;
1368-
}
1369-
13701363
/**
13711364
* Gets the ideal buffer size for processing streams of data.
13721365
*

0 commit comments

Comments
 (0)