Skip to content

Commit

Permalink
(Android) Support new media storage permissions on Android 13 / API 33.
Browse files Browse the repository at this point in the history
Resolves dpa99c#488.
  • Loading branch information
ath0mas committed Sep 17, 2023
1 parent d0561bc commit 6f4762e
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 21 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3623,7 +3623,13 @@ While the [cordova-diagnostic-plugin-example](https://github.com/dpa99c/cordova-

##### Android Camera permissions

Note that the Android variant of [`requestCameraAuthorization()`](#requestcameraauthorization) requests the `READ_EXTERNAL_STORAGE` permission, in addition to the `CAMERA` permission.
Note that the Android variant of [`requestCameraAuthorization()`](#requestcameraauthorization), in addition to the `CAMERA` permission, by default also requests storage permissions.
This is because the Android camera API requires access to the device's storage to store captured images and videos.

On Android <=12, this requires the `WRITE_EXTERNAL_STORAGE` and `READ_EXTERNAL_STORAGE` permissions.
On Android >12, this requires the `READ_MEDIA_IMAGES` and `READ_MEDIA_VIDEO` permissions.

the `READ_EXTERNAL_STORAGE` permission.
This is because the [[email protected]+](https://github.com/apache/cordova-plugin-camera) requires both of these permissions.

So to use this method in conjunction with the Cordova camera plugin, make sure you are using the most recent `cordova-plugin-camera` release: v2.2.0 or above.
Expand Down
20 changes: 20 additions & 0 deletions src/android/Diagnostic.java
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,17 @@ protected String[] jsonArrayToStringArray(JSONArray array) throws JSONException{
return arr;
}

protected JSONArray stringArrayToJsonArray(String[] array) throws JSONException{
if(array==null)
return null;

JSONArray arr = new JSONArray();
for(int i=0; i<array.length; i++) {
arr.put(i, array[i]);
}
return arr;
}

protected CallbackContext getContextById(String requestId) throws Exception{
if (!callbackContexts.containsKey(requestId)) {
throw new Exception("No context found for request id=" + requestId);
Expand Down Expand Up @@ -815,6 +826,15 @@ protected int getCurrentBatteryLevel(){



protected String[] concatStrings(String[] A, String[] B) {
int aLen = A.length;
int bLen = B.length;
String[] C= new String[aLen+bLen];
System.arraycopy(A, 0, C, 0, aLen);
System.arraycopy(B, 0, C, aLen, bLen);
return C;
}

/************
* Overrides
***********/
Expand Down
41 changes: 39 additions & 2 deletions src/android/Diagnostic_Camera.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ Licensed to the Apache Software Foundation (ASF) under one
import org.apache.cordova.CordovaWebView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.Objects;

/**
* Diagnostic plugin implementation for Android
Expand All @@ -49,6 +52,16 @@ public class Diagnostic_Camera extends CordovaPlugin{
*/
public static final String TAG = "Diagnostic_Camera";

protected static final String cameraPermission = "CAMERA";
protected static String[] storagePermissions;
static {
if (android.os.Build.VERSION.SDK_INT >= 33) { // Build.VERSION_CODES.TIRAMISU / Android 13
storagePermissions = new String[]{ "READ_MEDIA_IMAGES", "READ_MEDIA_VIDEO" };
} else {
storagePermissions = new String[]{ "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE" };
}
}


/*************
* Variables *
Expand Down Expand Up @@ -106,12 +119,16 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
try {
if(action.equals("isCameraPresent")) {
callbackContext.success(isCameraPresent() ? 1 : 0);
} else if(action.equals("requestCameraAuthorization")) {
requestCameraAuthorization(args, callbackContext);
} else if(action.equals("getCameraAuthorizationStatus")) {
getCameraAuthorizationStatus(args, callbackContext);
} else {
diagnostic.handleError("Invalid action");
return false;
}
}catch(Exception e ) {
diagnostic.handleError("Exception occurred: ".concat(e.getMessage()));
diagnostic.handleError("Exception occurred: ".concat(Objects.requireNonNull(e.getMessage())));
return false;
}
return true;
Expand All @@ -120,7 +137,7 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
public boolean isCameraPresent() {
int numberOfCameras = Camera.getNumberOfCameras();
PackageManager pm = this.cordova.getActivity().getPackageManager();
final boolean deviceHasCameraFlag = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA);
final boolean deviceHasCameraFlag = android.os.Build.VERSION.SDK_INT >= 32 ? pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) : pm.hasSystemFeature(PackageManager.FEATURE_CAMERA);
boolean result = (deviceHasCameraFlag && numberOfCameras>0 );
return result;
}
Expand All @@ -130,5 +147,25 @@ public boolean isCameraPresent() {
* Internals
***********/

private String[] getPermissions(boolean externalStorage){
String[] permissions = {cameraPermission};
if(externalStorage){
permissions = Diagnostic.instance.concatStrings(permissions, storagePermissions);
}
return permissions;
}

private void requestCameraAuthorization(JSONArray args, CallbackContext callbackContext) throws Exception{
boolean externalStorage = args.getBoolean(0);
String[] permissions = getPermissions(externalStorage);
int requestId = Diagnostic.instance.storeContextByRequestId(callbackContext);
Diagnostic.instance._requestRuntimePermissions(Diagnostic.instance.stringArrayToJsonArray(permissions), requestId);
}

private void getCameraAuthorizationStatus(JSONArray args, CallbackContext callbackContext) throws Exception{
boolean externalStorage = args.getBoolean(0);
String[] permissions = getPermissions(externalStorage);
JSONObject statuses = Diagnostic.instance._getPermissionsAuthorizationStatus(permissions);
callbackContext.success(statuses);
}
}
36 changes: 18 additions & 18 deletions www/android/diagnostic.camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ var Diagnostic_Camera = (function(){

function combineCameraStatuses(statuses){
var cameraStatus = statuses[Diagnostic.permission.CAMERA],
mediaStatus = statuses[Diagnostic.permission.READ_EXTERNAL_STORAGE],
storageStatus = statuses[Diagnostic.permission.READ_EXTERNAL_STORAGE] || statuses[Diagnostic.permission.READ_MEDIA_IMAGES],
status;

if(cameraStatus === Diagnostic.permissionStatus.DENIED_ALWAYS || mediaStatus === Diagnostic.permissionStatus.DENIED_ALWAYS){
if(cameraStatus === Diagnostic.permissionStatus.DENIED_ALWAYS || storageStatus === Diagnostic.permissionStatus.DENIED_ALWAYS){
status = Diagnostic.permissionStatus.DENIED_ALWAYS;
}else if(cameraStatus === Diagnostic.permissionStatus.DENIED_ONCE || mediaStatus === Diagnostic.permissionStatus.DENIED_ONCE){
}else if(cameraStatus === Diagnostic.permissionStatus.DENIED_ONCE || storageStatus === Diagnostic.permissionStatus.DENIED_ONCE){
status = Diagnostic.permissionStatus.DENIED_ONCE;
}else if(cameraStatus === Diagnostic.permissionStatus.NOT_REQUESTED || mediaStatus === Diagnostic.permissionStatus.NOT_REQUESTED){
}else if(cameraStatus === Diagnostic.permissionStatus.NOT_REQUESTED || storageStatus === Diagnostic.permissionStatus.NOT_REQUESTED){
status = Diagnostic.permissionStatus.NOT_REQUESTED;
}else{
status = Diagnostic.permissionStatus.GRANTED;
Expand Down Expand Up @@ -130,22 +130,22 @@ var Diagnostic_Camera = (function(){
* - {Function} successCallback - function to call on successful request for runtime permissions.
* This callback function is passed a single string parameter which defines the resulting authorisation status as a value in cordova.plugins.diagnostic.permissionStatus.
* - {Function} errorCallback - function to call on failure to request authorisation.
* - {Boolean} externalStorage - (Android only) If true, requests permission for READ_EXTERNAL_STORAGE in addition to CAMERA run-time permission.
* [email protected]+ requires both of these permissions. Defaults to true.
* - {Boolean} externalStorage - (Android only) If true, requests storage permissions for in addition to CAMERA run-time permission.
* Defaults to true.
*/
Diagnostic_Camera.requestCameraAuthorization = function(params){
params = mapFromLegacyCameraApi.apply(this, arguments);

var permissions = [Diagnostic.permission.CAMERA];
if(params.externalStorage !== false){
permissions.push(Diagnostic.permission.READ_EXTERNAL_STORAGE);
}

params.successCallback = params.successCallback || function(){};
var onSuccess = function(statuses){
params.successCallback(numberOfKeys(statuses) > 1 ? combineCameraStatuses(statuses): statuses[Diagnostic.permission.CAMERA]);
};
Diagnostic.requestRuntimePermissions(onSuccess, params.errorCallback, permissions);

return cordova.exec(onSuccess,
params.errorCallback,
'Diagnostic_Camera',
'requestCameraAuthorization',
[!!params.externalStorage]);
};

/**
Expand All @@ -161,16 +161,16 @@ var Diagnostic_Camera = (function(){
Diagnostic_Camera.getCameraAuthorizationStatus = function(params){
params = mapFromLegacyCameraApi.apply(this, arguments);

var permissions = [Diagnostic.permission.CAMERA];
if(params.externalStorage !== false){
permissions.push(Diagnostic.permission.READ_EXTERNAL_STORAGE);
}

params.successCallback = params.successCallback || function(){};
var onSuccess = function(statuses){
params.successCallback(numberOfKeys(statuses) > 1 ? combineCameraStatuses(statuses): statuses[Diagnostic.permission.CAMERA]);
};
Diagnostic.getPermissionsAuthorizationStatus(onSuccess, params.errorCallback, permissions);

return cordova.exec(onSuccess,
params.errorCallback,
'Diagnostic_Camera',
'getCameraAuthorizationStatus',
[!!params.externalStorage]);
};

/**
Expand Down

0 comments on commit 6f4762e

Please sign in to comment.