diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..4bd6da9
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,23 @@
+
+
+### Platforms affected
+
+
+### What does this PR do?
+
+
+### What testing has been done on this change?
+
+
+### Checklist
+- [ ] [ICLA](http://www.apache.org/licenses/icla.txt) has been signed and submitted to secretary@apache.org.
+- [ ] [Reported an issue](http://cordova.apache.org/contribute/issues.html) in the JIRA database
+- [ ] Commit message follows the format: "CB-3232: (android) Fix bug with resolving file paths", where CB-xxxx is the JIRA ID & "android" is the platform affected.
+- [ ] Added automated test coverage as appropriate for this change.
diff --git a/README.md b/README.md
index 8102670..a15680c 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,9 @@ description: Take pictures with the device camera.
# under the License.
-->
-[](https://travis-ci.org/apache/cordova-plugin-camera)
+|Android|iOS| Windows 8.1 Store | Windows 8.1 Phone | Windows 10 Store | Travis CI |
+|:-:|:-:|:-:|:-:|:-:|:-:|
+|[](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=android,PLUGIN=cordova-plugin-camera/)|[](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=ios,PLUGIN=cordova-plugin-camera/)|[](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=windows-8.1-store,PLUGIN=cordova-plugin-camera/)|[](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=windows-8.1-phone,PLUGIN=cordova-plugin-camera/)|[](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=windows-10-store,PLUGIN=cordova-plugin-camera/)|[](https://travis-ci.org/apache/cordova-plugin-camera)
# cordova-plugin-camera
@@ -73,7 +75,7 @@ Documentation consists of template and API docs produced from the plugin JS code
---
-# API Reference
+# API Reference
* [camera](#module_camera)
@@ -114,26 +116,20 @@ Once the user snaps the photo, the camera application closes and the application
If `Camera.sourceType` is `Camera.PictureSourceType.PHOTOLIBRARY` or
`Camera.PictureSourceType.SAVEDPHOTOALBUM`, then a dialog displays
-that allows users to select an existing image. The
-`camera.getPicture` function returns a [`CameraPopoverHandle`](#module_CameraPopoverHandle) object,
-which can be used to reposition the image selection dialog, for
-example, when the device orientation changes.
+that allows users to select an existing image.
The return value is sent to the [`cameraSuccess`](#module_camera.onSuccess) callback function, in
one of the following formats, depending on the specified
`cameraOptions`:
- A `String` containing the Base64-encoded photo image.
-
- A `String` representing the image file location on local storage (default).
You can do whatever you want with the encoded image or URI, for
example:
- Render the image in an `
` tag, as in the example below
-
- Save the data locally (`LocalStorage`, [Lawnchair](http://brianleroux.github.com/lawnchair/), etc.)
-
- Post the data to a remote server
__NOTE__: Photo resolution on newer devices is quite good. Photos
@@ -256,6 +252,12 @@ Optional parameters to customize the camera settings.
### Camera.DestinationType : enum
+Defines the output format of `Camera.getPicture` call.
+_Note:_ On iOS passing `DestinationType.NATIVE_URI` along with
+`PictureSourceType.PHOTOLIBRARY` or `PictureSourceType.SAVEDPHOTOALBUM` will
+disable any image modifications (resize, quality change, cropping, etc.) due
+to implementation specific.
+
**Kind**: static enum property of [Camera](#module_Camera)
**Properties**
@@ -291,14 +293,19 @@ Optional parameters to customize the camera settings.
### Camera.PictureSourceType : enum
+Defines the output format of `Camera.getPicture` call.
+_Note:_ On iOS passing `PictureSourceType.PHOTOLIBRARY` or `PictureSourceType.SAVEDPHOTOALBUM`
+along with `DestinationType.NATIVE_URI` will disable any image modifications (resize, quality
+change, cropping, etc.) due to implementation specific.
+
**Kind**: static enum property of [Camera](#module_Camera)
**Properties**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
-| PHOTOLIBRARY | number | 0 | Choose image from picture library (same as SAVEDPHOTOALBUM for Android) |
+| PHOTOLIBRARY | number | 0 | Choose image from the device's photo library (same as SAVEDPHOTOALBUM for Android) |
| CAMERA | number | 1 | Take picture from camera |
-| SAVEDPHOTOALBUM | number | 2 | Choose image from picture library (same as PHOTOLIBRARY for Android) |
+| SAVEDPHOTOALBUM | number | 2 | Choose image only from the device's Camera Roll album (same as PHOTOLIBRARY for Android) |
@@ -362,7 +369,7 @@ __Supported Platforms__
**Example**
```js
-var cameraPopoverHandle = navigator.camera.getPicture(onSuccess, onFail,
+navigator.camera.getPicture(onSuccess, onFail,
{
destinationType: Camera.DestinationType.FILE_URI,
sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
@@ -371,6 +378,7 @@ var cameraPopoverHandle = navigator.camera.getPicture(onSuccess, onFail,
// Reposition the popover if the orientation changes.
window.onorientationchange = function() {
+ var cameraPopoverHandle = new CameraPopoverHandle();
var cameraPopoverOptions = new CameraPopoverOptions(0, 0, 100, 100, Camera.PopoverArrowDirection.ARROW_ANY);
cameraPopoverHandle.setPosition(cameraPopoverOptions);
}
@@ -464,6 +472,16 @@ displays:
Invoking the native camera application while the device is connected
via Zune does not work, and triggers an error callback.
+#### Windows quirks
+
+On Windows Phone 8.1 using `SAVEDPHOTOALBUM` or `PHOTOLIBRARY` as a source type causes application to suspend until file picker returns the selected image and
+then restore with start page as defined in app's `config.xml`. In case when `camera.getPicture` was called from different page, this will lead to reloading
+start page from scratch and success and error callbacks will never be called.
+
+To avoid this we suggest using SPA pattern or call `camera.getPicture` only from your app's start page.
+
+More information about Windows Phone 8.1 picker APIs is here: [How to continue your Windows Phone app after calling a file picker](https://msdn.microsoft.com/en-us/library/windows/apps/dn720490.aspx)
+
#### Tizen Quirks
Tizen only supports a `destinationType` of
@@ -529,6 +547,8 @@ Tizen only supports a `destinationType` of
- When using `destinationType.NATIVE_URI` and `sourceType.CAMERA`, photos are saved in the saved photo album regardless on the value of `saveToPhotoAlbum` parameter.
+- When using `destinationType.NATIVE_URI` and `sourceType.PHOTOLIBRARY` or `sourceType.SAVEDPHOTOALBUM`, all editing options are ignored and link is returned to original picture.
+
#### Tizen Quirks
- options not supported
@@ -618,7 +638,7 @@ function displayImage(imgUri) {
}
```
-To display the image on some platforms, you might need to include the main part of the URI in the Content-Security-Policy element in index.html. For example, on Windows 10, you can include `ms-appdata:` in your element. Here is an example.
+To display the image on some platforms, you might need to include the main part of the URI in the Content-Security-Policy `` element in index.html. For example, on Windows 10, you can include `ms-appdata:` in your `` element. Here is an example.
```html
diff --git a/appium-tests/android/android.spec.js b/appium-tests/android/android.spec.js
index 7a9a71b..0e00bab 100644
--- a/appium-tests/android/android.spec.js
+++ b/appium-tests/android/android.spec.js
@@ -52,6 +52,12 @@ describe('Camera tests Android.', function () {
var screenHeight = DEFAULT_SCREEN_HEIGHT;
// promise count to use in promise ID
var promiseCount = 0;
+ // determine if Appium session is created successfully
+ var appiumSessionStarted = false;
+ // determine if camera is present on the device/emulator
+ var cameraAvailable = false;
+ // a path to the image we add to the gallery before test run
+ var fillerImagePath;
function getNextPromiseId() {
promiseCount += 1;
@@ -72,9 +78,9 @@ describe('Camera tests Android.', function () {
});
}
- // generates test specs by combining all the specified options
+ // combinines specified options in all possible variations
// you can add more options to test more scenarios
- function generateSpecs() {
+ function generateOptions() {
var sourceTypes = [
cameraConstants.PictureSourceType.CAMERA,
cameraConstants.PictureSourceType.PHOTOLIBRARY
@@ -112,12 +118,13 @@ describe('Camera tests Android.', function () {
tapTile
.tap({
x: Math.round(screenWidth / 4),
- y: Math.round(screenHeight / 5)
+ y: Math.round(screenHeight / 4)
});
swipeRight
- .press({x: 10, y: 100})
+ .press({x: 10, y: 150})
.wait(300)
- .moveTo({x: Math.round(screenWidth / 2), y: 0})
+ .moveTo({x: Math.round(screenWidth - (screenWidth / 8)), y: 0})
+ .wait(1500)
.release()
.wait(1000);
if (options.allowEdit) {
@@ -127,10 +134,18 @@ describe('Camera tests Android.', function () {
.performTouchAction(tapTile);
}
return driver
+ .waitForElementByXPath('//android.widget.TextView[@text="Gallery"]', 20000)
+ .elementByXPath('//android.widget.TextView[@text="Gallery"]')
+ .elementByXPath('//android.widget.TextView[@text="Gallery"]')
+ .elementByXPath('//android.widget.TextView[@text="Gallery"]')
.elementByXPath('//android.widget.TextView[@text="Gallery"]')
.fail(function () {
return driver
.performTouchAction(swipeRight)
+ .waitForElementByXPath('//android.widget.TextView[@text="Gallery"]', 20000)
+ .elementByXPath('//android.widget.TextView[@text="Gallery"]')
+ .elementByXPath('//android.widget.TextView[@text="Gallery"]')
+ .elementByXPath('//android.widget.TextView[@text="Gallery"]')
.elementByXPath('//android.widget.TextView[@text="Gallery"]');
})
.click()
@@ -140,9 +155,13 @@ describe('Camera tests Android.', function () {
}
// taking a picture from camera
return driver
- .waitForElementByXPath('//android.widget.ImageView[contains(@resource-id,\'shutter\')]', MINUTE)
+ .waitForElementByXPath('//android.widget.ImageView[contains(@resource-id,\'shutter\')]', MINUTE / 2)
+ .elementByXPath('//android.widget.ImageView[contains(@resource-id,\'shutter\')]')
+ .elementByXPath('//android.widget.ImageView[contains(@resource-id,\'shutter\')]')
.click()
- .waitForElementByXPath('//android.widget.ImageView[contains(@resource-id,\'done\')]', MINUTE)
+ .waitForElementByXPath('//android.widget.ImageView[contains(@resource-id,\'done\')]', MINUTE / 2)
+ .elementByXPath('//android.widget.ImageView[contains(@resource-id,\'done\')]')
+ .elementByXPath('//android.widget.ImageView[contains(@resource-id,\'done\')]')
.click();
})
.then(function () {
@@ -156,44 +175,31 @@ describe('Camera tests Android.', function () {
}
})
.fail(function (failure) {
- console.log(failure);
- fail(failure);
+ throw failure;
});
}
// checks if the picture was successfully taken
// if shouldLoad is falsy, ensures that the error callback was called
- function checkPicture(shouldLoad) {
+ function checkPicture(shouldLoad, options) {
+ if (!options) {
+ options = {};
+ }
return driver
.context(webviewContext)
- .setAsyncScriptTimeout(MINUTE)
- .executeAsync(cameraHelper.checkPicture, [getCurrentPromiseId()])
+ .setAsyncScriptTimeout(MINUTE / 2)
+ .executeAsync(cameraHelper.checkPicture, [getCurrentPromiseId(), options])
.then(function (result) {
if (shouldLoad) {
- expect(result.length).toBeGreaterThan(0);
- if (result.indexOf('ERROR') >= 0) {
- return fail(result);
+ if (result !== 'OK') {
+ fail(result);
}
- } else {
- if (result.indexOf('ERROR') === -1) {
- return fail('Unexpected success callback with result: ' + result);
- }
- expect(result.indexOf('ERROR')).toBe(0);
+ } else if (result.indexOf('ERROR') === -1) {
+ throw 'Unexpected success callback with result: ' + result;
}
});
}
- function runCombinedSpec(spec) {
- return driver
- .then(function () {
- return getPicture(spec.options);
- })
- .then(function () {
- return checkPicture(true);
- })
- .fail(saveScreenshotAndFail);
- }
-
// deletes the latest image from the gallery
function deleteImage() {
var holdTile = new wd.TouchAction();
@@ -216,32 +222,93 @@ describe('Camera tests Android.', function () {
function getDriver() {
driver = wdHelper.getDriver('Android');
- return wdHelper.getWebviewContext(driver)
+ return driver.getWebviewContext()
.then(function(context) {
webviewContext = context;
return driver.context(webviewContext);
})
+ .waitForDeviceReady()
+ .injectLibraries()
+ .deleteFillerImage(fillerImagePath)
.then(function () {
- return wdHelper.waitForDeviceReady(driver);
+ fillerImagePath = null;
})
- .then(function () {
- return wdHelper.injectLibraries(driver);
+ .addFillerImage()
+ .then(function (result) {
+ if (result && result.indexOf('ERROR:') === 0) {
+ throw new Error(result);
+ } else {
+ fillerImagePath = result;
+ }
});
}
+ function recreateSession() {
+ return driver
+ .quit()
+ .finally(function () {
+ return getDriver();
+ });
+ }
+
+ function tryRunSpec(spec) {
+ return driver
+ .then(spec)
+ .fail(function () {
+ return recreateSession()
+ .then(spec)
+ .fail(function() {
+ return recreateSession()
+ .then(spec);
+ });
+ })
+ .fail(saveScreenshotAndFail);
+ }
+
+ // produces a generic spec function which
+ // takes a picture with specified options
+ // and then verifies it
+ function generateSpec(options) {
+ return function () {
+ return driver
+ .then(function () {
+ return getPicture(options);
+ })
+ .then(function () {
+ return checkPicture(true, options);
+ });
+ };
+ }
+
+ function checkSession(done) {
+ if (!appiumSessionStarted) {
+ fail('Failed to start a session');
+ done();
+ }
+ }
+
+ function checkCamera(pending) {
+ if (!cameraAvailable) {
+ pending('This test requires camera');
+ }
+ }
+
it('camera.ui.util configuring driver and starting a session', function (done) {
getDriver()
- .fail(fail)
+ .then(function () {
+ appiumSessionStarted = true;
+ }, fail)
.done(done);
- }, 5 * MINUTE);
+ }, 10 * MINUTE);
it('camera.ui.util determine screen dimensions', function (done) {
- return driver
+ checkSession(done);
+ driver
.context(webviewContext)
.execute(function () {
return {
- 'width': window.innerWidth,
- 'height': window.innerHeight
+ 'width': screen.availWidth,
+ 'height': screen.availHeight
};
}, [])
.then(function (size) {
@@ -251,131 +318,287 @@ describe('Camera tests Android.', function () {
.done(done);
}, MINUTE);
+ it('camera.ui.util determine camera availability', function (done) {
+ checkSession(done);
+ var opts = {
+ sourceType: cameraConstants.PictureSourceType.CAMERA,
+ saveToPhotoAlbum: false
+ };
+
+ return driver
+ .then(function () {
+ return getPicture(opts);
+ })
+ .then(function () {
+ cameraAvailable = true;
+ }, function () {
+ return recreateSession();
+ })
+ .done(done);
+ }, 5 * MINUTE);
+
describe('Specs.', function () {
// getPicture() with saveToPhotoLibrary = true
- it('camera.ui.spec.1 Saving the picture to photo library', function (done) {
- var options = {
+ it('camera.ui.spec.1 Saving a picture to the photo library', function (done) {
+ checkSession(done);
+ checkCamera(pending);
+ var spec = generateSpec({
quality: 50,
allowEdit: false,
sourceType: cameraConstants.PictureSourceType.CAMERA,
saveToPhotoAlbum: true
- };
- driver
- .then(function () {
- return getPicture(options);
- })
+ });
+
+ tryRunSpec(spec)
.then(function () {
isTestPictureSaved = true;
- return checkPicture(true);
})
- .fail(saveScreenshotAndFail)
.done(done);
- }, 3 * MINUTE);
+ }, 10 * MINUTE);
// getPicture() with mediaType: VIDEO, sourceType: PHOTOLIBRARY
it('camera.ui.spec.2 Selecting only videos', function (done) {
- var options = { sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
- mediaType: cameraConstants.MediaType.VIDEO };
- driver
- .then(function () {
- return getPicture(options, true);
- })
- .context('NATIVE_APP')
- .then(function () {
- // try to find "Gallery" menu item
- // if there's none, the gallery should be already opened
- return driver
- .elementByXPath('//android.widget.TextView[@text="Gallery"]')
- .then(function (element) {
- return element.click();
- }, function () {
- return driver;
- });
- })
- .then(function () {
- // if the gallery is opened on the videos page,
- // there should be a "Choose video" caption
- return driver
- .elementByXPath('//*[@text="Choose video"]')
- .fail(function () {
- throw 'Couldn\'t find "Choose video" element.';
- });
- })
- .deviceKeyEvent(BACK_BUTTON)
- .elementByXPath('//android.widget.TextView[@text="Gallery"]')
- .deviceKeyEvent(BACK_BUTTON)
- .finally(function () {
- return driver
- .elementById('action_bar_title')
- .then(function () {
- // success means we're still in native app
- return driver
- .deviceKeyEvent(BACK_BUTTON);
- }, function () {
- // error means we're already in webview
- return driver;
- });
- })
- .fail(saveScreenshotAndFail)
- .done(done);
- }, 3 * MINUTE);
+ checkSession(done);
+ var spec = function () {
+ var options = { sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
+ mediaType: cameraConstants.MediaType.VIDEO };
+ return driver
+ .then(function () {
+ return getPicture(options, true);
+ })
+ .context('NATIVE_APP')
+ .then(function () {
+ // try to find "Gallery" menu item
+ // if there's none, the gallery should be already opened
+ return driver
+ .waitForElementByXPath('//android.widget.TextView[@text="Gallery"]', 20000)
+ .elementByXPath('//android.widget.TextView[@text="Gallery"]')
+ .elementByXPath('//android.widget.TextView[@text="Gallery"]')
+ .then(function (element) {
+ return element.click();
+ }, function () {
+ return driver;
+ });
+ })
+ .then(function () {
+ // if the gallery is opened on the videos page,
+ // there should be a "Choose video" caption
+ return driver
+ .elementByXPath('//*[@text="Choose video"]')
+ .fail(function () {
+ throw 'Couldn\'t find "Choose video" element.';
+ });
+ })
+ .deviceKeyEvent(BACK_BUTTON)
+ .elementByXPath('//android.widget.TextView[@text="Gallery"]')
+ .deviceKeyEvent(BACK_BUTTON)
+ .finally(function () {
+ return driver
+ .elementById('action_bar_title')
+ .then(function () {
+ // success means we're still in native app
+ return driver
+ .deviceKeyEvent(BACK_BUTTON)
+ // give native app some time to close
+ .sleep(2000)
+ // try again! because every ~30th build
+ // on Sauce Labs this backbutton doesn't work
+ .elementById('action_bar_title')
+ .then(function () {
+ // success means we're still in native app
+ return driver
+ .deviceKeyEvent(BACK_BUTTON);
+ }, function () {
+ // error means we're already in webview
+ return driver;
+ });
+ }, function () {
+ // error means we're already in webview
+ return driver;
+ });
+ });
+ };
+ tryRunSpec(spec).done(done);
+ }, 10 * MINUTE);
// getPicture(), then dismiss
// wait for the error callback to be called
it('camera.ui.spec.3 Dismissing the camera', function (done) {
- var options = { quality: 50,
- allowEdit: true,
- sourceType: cameraConstants.PictureSourceType.CAMERA,
- destinationType: cameraConstants.DestinationType.FILE_URI };
- driver
- .then(function () {
- return getPicture(options, true);
- })
- .context("NATIVE_APP")
- .waitForElementByXPath('//android.widget.ImageView[contains(@resource-id,\'cancel\')]', MINUTE / 2)
- .click()
- .then(function () {
- return checkPicture(false);
- })
- .fail(saveScreenshotAndFail)
- .done(done);
- }, 3 * MINUTE);
+ checkSession(done);
+ checkCamera(pending);
+ var spec = function () {
+ var options = {
+ quality: 50,
+ allowEdit: true,
+ sourceType: cameraConstants.PictureSourceType.CAMERA,
+ destinationType: cameraConstants.DestinationType.FILE_URI
+ };
+ return driver
+ .then(function () {
+ return getPicture(options, true);
+ })
+ .context("NATIVE_APP")
+ .waitForElementByXPath('//android.widget.ImageView[contains(@resource-id,\'cancel\')]', MINUTE / 2)
+ .elementByXPath('//android.widget.ImageView[contains(@resource-id,\'cancel\')]')
+ .elementByXPath('//android.widget.ImageView[contains(@resource-id,\'cancel\')]')
+ .click()
+ .then(function () {
+ return checkPicture(false);
+ });
+ };
+
+ tryRunSpec(spec).done(done);
+ }, 10 * MINUTE);
// getPicture(), then take picture but dismiss the edit
// wait for the error callback to be called
it('camera.ui.spec.4 Dismissing the edit', function (done) {
- var options = { quality: 50,
- allowEdit: true,
- sourceType: cameraConstants.PictureSourceType.CAMERA,
- destinationType: cameraConstants.DestinationType.FILE_URI };
- driver
- .then(function () {
- return getPicture(options, true);
- })
- .context('NATIVE_APP')
- .waitForElementByXPath('//android.widget.ImageView[contains(@resource-id,\'shutter\')]', MINUTE / 2)
- .click()
- .waitForElementByXPath('//android.widget.ImageView[contains(@resource-id,\'done\')]', MINUTE / 2)
- .click()
- .waitForElementByXPath('//*[contains(@resource-id,\'discard\')]', MINUTE / 2)
- .click()
- .then(function () {
- return checkPicture(false);
- })
- .fail(saveScreenshotAndFail)
- .done(done);
- }, 3 * MINUTE);
+ checkSession(done);
+ checkCamera(pending);
+ var spec = function () {
+ var options = {
+ quality: 50,
+ allowEdit: true,
+ sourceType: cameraConstants.PictureSourceType.CAMERA,
+ destinationType: cameraConstants.DestinationType.FILE_URI
+ };
+ return driver
+ .then(function () {
+ return getPicture(options, true);
+ })
+ .context('NATIVE_APP')
+ .waitForElementByXPath('//android.widget.ImageView[contains(@resource-id,\'shutter\')]', MINUTE / 2)
+ .elementByXPath('//android.widget.ImageView[contains(@resource-id,\'shutter\')]')
+ .elementByXPath('//android.widget.ImageView[contains(@resource-id,\'shutter\')]')
+ .click()
+ .waitForElementByXPath('//android.widget.ImageView[contains(@resource-id,\'done\')]', MINUTE / 2)
+ .elementByXPath('//android.widget.ImageView[contains(@resource-id,\'done\')]')
+ .elementByXPath('//android.widget.ImageView[contains(@resource-id,\'done\')]')
+ .click()
+ .waitForElementByXPath('//*[contains(@resource-id,\'discard\')]', MINUTE / 2)
+ .elementByXPath('//*[contains(@resource-id,\'discard\')]')
+ .elementByXPath('//*[contains(@resource-id,\'discard\')]')
+ .click()
+ .then(function () {
+ return checkPicture(false);
+ });
+ };
+
+ tryRunSpec(spec).done(done);
+ }, 10 * MINUTE);
+
+ it('camera.ui.spec.5 Verifying target image size, sourceType=CAMERA', function (done) {
+ checkSession(done);
+ checkCamera(pending);
+ var spec = generateSpec({
+ quality: 50,
+ allowEdit: false,
+ sourceType: cameraConstants.PictureSourceType.CAMERA,
+ saveToPhotoAlbum: false,
+ targetWidth: 210,
+ targetHeight: 210
+ });
+
+ tryRunSpec(spec).done(done);
+ }, 10 * MINUTE);
+
+ it('camera.ui.spec.6 Verifying target image size, sourceType=PHOTOLIBRARY', function (done) {
+ checkSession(done);
+ var spec = generateSpec({
+ quality: 50,
+ allowEdit: false,
+ sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
+ saveToPhotoAlbum: false,
+ targetWidth: 210,
+ targetHeight: 210
+ });
+
+ tryRunSpec(spec).done(done);
+ }, 10 * MINUTE);
+
+ it('camera.ui.spec.7 Verifying target image size, sourceType=CAMERA, DestinationType=NATIVE_URI', function (done) {
+ checkSession(done);
+ checkCamera(pending);
+ var spec = generateSpec({
+ quality: 50,
+ allowEdit: false,
+ sourceType: cameraConstants.PictureSourceType.CAMERA,
+ destinationType: cameraConstants.DestinationType.NATIVE_URI,
+ saveToPhotoAlbum: false,
+ targetWidth: 210,
+ targetHeight: 210
+ });
+
+ tryRunSpec(spec).done(done);
+ }, 10 * MINUTE);
+
+ it('camera.ui.spec.8 Verifying target image size, sourceType=PHOTOLIBRARY, DestinationType=NATIVE_URI', function (done) {
+ checkSession(done);
+ var spec = generateSpec({
+ quality: 50,
+ allowEdit: false,
+ sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
+ destinationType: cameraConstants.DestinationType.NATIVE_URI,
+ saveToPhotoAlbum: false,
+ targetWidth: 210,
+ targetHeight: 210
+ });
+
+ tryRunSpec(spec).done(done);
+ }, 10 * MINUTE);
+
+ it('camera.ui.spec.9 Verifying target image size, sourceType=CAMERA, DestinationType=NATIVE_URI, quality=100', function (done) {
+ checkSession(done);
+ checkCamera(pending);
+ var spec = generateSpec({
+ quality: 100,
+ allowEdit: true,
+ sourceType: cameraConstants.PictureSourceType.CAMERA,
+ destinationType: cameraConstants.DestinationType.NATIVE_URI,
+ saveToPhotoAlbum: false,
+ targetWidth: 305,
+ targetHeight: 305
+ });
+
+ tryRunSpec(spec).done(done);
+ }, 10 * MINUTE);
+
+ it('camera.ui.spec.10 Verifying target image size, sourceType=PHOTOLIBRARY, DestinationType=NATIVE_URI, quality=100', function (done) {
+ checkSession(done);
+ var spec = generateSpec({
+ quality: 100,
+ allowEdit: true,
+ sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
+ destinationType: cameraConstants.DestinationType.NATIVE_URI,
+ saveToPhotoAlbum: false,
+ targetWidth: 305,
+ targetHeight: 305
+ });
+
+ tryRunSpec(spec).done(done);
+ }, 10 * MINUTE);
// combine various options for getPicture()
- generateSpecs().forEach(function (spec) {
- it('camera.ui.spec.5.' + spec.id + ' Combining options. ' + spec.description, function (done) {
- runCombinedSpec(spec)
- .done(done);
- }, 3 * MINUTE);
+ generateOptions().forEach(function (spec) {
+ it('camera.ui.spec.11.' + spec.id + ' Combining options. ' + spec.description, function (done) {
+ checkSession(done);
+ if (spec.options.sourceType == cameraConstants.PictureSourceType.CAMERA) {
+ checkCamera(pending);
+ }
+ var s = generateSpec(spec.options);
+ tryRunSpec(s).done(done);
+ }, 10 * MINUTE);
});
+ it('camera.ui.util Delete filler picture from device library', function (done) {
+ driver
+ .context(webviewContext)
+ .deleteFillerImage(fillerImagePath)
+ .done(done);
+ }, MINUTE);
- it('camera.ui.util Delete test image from device library', function (done) {
+ it('camera.ui.util Delete taken picture from device library', function (done) {
+ checkSession(done);
if (!isTestPictureSaved) {
// couldn't save test picture earlier, so nothing to delete here
done();
@@ -383,7 +606,7 @@ describe('Camera tests Android.', function () {
}
// delete exactly one latest picture
// this should be the picture we've taken in the first spec
- return driver
+ driver
.context('NATIVE_APP')
.deviceKeyEvent(BACK_BUTTON)
.sleep(1000)
@@ -408,6 +631,7 @@ describe('Camera tests Android.', function () {
});
it('camera.ui.util Destroy the session', function (done) {
+ checkSession(done);
driver
.quit()
.done(done);
diff --git a/appium-tests/helpers/cameraHelper.js b/appium-tests/helpers/cameraHelper.js
index 4f33163..a16916f 100644
--- a/appium-tests/helpers/cameraHelper.js
+++ b/appium-tests/helpers/cameraHelper.js
@@ -1,5 +1,5 @@
/*jshint node: true */
-/* global Q */
+/* global Q, resolveLocalFileSystemURL, Camera, cordova */
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
@@ -94,7 +94,13 @@ module.exports.generateSpecs = function (sourceTypes, destinationTypes, encoding
return specs;
};
+// calls getPicture() and saves the result in promise
+// note that this function is executed in the context of tested app
+// and not in the context of tests
module.exports.getPicture = function (opts, pid) {
+ if (navigator._appiumPromises[pid - 1]) {
+ navigator._appiumPromises[pid - 1] = null;
+ }
navigator._appiumPromises[pid] = Q.defer();
navigator.camera.getPicture(function (result) {
navigator._appiumPromises[pid].resolve(result);
@@ -103,11 +109,197 @@ module.exports.getPicture = function (opts, pid) {
}, opts);
};
-module.exports.checkPicture = function (pid, cb) {
+// verifies taken picture when the promise is resolved,
+// calls a callback with 'OK' if everything is good,
+// calls a callback with 'ERROR: ' if something is wrong
+// note that this function is executed in the context of tested app
+// and not in the context of tests
+module.exports.checkPicture = function (pid, options, cb) {
+ var isIos = cordova.platformId === "ios";
+ var isAndroid = cordova.platformId === "android";
+ // skip image type check if it's unmodified on Android:
+ // https://github.com/apache/cordova-plugin-camera/#android-quirks-1
+ var skipFileTypeCheckAndroid = isAndroid && options.quality === 100 &&
+ !options.targetWidth && !options.targetHeight &&
+ !options.correctOrientation;
+
+ // Skip image type check if destination is NATIVE_URI and source - device's photoalbum
+ // https://github.com/apache/cordova-plugin-camera/#ios-quirks-1
+ var skipFileTypeCheckiOS = isIos && options.destinationType === Camera.DestinationType.NATIVE_URI &&
+ (options.sourceType === Camera.PictureSourceType.PHOTOLIBRARY ||
+ options.sourceType === Camera.PictureSourceType.SAVEDPHOTOALBUM);
+
+ var skipFileTypeCheck = skipFileTypeCheckAndroid || skipFileTypeCheckiOS;
+
+ var desiredType = 'JPEG';
+ var mimeType = 'image/jpeg';
+ if (options.encodingType === Camera.EncodingType.PNG) {
+ desiredType = 'PNG';
+ mimeType = 'image/png';
+ }
+
+ function errorCallback(msg) {
+ if (msg.hasOwnProperty('message')) {
+ msg = msg.message;
+ }
+ cb('ERROR: ' + msg);
+ }
+
+ // verifies the image we get from plugin
+ function verifyResult(result) {
+ if (result.length === 0) {
+ errorCallback('The result is empty.');
+ return;
+ } else if (isIos && options.destinationType === Camera.DestinationType.NATIVE_URI && result.indexOf('assets-library:') !== 0) {
+ errorCallback('Expected "' + result.substring(0, 150) + '"to start with "assets-library:"');
+ return;
+ } else if (isIos && options.destinationType === Camera.DestinationType.FILE_URI && result.indexOf('file:') !== 0) {
+ errorCallback('Expected "' + result.substring(0, 150) + '"to start with "file:"');
+ return;
+ }
+
+ try {
+ window.atob(result);
+ // if we got here it is a base64 string (DATA_URL)
+ result = "data:" + mimeType + ";base64," + result;
+ } catch (e) {
+ // not DATA_URL
+ if (options.destinationType === Camera.DestinationType.DATA_URL) {
+ errorCallback('Expected ' + result.substring(0, 150) + 'not to be DATA_URL');
+ return;
+ }
+ }
+ try {
+ if (result.indexOf('file:') === 0 ||
+ result.indexOf('content:') === 0 ||
+ result.indexOf('assets-library:') === 0) {
+
+ if (!window.resolveLocalFileSystemURL) {
+ errorCallback('Cannot read file. Please install cordova-plugin-file to fix this.');
+ return;
+ }
+ resolveLocalFileSystemURL(result, function (entry) {
+ if (skipFileTypeCheck) {
+ displayFile(entry);
+ } else {
+ verifyFile(entry);
+ }
+ });
+ } else {
+ displayImage(result);
+ }
+ } catch (e) {
+ errorCallback(e);
+ }
+ }
+
+ // verifies that the file type matches the requested type
+ function verifyFile(entry) {
+ try {
+ var reader = new FileReader();
+ reader.onloadend = function(e) {
+ var arr = (new Uint8Array(e.target.result)).subarray(0, 4);
+ var header = '';
+ for(var i = 0; i < arr.length; i++) {
+ header += arr[i].toString(16);
+ }
+ var actualType = 'unknown';
+
+ switch (header) {
+ case "89504e47":
+ actualType = 'PNG';
+ break;
+ case 'ffd8ffe0':
+ case 'ffd8ffe1':
+ case 'ffd8ffe2':
+ actualType = 'JPEG';
+ break;
+ }
+
+ if (actualType === desiredType) {
+ displayFile(entry);
+ } else {
+ errorCallback('File type mismatch. Expected ' + desiredType + ', got ' + actualType);
+ }
+ };
+ reader.onerror = function (e) {
+ errorCallback(e);
+ };
+ entry.file(function (file) {
+ reader.readAsArrayBuffer(file);
+ }, function (e) {
+ errorCallback(e);
+ });
+ } catch (e) {
+ errorCallback(e);
+ }
+ }
+
+ // reads the file, then displays the image
+ function displayFile(entry) {
+ function onFileReceived(file) {
+ var reader = new FileReader();
+ reader.onerror = function (e) {
+ errorCallback(e);
+ };
+ reader.onloadend = function (evt) {
+ displayImage(evt.target.result);
+ };
+ reader.readAsDataURL(file);
+ }
+
+ entry.file(onFileReceived, function (e) {
+ errorCallback(e);
+ });
+ }
+
+ function displayImage(image) {
+ try {
+ var imgEl = document.getElementById('camera_test_image');
+ if (!imgEl) {
+ imgEl = document.createElement('img');
+ imgEl.id = 'camera_test_image';
+ document.body.appendChild(imgEl);
+ }
+ var timedOut = false;
+ var loadTimeout = setTimeout(function () {
+ timedOut = true;
+ imgEl.src = '';
+ errorCallback('The image did not load: ' + image.substring(0, 150));
+ }, 10000);
+ var done = function (status) {
+ if (!timedOut) {
+ clearTimeout(loadTimeout);
+ imgEl.src = '';
+ cb(status);
+ }
+ };
+ imgEl.onload = function () {
+ try {
+ // aspect ratio is preserved so only one dimension should match
+ if ((typeof options.targetWidth === 'number' && imgEl.naturalWidth !== options.targetWidth) &&
+ (typeof options.targetHeight === 'number' && imgEl.naturalHeight !== options.targetHeight))
+ {
+ done('ERROR: Wrong image size: ' + imgEl.naturalWidth + 'x' + imgEl.naturalHeight +
+ '. Requested size: ' + options.targetWidth + 'x' + options.targetHeight);
+ } else {
+ done('OK');
+ }
+ } catch (e) {
+ errorCallback(e);
+ }
+ };
+ imgEl.src = image;
+ } catch (e) {
+ errorCallback(e);
+ }
+ }
+
navigator._appiumPromises[pid].promise
.then(function (result) {
- cb(result);
- }, function (err) {
- cb('ERROR: ' + err);
+ verifyResult(result);
+ })
+ .fail(function (e) {
+ errorCallback(e);
});
};
diff --git a/appium-tests/ios/ios.spec.js b/appium-tests/ios/ios.spec.js
index ea856a7..cab45f6 100644
--- a/appium-tests/ios/ios.spec.js
+++ b/appium-tests/ios/ios.spec.js
@@ -30,7 +30,6 @@
var wdHelper = global.WD_HELPER;
var screenshotHelper = global.SCREENSHOT_HELPER;
-var wd = wdHelper.getWD();
var isDevice = global.DEVICE;
var cameraConstants = require('../../www/CameraConstants');
var cameraHelper = require('../helpers/cameraHelper');
@@ -44,6 +43,8 @@ describe('Camera tests iOS.', function () {
var webviewContext = DEFAULT_WEBVIEW_CONTEXT;
// promise count to use in promise ID
var promiseCount = 0;
+ // going to set this to false if session is created successfully
+ var failedToStart = true;
function getNextPromiseId() {
promiseCount += 1;
@@ -66,7 +67,7 @@ describe('Camera tests iOS.', function () {
// generates test specs by combining all the specified options
// you can add more options to test more scenarios
- function generateSpecs() {
+ function generateOptions() {
var sourceTypes = cameraConstants.PictureSourceType;
var destinationTypes = cameraConstants.DestinationType;
var encodingTypes = cameraConstants.EncodingType;
@@ -81,17 +82,8 @@ describe('Camera tests iOS.', function () {
.elementByXPath('//*[@label="Use"]')
.click()
.fail(function () {
- return driver
- // For some reason "Choose" element is not clickable by standard Appium methods
- // So getting its position and tapping there using TouchAction
- .elementByXPath('//UIAButton[@label="Choose"]')
- .getLocation()
- .then(function (loc) {
- var tapChoose = new wd.TouchAction();
- tapChoose.tap(loc);
- return driver
- .performTouchAction(tapChoose);
- });
+ // For some reason "Choose" element is not clickable by standard Appium methods
+ return wdHelper.tapElementByXPath('//UIAButton[@label="Choose"]', driver);
});
}
@@ -136,12 +128,14 @@ describe('Camera tests iOS.', function () {
if (cancelCamera) {
return driver
.waitForElementByXPath('//*[@label="Cancel"]', MINUTE / 2)
+ .elementByXPath('//*[@label="Cancel"]')
+ .elementByXPath('//*[@label="Cancel"]')
.click();
}
return driver
.waitForElementByXPath('//*[@label="Take Picture"]', MINUTE / 2)
.click()
- .elementByXPath('//*[@label="Use Photo"]')
+ .waitForElementByXPath('//*[@label="Use Photo"]', MINUTE / 2)
.click();
})
.fail(fail);
@@ -149,33 +143,34 @@ describe('Camera tests iOS.', function () {
// checks if the picture was successfully taken
// if shouldLoad is falsy, ensures that the error callback was called
- function checkPicture(shouldLoad) {
+ function checkPicture(shouldLoad, options) {
+ if (!options) {
+ options = {};
+ }
return driver
.context(webviewContext)
- .setAsyncScriptTimeout(MINUTE)
- .executeAsync(cameraHelper.checkPicture, [getCurrentPromiseId()])
+ .setAsyncScriptTimeout(MINUTE / 2)
+ .executeAsync(cameraHelper.checkPicture, [getCurrentPromiseId(), options])
.then(function (result) {
if (shouldLoad) {
- expect(result.length).toBeGreaterThan(0);
- if (result.indexOf('ERROR') >= 0) {
- return fail(result);
+ if (result !== 'OK') {
+ fail(result);
}
- } else {
- if (result.indexOf('ERROR') === -1) {
- return fail('Unexpected success callback with result: ' + result);
- }
- expect(result.indexOf('ERROR')).toBe(0);
+ } else if (result.indexOf('ERROR') === -1) {
+ throw 'Unexpected success callback with result: ' + result;
}
});
}
- function runCombinedSpec(spec) {
+ // takes a picture with the specified options
+ // and then verifies it
+ function runSpec(options) {
return driver
.then(function () {
- return getPicture(spec.options);
+ return getPicture(options);
})
.then(function () {
- return checkPicture(true);
+ return checkPicture(true, options);
})
.fail(saveScreenshotAndFail);
}
@@ -195,15 +190,25 @@ describe('Camera tests iOS.', function () {
});
}
+ function checkSession(done) {
+ if (failedToStart) {
+ fail('Failed to start a session');
+ done();
+ }
+ }
+
it('camera.ui.util configure driver and start a session', function (done) {
getDriver()
- .fail(fail)
- .finally(done);
- }, 5 * MINUTE);
+ .then(function () {
+ failedToStart = false;
+ }, fail)
+ .done(done);
+ }, 10 * MINUTE);
describe('Specs.', function () {
// getPicture() with mediaType: VIDEO, sourceType: PHOTOLIBRARY
it('camera.ui.spec.1 Selecting only videos', function (done) {
+ checkSession(done);
var options = { sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
mediaType: cameraConstants.MediaType.VIDEO };
driver
@@ -219,11 +224,12 @@ describe('Camera tests iOS.', function () {
// getPicture(), then dismiss
// wait for the error callback to be called
it('camera.ui.spec.2 Dismissing the camera', function (done) {
- // camera is not available on the iOS simulator
+ checkSession(done);
if (!isDevice) {
- pending();
+ pending('Camera is not available on iOS simulator');
}
- var options = { sourceType: cameraConstants.PictureSourceType.CAMERA };
+ var options = { sourceType: cameraConstants.PictureSourceType.CAMERA,
+ saveToPhotoAlbum: false };
driver
.then(function () {
return getPicture(options, true);
@@ -235,20 +241,175 @@ describe('Camera tests iOS.', function () {
.done(done);
}, 3 * MINUTE);
+ it('camera.ui.spec.3 Verifying target image size, sourceType=CAMERA', function (done) {
+ checkSession(done);
+ if (!isDevice) {
+ pending('Camera is not available on iOS simulator');
+ }
+ var options = {
+ quality: 50,
+ allowEdit: false,
+ sourceType: cameraConstants.PictureSourceType.CAMERA,
+ saveToPhotoAlbum: false,
+ targetWidth: 210,
+ targetHeight: 210
+ };
+
+ runSpec(options).done(done);
+ }, 3 * MINUTE);
+
+ it('camera.ui.spec.4 Verifying target image size, sourceType=SAVEDPHOTOALBUM', function (done) {
+ checkSession(done);
+ var options = {
+ quality: 50,
+ allowEdit: false,
+ sourceType: cameraConstants.PictureSourceType.SAVEDPHOTOALBUM,
+ saveToPhotoAlbum: false,
+ targetWidth: 210,
+ targetHeight: 210
+ };
+
+ runSpec(options).done(done);
+ }, 3 * MINUTE);
+
+ it('camera.ui.spec.5 Verifying target image size, sourceType=PHOTOLIBRARY', function (done) {
+ checkSession(done);
+ var options = {
+ quality: 50,
+ allowEdit: false,
+ sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
+ saveToPhotoAlbum: false,
+ targetWidth: 210,
+ targetHeight: 210
+ };
+
+ runSpec(options).done(done);
+ }, 3 * MINUTE);
+
+ it('camera.ui.spec.6 Verifying target image size, sourceType=CAMERA, destinationType=FILE_URL', function (done) {
+ // remove this line if you don't mind the tests leaving a photo saved on device
+ pending('Cannot prevent iOS from saving the picture to photo library');
+
+ checkSession(done);
+ if (!isDevice) {
+ pending('Camera is not available on iOS simulator');
+ }
+ var options = {
+ quality: 50,
+ allowEdit: false,
+ sourceType: cameraConstants.PictureSourceType.CAMERA,
+ destinationType: cameraConstants.DestinationType.FILE_URL,
+ saveToPhotoAlbum: false,
+ targetWidth: 210,
+ targetHeight: 210
+ };
+
+ runSpec(options).done(done);
+ }, 3 * MINUTE);
+
+ it('camera.ui.spec.7 Verifying target image size, sourceType=SAVEDPHOTOALBUM, destinationType=FILE_URL', function (done) {
+ checkSession(done);
+ var options = {
+ quality: 50,
+ allowEdit: false,
+ sourceType: cameraConstants.PictureSourceType.SAVEDPHOTOALBUM,
+ destinationType: cameraConstants.DestinationType.FILE_URL,
+ saveToPhotoAlbum: false,
+ targetWidth: 210,
+ targetHeight: 210
+ };
+
+ runSpec(options).done(done);
+ }, 3 * MINUTE);
+
+ it('camera.ui.spec.8 Verifying target image size, sourceType=PHOTOLIBRARY, destinationType=FILE_URL', function (done) {
+ checkSession(done);
+ var options = {
+ quality: 50,
+ allowEdit: false,
+ sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
+ destinationType: cameraConstants.DestinationType.FILE_URL,
+ saveToPhotoAlbum: false,
+ targetWidth: 210,
+ targetHeight: 210
+ };
+
+ runSpec(options).done(done);
+ }, 3 * MINUTE);
+
+ it('camera.ui.spec.9 Verifying target image size, sourceType=CAMERA, destinationType=FILE_URL, quality=100', function (done) {
+ // remove this line if you don't mind the tests leaving a photo saved on device
+ pending('Cannot prevent iOS from saving the picture to photo library');
+
+ checkSession(done);
+ if (!isDevice) {
+ pending('Camera is not available on iOS simulator');
+ }
+ var options = {
+ quality: 100,
+ allowEdit: false,
+ sourceType: cameraConstants.PictureSourceType.CAMERA,
+ destinationType: cameraConstants.DestinationType.FILE_URL,
+ saveToPhotoAlbum: false,
+ targetWidth: 305,
+ targetHeight: 305
+ };
+ runSpec(options).done(done);
+ }, 3 * MINUTE);
+
+ it('camera.ui.spec.10 Verifying target image size, sourceType=SAVEDPHOTOALBUM, destinationType=FILE_URL, quality=100', function (done) {
+ checkSession(done);
+ var options = {
+ quality: 100,
+ allowEdit: false,
+ sourceType: cameraConstants.PictureSourceType.SAVEDPHOTOALBUM,
+ destinationType: cameraConstants.DestinationType.FILE_URL,
+ saveToPhotoAlbum: false,
+ targetWidth: 305,
+ targetHeight: 305
+ };
+
+ runSpec(options).done(done);
+ }, 3 * MINUTE);
+
+ it('camera.ui.spec.11 Verifying target image size, sourceType=PHOTOLIBRARY, destinationType=FILE_URL, quality=100', function (done) {
+ checkSession(done);
+ var options = {
+ quality: 100,
+ allowEdit: false,
+ sourceType: cameraConstants.PictureSourceType.PHOTOLIBRARY,
+ destinationType: cameraConstants.DestinationType.FILE_URL,
+ saveToPhotoAlbum: false,
+ targetWidth: 305,
+ targetHeight: 305
+ };
+
+ runSpec(options).done(done);
+ }, 3 * MINUTE);
+
// combine various options for getPicture()
- generateSpecs().forEach(function (spec) {
- it('camera.ui.spec.3.' + spec.id + ' Combining options. ' + spec.description, function (done) {
- // camera is not available on iOS simulator
+ generateOptions().forEach(function (spec) {
+ it('camera.ui.spec.12.' + spec.id + ' Combining options. ' + spec.description, function (done) {
+ checkSession(done);
if (!isDevice && spec.options.sourceType === cameraConstants.PictureSourceType.CAMERA) {
- pending();
+ pending('Camera is not available on iOS simulator');
}
- runCombinedSpec(spec).done(done);
+
+ // remove this check if you don't mind the tests leaving a photo saved on device
+ if (spec.options.sourceType === cameraConstants.PictureSourceType.CAMERA &&
+ spec.options.destinationType === cameraConstants.DestinationType.NATIVE_URI) {
+ pending('Skipping: cannot prevent iOS from saving the picture to photo library and cannot delete it. ' +
+ 'For more info, see iOS quirks here: https://github.com/apache/cordova-plugin-camera#ios-quirks-1');
+ }
+
+ runSpec(spec.options).done(done);
}, 3 * MINUTE);
});
});
- it('camera.ui.util.4 Destroy the session', function (done) {
+ it('camera.ui.util Destroy the session', function (done) {
+ checkSession(done);
driver
.quit()
.done(done);
diff --git a/jsdoc2md/TEMPLATE.md b/jsdoc2md/TEMPLATE.md
index d554107..65cdbbc 100644
--- a/jsdoc2md/TEMPLATE.md
+++ b/jsdoc2md/TEMPLATE.md
@@ -4,7 +4,9 @@ description: Take pictures with the device camera.
---
{{>cdv-license~}}
-[](https://travis-ci.org/apache/cordova-plugin-camera)
+|Android|iOS| Windows 8.1 Store | Windows 8.1 Phone | Windows 10 Store | Travis CI |
+|:-:|:-:|:-:|:-:|:-:|:-:|
+|[](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=android,PLUGIN=cordova-plugin-camera/)|[](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=ios,PLUGIN=cordova-plugin-camera/)|[](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=windows-8.1-store,PLUGIN=cordova-plugin-camera/)|[](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=windows-8.1-phone,PLUGIN=cordova-plugin-camera/)|[](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=windows-10-store,PLUGIN=cordova-plugin-camera/)|[](https://travis-ci.org/apache/cordova-plugin-camera)
# cordova-plugin-camera
@@ -15,7 +17,7 @@ the system's image library.
---
-# API Reference
+# API Reference
{{#orphans~}}
{{>member-index}}
@@ -120,6 +122,16 @@ displays:
Invoking the native camera application while the device is connected
via Zune does not work, and triggers an error callback.
+#### Windows quirks
+
+On Windows Phone 8.1 using `SAVEDPHOTOALBUM` or `PHOTOLIBRARY` as a source type causes application to suspend until file picker returns the selected image and
+then restore with start page as defined in app's `config.xml`. In case when `camera.getPicture` was called from different page, this will lead to reloading
+start page from scratch and success and error callbacks will never be called.
+
+To avoid this we suggest using SPA pattern or call `camera.getPicture` only from your app's start page.
+
+More information about Windows Phone 8.1 picker APIs is here: [How to continue your Windows Phone app after calling a file picker](https://msdn.microsoft.com/en-us/library/windows/apps/dn720490.aspx)
+
#### Tizen Quirks
Tizen only supports a `destinationType` of
@@ -185,6 +197,8 @@ Tizen only supports a `destinationType` of
- When using `destinationType.NATIVE_URI` and `sourceType.CAMERA`, photos are saved in the saved photo album regardless on the value of `saveToPhotoAlbum` parameter.
+- When using `destinationType.NATIVE_URI` and `sourceType.PHOTOLIBRARY` or `sourceType.SAVEDPHOTOALBUM`, all editing options are ignored and link is returned to original picture.
+
#### Tizen Quirks
- options not supported
@@ -274,7 +288,7 @@ function displayImage(imgUri) {
}
```
-To display the image on some platforms, you might need to include the main part of the URI in the Content-Security-Policy element in index.html. For example, on Windows 10, you can include `ms-appdata:` in your element. Here is an example.
+To display the image on some platforms, you might need to include the main part of the URI in the Content-Security-Policy `` element in index.html. For example, on Windows 10, you can include `ms-appdata:` in your `` element. Here is an example.
```html
diff --git a/plugin.xml b/plugin.xml
index 99769ab..0911fb8 100644
--- a/plugin.xml
+++ b/plugin.xml
@@ -150,6 +150,11 @@
+
+
+ $CAMERA_USAGE_DESCRIPTION
+
+
diff --git a/src/android/CameraLauncher.java b/src/android/CameraLauncher.java
index 60b07fe..69cb5db 100644
--- a/src/android/CameraLauncher.java
+++ b/src/android/CameraLauncher.java
@@ -59,7 +59,6 @@ import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.util.Base64;
-import android.util.Log;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -140,7 +139,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
this.targetWidth = 0;
this.encodingType = JPEG;
this.mediaType = PICTURE;
- this.mQuality = 80;
+ this.mQuality = 50;
//Take the values from the arguments if they're not already defined (this is tricky)
this.destType = args.getInt(1);
@@ -366,7 +365,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
}
- File photo = createCaptureFile(encodingType);
+ File photo = createCaptureFile(JPEG);
croppedUri = Uri.fromFile(photo);
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, croppedUri);
} else {
@@ -428,14 +427,14 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
cropIntent, CROP_CAMERA + destType);
}
} catch (ActivityNotFoundException anfe) {
- Log.e(LOG_TAG, "Crop operation not supported on this device");
+ LOG.e(LOG_TAG, "Crop operation not supported on this device");
try {
processResultFromCamera(destType, cameraIntent);
}
catch (IOException e)
{
e.printStackTrace();
- Log.e(LOG_TAG, "Unable to write to file");
+ LOG.e(LOG_TAG, "Unable to write to file");
}
}
}
@@ -496,7 +495,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
// Double-check the bitmap.
if (bitmap == null) {
- Log.d(LOG_TAG, "I either have a null image path or bitmap");
+ LOG.d(LOG_TAG, "I either have a null image path or bitmap");
this.failPicture("Unable to create bitmap!");
return;
}
@@ -536,7 +535,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
// Double-check the bitmap.
if (bitmap == null) {
- Log.d(LOG_TAG, "I either have a null image path or bitmap");
+ LOG.d(LOG_TAG, "I either have a null image path or bitmap");
this.failPicture("Unable to create bitmap!");
return;
}
@@ -588,6 +587,18 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
this.cordova.getActivity().sendBroadcast(mediaScanIntent);
}
+ /**
+ * Converts output image format int value to string value of mime type.
+ * @param outputFormat int Output format of camera API.
+ * Must be value of either JPEG or PNG constant
+ * @return String String value of mime type or empty string if mime type is not supported
+ */
+ private String getMimetypeForFormat(int outputFormat) {
+ if (outputFormat == PNG) return "image/png";
+ if (outputFormat == JPEG) return "image/jpeg";
+ return "";
+ }
+
private String outputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
@@ -639,7 +650,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
int rotate = 0;
String fileLocation = FileHelper.getRealPath(uri, this.cordova);
- Log.d(LOG_TAG, "File locaton is: " + fileLocation);
+ LOG.d(LOG_TAG, "File locaton is: " + fileLocation);
// If you ask for video or all media type you will automatically get back a file URI
// and there will be no attempt to resize any returned data
@@ -647,18 +658,21 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
this.callbackContext.success(fileLocation);
}
else {
+ String uriString = uri.toString();
+ // Get the path to the image. Makes loading so much easier.
+ String mimeType = FileHelper.getMimeType(uriString, this.cordova);
+
// This is a special case to just return the path as no scaling,
// rotating, nor compressing needs to be done
if (this.targetHeight == -1 && this.targetWidth == -1 &&
- (destType == FILE_URI || destType == NATIVE_URI) && !this.correctOrientation) {
- this.callbackContext.success(uri.toString());
+ (destType == FILE_URI || destType == NATIVE_URI) && !this.correctOrientation &&
+ mimeType.equalsIgnoreCase(getMimetypeForFormat(encodingType)))
+ {
+ this.callbackContext.success(uriString);
} else {
- String uriString = uri.toString();
- // Get the path to the image. Makes loading so much easier.
- String mimeType = FileHelper.getMimeType(uriString, this.cordova);
// If we don't have a valid image so quit.
if (!("image/jpeg".equalsIgnoreCase(mimeType) || "image/png".equalsIgnoreCase(mimeType))) {
- Log.d(LOG_TAG, "I either have a null image path or bitmap");
+ LOG.d(LOG_TAG, "I either have a null image path or bitmap");
this.failPicture("Unable to retrieve path to picture!");
return;
}
@@ -669,7 +683,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
e.printStackTrace();
}
if (bitmap == null) {
- Log.d(LOG_TAG, "I either have a null image path or bitmap");
+ LOG.d(LOG_TAG, "I either have a null image path or bitmap");
this.failPicture("Unable to create bitmap!");
return;
}
@@ -683,7 +697,9 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
else if (destType == FILE_URI || destType == NATIVE_URI) {
// Did we modify the image?
if ( (this.targetHeight > 0 && this.targetWidth > 0) ||
- (this.correctOrientation && this.orientationCorrected) ) {
+ (this.correctOrientation && this.orientationCorrected) ||
+ !mimeType.equalsIgnoreCase(getMimetypeForFormat(encodingType)))
+ {
try {
String modifiedPath = this.outputModifiedBitmap(bitmap, uri);
// The modified image is cached by the app in order to get around this and not have to delete you
@@ -733,7 +749,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
processResultFromCamera(destType, intent);
} catch (IOException e) {
e.printStackTrace();
- Log.e(LOG_TAG, "Unable to write to file");
+ LOG.e(LOG_TAG, "Unable to write to file");
}
}// If cancelled
diff --git a/src/android/CameraLauncherMaster.java b/src/android/CameraLauncherMaster.java
new file mode 100644
index 0000000..506d6d4
--- /dev/null
+++ b/src/android/CameraLauncherMaster.java
@@ -0,0 +1,1307 @@
+/*
+ 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.camera;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CordovaResourceApi;
+import org.apache.cordova.LOG;
+import org.apache.cordova.PermissionHelper;
+import org.apache.cordova.PluginResult;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.media.ExifInterface;
+import android.media.MediaScannerConnection;
+import android.media.MediaScannerConnection.MediaScannerConnectionClient;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.util.Base64;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PermissionInfo;
+
+/**
+ * This class launches the camera view, allows the user to take a picture, closes the camera view,
+ * and returns the captured image. When the camera view is closed, the screen displayed before
+ * the camera view was shown is redisplayed.
+ */
+public class CameraLauncher extends CordovaPlugin implements MediaScannerConnectionClient {
+
+ private static final int DATA_URL = 0; // Return base64 encoded string
+ private static final int FILE_URI = 1; // Return file uri (content://media/external/images/media/2 for Android)
+ private static final int NATIVE_URI = 2; // On Android, this is the same as FILE_URI
+
+ private static final int PHOTOLIBRARY = 0; // Choose image from picture library (same as SAVEDPHOTOALBUM for Android)
+ private static final int CAMERA = 1; // Take picture from camera
+ private static final int SAVEDPHOTOALBUM = 2; // Choose image from picture library (same as PHOTOLIBRARY for Android)
+
+ private static final int PICTURE = 0; // allow selection of still pictures only. DEFAULT. Will return format specified via DestinationType
+ private static final int VIDEO = 1; // allow selection of video only, ONLY RETURNS URL
+ private static final int ALLMEDIA = 2; // allow selection from all media types
+
+ private static final int JPEG = 0; // Take a picture of type JPEG
+ private static final int PNG = 1; // Take a picture of type PNG
+ private static final String GET_PICTURE = "Get Picture";
+ private static final String GET_VIDEO = "Get Video";
+ private static final String GET_All = "Get All";
+
+ public static final int PERMISSION_DENIED_ERROR = 20;
+ public static final int TAKE_PIC_SEC = 0;
+ public static final int SAVE_TO_ALBUM_SEC = 1;
+
+ private static final String LOG_TAG = "CameraLauncher";
+
+ //Where did this come from?
+ private static final int CROP_CAMERA = 100;
+
+ private int mQuality; // Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality)
+ private int targetWidth; // desired width of the image
+ private int targetHeight; // desired height of the image
+ private Uri imageUri; // Uri of captured image
+ private int encodingType; // Type of encoding to use
+ private int mediaType; // What type of media to retrieve
+ private int destType; // Source type (needs to be saved for the permission handling)
+ private int srcType; // Destination type (needs to be saved for permission handling)
+ private boolean saveToPhotoAlbum; // Should the picture be saved to the device's photo album
+ private boolean correctOrientation; // Should the pictures orientation be corrected
+ private boolean orientationCorrected; // Has the picture's orientation been corrected
+ private boolean allowEdit; // Should we allow the user to crop the image.
+
+ protected final static String[] permissions = { Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE };
+
+ public CallbackContext callbackContext;
+ private int numPics;
+
+ private MediaScannerConnection conn; // Used to update gallery app with newly-written files
+ private Uri scanMe; // Uri of image to be added to content store
+ private Uri croppedUri;
+ private ExifHelper exifData; // Exif data from source
+
+
+ /**
+ * Executes the request and returns PluginResult.
+ *
+ * @param action The action to execute.
+ * @param args JSONArry of arguments for the plugin.
+ * @param callbackContext The callback id used when calling back into JavaScript.
+ * @return A PluginResult object with a status and message.
+ */
+ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+ this.callbackContext = callbackContext;
+
+ if (action.equals("takePicture")) {
+ this.srcType = CAMERA;
+ this.destType = FILE_URI;
+ this.saveToPhotoAlbum = false;
+ this.targetHeight = 0;
+ this.targetWidth = 0;
+ this.encodingType = JPEG;
+ this.mediaType = PICTURE;
+ this.mQuality = 50;
+
+ //Take the values from the arguments if they're not already defined (this is tricky)
+ this.destType = args.getInt(1);
+ this.srcType = args.getInt(2);
+ this.mQuality = args.getInt(0);
+ this.targetWidth = args.getInt(3);
+ this.targetHeight = args.getInt(4);
+ this.encodingType = args.getInt(5);
+ this.mediaType = args.getInt(6);
+ this.allowEdit = args.getBoolean(7);
+ this.correctOrientation = args.getBoolean(8);
+ this.saveToPhotoAlbum = args.getBoolean(9);
+
+ // If the user specifies a 0 or smaller width/height
+ // make it -1 so later comparisons succeed
+ if (this.targetWidth < 1) {
+ this.targetWidth = -1;
+ }
+ if (this.targetHeight < 1) {
+ this.targetHeight = -1;
+ }
+
+ // We don't return full-quality PNG files. The camera outputs a JPEG
+ // so requesting it as a PNG provides no actual benefit
+ if (this.targetHeight == -1 && this.targetWidth == -1 && this.mQuality == 100 &&
+ !this.correctOrientation && this.encodingType == PNG && this.srcType == CAMERA) {
+ this.encodingType = JPEG;
+ }
+
+ try {
+ if (this.srcType == CAMERA) {
+ this.callTakePicture(destType, encodingType);
+ }
+ else if ((this.srcType == PHOTOLIBRARY) || (this.srcType == SAVEDPHOTOALBUM)) {
+ // FIXME: Stop always requesting the permission
+ if(!PermissionHelper.hasPermission(this, permissions[0])) {
+ PermissionHelper.requestPermission(this, SAVE_TO_ALBUM_SEC, Manifest.permission.READ_EXTERNAL_STORAGE);
+ } else {
+ this.getImage(this.srcType, destType, encodingType);
+ }
+ }
+ }
+ catch (IllegalArgumentException e)
+ {
+ callbackContext.error("Illegal Argument Exception");
+ PluginResult r = new PluginResult(PluginResult.Status.ERROR);
+ callbackContext.sendPluginResult(r);
+ return true;
+ }
+
+ PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT);
+ r.setKeepCallback(true);
+ callbackContext.sendPluginResult(r);
+
+ return true;
+ }
+ return false;
+ }
+
+ //--------------------------------------------------------------------------
+ // LOCAL METHODS
+ //--------------------------------------------------------------------------
+
+ private String getTempDirectoryPath() {
+ File cache = null;
+
+ // SD Card Mounted
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ cache = cordova.getActivity().getExternalCacheDir();
+ }
+ // Use internal storage
+ else {
+ cache = cordova.getActivity().getCacheDir();
+ }
+
+ // Create the cache directory if it doesn't exist
+ cache.mkdirs();
+ return cache.getAbsolutePath();
+ }
+
+ /**
+ * Take a picture with the camera.
+ * When an image is captured or the camera view is cancelled, the result is returned
+ * in CordovaActivity.onActivityResult, which forwards the result to this.onActivityResult.
+ *
+ * The image can either be returned as a base64 string or a URI that points to the file.
+ * To display base64 string in an img tag, set the source to:
+ * img.src="data:image/jpeg;base64,"+result;
+ * or to display URI in an img tag
+ * img.src=result;
+ *
+ * @param returnType Set the type of image to return.
+ * @param encodingType JPEG or PNG
+ */
+ public void callTakePicture(int returnType, int encodingType) {
+ boolean saveAlbumPermission = PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
+ boolean takePicturePermission = PermissionHelper.hasPermission(this, Manifest.permission.CAMERA);
+
+ // CB-10120: 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.
+
+ if (!takePicturePermission) {
+ takePicturePermission = true;
+ 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)) {
+ takePicturePermission = false;
+ break;
+ }
+ }
+ }
+ } catch (NameNotFoundException e) {
+ // We are requesting the info for our package, so this should
+ // never be caught
+ }
+ }
+
+ if (takePicturePermission && saveAlbumPermission) {
+ takePicture(returnType, encodingType);
+ } else if (saveAlbumPermission && !takePicturePermission) {
+ PermissionHelper.requestPermission(this, TAKE_PIC_SEC, Manifest.permission.CAMERA);
+ } else if (!saveAlbumPermission && takePicturePermission) {
+ PermissionHelper.requestPermission(this, TAKE_PIC_SEC, Manifest.permission.READ_EXTERNAL_STORAGE);
+ } else {
+ PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, permissions);
+ }
+ }
+
+ public void takePicture(int returnType, int encodingType)
+ {
+ // Save the number of images currently on disk for later
+ this.numPics = queryImgDB(whichContentStore()).getCount();
+
+ // Let's use the intent and see what happens
+ Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+
+ // Specify file so that large image is captured and returned
+ File photo = createCaptureFile(encodingType);
+ intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(photo));
+ this.imageUri = Uri.fromFile(photo);
+
+ if (this.cordova != null) {
+ // Let's check to make sure the camera is actually installed. (Legacy Nexus 7 code)
+ PackageManager mPm = this.cordova.getActivity().getPackageManager();
+ if(intent.resolveActivity(mPm) != null)
+ {
+
+ this.cordova.startActivityForResult((CordovaPlugin) this, intent, (CAMERA + 1) * 16 + returnType + 1);
+ }
+ else
+ {
+ LOG.d(LOG_TAG, "Error: You don't have a default camera. Your device may not be CTS complaint.");
+ }
+ }
+// else
+// LOG.d(LOG_TAG, "ERROR: You must use the CordovaInterface for this to work correctly. Please implement it in your activity");
+ }
+
+ /**
+ * Create a file in the applications temporary directory based upon the supplied encoding.
+ *
+ * @param encodingType of the image to be taken
+ * @return a File object pointing to the temporary picture
+ */
+ private File createCaptureFile(int encodingType) {
+ return createCaptureFile(encodingType, "");
+ }
+
+ /**
+ * Create a file in the applications temporary directory based upon the supplied encoding.
+ *
+ * @param encodingType of the image to be taken
+ * @param fileName or resultant File object.
+ * @return a File object pointing to the temporary picture
+ */
+ private File createCaptureFile(int encodingType, String fileName) {
+ if (fileName.isEmpty()) {
+ fileName = ".Pic";
+ }
+
+ if (encodingType == JPEG) {
+ fileName = fileName + ".jpg";
+ } else if (encodingType == PNG) {
+ fileName = fileName + ".png";
+ } else {
+ throw new IllegalArgumentException("Invalid Encoding Type: " + encodingType);
+ }
+
+ return new File(getTempDirectoryPath(), fileName);
+ }
+
+
+
+ /**
+ * Get image from photo library.
+ *
+ * @param srcType The album to get image from.
+ * @param returnType Set the type of image to return.
+ * @param encodingType
+ */
+ // TODO: Images selected from SDCARD don't display correctly, but from CAMERA ALBUM do!
+ // TODO: Images from kitkat filechooser not going into crop function
+ public void getImage(int srcType, int returnType, int encodingType) {
+ Intent intent = new Intent();
+ String title = GET_PICTURE;
+ croppedUri = null;
+ if (this.mediaType == PICTURE) {
+ intent.setType("image/*");
+ if (this.allowEdit) {
+ intent.setAction(Intent.ACTION_PICK);
+ intent.putExtra("crop", "true");
+ if (targetWidth > 0) {
+ intent.putExtra("outputX", targetWidth);
+ }
+ if (targetHeight > 0) {
+ intent.putExtra("outputY", targetHeight);
+ }
+ if (targetHeight > 0 && targetWidth > 0 && targetWidth == targetHeight) {
+ intent.putExtra("aspectX", 1);
+ intent.putExtra("aspectY", 1);
+ }
+ File photo = createCaptureFile(JPEG);
+ croppedUri = Uri.fromFile(photo);
+ intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, croppedUri);
+ } else {
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ }
+ } else if (this.mediaType == VIDEO) {
+ intent.setType("video/*");
+ title = GET_VIDEO;
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ } else if (this.mediaType == ALLMEDIA) {
+ // I wanted to make the type 'image/*, video/*' but this does not work on all versions
+ // of android so I had to go with the wildcard search.
+ intent.setType("*/*");
+ title = GET_All;
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ }
+ if (this.cordova != null) {
+ this.cordova.startActivityForResult((CordovaPlugin) this, Intent.createChooser(intent,
+ new String(title)), (srcType + 1) * 16 + returnType + 1);
+ }
+ }
+
+ /**
+ * Brings up the UI to perform crop on passed image URI
+ *
+ * @param picUri
+ */
+ private void performCrop(Uri picUri, int destType, Intent cameraIntent) {
+ try {
+ Intent cropIntent = new Intent("com.android.camera.action.CROP");
+ // indicate image type and Uri
+ cropIntent.setDataAndType(picUri, "image/*");
+ // set crop properties
+ cropIntent.putExtra("crop", "true");
+
+ // indicate output X and Y
+ if (targetWidth > 0) {
+ cropIntent.putExtra("outputX", targetWidth);
+ }
+ if (targetHeight > 0) {
+ cropIntent.putExtra("outputY", targetHeight);
+ }
+ if (targetHeight > 0 && targetWidth > 0 && targetWidth == targetHeight) {
+ cropIntent.putExtra("aspectX", 1);
+ cropIntent.putExtra("aspectY", 1);
+ }
+ // create new file handle to get full resolution crop
+ croppedUri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + ""));
+ cropIntent.putExtra("output", croppedUri);
+
+ // start the activity - we handle returning in onActivityResult
+
+ if (this.cordova != null) {
+ this.cordova.startActivityForResult((CordovaPlugin) this,
+ cropIntent, CROP_CAMERA + destType);
+ }
+ } catch (ActivityNotFoundException anfe) {
+ LOG.e(LOG_TAG, "Crop operation not supported on this device");
+ try {
+ processResultFromCamera(destType, cameraIntent);
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ LOG.e(LOG_TAG, "Unable to write to file");
+ }
+ }
+ }
+
+ /**
+ * Applies all needed transformation to the image received from the camera.
+ *
+ * @param destType In which form should we return the image
+ * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
+ */
+ private void processResultFromCamera(int destType, Intent intent) throws IOException {
+ int rotate = 0;
+
+ // Create an ExifHelper to save the exif data that is lost during compression
+ ExifHelper exif = new ExifHelper();
+ String sourcePath = (this.allowEdit && this.croppedUri != null) ?
+ FileHelper.stripFileProtocol(this.croppedUri.toString()) :
+ FileHelper.stripFileProtocol(this.imageUri.toString());
+
+ if (this.encodingType == JPEG) {
+ try {
+ //We don't support PNG, so let's not pretend we do
+ exif.createInFile(sourcePath);
+ exif.readExifData();
+ rotate = exif.getOrientation();
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ Bitmap bitmap = null;
+ Uri galleryUri = null;
+
+ // CB-5479 When this option is given the unchanged image should be saved
+ // in the gallery and the modified image is saved in the temporary
+ // directory
+ if (this.saveToPhotoAlbum) {
+ galleryUri = Uri.fromFile(new File(getPicturesPath()));
+
+ if(this.allowEdit && this.croppedUri != null) {
+ writeUncompressedImage(this.croppedUri, galleryUri);
+ } else {
+ writeUncompressedImage(this.imageUri, galleryUri);
+ }
+
+ refreshGallery(galleryUri);
+ }
+
+ // If sending base64 image back
+ if (destType == DATA_URL) {
+ bitmap = getScaledAndRotatedBitmap(sourcePath);
+
+ if (bitmap == null) {
+ // Try to get the bitmap from intent.
+ bitmap = (Bitmap)intent.getExtras().get("data");
+ }
+
+ // Double-check the bitmap.
+ if (bitmap == null) {
+ LOG.d(LOG_TAG, "I either have a null image path or bitmap");
+ this.failPicture("Unable to create bitmap!");
+ return;
+ }
+
+
+ this.processPicture(bitmap, this.encodingType);
+
+ if (!this.saveToPhotoAlbum) {
+ checkForDuplicateImage(DATA_URL);
+ }
+ }
+
+ // If sending filename back
+ else if (destType == FILE_URI || destType == NATIVE_URI) {
+ // If all this is true we shouldn't compress the image.
+ if (this.targetHeight == -1 && this.targetWidth == -1 && this.mQuality == 100 &&
+ !this.correctOrientation) {
+
+ // If we saved the uncompressed photo to the album, we can just
+ // return the URI we already created
+ if (this.saveToPhotoAlbum) {
+ this.callbackContext.success(galleryUri.toString());
+ } else {
+ Uri uri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + ""));
+
+ if(this.allowEdit && this.croppedUri != null) {
+ writeUncompressedImage(this.croppedUri, uri);
+ } else {
+ writeUncompressedImage(this.imageUri, uri);
+ }
+
+ this.callbackContext.success(uri.toString());
+ }
+ } else {
+ Uri uri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + ""));
+ bitmap = getScaledAndRotatedBitmap(sourcePath);
+
+ // Double-check the bitmap.
+ if (bitmap == null) {
+ LOG.d(LOG_TAG, "I either have a null image path or bitmap");
+ this.failPicture("Unable to create bitmap!");
+ return;
+ }
+
+
+ // Add compressed version of captured image to returned media store Uri
+ OutputStream os = this.cordova.getActivity().getContentResolver().openOutputStream(uri);
+ CompressFormat compressFormat = encodingType == JPEG ?
+ CompressFormat.JPEG :
+ CompressFormat.PNG;
+
+ bitmap.compress(compressFormat, this.mQuality, os);
+ os.close();
+
+ // Restore exif data to file
+ if (this.encodingType == JPEG) {
+ String exifPath;
+ exifPath = uri.getPath();
+ exif.createOutFile(exifPath);
+ exif.writeExifData();
+ }
+
+ // Send Uri back to JavaScript for viewing image
+ this.callbackContext.success(uri.toString());
+
+ }
+ } else {
+ throw new IllegalStateException();
+ }
+
+ this.cleanup(FILE_URI, this.imageUri, galleryUri, bitmap);
+ bitmap = null;
+ }
+
+private String getPicutresPath()
+{
+ String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
+ String imageFileName = "IMG_" + timeStamp + (this.encodingType == JPEG ? ".jpg" : ".png");
+ File storageDir = Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_PICTURES);
+ String galleryPath = storageDir.getAbsolutePath() + "/" + imageFileName;
+ return galleryPath;
+}
+
+private void refreshGallery(Uri contentUri)
+{
+ Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ mediaScanIntent.setData(contentUri);
+ this.cordova.getActivity().sendBroadcast(mediaScanIntent);
+}
+
+ /**
+ * Converts output image format int value to string value of mime type.
+ * @param outputFormat int Output format of camera API.
+ * Must be value of either JPEG or PNG constant
+ * @return String String value of mime type or empty string if mime type is not supported
+ */
+ private String getMimetypeForFormat(int outputFormat) {
+ if (outputFormat == PNG) return "image/png";
+ if (outputFormat == JPEG) return "image/jpeg";
+ return "";
+ }
+
+ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
+ // Some content: URIs do not map to file paths (e.g. picasa).
+ String realPath = FileHelper.getRealPath(uri, this.cordova);
+
+ // Get filename from uri
+ String fileName = realPath != null ?
+ realPath.substring(realPath.lastIndexOf('/') + 1, realPath.lastIndexOf(".") + 1) :
+ "modified.";
+
+ // Append filename extension based on output encoding type
+ fileName += (this.encodingType == JPEG ? "jpg" : "png");
+
+ String modifiedPath = getTempDirectoryPath() + "/" + fileName;
+
+ OutputStream os = new FileOutputStream(modifiedPath);
+ CompressFormat compressFormat = this.encodingType == JPEG ?
+ CompressFormat.JPEG :
+ CompressFormat.PNG;
+
+ bitmap.compress(compressFormat, this.mQuality, os);
+ os.close();
+
+ if (realPath != null && this.encodingType == JPEG) {
+ // Create an ExifHelper to save the exif data that is lost during compression
+ ExifHelper exif = new ExifHelper();
+
+ try {
+ exif.createInFile(realPath);
+ exif.readExifData();
+ if (this.correctOrientation && this.orientationCorrected) {
+ exif.resetOrientation();
+ }
+ exif.createOutFile(modifiedPath);
+ exif.writeExifData();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return modifiedPath;
+ }
+
+
+
+/**
+ * Applies all needed transformation to the image received from the gallery.
+ *
+ * @param destType In which form should we return the image
+ * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
+ */
+ private void processResultFromGallery(int destType, Intent intent) {
+ Uri uri = intent.getData();
+ if (uri == null) {
+ if (croppedUri != null) {
+ uri = croppedUri;
+ } else {
+ this.failPicture("null data from photo library");
+ return;
+ }
+ }
+ int rotate = 0;
+
+ String fileLocation = FileHelper.getRealPath(uri, this.cordova);
+ LOG.d(LOG_TAG, "File locaton is: " + fileLocation);
+
+ // If you ask for video or all media type you will automatically get back a file URI
+ // and there will be no attempt to resize any returned data
+ if (this.mediaType != PICTURE) {
+ this.callbackContext.success(fileLocation);
+ }
+ else {
+ String uriString = uri.toString();
+ // Get the path to the image. Makes loading so much easier.
+ String mimeType = FileHelper.getMimeType(uriString, this.cordova);
+
+ // This is a special case to just return the path as no scaling,
+ // rotating, nor compressing needs to be done
+ if (this.targetHeight == -1 && this.targetWidth == -1 &&
+ (destType == FILE_URI || destType == NATIVE_URI) && !this.correctOrientation &&
+ mimeType.equalsIgnoreCase(getMimetypeForFormat(encodingType)))
+ {
+ this.callbackContext.success(uriString);
+ } else {
+ // If we don't have a valid image so quit.
+ if (!("image/jpeg".equalsIgnoreCase(mimeType) || "image/png".equalsIgnoreCase(mimeType))) {
+ LOG.d(LOG_TAG, "I either have a null image path or bitmap");
+ this.failPicture("Unable to retrieve path to picture!");
+ return;
+ }
+ Bitmap bitmap = null;
+ try {
+ bitmap = getScaledBitmap(uriString);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ if (bitmap == null) {
+ LOG.d(LOG_TAG, "I either have a null image path or bitmap");
+ this.failPicture("Unable to create bitmap!");
+ return;
+ }
+
+ if (this.correctOrientation) {
+ rotate = getImageOrientation(uri);
+ if (rotate != 0) {
+ Matrix matrix = new Matrix();
+ matrix.setRotate(rotate);
+ try {
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
+ this.orientationCorrected = true;
+ } catch (OutOfMemoryError oom) {
+ this.orientationCorrected = false;
+ }
+ }
+ }
+
+ // If sending base64 image back
+ if (destType == DATA_URL) {
+ this.processPicture(bitmap, this.encodingType);
+ }
+
+ // If sending filename back
+ else if (destType == FILE_URI || destType == NATIVE_URI) {
+ // Did we modify the image?
+ if ( (this.targetHeight > 0 && this.targetWidth > 0) ||
+ (this.correctOrientation && this.orientationCorrected) ||
+ !mimeType.equalsIgnoreCase(getMimetypeForFormat(encodingType)))
+ {
+ try {
+ String modifiedPath = this.ouputModifiedBitmap(bitmap, uri);
+ // The modified image is cached by the app in order to get around this and not have to delete you
+ // application cache I'm adding the current system time to the end of the file url.
+ this.callbackContext.success("file://" + modifiedPath + "?" + System.currentTimeMillis());
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ this.failPicture("Error retrieving image.");
+ }
+ }
+ else {
+ this.callbackContext.success(fileLocation);
+ }
+ }
+ if (bitmap != null) {
+ bitmap.recycle();
+ bitmap = null;
+ }
+ System.gc();
+ }
+ }
+ }
+
+ /**
+ * Called when the camera view exits.
+ *
+ * @param requestCode The request code originally supplied to startActivityForResult(),
+ * allowing you to identify who this result came from.
+ * @param resultCode The integer result code returned by the child activity through its setResult().
+ * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
+ */
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+
+ // Get src and dest types from request code for a Camera Activity
+ int srcType = (requestCode / 16) - 1;
+ int destType = (requestCode % 16) - 1;
+
+ // If Camera Crop
+ if (requestCode >= CROP_CAMERA) {
+ if (resultCode == Activity.RESULT_OK) {
+
+ // Because of the inability to pass through multiple intents, this hack will allow us
+ // to pass arcane codes back.
+ destType = requestCode - CROP_CAMERA;
+ try {
+ processResultFromCamera(destType, intent);
+ } catch (IOException e) {
+ e.printStackTrace();
+ LOG.e(LOG_TAG, "Unable to write to file");
+ }
+
+ }// If cancelled
+ else if (resultCode == Activity.RESULT_CANCELED) {
+ this.failPicture("Camera cancelled.");
+ }
+
+ // If something else
+ else {
+ this.failPicture("Did not complete!");
+ }
+ }
+ // If CAMERA
+ else if (srcType == CAMERA) {
+ // If image available
+ if (resultCode == Activity.RESULT_OK) {
+ try {
+ if(this.allowEdit)
+ {
+ Uri tmpFile = Uri.fromFile(createCaptureFile(this.encodingType));
+ performCrop(tmpFile, destType, intent);
+ }
+ else {
+ this.processResultFromCamera(destType, intent);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ this.failPicture("Error capturing image.");
+ }
+ }
+
+ // If cancelled
+ else if (resultCode == Activity.RESULT_CANCELED) {
+ this.failPicture("Camera cancelled.");
+ }
+
+ // If something else
+ else {
+ this.failPicture("Did not complete!");
+ }
+ }
+ // If retrieving photo from library
+ else if ((srcType == PHOTOLIBRARY) || (srcType == SAVEDPHOTOALBUM)) {
+ if (resultCode == Activity.RESULT_OK && intent != null) {
+ final Intent i = intent;
+ final int finalDestType = destType;
+ cordova.getThreadPool().execute(new Runnable() {
+ public void run() {
+ processResultFromGallery(finalDestType, i);
+ }
+ });
+ }
+ else if (resultCode == Activity.RESULT_CANCELED) {
+ this.failPicture("Selection cancelled.");
+ }
+ else {
+ this.failPicture("Selection did not complete!");
+ }
+ }
+ }
+
+ private int getImageOrientation(Uri uri) {
+ int rotate = 0;
+ String[] cols = { MediaStore.Images.Media.ORIENTATION };
+ try {
+ Cursor cursor = cordova.getActivity().getContentResolver().query(uri,
+ cols, null, null, null);
+ if (cursor != null) {
+ cursor.moveToPosition(0);
+ rotate = cursor.getInt(0);
+ cursor.close();
+ }
+ } catch (Exception e) {
+ // You can get an IllegalArgumentException if ContentProvider doesn't support querying for orientation.
+ }
+ return rotate;
+ }
+
+ /**
+ * Figure out if the bitmap should be rotated. For instance if the picture was taken in
+ * portrait mode
+ *
+ * @param rotate
+ * @param bitmap
+ * @return rotated bitmap
+ */
+ private Bitmap getRotatedBitmap(int rotate, Bitmap bitmap, ExifHelper exif) {
+ Matrix matrix = new Matrix();
+ if (rotate == 180) {
+ matrix.setRotate(rotate);
+ } else {
+ matrix.setRotate(rotate, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2);
+ }
+
+ try
+ {
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
+ exif.resetOrientation();
+ }
+ catch (OutOfMemoryError oom)
+ {
+ // You can run out of memory if the image is very large:
+ // http://simonmacdonald.blogspot.ca/2012/07/change-to-camera-code-in-phonegap-190.html
+ // If this happens, simply do not rotate the image and return it unmodified.
+ // If you do not catch the OutOfMemoryError, the Android app crashes.
+ }
+
+ return bitmap;
+ }
+
+ /**
+ * In the special case where the default width, height and quality are unchanged
+ * we just write the file out to disk saving the expensive Bitmap.compress function.
+ *
+ * @param uri
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ private void writeUncompressedImage(Uri src, Uri dest) throws FileNotFoundException,
+ IOException {
+ FileInputStream fis = null;
+ OutputStream os = null;
+ try {
+ fis = new FileInputStream(FileHelper.stripFileProtocol(src.toString()));
+ os = this.cordova.getActivity().getContentResolver().openOutputStream(dest);
+ byte[] buffer = new byte[4096];
+ int len;
+ while ((len = fis.read(buffer)) != -1) {
+ os.write(buffer, 0, len);
+ }
+ os.flush();
+ } finally {
+ if (os != null) {
+ try {
+ os.close();
+ } catch (IOException e) {
+ LOG.d(LOG_TAG,"Exception while closing output stream.");
+ }
+ }
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException e) {
+ LOG.d(LOG_TAG,"Exception while closing file input stream.");
+ }
+ }
+ }
+ }
+
+ /**
+ * Create entry in media store for image
+ *
+ * @return uri
+ */
+ private Uri getUriFromMediaStore() {
+ ContentValues values = new ContentValues();
+ values.put(android.provider.MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
+ Uri uri;
+ try {
+ uri = this.cordova.getActivity().getContentResolver().insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+ } catch (RuntimeException 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 (RuntimeException ex) {
+ LOG.d(LOG_TAG, "Can't write to internal media storage.");
+ return null;
+ }
+ }
+ return uri;
+ }
+
+ /**
+ * Return a scaled bitmap based on the target width and height
+ *
+ * @param imagePath
+ * @return
+ * @throws IOException
+ */
+ private Bitmap getScaledBitmap(String imageUrl) throws IOException {
+ // If no new width or height were specified return the original bitmap
+ if (this.targetWidth <= 0 && this.targetHeight <= 0) {
+ InputStream fileStream = null;
+ Bitmap image = null;
+ try {
+ fileStream = FileHelper.getInputStreamFromUriString(imageUrl, cordova);
+ image = BitmapFactory.decodeStream(fileStream);
+ } finally {
+ if (fileStream != null) {
+ try {
+ fileStream.close();
+ } catch (IOException e) {
+ LOG.d(LOG_TAG,"Exception while closing file input stream.");
+ }
+ }
+ }
+ return image;
+ }
+
+ // figure out the original width and height of the image
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ InputStream fileStream = null;
+ try {
+ fileStream = FileHelper.getInputStreamFromUriString(imageUrl, cordova);
+ BitmapFactory.decodeStream(fileStream, null, options);
+ } finally {
+ if (fileStream != null) {
+ try {
+ fileStream.close();
+ } catch (IOException e) {
+ LOG.d(LOG_TAG,"Exception while closing file input stream.");
+ }
+ }
+ }
+
+ //CB-2292: WTF? Why is the width null?
+ if(options.outWidth == 0 || options.outHeight == 0)
+ {
+ return null;
+ }
+
+ // determine the correct aspect ratio
+ int[] widthHeight = calculateAspectRatio(options.outWidth, options.outHeight);
+
+ // Load in the smallest bitmap possible that is closest to the size we want
+ options.inJustDecodeBounds = false;
+ options.inSampleSize = calculateSampleSize(options.outWidth, options.outHeight, this.targetWidth, this.targetHeight);
+ Bitmap unscaledBitmap = null;
+ try {
+ fileStream = FileHelper.getInputStreamFromUriString(imageUrl, cordova);
+ unscaledBitmap = BitmapFactory.decodeStream(fileStream, null, options);
+ } finally {
+ if (fileStream != null) {
+ try {
+ fileStream.close();
+ } catch (IOException e) {
+ LOG.d(LOG_TAG,"Exception while closing file input stream.");
+ }
+ }
+ }
+ if (unscaledBitmap == null) {
+ return null;
+ }
+
+ return Bitmap.createScaledBitmap(unscaledBitmap, widthHeight[0], widthHeight[1], true);
+ }
+
+ /**
+ * Maintain the aspect ratio so the resulting image does not look smooshed
+ *
+ * @param origWidth
+ * @param origHeight
+ * @return
+ */
+ public int[] calculateAspectRatio(int origWidth, int origHeight) {
+ int newWidth = this.targetWidth;
+ int newHeight = this.targetHeight;
+
+ // If no new width or height were specified return the original bitmap
+ if (newWidth <= 0 && newHeight <= 0) {
+ newWidth = origWidth;
+ newHeight = origHeight;
+ }
+ // Only the width was specified
+ else if (newWidth > 0 && newHeight <= 0) {
+ newHeight = (newWidth * origHeight) / origWidth;
+ }
+ // only the height was specified
+ else if (newWidth <= 0 && newHeight > 0) {
+ newWidth = (newHeight * origWidth) / origHeight;
+ }
+ // If the user specified both a positive width and height
+ // (potentially different aspect ratio) then the width or height is
+ // scaled so that the image fits while maintaining aspect ratio.
+ // Alternatively, the specified width and height could have been
+ // kept and Bitmap.SCALE_TO_FIT specified when scaling, but this
+ // would result in whitespace in the new image.
+ else {
+ double newRatio = newWidth / (double) newHeight;
+ double origRatio = origWidth / (double) origHeight;
+
+ if (origRatio > newRatio) {
+ newHeight = (newWidth * origHeight) / origWidth;
+ } else if (origRatio < newRatio) {
+ newWidth = (newHeight * origWidth) / origHeight;
+ }
+ }
+
+ int[] retval = new int[2];
+ retval[0] = newWidth;
+ retval[1] = newHeight;
+ return retval;
+ }
+
+ /**
+ * Figure out what ratio we can load our image into memory at while still being bigger than
+ * our desired width and height
+ *
+ * @param srcWidth
+ * @param srcHeight
+ * @param dstWidth
+ * @param dstHeight
+ * @return
+ */
+ public static int calculateSampleSize(int srcWidth, int srcHeight, int dstWidth, int dstHeight) {
+ final float srcAspect = (float)srcWidth / (float)srcHeight;
+ final float dstAspect = (float)dstWidth / (float)dstHeight;
+
+ if (srcAspect > dstAspect) {
+ return srcWidth / dstWidth;
+ } else {
+ return srcHeight / dstHeight;
+ }
+ }
+
+ /**
+ * Creates a cursor that can be used to determine how many images we have.
+ *
+ * @return a cursor
+ */
+ private Cursor queryImgDB(Uri contentStore) {
+ return this.cordova.getActivity().getContentResolver().query(
+ contentStore,
+ new String[] { MediaStore.Images.Media._ID },
+ null,
+ null,
+ null);
+ }
+
+ /**
+ * Cleans up after picture taking. Checking for duplicates and that kind of stuff.
+ * @param newImage
+ */
+ private void cleanup(int imageType, Uri oldImage, Uri newImage, Bitmap bitmap) {
+ if (bitmap != null) {
+ bitmap.recycle();
+ }
+
+ // Clean up initial camera-written image file.
+ (new File(FileHelper.stripFileProtocol(oldImage.toString()))).delete();
+
+ checkForDuplicateImage(imageType);
+ // Scan for the gallery to update pic refs in gallery
+ if (this.saveToPhotoAlbum && newImage != null) {
+ this.scanForGallery(newImage);
+ }
+
+ System.gc();
+ }
+
+ /**
+ * Used to find out if we are in a situation where the Camera Intent adds to images
+ * to the content store. If we are using a FILE_URI and the number of images in the DB
+ * increases by 2 we have a duplicate, when using a DATA_URL the number is 1.
+ *
+ * @param type FILE_URI or DATA_URL
+ */
+ private void checkForDuplicateImage(int type) {
+ int diff = 1;
+ Uri contentStore = whichContentStore();
+ Cursor cursor = queryImgDB(contentStore);
+ int currentNumOfImages = cursor.getCount();
+
+ if (type == FILE_URI && this.saveToPhotoAlbum) {
+ diff = 2;
+ }
+
+ // delete the duplicate file if the difference is 2 for file URI or 1 for Data URL
+ if ((currentNumOfImages - numPics) == diff) {
+ cursor.moveToLast();
+ int id = Integer.valueOf(cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media._ID)));
+ if (diff == 2) {
+ id--;
+ }
+ Uri uri = Uri.parse(contentStore + "/" + id);
+ this.cordova.getActivity().getContentResolver().delete(uri, null, null);
+ cursor.close();
+ }
+ }
+
+ /**
+ * Determine if we are storing the images in internal or external storage
+ * @return Uri
+ */
+ private Uri whichContentStore() {
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ return android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ } else {
+ return android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI;
+ }
+ }
+
+ /**
+ * Compress bitmap using jpeg, convert to Base64 encoded string, and return to JavaScript.
+ *
+ * @param bitmap
+ */
+ public void processPicture(Bitmap bitmap, int encodingType) {
+ ByteArrayOutputStream jpeg_data = new ByteArrayOutputStream();
+ CompressFormat compressFormat = encodingType == JPEG ?
+ CompressFormat.JPEG :
+ CompressFormat.PNG;
+
+ try {
+ if (bitmap.compress(compressFormat, mQuality, jpeg_data)) {
+ byte[] code = jpeg_data.toByteArray();
+ byte[] output = Base64.encode(code, Base64.NO_WRAP);
+ String js_out = new String(output);
+ this.callbackContext.success(js_out);
+ js_out = null;
+ output = null;
+ code = null;
+ }
+ } catch (Exception e) {
+ this.failPicture("Error compressing image.");
+ }
+ jpeg_data = null;
+ }
+
+ /**
+ * Send error message to JavaScript.
+ *
+ * @param err
+ */
+ public void failPicture(String err) {
+ this.callbackContext.error(err);
+ }
+
+ private void scanForGallery(Uri newImage) {
+ this.scanMe = newImage;
+ if(this.conn != null) {
+ this.conn.disconnect();
+ }
+ this.conn = new MediaScannerConnection(this.cordova.getActivity().getApplicationContext(), this);
+ conn.connect();
+ }
+
+ public void onMediaScannerConnected() {
+ try{
+ this.conn.scanFile(this.scanMe.toString(), "image/*");
+ } catch (java.lang.IllegalStateException e){
+ LOG.e(LOG_TAG, "Can't scan file in MediaScanner after taking picture");
+ }
+
+ }
+
+ public void onScanCompleted(String path, Uri uri) {
+ this.conn.disconnect();
+ }
+
+
+ public void onRequestPermissionResult(int requestCode, String[] permissions,
+ int[] grantResults) throws JSONException
+ {
+ for(int r:grantResults)
+ {
+ if(r == PackageManager.PERMISSION_DENIED)
+ {
+ this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR));
+ return;
+ }
+ }
+ switch(requestCode)
+ {
+ case TAKE_PIC_SEC:
+ takePicture(this.destType, this.encodingType);
+ break;
+ case SAVE_TO_ALBUM_SEC:
+ this.getImage(this.srcType, this.destType, this.encodingType);
+ break;
+ }
+ }
+
+ /**
+ * Taking or choosing a picture launches another Activity, so we need to implement the
+ * save/restore APIs to handle the case where the CordovaActivity is killed by the OS
+ * before we get the launched Activity's result.
+ */
+ public Bundle onSaveInstanceState() {
+ Bundle state = new Bundle();
+ state.putInt("destType", this.destType);
+ state.putInt("srcType", this.srcType);
+ state.putInt("mQuality", this.mQuality);
+ state.putInt("targetWidth", this.targetWidth);
+ state.putInt("targetHeight", this.targetHeight);
+ state.putInt("encodingType", this.encodingType);
+ state.putInt("mediaType", this.mediaType);
+ state.putInt("numPics", this.numPics);
+ state.putBoolean("allowEdit", this.allowEdit);
+ state.putBoolean("correctOrientation", this.correctOrientation);
+ state.putBoolean("saveToPhotoAlbum", this.saveToPhotoAlbum);
+
+ if(this.croppedUri != null) {
+ state.putString("croppedUri", this.croppedUri.toString());
+ }
+
+ if(this.imageUri != null) {
+ state.putString("imageUri", this.imageUri.toString());
+ }
+
+ return state;
+ }
+
+ public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) {
+ this.destType = state.getInt("destType");
+ this.srcType = state.getInt("srcType");
+ this.mQuality = state.getInt("mQuality");
+ this.targetWidth = state.getInt("targetWidth");
+ this.targetHeight = state.getInt("targetHeight");
+ this.encodingType = state.getInt("encodingType");
+ this.mediaType = state.getInt("mediaType");
+ this.numPics = state.getInt("numPics");
+ this.allowEdit = state.getBoolean("allowEdit");
+ this.correctOrientation = state.getBoolean("correctOrientation");
+ this.saveToPhotoAlbum = state.getBoolean("saveToPhotoAlbum");
+
+ if(state.containsKey("croppedUri")) {
+ this.croppedUri = Uri.parse(state.getString("croppedUri"));
+ }
+
+ if(state.containsKey("imageUri")) {
+ this.imageUri = Uri.parse(state.getString("imageUri"));
+ }
+
+ this.callbackContext = callbackContext;
+ }
+}
\ No newline at end of file
diff --git a/src/browser/CameraProxy.js b/src/browser/CameraProxy.js
index 8f95c6f..635a9fb 100644
--- a/src/browser/CameraProxy.js
+++ b/src/browser/CameraProxy.js
@@ -23,11 +23,12 @@ var HIGHEST_POSSIBLE_Z_INDEX = 2147483647;
function takePicture(success, error, opts) {
if (opts && opts[2] === 1) {
- capture(success, error);
+ capture(success, error, opts);
} else {
var input = document.createElement('input');
input.style.position = 'relative';
input.style.zIndex = HIGHEST_POSSIBLE_Z_INDEX;
+ input.className = 'cordova-camera-select';
input.type = 'file';
input.name = 'files[]';
@@ -48,28 +49,36 @@ function takePicture(success, error, opts) {
}
}
-function capture(success, errorCallback) {
+function capture(success, errorCallback, opts) {
var localMediaStream;
+ var targetWidth = opts[3];
+ var targetHeight = opts[4];
+
+ targetWidth = targetWidth == -1?320:targetWidth;
+ targetHeight = targetHeight == -1?240:targetHeight;
var video = document.createElement('video');
var button = document.createElement('button');
var parent = document.createElement('div');
parent.style.position = 'relative';
parent.style.zIndex = HIGHEST_POSSIBLE_Z_INDEX;
+ parent.className = 'cordova-camera-capture';
parent.appendChild(video);
parent.appendChild(button);
- video.width = 320;
- video.height = 240;
+ video.width = targetWidth;
+ video.height = targetHeight;
button.innerHTML = 'Capture!';
button.onclick = function() {
// create a canvas and capture a frame from video stream
var canvas = document.createElement('canvas');
- canvas.getContext('2d').drawImage(video, 0, 0, 320, 240);
+ canvas.width = targetWidth;
+ canvas.height = targetHeight;
+ canvas.getContext('2d').drawImage(video, 0, 0, targetWidth, targetHeight);
// convert image stored in canvas to base64 encoded image
- var imageData = canvas.toDataURL('img/png');
+ var imageData = canvas.toDataURL('image/png');
imageData = imageData.replace('data:image/png;base64,', '');
// stop video stream, remove video and button.
diff --git a/src/ios/CDVCamera.m b/src/ios/CDVCamera.m
index 42dd469..019141b 100644
--- a/src/ios/CDVCamera.m
+++ b/src/ios/CDVCamera.m
@@ -240,9 +240,13 @@ static NSString* toBase64(NSData* data) {
- (void)repositionPopover:(CDVInvokedUrlCommand*)command
{
- NSDictionary* options = [command argumentAtIndex:0 withDefault:nil];
+ if (([[self pickerController] pickerPopoverController] != nil) && [[[self pickerController] pickerPopoverController] isPopoverVisible]) {
- [self displayPopover:options];
+ [[[self pickerController] pickerPopoverController] dismissPopoverAnimated:NO];
+
+ NSDictionary* options = [command argumentAtIndex:0 withDefault:nil];
+ [self displayPopover:options];
+ }
}
- (NSInteger)integerValueForKey:(NSDictionary*)dict key:(NSString*)key defaultValue:(NSInteger)defaultValue
@@ -520,9 +524,11 @@ static NSString* toBase64(NSData* data) {
NSString* mediaType = [info objectForKey:UIImagePickerControllerMediaType];
if ([mediaType isEqualToString:(NSString*)kUTTypeImage]) {
[weakSelf resultForImage:cameraPicker.pictureOptions info:info completion:^(CDVPluginResult* res) {
- [weakSelf.commandDelegate sendPluginResult:res callbackId:cameraPicker.callbackId];
- weakSelf.hasPendingOperation = NO;
- weakSelf.pickerController = nil;
+ if (![self usesGeolocation] || picker.sourceType != UIImagePickerControllerSourceTypeCamera) {
+ [weakSelf.commandDelegate sendPluginResult:res callbackId:cameraPicker.callbackId];
+ weakSelf.hasPendingOperation = NO;
+ weakSelf.pickerController = nil;
+ }
}];
}
else {
diff --git a/src/windows/CameraProxy.js b/src/windows/CameraProxy.js
index 0862f35..eb10cd2 100644
--- a/src/windows/CameraProxy.js
+++ b/src/windows/CameraProxy.js
@@ -79,10 +79,14 @@ var HIGHEST_POSSIBLE_Z_INDEX = 2147483647;
// Resize method
function resizeImage(successCallback, errorCallback, file, targetWidth, targetHeight, encodingType) {
var tempPhotoFileName = "";
+ var targetContentType = "";
+
if (encodingType == Camera.EncodingType.PNG) {
tempPhotoFileName = "camera_cordova_temp_return.png";
+ targetContentType = "image/png";
} else {
tempPhotoFileName = "camera_cordova_temp_return.jpg";
+ targetContentType = "image/jpeg";
}
var storageFolder = getAppData().localFolder;
@@ -108,7 +112,7 @@ function resizeImage(successCallback, errorCallback, file, targetWidth, targetHe
canvas.getContext("2d").drawImage(this, 0, 0, imageWidth, imageHeight);
- var fileContent = canvas.toDataURL(file.contentType).split(',')[1];
+ var fileContent = canvas.toDataURL(targetContentType).split(',')[1];
var storageFolder = getAppData().localFolder;
@@ -745,7 +749,7 @@ function takePictureFromCameraWindows(successCallback, errorCallback, args) {
cameraCaptureUI.photoSettings.maxResolution = maxRes;
var cameraPicture;
-
+
// define focus handler for windows phone 10.0
var savePhotoOnFocus = function () {
window.removeEventListener("focus", savePhotoOnFocus);
@@ -760,7 +764,7 @@ function takePictureFromCameraWindows(successCallback, errorCallback, args) {
};
// if windows phone 10, add and delete focus eventHandler to capture the focus back from cameraUI to app
- if (navigator.appVersion.indexOf('Windows Phone 10.0') >= 0) {
+ if (navigator.appVersion.indexOf('Windows Phone 10.0') >= 0) {
window.addEventListener("focus", savePhotoOnFocus);
}
diff --git a/tests/tests.js b/tests/tests.js
index 1513f14..222a839 100644
--- a/tests/tests.js
+++ b/tests/tests.js
@@ -19,7 +19,7 @@
*
*/
-/* globals Camera, resolveLocalFileSystemURI, FileEntry, CameraPopoverOptions, FileTransfer, FileUploadOptions, LocalFileSystem, MSApp */
+/* globals Camera, resolveLocalFileSystemURL, FileEntry, CameraPopoverOptions, FileTransfer, FileUploadOptions, LocalFileSystem, MSApp */
/* jshint jasmine: true */
exports.defineAutoTests = function () {
@@ -142,11 +142,11 @@ exports.defineManualTests = function (contentEl, createActionButton) {
setPicture(data);
// TODO: Fix resolveLocalFileSystemURI to work with native-uri.
if (pictureUrl.indexOf('file:') === 0 || pictureUrl.indexOf('content:') === 0 || pictureUrl.indexOf('ms-appdata:') === 0 || pictureUrl.indexOf('assets-library:') === 0) {
- resolveLocalFileSystemURI(data, function (e) {
+ resolveLocalFileSystemURL(data, function (e) {
fileEntry = e;
- logCallback('resolveLocalFileSystemURI()', true)(e.toURL());
+ logCallback('resolveLocalFileSystemURL()', true)(e.toURL());
readFile();
- }, logCallback('resolveLocalFileSystemURI()', false));
+ }, logCallback('resolveLocalFileSystemURL()', false));
} else if (pictureUrl.indexOf('data:image/jpeg;base64') === 0) {
// do nothing
} else {
@@ -251,7 +251,7 @@ exports.defineManualTests = function (contentEl, createActionButton) {
//cleanup
//rename moved file back to original name so other tests can reference image
- resolveLocalFileSystemURI(destDirEntry.nativeURL+'moved_file.png', function(fileEntry) {
+ resolveLocalFileSystemURL(destDirEntry.nativeURL+'moved_file.png', function(fileEntry) {
fileEntry.moveTo(destDirEntry, origName, logCallback('FileEntry.moveTo', true), logCallback('FileEntry.moveTo', false));
console.log('Cleanup: successfully renamed file back to original name');
}, function () {
@@ -259,7 +259,7 @@ exports.defineManualTests = function (contentEl, createActionButton) {
});
//remove copied file
- resolveLocalFileSystemURI(destDirEntry.nativeURL+'copied_file.png', function(fileEntry) {
+ resolveLocalFileSystemURL(destDirEntry.nativeURL+'copied_file.png', function(fileEntry) {
fileEntry.remove(logCallback('FileEntry.remove', true), logCallback('FileEntry.remove', false));
console.log('Cleanup: successfully removed copied file');
}, function () {
diff --git a/www/Camera.js b/www/Camera.js
index 2f9154b..d006787 100644
--- a/www/Camera.js
+++ b/www/Camera.js
@@ -89,26 +89,20 @@ for (var key in Camera) {
*
* If `Camera.sourceType` is `Camera.PictureSourceType.PHOTOLIBRARY` or
* `Camera.PictureSourceType.SAVEDPHOTOALBUM`, then a dialog displays
- * that allows users to select an existing image. The
- * `camera.getPicture` function returns a [`CameraPopoverHandle`]{@link module:CameraPopoverHandle} object,
- * which can be used to reposition the image selection dialog, for
- * example, when the device orientation changes.
+ * that allows users to select an existing image.
*
* The return value is sent to the [`cameraSuccess`]{@link module:camera.onSuccess} callback function, in
* one of the following formats, depending on the specified
* `cameraOptions`:
*
* - A `String` containing the Base64-encoded photo image.
- *
* - A `String` representing the image file location on local storage (default).
*
* You can do whatever you want with the encoded image or URI, for
* example:
*
* - Render the image in an `
` tag, as in the example below
- *
* - Save the data locally (`LocalStorage`, [Lawnchair](http://brianleroux.github.com/lawnchair/), etc.)
- *
* - Post the data to a remote server
*
* __NOTE__: Photo resolution on newer devices is quite good. Photos
diff --git a/www/CameraConstants.js b/www/CameraConstants.js
index f4b0694..9974c15 100644
--- a/www/CameraConstants.js
+++ b/www/CameraConstants.js
@@ -24,6 +24,13 @@
*/
module.exports = {
/**
+ * @description
+ * Defines the output format of `Camera.getPicture` call.
+ * _Note:_ On iOS passing `DestinationType.NATIVE_URI` along with
+ * `PictureSourceType.PHOTOLIBRARY` or `PictureSourceType.SAVEDPHOTOALBUM` will
+ * disable any image modifications (resize, quality change, cropping, etc.) due
+ * to implementation specific.
+ *
* @enum {number}
*/
DestinationType:{
@@ -55,14 +62,20 @@ module.exports = {
ALLMEDIA : 2
},
/**
+ * @description
+ * Defines the output format of `Camera.getPicture` call.
+ * _Note:_ On iOS passing `PictureSourceType.PHOTOLIBRARY` or `PictureSourceType.SAVEDPHOTOALBUM`
+ * along with `DestinationType.NATIVE_URI` will disable any image modifications (resize, quality
+ * change, cropping, etc.) due to implementation specific.
+ *
* @enum {number}
*/
PictureSourceType:{
- /** Choose image from picture library (same as SAVEDPHOTOALBUM for Android) */
+ /** Choose image from the device's photo library (same as SAVEDPHOTOALBUM for Android) */
PHOTOLIBRARY : 0,
/** Take picture from camera */
CAMERA : 1,
- /** Choose image from picture library (same as PHOTOLIBRARY for Android) */
+ /** Choose image only from the device's Camera Roll album (same as PHOTOLIBRARY for Android) */
SAVEDPHOTOALBUM : 2
},
/**
diff --git a/www/ios/CameraPopoverHandle.js b/www/ios/CameraPopoverHandle.js
index 3990e18..8b92137 100644
--- a/www/ios/CameraPopoverHandle.js
+++ b/www/ios/CameraPopoverHandle.js
@@ -33,7 +33,7 @@ var exec = require('cordova/exec');
* - iOS
*
* @example
- * var cameraPopoverHandle = navigator.camera.getPicture(onSuccess, onFail,
+ * navigator.camera.getPicture(onSuccess, onFail,
* {
* destinationType: Camera.DestinationType.FILE_URI,
* sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
@@ -42,13 +42,19 @@ var exec = require('cordova/exec');
*
* // Reposition the popover if the orientation changes.
* window.onorientationchange = function() {
+ * var cameraPopoverHandle = new CameraPopoverHandle();
* var cameraPopoverOptions = new CameraPopoverOptions(0, 0, 100, 100, Camera.PopoverArrowDirection.ARROW_ANY);
* cameraPopoverHandle.setPosition(cameraPopoverOptions);
* }
* @module CameraPopoverHandle
*/
var CameraPopoverHandle = function() {
- /** Set the position of the popover.
+ /**
+ * Can be used to reposition the image selection dialog,
+ * for example, when the device orientation changes.
+ * @memberof CameraPopoverHandle
+ * @instance
+ * @method setPosition
* @param {module:CameraPopoverOptions} popoverOptions
*/
this.setPosition = function(popoverOptions) {