From d5ba9923328b3d8cb6a00b3b7aa0d3d7ea9d48bf Mon Sep 17 00:00:00 2001 From: Norman Breau Date: Sat, 28 Dec 2024 15:27:17 -0400 Subject: [PATCH] feat(android)!: remove unnecessary permissions (#308) * remove READ/WRITE_EXTERNAL_STORAGE permissions * refactor(android): Fixed code formatting to be consistent with existing format --- plugin.xml | 5 -- src/android/Capture.java | 148 ++++++++++++++++++++++++++------------- 2 files changed, 101 insertions(+), 52 deletions(-) diff --git a/plugin.xml b/plugin.xml index c9ca1c58..e8975ed0 100644 --- a/plugin.xml +++ b/plugin.xml @@ -88,11 +88,6 @@ xmlns:android="http://schemas.android.com/apk/res/android" - - - - - diff --git a/src/android/Capture.java b/src/android/Capture.java index fd2b56c5..29111f24 100644 --- a/src/android/Capture.java +++ b/src/android/Capture.java @@ -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; @@ -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 { @@ -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(); @@ -228,34 +223,6 @@ private JSONObject getAudioVideoData(String filePath, JSONObject obj, boolean vi return obj; } - private boolean isMissingPermissions(Request req, List permissions) { - List 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 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"); @@ -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()); @@ -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); @@ -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); @@ -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); @@ -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()); @@ -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){ @@ -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); @@ -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) { @@ -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; + } }