Repository: cordova-plugin-media-capture Updated Branches: refs/heads/master 086789870 -> 7646d75bc
CB-10670, CB-10994 android: Marshmallow permissions Fixes security exceptions caused by a missing request for WRITE_EXTERNAL_STORAGE when capturing an image and not requesting the CAMERA permission in the case where it has been inserted into the manifest by some other plugin. This closes #59 Project: http://git-wip-us.apache.org/repos/asf/cordova-plugin-media-capture/repo Commit: http://git-wip-us.apache.org/repos/asf/cordova-plugin-media-capture/commit/7646d75b Tree: http://git-wip-us.apache.org/repos/asf/cordova-plugin-media-capture/tree/7646d75b Diff: http://git-wip-us.apache.org/repos/asf/cordova-plugin-media-capture/diff/7646d75b Branch: refs/heads/master Commit: 7646d75bcac0ec745f3c02dec0cea8cfe819463e Parents: 0867898 Author: Richard Knoll <[email protected]> Authored: Mon Mar 21 15:21:30 2016 -0700 Committer: Richard Knoll <[email protected]> Committed: Wed Mar 30 13:46:11 2016 -0700 ---------------------------------------------------------------------- README.md | 2 + plugin.xml | 26 +- src/android/Capture.java | 424 +++++++++++++++++++-------------- src/android/PendingRequests.java | 132 ++++++++++ src/android/PermissionHelper.java | 138 +++++++++++ www/CaptureError.js | 2 + 6 files changed, 529 insertions(+), 195 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cordova-plugin-media-capture/blob/7646d75b/README.md ---------------------------------------------------------------------- diff --git a/README.md b/README.md index 7877681..4bdc3da 100644 --- a/README.md +++ b/README.md @@ -423,6 +423,8 @@ Each `MediaFile` object describes a captured media file. - `CaptureError.CAPTURE_NO_MEDIA_FILES`: The user exits the camera or audio capture application before capturing anything. +- `CaptureError.CAPTURE_PERMISSION_DENIED`: The user denied a permission required to perform the given capture request. + - `CaptureError.CAPTURE_NOT_SUPPORTED`: The requested capture operation is not supported. ## CaptureErrorCB http://git-wip-us.apache.org/repos/asf/cordova-plugin-media-capture/blob/7646d75b/plugin.xml ---------------------------------------------------------------------- diff --git a/plugin.xml b/plugin.xml index 37679f3..b9528af 100644 --- a/plugin.xml +++ b/plugin.xml @@ -30,13 +30,13 @@ xmlns:rim="http://www.blackberry.com/ns/widgets" <keywords>cordova,media,capture</keywords> <repo>https://git-wip-us.apache.org/repos/asf/cordova-plugin-media-capture.git</repo> <issue>https://issues.apache.org/jira/browse/CB/component/12320646</issue> - + <dependency id="cordova-plugin-file" version="^4.0.0" /> <js-module src="www/CaptureAudioOptions.js" name="CaptureAudioOptions"> <clobbers target="CaptureAudioOptions" /> </js-module> - + <js-module src="www/CaptureImageOptions.js" name="CaptureImageOptions"> <clobbers target="CaptureImageOptions" /> </js-module> @@ -44,7 +44,7 @@ xmlns:rim="http://www.blackberry.com/ns/widgets" <js-module src="www/CaptureVideoOptions.js" name="CaptureVideoOptions"> <clobbers target="CaptureVideoOptions" /> </js-module> - + <js-module src="www/CaptureError.js" name="CaptureError"> <clobbers target="CaptureError" /> </js-module> @@ -52,11 +52,11 @@ xmlns:rim="http://www.blackberry.com/ns/widgets" <js-module src="www/MediaFileData.js" name="MediaFileData"> <clobbers target="MediaFileData" /> </js-module> - + <js-module src="www/MediaFile.js" name="MediaFile"> <clobbers target="MediaFile" /> </js-module> - + <js-module src="www/capture.js" name="capture"> <clobbers target="navigator.device.capture" /> </js-module> @@ -68,7 +68,7 @@ xmlns:rim="http://www.blackberry.com/ns/widgets" <param name="android-package" value="org.apache.cordova.mediacapture.Capture"/> </feature> </config-file> - + <config-file target="AndroidManifest.xml" parent="/*"> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_VIDEO"/> @@ -77,8 +77,10 @@ xmlns:rim="http://www.blackberry.com/ns/widgets" <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/PermissionHelper.java" target-dir="src/org/apache/cordova/mediacapture" /> + <source-file src="src/android/PendingRequests.java" target-dir="src/org/apache/cordova/mediacapture" /> </platform> - + <!-- amazon-fireos --> <platform name="amazon-fireos"> <config-file target="res/xml/config.xml" parent="/*"> @@ -86,7 +88,7 @@ xmlns:rim="http://www.blackberry.com/ns/widgets" <param name="android-package" value="org.apache.cordova.mediacapture.Capture"/> </feature> </config-file> - + <config-file target="AndroidManifest.xml" parent="/*"> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_VIDEO"/> @@ -96,7 +98,7 @@ xmlns:rim="http://www.blackberry.com/ns/widgets" <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" /> </platform> - + <!-- ubuntu --> <platform name="ubuntu"> @@ -122,16 +124,16 @@ xmlns:rim="http://www.blackberry.com/ns/widgets" </platform> <!-- ios --> - <platform name="ios"> + <platform name="ios"> <config-file target="config.xml" parent="/*"> <feature name="Capture"> - <param name="ios-package" value="CDVCapture" /> + <param name="ios-package" value="CDVCapture" /> </feature> </config-file> <header-file src="src/ios/CDVCapture.h" /> <source-file src="src/ios/CDVCapture.m" /> <resource-file src="src/ios/CDVCapture.bundle" /> - + <framework src="CoreGraphics.framework" /> <framework src="MobileCoreServices.framework" /> </platform> http://git-wip-us.apache.org/repos/asf/cordova-plugin-media-capture/blob/7646d75b/src/android/Capture.java ---------------------------------------------------------------------- diff --git a/src/android/Capture.java b/src/android/Capture.java index 8afed36..9e60c38 100644 --- a/src/android/Capture.java +++ b/src/android/Capture.java @@ -35,14 +35,17 @@ import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.LOG; import org.apache.cordova.PluginManager; -import org.apache.cordova.PluginResult; +import org.apache.cordova.mediacapture.PendingRequests.Request; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import android.Manifest; import android.app.Activity; import android.content.ContentValues; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.database.Cursor; import android.graphics.BitmapFactory; import android.media.MediaPlayer; @@ -67,14 +70,13 @@ public class Capture extends CordovaPlugin { // private static final int CAPTURE_APPLICATION_BUSY = 1; // private static final int CAPTURE_INVALID_ARGUMENT = 2; private static final int CAPTURE_NO_MEDIA_FILES = 3; + private static final int CAPTURE_PERMISSION_DENIED = 4; + + private boolean cameraPermissionInManifest; // Whether or not the CAMERA permission is declared in AndroidManifest.xml + + private final PendingRequests pendingRequests = new PendingRequests(); - private CallbackContext callbackContext; // The callback context from which we were invoked. - private long limit; // the number of pics/vids/clips to take - private int duration; // optional max duration of video recording in seconds - private JSONArray results; // The array of results to be returned to the user private int numPics; // Number of pictures before capture activity - private int quality; // Quality level for video capture 0 low, 1 high - //private CordovaInterface cordova; // public void setContext(Context mCtx) // { @@ -85,33 +87,50 @@ public class Capture extends CordovaPlugin { // } @Override - public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { - this.callbackContext = callbackContext; - this.limit = 1; - this.duration = 0; - this.results = new JSONArray(); - this.quality = 1; + protected void pluginInitialize() { + super.pluginInitialize(); - JSONObject options = args.optJSONObject(0); - if (options != null) { - limit = options.optLong("limit", 1); - duration = options.optInt("duration", 0); - quality = options.optInt("quality", 1); + // CB-10670: The CAMERA permission does not need to be requested unless it is declared + // in AndroidManifest.xml. This plugin does not declare it, but others may and so we must + // check the package info to determine if the permission is present. + + cameraPermissionInManifest = false; + try { + PackageManager packageManager = this.cordova.getActivity().getPackageManager(); + String[] permissionsInPackage = packageManager.getPackageInfo(this.cordova.getActivity().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions; + if (permissionsInPackage != null) { + for (String permission : permissionsInPackage) { + if (permission.equals(Manifest.permission.CAMERA)) { + cameraPermissionInManifest = true; + break; + } + } + } + } catch (NameNotFoundException e) { + // We are requesting the info for our package, so this should + // never be caught + LOG.e(LOG_TAG, "Failed checking for CAMERA permission in manifest", e); } + } + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { if (action.equals("getFormatData")) { JSONObject obj = getFormatData(args.getString(0), args.getString(1)); callbackContext.success(obj); return true; } - else if (action.equals("captureAudio")) { - this.captureAudio(); + + JSONObject options = args.optJSONObject(0); + + if (action.equals("captureAudio")) { + this.captureAudio(pendingRequests.createRequest(CAPTURE_AUDIO, options, callbackContext)); } else if (action.equals("captureImage")) { - this.captureImage(); + this.captureImage(pendingRequests.createRequest(CAPTURE_IMAGE, options, callbackContext)); } else if (action.equals("captureVideo")) { - this.captureVideo(duration, quality); + this.captureVideo(pendingRequests.createRequest(CAPTURE_VIDEO, options, callbackContext)); } else { return false; @@ -201,10 +220,10 @@ public class Capture extends CordovaPlugin { /** * Sets up an intent to capture audio. Result handled by onActivityResult() */ - private void captureAudio() { + private void captureAudio(Request req) { Intent intent = new Intent(android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION); - this.cordova.startActivityForResult((CordovaPlugin) this, intent, CAPTURE_AUDIO); + this.cordova.startActivityForResult((CordovaPlugin) this, intent, req.requestCode); } private String getTempDirectoryPath() { @@ -221,24 +240,40 @@ public class Capture extends CordovaPlugin { /** * Sets up an intent to capture images. Result handled by onActivityResult() */ - private void captureImage() { - // Save the number of images currently on disk for later - this.numPics = queryImgDB(whichContentStore()).getCount(); + private void captureImage(Request req) { + boolean needExternalStoragePermission = + !PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); + + boolean needCameraPermission = cameraPermissionInManifest && + !PermissionHelper.hasPermission(this, Manifest.permission.CAMERA); + + if (needExternalStoragePermission || needCameraPermission) { + if (needExternalStoragePermission && needCameraPermission) { + PermissionHelper.requestPermissions(this, req.requestCode, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA}); + } else if (needExternalStoragePermission) { + PermissionHelper.requestPermission(this, req.requestCode, Manifest.permission.READ_EXTERNAL_STORAGE); + } else { + PermissionHelper.requestPermission(this, req.requestCode, Manifest.permission.CAMERA); + } + } else { + // Save the number of images currently on disk for later + this.numPics = queryImgDB(whichContentStore()).getCount(); - Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); + Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); - // Specify file so that large image is captured and returned - File photo = new File(getTempDirectoryPath(), "Capture.jpg"); - try { - // the ACTION_IMAGE_CAPTURE is run under different credentials and has to be granted write permissions - createWritableFile(photo); - } catch (IOException ex) { - this.fail(createErrorObject(CAPTURE_INTERNAL_ERR, ex.toString())); - return; - } - intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(photo)); + // Specify file so that large image is captured and returned + File photo = new File(getTempDirectoryPath(), "Capture.jpg"); + try { + // the ACTION_IMAGE_CAPTURE is run under different credentials and has to be granted write permissions + createWritableFile(photo); + } catch (IOException ex) { + pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_INTERNAL_ERR, ex.toString())); + return; + } + intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(photo)); - this.cordova.startActivityForResult((CordovaPlugin) this, intent, CAPTURE_IMAGE); + this.cordova.startActivityForResult((CordovaPlugin) this, intent, req.requestCode); + } } private static void createWritableFile(File file) throws IOException { @@ -249,14 +284,18 @@ public class Capture extends CordovaPlugin { /** * Sets up an intent to capture video. Result handled by onActivityResult() */ - private void captureVideo(int duration, int quality) { - Intent intent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE); + private void captureVideo(Request req) { + if(cameraPermissionInManifest && !PermissionHelper.hasPermission(this, Manifest.permission.CAMERA)) { + PermissionHelper.requestPermission(this, req.requestCode, Manifest.permission.CAMERA); + } else { + Intent intent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE); - if(Build.VERSION.SDK_INT > 7){ - intent.putExtra("android.intent.extra.durationLimit", duration); - intent.putExtra("android.intent.extra.videoQuality", quality); + if(Build.VERSION.SDK_INT > 7){ + intent.putExtra("android.intent.extra.durationLimit", req.duration); + intent.putExtra("android.intent.extra.videoQuality", req.quality); + } + this.cordova.startActivityForResult((CordovaPlugin) this, intent, req.requestCode); } - this.cordova.startActivityForResult((CordovaPlugin) this, intent, CAPTURE_VIDEO); } /** @@ -269,153 +308,147 @@ public class Capture extends CordovaPlugin { * @throws JSONException */ public void onActivityResult(int requestCode, int resultCode, final Intent intent) { + final Request req = pendingRequests.get(requestCode); // Result received okay if (resultCode == Activity.RESULT_OK) { - // An audio clip was requested - if (requestCode == CAPTURE_AUDIO) { - - final Capture that = this; - Runnable captureAudio = new Runnable() { - - @Override - public void run() { - // Get the uri of the audio clip - Uri data = intent.getData(); - // create a file object from the uri - results.put(createMediaFile(data)); - - if (results.length() >= limit) { - // Send Uri back to JavaScript for listening to audio - that.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, results)); - } else { - // still need to capture more audio clips - captureAudio(); - } - } - }; - this.cordova.getThreadPool().execute(captureAudio); - } else if (requestCode == CAPTURE_IMAGE) { - // For some reason if I try to do: - // Uri data = intent.getData(); - // It crashes in the emulator and on my phone with a null pointer exception - // To work around it I had to grab the code from CameraLauncher.java - - final Capture that = this; - Runnable captureImage = new Runnable() { - @Override - public void run() { - try { - // TODO Auto-generated method stub - // Create entry in media store for image - // (Don't use insertImage() because it uses default compression setting of 50 - no way to change it) - ContentValues values = new ContentValues(); - values.put(android.provider.MediaStore.Images.Media.MIME_TYPE, IMAGE_JPEG); - Uri uri = null; - try { - uri = that.cordova.getActivity().getContentResolver().insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); - } catch (UnsupportedOperationException e) { - LOG.d(LOG_TAG, "Can't write to external media storage."); - try { - uri = that.cordova.getActivity().getContentResolver().insert(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, values); - } catch (UnsupportedOperationException ex) { - LOG.d(LOG_TAG, "Can't write to internal media storage."); - that.fail(createErrorObject(CAPTURE_INTERNAL_ERR, "Error capturing image - no media storage found.")); - return; - } - } - FileInputStream fis = new FileInputStream(getTempDirectoryPath() + "/Capture.jpg"); - OutputStream os = that.cordova.getActivity().getContentResolver().openOutputStream(uri); - byte[] buffer = new byte[4096]; - int len; - while ((len = fis.read(buffer)) != -1) { - os.write(buffer, 0, len); - } - os.flush(); - os.close(); - fis.close(); - - // Add image to results - results.put(createMediaFile(uri)); - - checkForDuplicateImage(); - - if (results.length() >= limit) { - // Send Uri back to JavaScript for viewing image - that.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, results)); - } else { - // still need to capture more images - captureImage(); - } - } catch (IOException e) { - e.printStackTrace(); - that.fail(createErrorObject(CAPTURE_INTERNAL_ERR, "Error capturing image.")); - } + Runnable processActivityResult = new Runnable() { + @Override + public void run() { + switch(req.action) { + case CAPTURE_AUDIO: + onAudioActivityResult(req, intent); + break; + case CAPTURE_IMAGE: + onImageActivityResult(req); + break; + case CAPTURE_VIDEO: + onVideoActivityResult(req, intent); + break; } - }; - this.cordova.getThreadPool().execute(captureImage); - } else if (requestCode == CAPTURE_VIDEO) { - - final Capture that = this; - Runnable captureVideo = new Runnable() { - - @Override - public void run() { - - Uri data = null; - - if (intent != null){ - // Get the uri of the video clip - data = intent.getData(); - } - - if( data == null){ - File movie = new File(getTempDirectoryPath(), "Capture.avi"); - data = Uri.fromFile(movie); - } - - // create a file object from the uri - if(data == null) - { - that.fail(createErrorObject(CAPTURE_NO_MEDIA_FILES, "Error: data is null")); - } - else - { - results.put(createMediaFile(data)); - - if (results.length() >= limit) { - // Send Uri back to JavaScript for viewing video - that.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, results)); - } else { - // still need to capture more video clips - captureVideo(duration, quality); - } - } - } - }; - this.cordova.getThreadPool().execute(captureVideo); - } + } + }; + + this.cordova.getThreadPool().execute(processActivityResult); } // If canceled else if (resultCode == Activity.RESULT_CANCELED) { // If we have partial results send them back to the user - if (results.length() > 0) { - this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, results)); + if (req.results.length() > 0) { + pendingRequests.resolveWithSuccess(req); } // user canceled the action else { - this.fail(createErrorObject(CAPTURE_NO_MEDIA_FILES, "Canceled.")); + pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NO_MEDIA_FILES, "Canceled.")); } } // If something else else { // If we have partial results send them back to the user - if (results.length() > 0) { - this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, results)); + if (req.results.length() > 0) { + pendingRequests.resolveWithSuccess(req); } // something bad happened else { - this.fail(createErrorObject(CAPTURE_NO_MEDIA_FILES, "Did not complete!")); + pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NO_MEDIA_FILES, "Did not complete!")); + } + } + } + + + public void onAudioActivityResult(Request req, Intent intent) { + // Get the uri of the audio clip + Uri data = intent.getData(); + // create a file object from the uri + req.results.put(createMediaFile(data)); + + if (req.results.length() >= req.limit) { + // Send Uri back to JavaScript for listening to audio + pendingRequests.resolveWithSuccess(req); + } else { + // still need to capture more audio clips + captureAudio(req); + } + } + + public void onImageActivityResult(Request req) { + // For some reason if I try to do: + // Uri data = intent.getData(); + // It crashes in the emulator and on my phone with a null pointer exception + // To work around it I had to grab the code from CameraLauncher.java + try { + // Create entry in media store for image + // (Don't use insertImage() because it uses default compression setting of 50 - no way to change it) + ContentValues values = new ContentValues(); + values.put(android.provider.MediaStore.Images.Media.MIME_TYPE, IMAGE_JPEG); + Uri uri = null; + try { + uri = this.cordova.getActivity().getContentResolver().insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + } catch (UnsupportedOperationException e) { + LOG.d(LOG_TAG, "Can't write to external media storage."); + try { + uri = this.cordova.getActivity().getContentResolver().insert(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, values); + } catch (UnsupportedOperationException ex) { + LOG.d(LOG_TAG, "Can't write to internal media storage."); + pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_INTERNAL_ERR, "Error capturing image - no media storage found.")); + return; + } + } + FileInputStream fis = new FileInputStream(getTempDirectoryPath() + "/Capture.jpg"); + OutputStream os = this.cordova.getActivity().getContentResolver().openOutputStream(uri); + byte[] buffer = new byte[4096]; + int len; + while ((len = fis.read(buffer)) != -1) { + os.write(buffer, 0, len); + } + os.flush(); + os.close(); + fis.close(); + + // Add image to results + req.results.put(createMediaFile(uri)); + + checkForDuplicateImage(); + + if (req.results.length() >= req.limit) { + // Send Uri back to JavaScript for viewing image + pendingRequests.resolveWithSuccess(req); + } else { + // still need to capture more images + captureImage(req); + } + } catch (IOException e) { + e.printStackTrace(); + pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_INTERNAL_ERR, "Error capturing image.")); + } + } + + public void onVideoActivityResult(Request req, Intent intent) { + Uri data = null; + + if (intent != null){ + // Get the uri of the video clip + data = intent.getData(); + } + + if( data == null){ + File movie = new File(getTempDirectoryPath(), "Capture.avi"); + data = Uri.fromFile(movie); + } + + // create a file object from the uri + if(data == null) { + pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NO_MEDIA_FILES, "Error: data is null")); + } + else { + req.results.put(createMediaFile(data)); + + if (req.results.length() >= req.limit) { + // Send Uri back to JavaScript for viewing video + pendingRequests.resolveWithSuccess(req); + } else { + // still need to capture more video clips + captureVideo(req); } } } @@ -492,16 +525,6 @@ public class Capture extends CordovaPlugin { } /** - * Send error message to JavaScript. - * - * @param err - */ - public void fail(JSONObject err) { - this.callbackContext.error(err); - } - - - /** * Creates a cursor that can be used to determine how many images we have. * * @return a cursor @@ -544,4 +567,39 @@ public class Capture extends CordovaPlugin { return android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI; } } + + private void executeRequest(Request req) { + switch (req.action) { + case CAPTURE_AUDIO: + this.captureAudio(req); + break; + case CAPTURE_IMAGE: + this.captureImage(req); + break; + case CAPTURE_VIDEO: + this.captureVideo(req); + break; + } + } + + public void onRequestPermissionResult(int requestCode, String[] permissions, + int[] grantResults) throws JSONException { + Request req = pendingRequests.get(requestCode); + + if (req != null) { + boolean success = true; + for(int r:grantResults) { + if (r == PackageManager.PERMISSION_DENIED) { + success = false; + break; + } + } + + if (success) { + executeRequest(req); + } else { + pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_PERMISSION_DENIED, "Permission denied.")); + } + } + } } http://git-wip-us.apache.org/repos/asf/cordova-plugin-media-capture/blob/7646d75b/src/android/PendingRequests.java ---------------------------------------------------------------------- diff --git a/src/android/PendingRequests.java b/src/android/PendingRequests.java new file mode 100644 index 0000000..5676bd9 --- /dev/null +++ b/src/android/PendingRequests.java @@ -0,0 +1,132 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +package org.apache.cordova.mediacapture; + +import android.util.SparseArray; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Holds the pending javascript requests for the plugin + */ +public class PendingRequests { + private int currentReqId = 0; + private SparseArray<Request> requests = new SparseArray<Request>(); + + /** + * Creates a request and adds it to the array of pending requests. Each created request gets a + * unique result code for use with startActivityForResult() and requestPermission() + * @param action The action this request corresponds to (capture image, capture audio, etc.) + * @param options The options for this request passed from the javascript + * @param callbackContext The CallbackContext to return the result to + * @return The newly created Request object with a unique result code + * @throws JSONException + */ + public synchronized Request createRequest(int action, JSONObject options, CallbackContext callbackContext) throws JSONException { + Request req = new Request(action, options, callbackContext); + requests.put(req.requestCode, req); + return req; + } + + /** + * Gets the request corresponding to this request code + * @param requestCode The request code for the desired request + * @return The request corresponding to the given request code or null if such a + * request is not found + */ + public synchronized Request get(int requestCode) { + return requests.get(requestCode); + } + + /** + * Removes the request from the array of pending requests and sends an error plugin result + * to the CallbackContext that contains the given error object + * @param req The request to be resolved + * @param error The error to be returned to the CallbackContext + */ + public synchronized void resolveWithFailure(Request req, JSONObject error) { + req.callbackContext.error(error); + requests.remove(req.requestCode); + } + + /** + * Removes the request from the array of pending requests and sends a successful plugin result + * to the CallbackContext that contains the result of the request + * @param req The request to be resolved + */ + public synchronized void resolveWithSuccess(Request req) { + req.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, req.results)); + requests.remove(req.requestCode); + } + + + /** + * Each request gets a unique ID that represents its request code when calls are made to + * Activities and for permission requests + * @return A unique request code + */ + private synchronized int incrementCurrentReqId() { + return currentReqId ++; + } + + /** + * Holds the options and CallbackContext for a capture request made to the plugin. + */ + public class Request { + + // Unique int used to identify this request in any Android Permission or Activity callbacks + public int requestCode; + + // The action that this request is performing + public int action; + + // The number of pics/vids/audio clips to take (CAPTURE_IMAGE, CAPTURE_VIDEO, CAPTURE_AUDIO) + public long limit = 1; + + // Optional max duration of recording in seconds (CAPTURE_VIDEO only) + public int duration = 0; + + // Quality level for video capture 0 low, 1 high (CAPTURE_VIDEO only) + public int quality = 1; + + // The array of results to be returned to the javascript callback on success + public JSONArray results = new JSONArray(); + + // The callback context for this plugin request + private CallbackContext callbackContext; + + private Request(int action, JSONObject options, CallbackContext callbackContext) throws JSONException { + this.callbackContext = callbackContext; + this.action = action; + + if (options != null) { + this.limit = options.optLong("limit", 1); + this.duration = options.optInt("duration", 0); + this.quality = options.optInt("quality", 1); + } + + this.requestCode = incrementCurrentReqId(); + } + } +} http://git-wip-us.apache.org/repos/asf/cordova-plugin-media-capture/blob/7646d75b/src/android/PermissionHelper.java ---------------------------------------------------------------------- diff --git a/src/android/PermissionHelper.java b/src/android/PermissionHelper.java new file mode 100644 index 0000000..e4d2f98 --- /dev/null +++ b/src/android/PermissionHelper.java @@ -0,0 +1,138 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova.mediacapture; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.LOG; + +import android.content.pm.PackageManager; + +/** + * This class provides reflective methods for permission requesting and checking so that plugins + * written for cordova-android 5.0.0+ can still compile with earlier cordova-android versions. + */ +public class PermissionHelper { + private static final String LOG_TAG = "CordovaPermissionHelper"; + + /** + * Requests a "dangerous" permission for the application at runtime. This is a helper method + * alternative to cordovaInterface.requestPermission() that does not require the project to be + * built with cordova-android 5.0.0+ + * + * @param plugin The plugin the permission is being requested for + * @param requestCode A requestCode to be passed to the plugin's onRequestPermissionResult() + * along with the result of the permission request + * @param permission The permission to be requested + */ + public static void requestPermission(CordovaPlugin plugin, int requestCode, String permission) { + PermissionHelper.requestPermissions(plugin, requestCode, new String[] {permission}); + } + + /** + * Requests "dangerous" permissions for the application at runtime. This is a helper method + * alternative to cordovaInterface.requestPermissions() that does not require the project to be + * built with cordova-android 5.0.0+ + * + * @param plugin The plugin the permissions are being requested for + * @param requestCode A requestCode to be passed to the plugin's onRequestPermissionResult() + * along with the result of the permissions request + * @param permissions The permissions to be requested + */ + public static void requestPermissions(CordovaPlugin plugin, int requestCode, String[] permissions) { + try { + Method requestPermission = CordovaInterface.class.getDeclaredMethod( + "requestPermissions", CordovaPlugin.class, int.class, String[].class); + + // If there is no exception, then this is cordova-android 5.0.0+ + requestPermission.invoke(plugin.cordova, plugin, requestCode, permissions); + } catch (NoSuchMethodException noSuchMethodException) { + // cordova-android version is less than 5.0.0, so permission is implicitly granted + LOG.d(LOG_TAG, "No need to request permissions " + Arrays.toString(permissions)); + + // Notify the plugin that all were granted by using more reflection + deliverPermissionResult(plugin, requestCode, permissions); + } catch (IllegalAccessException illegalAccessException) { + // Should never be caught; this is a public method + LOG.e(LOG_TAG, "IllegalAccessException when requesting permissions " + Arrays.toString(permissions), illegalAccessException); + } catch(InvocationTargetException invocationTargetException) { + // This method does not throw any exceptions, so this should never be caught + LOG.e(LOG_TAG, "invocationTargetException when requesting permissions " + Arrays.toString(permissions), invocationTargetException); + } + } + + /** + * Checks at runtime to see if the application has been granted a permission. This is a helper + * method alternative to cordovaInterface.hasPermission() that does not require the project to + * be built with cordova-android 5.0.0+ + * + * @param plugin The plugin the permission is being checked against + * @param permission The permission to be checked + * + * @return True if the permission has already been granted and false otherwise + */ + public static boolean hasPermission(CordovaPlugin plugin, String permission) { + try { + Method hasPermission = CordovaInterface.class.getDeclaredMethod("hasPermission", String.class); + + // If there is no exception, then this is cordova-android 5.0.0+ + return (Boolean) hasPermission.invoke(plugin.cordova, permission); + } catch (NoSuchMethodException noSuchMethodException) { + // cordova-android version is less than 5.0.0, so permission is implicitly granted + LOG.d(LOG_TAG, "No need to check for permission " + permission); + return true; + } catch (IllegalAccessException illegalAccessException) { + // Should never be caught; this is a public method + LOG.e(LOG_TAG, "IllegalAccessException when checking permission " + permission, illegalAccessException); + } catch(InvocationTargetException invocationTargetException) { + // This method does not throw any exceptions, so this should never be caught + LOG.e(LOG_TAG, "invocationTargetException when checking permission " + permission, invocationTargetException); + } + return false; + } + + private static void deliverPermissionResult(CordovaPlugin plugin, int requestCode, String[] permissions) { + // Generate the request results + int[] requestResults = new int[permissions.length]; + Arrays.fill(requestResults, PackageManager.PERMISSION_GRANTED); + + try { + Method onRequestPermissionResult = CordovaPlugin.class.getDeclaredMethod( + "onRequestPermissionResult", int.class, String[].class, int[].class); + + onRequestPermissionResult.invoke(plugin, requestCode, permissions, requestResults); + } catch (NoSuchMethodException noSuchMethodException) { + // Should never be caught since the plugin must be written for cordova-android 5.0.0+ if it + // made it to this point + LOG.e(LOG_TAG, "NoSuchMethodException when delivering permissions results", noSuchMethodException); + } catch (IllegalAccessException illegalAccessException) { + // Should never be caught; this is a public method + LOG.e(LOG_TAG, "IllegalAccessException when delivering permissions results", illegalAccessException); + } catch(InvocationTargetException invocationTargetException) { + // This method may throw a JSONException. We are just duplicating cordova-android's + // exception handling behavior here; all it does is log the exception in CordovaActivity, + // print the stacktrace, and ignore it + LOG.e(LOG_TAG, "InvocationTargetException when delivering permissions results", invocationTargetException); + } + } +} http://git-wip-us.apache.org/repos/asf/cordova-plugin-media-capture/blob/7646d75b/www/CaptureError.js ---------------------------------------------------------------------- diff --git a/www/CaptureError.js b/www/CaptureError.js index 6fb7a47..9892709 100644 --- a/www/CaptureError.js +++ b/www/CaptureError.js @@ -34,6 +34,8 @@ CaptureError.CAPTURE_APPLICATION_BUSY = 1; CaptureError.CAPTURE_INVALID_ARGUMENT = 2; // User exited camera application or audio capture application before capturing anything. CaptureError.CAPTURE_NO_MEDIA_FILES = 3; +// User denied permissions required to perform the capture request. +CaptureError.CAPTURE_PERMISSION_DENIED = 4; // The requested capture operation is not supported. CaptureError.CAPTURE_NOT_SUPPORTED = 20; --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
