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]

Reply via email to