Merge remote-tracking branch 'refs/remotes/apache/master'

Rebase from Master
This commit is contained in:
swbradshaw 2016-08-26 20:30:35 -04:00
commit 0ed6406864
16 changed files with 2243 additions and 249 deletions

23
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,23 @@
<!--
Please make sure the checklist boxes are all checked before submitting the PR. The checklist
is intended as a quick reference, for complete details please see our Contributor Guidelines:
http://cordova.apache.org/contribute/contribute_guidelines.html
Thanks!
-->
### 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.

View File

@ -21,7 +21,9 @@ description: Take pictures with the device camera.
# under the License.
-->
[![Build Status](https://travis-ci.org/apache/cordova-plugin-camera.svg?branch=master)](https://travis-ci.org/apache/cordova-plugin-camera)
|Android|iOS| Windows 8.1 Store | Windows 8.1 Phone | Windows 10 Store | Travis CI |
|:-:|:-:|:-:|:-:|:-:|:-:|
|[![Build Status](http://cordova-ci.cloudapp.net:8080/buildStatus/icon?job=cordova-periodic-build/PLATFORM=android,PLUGIN=cordova-plugin-camera)](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=android,PLUGIN=cordova-plugin-camera/)|[![Build Status](http://cordova-ci.cloudapp.net:8080/buildStatus/icon?job=cordova-periodic-build/PLATFORM=ios,PLUGIN=cordova-plugin-camera)](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=ios,PLUGIN=cordova-plugin-camera/)|[![Build Status](http://cordova-ci.cloudapp.net:8080/buildStatus/icon?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-store,PLUGIN=cordova-plugin-camera/)|[![Build Status](http://cordova-ci.cloudapp.net:8080/buildStatus/icon?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-8.1-phone,PLUGIN=cordova-plugin-camera/)|[![Build Status](http://cordova-ci.cloudapp.net:8080/buildStatus/icon?job=cordova-periodic-build/PLATFORM=windows-10-store,PLUGIN=cordova-plugin-camera)](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=windows-10-store,PLUGIN=cordova-plugin-camera/)|[![Build Status](https://travis-ci.org/apache/cordova-plugin-camera.svg?branch=master)](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 <a name="reference"></a>
* [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 `<img>` 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.
<a name="module_Camera.DestinationType"></a>
### Camera.DestinationType : <code>enum</code>
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 <code>[Camera](#module_Camera)</code>
**Properties**
@ -291,14 +293,19 @@ Optional parameters to customize the camera settings.
<a name="module_Camera.PictureSourceType"></a>
### Camera.PictureSourceType : <code>enum</code>
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 <code>[Camera](#module_Camera)</code>
**Properties**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| PHOTOLIBRARY | <code>number</code> | <code>0</code> | Choose image from picture library (same as SAVEDPHOTOALBUM for Android) |
| PHOTOLIBRARY | <code>number</code> | <code>0</code> | Choose image from the device's photo library (same as SAVEDPHOTOALBUM for Android) |
| CAMERA | <code>number</code> | <code>1</code> | Take picture from camera |
| SAVEDPHOTOALBUM | <code>number</code> | <code>2</code> | Choose image from picture library (same as PHOTOLIBRARY for Android) |
| SAVEDPHOTOALBUM | <code>number</code> | <code>2</code> | Choose image only from the device's Camera Roll album (same as PHOTOLIBRARY for Android) |
<a name="module_Camera.PopoverArrowDirection"></a>
@ -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 <meta> element in index.html. For example, on Windows 10, you can include `ms-appdata:` in your <meta> 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 `<meta>` element in index.html. For example, on Windows 10, you can include `ms-appdata:` in your `<meta>` element. Here is an example.
```html
<meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: ms-appdata: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *">

View File

@ -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);

View File

@ -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: <error message>' 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);
});
};

View File

@ -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);

View File

@ -4,7 +4,9 @@ description: Take pictures with the device camera.
---
{{>cdv-license~}}
[![Build Status](https://travis-ci.org/apache/cordova-plugin-camera.svg?branch=master)](https://travis-ci.org/apache/cordova-plugin-camera)
|Android|iOS| Windows 8.1 Store | Windows 8.1 Phone | Windows 10 Store | Travis CI |
|:-:|:-:|:-:|:-:|:-:|:-:|
|[![Build Status](http://cordova-ci.cloudapp.net:8080/buildStatus/icon?job=cordova-periodic-build/PLATFORM=android,PLUGIN=cordova-plugin-camera)](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=android,PLUGIN=cordova-plugin-camera/)|[![Build Status](http://cordova-ci.cloudapp.net:8080/buildStatus/icon?job=cordova-periodic-build/PLATFORM=ios,PLUGIN=cordova-plugin-camera)](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=ios,PLUGIN=cordova-plugin-camera/)|[![Build Status](http://cordova-ci.cloudapp.net:8080/buildStatus/icon?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-store,PLUGIN=cordova-plugin-camera/)|[![Build Status](http://cordova-ci.cloudapp.net:8080/buildStatus/icon?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-8.1-phone,PLUGIN=cordova-plugin-camera/)|[![Build Status](http://cordova-ci.cloudapp.net:8080/buildStatus/icon?job=cordova-periodic-build/PLATFORM=windows-10-store,PLUGIN=cordova-plugin-camera)](http://cordova-ci.cloudapp.net:8080/job/cordova-periodic-build/PLATFORM=windows-10-store,PLUGIN=cordova-plugin-camera/)|[![Build Status](https://travis-ci.org/apache/cordova-plugin-camera.svg?branch=master)](https://travis-ci.org/apache/cordova-plugin-camera)
# cordova-plugin-camera
@ -15,7 +17,7 @@ the system's image library.
---
# API Reference
# API Reference <a name="reference"></a>
{{#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 <meta> element in index.html. For example, on Windows 10, you can include `ms-appdata:` in your <meta> 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 `<meta>` element in index.html. For example, on Windows 10, you can include `ms-appdata:` in your `<meta>` element. Here is an example.
```html
<meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: ms-appdata: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *">

View File

@ -150,6 +150,11 @@
<framework src="CoreGraphics.framework" />
<framework src="AVFoundation.framework" />
<preference name="CAMERA_USAGE_DESCRIPTION" default=" " />
<config-file target="*-Info.plist" parent="NSCameraUsageDescription">
<string>$CAMERA_USAGE_DESCRIPTION</string>
</config-file>
<config-file target="*-Info.plist" parent="NSLocationWhenInUseUsageDescription">
<string></string>
</config-file>

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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.

View File

@ -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 {

View File

@ -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);
}

View File

@ -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 () {

View File

@ -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 `<img>` 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

View File

@ -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
},
/**

View File

@ -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) {