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

feat(android)!: remove unnecessary permissions #308

Merged
merged 2 commits into from
Dec 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
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
breautek marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
Loading