Skip to content

Commit

Permalink
feat(android)!: remove unnecessary permissions (#308)
Browse files Browse the repository at this point in the history
* remove READ/WRITE_EXTERNAL_STORAGE permissions

* refactor(android): Fixed code formatting to be consistent with existing format
  • Loading branch information
breautek authored Dec 28, 2024
1 parent 38a8cca commit d5ba992
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 52 deletions.
5 changes: 0 additions & 5 deletions plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,6 @@ xmlns:android="http://schemas.android.com/apk/res/android"
</provider>
</config-file>

<config-file target="AndroidManifest.xml" parent="/*">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
</config-file>

<source-file src="src/android/Capture.java" target-dir="src/org/apache/cordova/mediacapture" />
<source-file src="src/android/FileHelper.java" target-dir="src/org/apache/cordova/mediacapture" />
<source-file src="src/android/PendingRequests.java" target-dir="src/org/apache/cordova/mediacapture" />
Expand Down
148 changes: 101 additions & 47 deletions src/android/Capture.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ Licensed to the Apache Software Foundation (ASF) under one
package org.apache.cordova.mediacapture;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
Expand Down Expand Up @@ -51,12 +55,15 @@ Licensed to the Apache Software Foundation (ASF) under one
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.icu.util.Output;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.system.Os;
import android.system.OsConstants;

public class Capture extends CordovaPlugin {

Expand All @@ -78,18 +85,6 @@ public class Capture extends CordovaPlugin {
private static final int CAPTURE_PERMISSION_DENIED = 4;
private static final int CAPTURE_NOT_SUPPORTED = 20;

private static final String[] storagePermissions;
static {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
storagePermissions = new String[] {};
} else {
storagePermissions = new String[] {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
}
}

private boolean cameraPermissionInManifest; // Whether or not the CAMERA permission is declared in AndroidManifest.xml

private final PendingRequests pendingRequests = new PendingRequests();
Expand Down Expand Up @@ -228,34 +223,6 @@ private JSONObject getAudioVideoData(String filePath, JSONObject obj, boolean vi
return obj;
}

private boolean isMissingPermissions(Request req, List<String> permissions) {
List<String> missingPermissions = new ArrayList<>();
for (String permission : permissions) {
if (!PermissionHelper.hasPermission(this, permission)) {
missingPermissions.add(permission);
}
}

boolean isMissingPermissions = !missingPermissions.isEmpty();
if (isMissingPermissions) {
String[] missing = missingPermissions.toArray(new String[missingPermissions.size()]);
PermissionHelper.requestPermissions(this, req.requestCode, missing);
}
return isMissingPermissions;
}

private boolean isMissingPermissions(Request req) {
return isMissingPermissions(req, Arrays.asList(storagePermissions));
}

private boolean isMissingCameraPermissions(Request req) {
List<String> cameraPermissions = new ArrayList<>(Arrays.asList(storagePermissions));
if (cameraPermissionInManifest) {
cameraPermissions.add(Manifest.permission.CAMERA);
}
return isMissingPermissions(req, cameraPermissions);
}

private String getTempDirectoryPath() {
File cache = new File(cordova.getActivity().getCacheDir(), "org.apache.cordova.mediacapture");

Expand All @@ -268,8 +235,6 @@ private String getTempDirectoryPath() {
* Sets up an intent to capture audio. Result handled by onActivityResult()
*/
private void captureAudio(Request req) {
if (isMissingPermissions(req)) return;

try {
Intent intent = new Intent(android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION);
String timeStamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
Expand All @@ -279,7 +244,6 @@ private void captureAudio(Request req) {
this.applicationId + ".cordova.plugin.mediacapture.provider",
audio);
this.audioAbsolutePath = audio.getAbsolutePath();
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, audioUri);
LOG.d(LOG_TAG, "Recording an audio and saving to: " + this.audioAbsolutePath);

this.cordova.startActivityForResult((CordovaPlugin) this, intent, req.requestCode);
Expand All @@ -288,11 +252,42 @@ private void captureAudio(Request req) {
}
}

/**
* Checks for and requests the camera permission if necessary.
*
* Returns a boolean which if true, signals that the permission has been granted, or that the
* permission isn't necessary and that the action may continue as normal.
*
* If the response is false, then the action should stop performing, as a permission prompt
* will be presented to the user. The action based on the request's requestCode will be invoked
* later.
*
* @param req
* @return
*/
private boolean requestCameraPermission(Request req) {
boolean cameraPermissionGranted = true; // We will default to true, but if the manifest
// declares the permission, then we need to check
// for the grant
if (cameraPermissionInManifest) {
cameraPermissionGranted = PermissionHelper.hasPermission(this, Manifest.permission.CAMERA);
}

if (!cameraPermissionGranted) {
PermissionHelper.requestPermissions(this, req.requestCode, new String[]{Manifest.permission.CAMERA});
return false;
}

return true;
}

/**
* Sets up an intent to capture images. Result handled by onActivityResult()
*/
private void captureImage(Request req) {
if (isMissingCameraPermissions(req)) return;
if (!requestCameraPermission(req)) {
return;
}

Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);

Expand All @@ -307,6 +302,8 @@ private void captureImage(Request req) {
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, imageUri);
LOG.d(LOG_TAG, "Taking a picture and saving to: " + this.imageAbsolutePath);

intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, imageUri);

this.cordova.startActivityForResult((CordovaPlugin) this, intent, req.requestCode);
Expand All @@ -316,7 +313,9 @@ private void captureImage(Request req) {
* Sets up an intent to capture video. Result handled by onActivityResult()
*/
private void captureVideo(Request req) {
if (isMissingCameraPermissions(req)) return;
if (!requestCameraPermission(req)) {
return;
}

Intent intent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE);
String timeStamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
Expand All @@ -328,6 +327,7 @@ private void captureVideo(Request req) {
movie);
this.videoAbsolutePath = movie.getAbsolutePath();
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, videoUri);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
LOG.d(LOG_TAG, "Recording a video and saving to: " + this.videoAbsolutePath);

if(Build.VERSION.SDK_INT > 7){
Expand Down Expand Up @@ -356,7 +356,7 @@ public void onActivityResult(int requestCode, int resultCode, final Intent inten
public void run() {
switch(req.action) {
case CAPTURE_AUDIO:
onAudioActivityResult(req);
onAudioActivityResult(req, intent);
break;
case CAPTURE_IMAGE:
onImageActivityResult(req);
Expand Down Expand Up @@ -394,8 +394,42 @@ else if (resultCode == Activity.RESULT_CANCELED) {
}
}

public void onAudioActivityResult(Request req, Intent intent) {
Uri uri = intent.getData();

InputStream input = null;
OutputStream output = null;
try {
if (uri == null) {
throw new IOException("Unable to open input audio");
}

input = this.cordova.getActivity().getContentResolver().openInputStream(uri);

if (input == null) {
throw new IOException("Unable to open input audio");
}

output = new FileOutputStream(this.audioAbsolutePath);

byte[] buffer = new byte[getPageSize()];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
} catch (FileNotFoundException e) {
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_INTERNAL_ERR, "Error: Unable to read input audio: File not found"));
} catch (IOException e) {
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_INTERNAL_ERR, "Error: Unable to read input audio"));
} finally {
try {
if (output != null) output.close();
if (input != null) input.close();
} catch (IOException ex) {
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_INTERNAL_ERR, "Error: Unable to copy input audio"));
}
}

public void onAudioActivityResult(Request req) {
// create a file object from the audio absolute path
JSONObject mediaFile = createMediaFileWithAbsolutePath(this.audioAbsolutePath);
if (mediaFile == null) {
Expand Down Expand Up @@ -577,4 +611,24 @@ public Bundle onSaveInstanceState() {
public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) {
pendingRequests.setLastSavedState(state, callbackContext);
}

/**
* Gets the ideal buffer size for processing streams of data.
*
* @return The page size of the device.
*/
private int getPageSize() {
// Get the page size of the device. Most devices will be 4096 (4kb)
// Newer devices may be 16kb
long ps = Os.sysconf(OsConstants._SC_PAGE_SIZE);

// sysconf returns a long because it's a general purpose API
// the expected value of a page size should not exceed an int,
// but we guard it here to avoid integer overflow just in case
if (ps > Integer.MAX_VALUE) {
ps = Integer.MAX_VALUE;
}

return (int) ps;
}
}

0 comments on commit d5ba992

Please sign in to comment.