Compare commits

...

97 Commits

Author SHA1 Message Date
Ali Asadi 9454f3e4d1 Add TopCrop option to ScaleType (#372) 2021-12-10 10:53:46 +01:00
Ilia Voitcekhovskii 7094f9850a Fix unused inSampleSize and add scaling to decoding step (#359)
* Fix bitmap decoding to use inSampleSize

* Add scaling (by max side) while decoding bitmap
2021-12-10 10:51:01 +01:00
John 7039cf2059 Pinch to zoom (#388)
* Update gradle and libs

* Implemented pinch to zoom in example app
2021-12-10 10:50:19 +01:00
zakimiiiii 09a551635c Modified file to correct name (#378) 2019-07-02 14:15:09 +02:00
Dionysis 72c7c81a42 Release v2.7.0 2019-03-02 16:44:27 +01:00
Dionysis a7287e363c Remove wrong imports 2019-03-02 16:41:39 +01:00
Dionysis 6156f4c897 Merge remote-tracking branch 'origin/master' 2019-03-02 16:40:48 +01:00
Dionysis 29be38b88b Fixes #191 2019-03-02 16:40:38 +01:00
Dionysis 69d99e1aea Fixes #191 2019-03-02 16:40:12 +01:00
Nicklas Ansman Giertz af9e8158b8 Remove lazy delegates where they aren't needed (#349) 2019-03-02 14:21:53 +01:00
Dionysis 4494d76bb1 Merge branch 'master' of github.com:RedApparat/Fotoapparat 2019-03-02 14:20:11 +01:00
Dionysis f1830853c3 Merge branch 'ansman-fix/pending-result-notify' 2019-03-02 14:19:32 +01:00
Dionysis 75181ecb92 Merge branch 'fix/pending-result-notify' of https://github.com/ansman/Fotoapparat into ansman-fix/pending-result-notify 2019-03-02 14:19:13 +01:00
Nicklas Ansman Giertz 697f996d17 Make Photo.toString not include the entire byte array (#351) 2019-03-02 14:14:31 +01:00
Dionysis Lorentzos 1783058ade Fixes orientation issues (#354)
* fix: preview orientation in sensorLandscape

* Fix orientation issues.

Fixes #262
Fixes #260
Fixes #253
Fixes #193
2019-03-02 14:13:18 +01:00
Alaeri f78c324f23 fix: preview orientation in sensorLandscape (#352) 2019-03-02 14:10:50 +01:00
Nicklas Ansman Giertz b1f6f491cd Make all the configuration objects have nicer toString values (#348) 2019-03-02 12:32:19 +01:00
Nicklas Ansman Giertz 46fe442a4e Always notify the callback in PendingResult
Previously it wouldn't be notified in some cases and in some cases
it would be notified off the main thread.
2019-02-21 13:06:30 -05:00
Wolfgang Schreurs 27c9a7a910 Set source and target compatability to Java 1.8 for library and dependencies (#343) 2019-02-20 18:33:08 +01:00
Nicklas Ansman Giertz f05fc12039 Fix a crash when trying to access with size of a Photo (#347)
* Fix a crash when trying to access the size of a Photo
2019-02-20 18:32:01 +01:00
Santi Aguilera b37c3df8c9 Remove app_name from library (#334) 2019-01-24 21:02:29 +01:00
Dick Lucas 353e8a326b Update gradle version (#338)
Using this library in our project forces us to use Gradle version 3.2.1 in order to match Fotoapparat. This is because if they are not matching, it causes an issue generating the R file.
2019-01-24 21:01:51 +01:00
Dionysis Lorentzos 9021507995 Auto upload on tags (#317)
* Deploy on tags

* Bump version to 2.6.1
2018-11-08 16:25:48 +01:00
dionysis 4a23a70565 Bump version to 2.6.0 2018-11-06 14:54:48 +01:00
Dionysis Lorentzos 9104a316ee Update README.md 2018-11-06 14:43:32 +01:00
Dionysis Lorentzos 33ce7fa483 Remove kotlin.experimental.coroutines 'enable' 2018-11-06 14:29:05 +01:00
Rafael Toledo 45368b3691 Migrates project to AndroidX and Kotlin 1.3 2018-11-06 14:29:05 +01:00
Dmitry Zaytsev 65023340e5 Update build tools (#307)
* Update build tools

* Updated Travis confi
2018-10-13 11:07:25 +02:00
Dima Zaytsev ede7e4cbbe Updated build tools 2018-10-13 10:41:52 +02:00
Dmitry Zaytsev 3bbec9b6d8 Removed mention of Videoapparat 2018-10-11 00:13:53 +02:00
Dima Zaytsev ec6cdf3d66 Bumped version to 2.5.0 2018-10-11 00:05:17 +02:00
Paul Woitaschek cb4c8d1ee1 Updated Coroutines (#306)
* Updated gradle to 4.10.2

* Updated kotlin to 1.2.71

* Updated coroutines to 0.30.2

* Overwrite cancel and delegate to the channel.

* Inline the cancel call.

* Handle the cancellation of the deferred value too.
2018-10-11 00:02:50 +02:00
Dima Zaytsev 245d74fb46 Reusing artifact version variable. Bumped version to 2.4.0 2018-09-09 22:10:17 +02:00
Ivan Martinović b67ee40be2 FocusView fix - focus is now initiated on tap instead on ACTION_DOWN event type (#292) 2018-09-09 21:59:18 +02:00
Ivan Martinović 66c7489708 Upgrade Zoom.VariableZoom to contain list of zoom ratios supported by camera (#291) 2018-09-09 21:57:45 +02:00
Dima Zaytsev 96fc39bd84 Updated maven gradle plugin 2018-08-13 23:47:03 +02:00
Dima Zaytsev c82b628b44 Fixed build issues in 2.3.2. Updated the version 2018-08-13 23:39:59 +02:00
Dima Zaytsev 7ee03f1a8c Updated README 2018-08-13 22:50:12 +02:00
Dima Zaytsev 816d4da6aa Updated version to 2.3.2 2018-08-13 22:49:10 +02:00
Dmitry Zaytsev 4fc20316d0 Using null FrameProcessor by default to avoid resource consumption (#284) 2018-08-13 22:45:24 +02:00
Dima Zaytsev 3b17c030f5 Fixed Bintray configuration for RxJava 1/2 adapters. Updated README. 2018-08-13 21:28:42 +02:00
Dima Zaytsev 0c17e2bf29 Fixed configuration to make it suitable for upload to jCenter 2018-08-10 16:19:03 +02:00
Dima Zaytsev 8189847088 Added Bintray configuration 2018-08-08 21:14:53 +02:00
dionysis 36670caf02 v2.3.1 2018-07-05 21:47:59 +02:00
Ahmed Hegazy 36d5a038ce Remove configureondemand gradle configuration. Fixes #269 2018-07-05 16:20:07 +02:00
dionysis 7c5878259d Fix failing configuration 2018-07-04 15:34:01 +02:00
dionysis 4b6c0447d3 Release v2.3.0 2018-07-04 14:54:23 +02:00
dionysis 7a04a38872 Update dependencies 2018-07-04 14:54:11 +02:00
Dionysis Lorentzos 18d6ae38ad Update README.md 2018-07-03 12:18:14 +02:00
Dmitry Zaytsev 767aae82fd Merge pull request #237 from radosdesign/master
CameraDevice: Release surface when closing camera device.
2018-05-29 19:57:34 +02:00
radoslav.dodek 61742958a0 CameraDevice: Release surface when closing camera device. 2018-04-03 20:20:38 +02:00
Dmitry Zaytsev bc8c122323 Merge pull request #219 from kphil/patch-1
Update README.md
2018-03-26 14:24:51 +02:00
Sergejs Luhmirins 982c7b8548 Add special handling for external lens positions (Fixes #224) 2018-03-14 09:39:27 +01:00
Rajat Kumar Gupta 5abcbf7e54 Update README.md 2018-03-10 15:15:42 +01:00
kphil 4f78de91e0 Update README.md 2018-03-09 18:17:27 +01:00
Diederik 1e247aab22 Review feedback changes.
Missed annotation mistakenly taken out.
2018-02-19 21:21:32 +01:00
Diederik 919e4a0262 Review feedback changes. 2018-02-19 21:21:32 +01:00
Diederik Hattingh 0d4c51f6be Added focus view to java example 2018-02-19 21:21:32 +01:00
Pavel Strelchenko ea4e1bac6e Add docs + remove min zoom level as it is always 0. 2018-02-17 12:03:41 +01:00
Pavel Strelchenko 9621cc986b Replace "maxZoom" and "canZoom" parameters with single "zoom" value, which contains object with min and max zoom level, if requested camera supports zoom, or "FixedZoom" object otherwise. 2018-02-17 12:03:41 +01:00
Pavel Strelchenko 5e1ed00cfe Add "max zoom" value into camera capabilities. 2018-02-17 12:03:41 +01:00
Dionysis Lorentzos 9a4fc6bb4e Update README.md 2018-02-06 23:50:36 +01:00
Dionysis Lorentzos 67b07dc1e8 Update README.md 2018-02-06 20:02:23 +01:00
Dionysis Lorentzos 962866f405 Update README.md 2018-02-06 20:00:50 +01:00
Dionysis Lorentzos 3f25da7449 Update README.md 2018-02-06 19:59:51 +01:00
Dionysis Lorentzos a6bb1a5c63 Update README.md 2018-02-02 17:44:30 +01:00
AndrewCKing 8db7e7f8b3 Fix naming conventions for exposure 2018-02-02 11:35:34 +01:00
AndrewCKing c2a97f18f5 Support exposure compensation 2018-02-02 11:35:34 +01:00
Dionysis 799395ae38 Use cache parameters because they are less expensive 2018-02-02 11:25:47 +01:00
Dionysis a17e601428 No need to cache the parameters. This will actually create more issues 2018-02-02 11:25:47 +01:00
Dionysis d89adb8134 Fixes #196 2018-02-01 14:21:38 +01:00
Dionysis Lorentzos 9a8bbb1021 Update README.md 2018-01-20 22:17:00 +01:00
Dmitry Zaytsev b55ebc6c68 Catching IOException when assigning display surface 2018-01-20 22:04:31 +01:00
Dmitry Zaytsev 631d216b13 Merge pull request #187 from Fotoapparat/revert_callbacks
Add callback for java only users in builder
2018-01-20 21:08:59 +01:00
Dmitry Zaytsev a31a856b55 Merge pull request #188 from Fotoapparat/fix/camera-threading
Using only one thread for operating with camera
2018-01-20 20:53:52 +01:00
Dmitry Zaytsev ff265c5c73 Merge branch 'master' of github.com:Fotoapparat/Fotoapparat into fix/camera-threading 2018-01-20 18:41:50 +01:00
Dmitry Zaytsev 78f5ad88c2 Making sure that task queue does not remain polluted with tasks. 2018-01-20 18:39:47 +01:00
Dmitry Zaytsev 35e30c12fe Properly canceling tasks. 2018-01-20 18:33:26 +01:00
Dmitry Zaytsev d0fec5f5b9 Using CameraExecutor in routines 2018-01-20 12:54:51 +01:00
Dmitry Zaytsev 7233866fcc Implemented CameraExecutor 2018-01-20 12:39:57 +01:00
Dmitry Zaytsev c7e3c01b2f Removed task executor 2018-01-20 12:22:57 +01:00
Dionysis 3de625f2cd Revert changes 2018-01-19 19:45:55 +01:00
Dionysis 72dcec0470 Unassign focus listener on stop. Fixes #185 2018-01-19 17:58:43 +01:00
Dionysis 8104644cfb Add callback for java only users in builder 2018-01-19 17:56:46 +01:00
Dionysis 58ffc4f9ea Add forgotten var assignments. Fixes #183 2018-01-19 17:04:06 +01:00
Dmitry Zaytsev 31d48c6154 Merge branch 'master' of github.com:Fotoapparat/Fotoapparat 2018-01-18 18:26:30 +01:00
Dmitry Zaytsev db791d9259 Using stable version of gradle plugin 2018-01-18 18:26:21 +01:00
Dionysis Lorentzos 82752f13c6 Merge pull request #180 from Fotoapparat/feature/rotation
Even if the screen orientation is locked, the device will render corr…
2018-01-18 17:21:10 +01:00
Dionysis 8214559d98 Improve logs in pending result 2018-01-18 12:55:52 +01:00
Dionysis 32e479cc7b Add infinity in default camera configuration 2018-01-18 12:31:47 +01:00
Dionysis 262aa4ed9b Merge branch 'master' into feature/rotation
# Conflicts:
#	fotoapparat/src/main/java/io/fotoapparat/hardware/CameraDevice.kt
#	fotoapparat/src/main/java/io/fotoapparat/hardware/Device.kt
#	fotoapparat/src/main/java/io/fotoapparat/preview/PreviewStream.kt
2018-01-11 23:06:32 +01:00
Dionysis 762fae3d22 Even if the screen orientation is locked, the device will render correctly the image rotation. 2018-01-11 23:01:36 +01:00
Dionysis Lorentzos d8c5cb6ada Merge pull request #176 from dewarder/fix-codestyle
Add typealiases. Codestyle improvements.
2018-01-11 21:20:50 +01:00
Artem Hluhovskyi 5125541274 Merge branch 'master' of https://github.com/Fotoapparat/Fotoapparat into fix-codestyle
# Conflicts:
#	fotoapparat/src/main/java/io/fotoapparat/exif/ExifWriter.kt
2018-01-11 18:48:25 +02:00
Artem Hluhovskyi da4ca3a699 Clean up some methods with apply() 2018-01-11 18:03:26 +02:00
Sandi Barisic 98dab42245 Accept rotation in degrees instead of Photos in ExifOrientationHelper 2018-01-11 15:37:57 +01:00
Artem Hluhovskyi e8dfac0fd6 Add typealiases for selectors. Clean up. 2018-01-11 14:41:39 +02:00
124 changed files with 2164 additions and 1439 deletions
+2
View File
@@ -6,4 +6,6 @@
build
/captures
.externalNativeBuild
.project
.settings
+13 -7
View File
@@ -3,15 +3,21 @@ jdk: oraclejdk8
android:
components:
- tools
- platform-tools
- tools
- build-tools-27.0.3
- android-27
- extra-android-m2repository
- tools
- platform-tools
- tools
- build-tools-28.0.3
- android-28
- extra-android-m2repository
before_install:
- yes | sdkmanager "platforms;android-27"
- yes | sdkmanager "platforms;android-28"
script:
- ./gradlew build test
deploy:
provider: script
script: ./gradlew bintrayUpload
on:
tags: true
+28 -33
View File
@@ -1,20 +1,19 @@
# Fotoapparat
![Build status](https://travis-ci.org/Fotoapparat/Fotoapparat.svg?branch=master)
![Build status](https://travis-ci.org/RedApparat/Fotoapparat.svg?branch=master)
![ ](sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png)
Camera API in Android is hard. Having 2 different API for new and old Camera does not make things any easier. But fret not, that is your lucky day! After several years of working with Camera we came up with Fotoapparat.
Camera API in Android is hard. Having 2 different API for new and old Camera does not make things any easier. But fret not, that is your lucky day! After several years of working with Camera, we came up with Fotoapparat.
What it provides:
- Camera API which does not allow you to shoot yourself in the foot.
- Simple yet powerful parameters customization.
- Standalone custom `CameraView` which can be integrated into any `Activity`.
- Fixes and workarounds for device-specific problems.
- Both Kotlin and Java friendly configurations.
- Last, but not least, non 0% test coverage.
- Last, but not least, non 0% test coverage.
Taking picture becomes as simple as:
@@ -23,9 +22,9 @@ val fotoapparat = Fotoapparat(
context = this,
view = cameraView
)
fotoapparat.start()
fotoapparat
.takePicture()
.saveToFile(someFile)
@@ -48,7 +47,7 @@ Add `CameraView` to your layout
Configure `Fotoapparat` instance.
```kotlin
```kotlin
Fotoapparat(
context = this,
view = cameraView, // view which will draw the camera preview
@@ -61,12 +60,12 @@ Fotoapparat(
),
cameraErrorCallback = { error -> } // (optional) log fatal errors
)
```
```
Check the [wiki for the `configuration` options e.g. change iso](https://github.com/Fotoapparat/Fotoapparat/wiki/Configuration-Kotlin)
Are you using Java only? See our [wiki for the java-friendly configuration](https://github.com/Fotoapparat/Fotoapparat/wiki/Configuration-Java).
### Step Three
Call `start()` and `stop()`. No rocket science here.
@@ -76,43 +75,43 @@ override fun onStart() {
super.onStart()
fotoapparat.start()
}
override fun onStop() {
super.onStop()
fotoapparat.stop()
}
```
### Take picture
### Take a picture
Finally we are ready to take picture. You have various options.
Finally, we are ready to take a picture. You have various options.
```kotlin
val photoResult = fotoapparat.takePicture()
// Asynchronously saves photo to file
photoResult.saveToFile(someFile)
// Asynchronously converts photo to bitmap and returns result on main thread
// Asynchronously converts photo to bitmap and returns the result on the main thread
photoResult
.toBitmap()
.whenAvailable { bitmapPhoto ->
val imageView = (ImageView) findViewById(R.id.result)
imageView.setImageBitmap(bitmapPhoto.bitmap)
imageView.setRotation(-bitmapPhoto.rotationDegrees)
}
// Of course you can also get a photo in a blocking way. Do not do it on main thread though.
// Of course, you can also get a photo in a blocking way. Do not do it on the main thread though.
val result = photoResult.toBitmap().await()
// Convert asynchronous events to RxJava 1.x/2.x types.
// See /fotoapparat-adapters/ module
// Convert asynchronous events to RxJava 1.x/2.x types.
// See /fotoapparat-adapters/ module
photoResult
.toBitmap()
.toSingle()
.subscribe { bitmapPhoto ->
.subscribe { bitmapPhoto ->
}
```
@@ -125,23 +124,23 @@ fotoapparat.updateConfiguration(
UpdateConfiguration(
flashMode = if (isChecked) torch() else off()
// ...
// all the parameters available in CameraConfiguration
// all the parameters available in CameraConfiguration
)
)
```
Or alternatively you may provide updates on an existing full configuration.
Or alternatively, you may provide updates on an existing full configuration.
```kotlin
val configuration = CameraConfiguration(
// A full configuration
// ...
)
fotoapparat.updateConfiguration(
configuration.copy(
flashMode = if (isChecked) torch() else off()
// all the parameters available in CameraConfiguration
// all the parameters available in CameraConfiguration
)
)
```
@@ -162,11 +161,7 @@ fotoapparat.switchTo(
Add dependency to your `build.gradle`
```groovy
repositories {
maven { url 'https://jitpack.io' }
}
compile 'io.fotoapparat.fotoapparat:library:2.1.2'
implementation 'io.fotoapparat:fotoapparat:2.7.0'
```
Camera permission will be automatically added to your `AndroidManifest.xml`. Do not forget to request this permission on Marshmallow and higher.
+19 -16
View File
@@ -1,30 +1,34 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
subprojects {
ext {
artifactVersion = '2.7.0'
}
}
buildscript {
ext {
versions = [
gradle : '4.4.1',
kotlin : '1.2.10',
kotlin : '1.3.50',
code : 1,
name : '1.0.0',
sdk : [
minimum: 14,
target : 27
target : 28
],
android: [
buildTools: '27.0.3',
support : '27.0.2'
buildTools: '28.0.3',
appcompat : '1.1.0',
annotation : '1.1.0',
exifinterface : '1.0.0'
],
rx : [
rxJava1: '1.2.9',
rxJava2: '2.0.8'
rxJava1: '1.3.8',
rxJava2: '2.2.12'
],
test : [
junit : '4.12',
mockito: '2.13.0'
],
util : [
commons: '2.5'
mockito: '2.23.0'
]
]
}
@@ -33,9 +37,11 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.0-alpha07'
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'
classpath 'com.android.tools.build:gradle:3.5.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4'
}
}
@@ -50,6 +56,3 @@ task clean(type: Delete) {
delete rootProject.buildDir
}
task wrapper(type: Wrapper) {
gradleVersion = versions.gradle
}
+100
View File
@@ -0,0 +1,100 @@
apply plugin: 'com.jfrog.bintray'
apply plugin: 'com.github.dcendents.android-maven'
ext {
bintrayOrganisation = 'fotoapparat'
bintrayRepo = 'fotoapparat'
publishedGroupId = 'io.fotoapparat'
siteUrl = 'https://github.com/RedApparat/Fotoapparat'
gitUrl = 'https://github.com/RedApparat/Fotoapparat.git'
developerId = 'fotoapparat'
developerName = 'Fotoapparat'
developerEmail = 'dmitry.zaicew@gmail.com'
licenseName = 'The Apache Software License, Version 2.0'
licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
allLicenses = ["Apache-2.0"]
}
group = publishedGroupId
version = libraryVersion
install {
repositories.mavenInstaller {
pom.project {
packaging 'aar'
groupId publishedGroupId
artifactId artifact
name libraryName
description libraryDescription
url siteUrl
licenses {
license {
name licenseName
url licenseUrl
}
}
developers {
developer {
id developerId
name developerName
email developerEmail
}
}
scm {
connection gitUrl
developerConnection gitUrl
url siteUrl
}
}
}
}
task sourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
task javadoc(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
failOnError = false
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives javadocJar
archives sourcesJar
}
bintray {
user = project.properties["bintray.user"] ?: System.getenv('BINTRAY_USER')
key = project.properties["bintray.apikey"] ?: System.getenv('BINTRAY_API_KEY')
configurations = ['archives']
pkg {
userOrg = bintrayOrganisation
repo = bintrayRepo
name = bintrayName
desc = libraryDescription
websiteUrl = siteUrl
vcsUrl = gitUrl
licenses = allLicenses
dryRun = false
publish = true
override = false
publicDownloadNumbers = true
version {
desc = libraryDescription
}
}
}
+18 -4
View File
@@ -11,17 +11,31 @@ android {
defaultConfig {
minSdkVersion versions.sdk.minimum
targetSdkVersion versions.sdk.target
}
archivesBaseName = 'adapter-rxjava'
archivesBaseName = 'adapter-rxjava'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
compileOnly project(':fotoapparat')
compileOnly "io.reactivex:rxjava:${versions.rx.rxJava1}"
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
testImplementation "io.reactivex:rxjava:${versions.rx.rxJava1}"
testImplementation "junit:junit:${versions.test.junit}"
}
}
ext {
bintrayName = 'adapter-rxjava'
libraryName = 'Fotoapparat Adapters - RxJava'
artifact = 'adapter-rxjava'
libraryVersion = artifactVersion
libraryDescription = 'RxJava1 adapter for Fotoapparat.'
}
apply from: '../../deploy.gradle'
+18 -4
View File
@@ -11,17 +11,31 @@ android {
defaultConfig {
minSdkVersion versions.sdk.minimum
targetSdkVersion versions.sdk.target
}
archivesBaseName = 'adapter-rxjava2'
archivesBaseName = 'adapter-rxjava2'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
compileOnly project(':fotoapparat')
compileOnly "io.reactivex.rxjava2:rxjava:${versions.rx.rxJava2}"
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
testImplementation "io.reactivex.rxjava2:rxjava:${versions.rx.rxJava2}"
testImplementation "junit:junit:${versions.test.junit}"
}
}
ext {
bintrayName = 'adapter-rxjava2'
libraryName = 'Fotoapparat Adapters - RxJava2'
artifact = 'adapter-rxjava2'
libraryVersion = artifactVersion
libraryDescription = 'RxJava2 adapter for Fotoapparat.'
}
apply from: '../../deploy.gradle'
+21 -10
View File
@@ -4,8 +4,6 @@ apply plugin: 'kotlin-android'
group = 'io.fotoapparat'
kotlin.experimental.coroutines 'enable'
android {
buildToolsVersion versions.android.buildTools
compileSdkVersion versions.sdk.target
@@ -13,8 +11,6 @@ android {
defaultConfig {
minSdkVersion versions.sdk.minimum
targetSdkVersion versions.sdk.target
archivesBaseName = 'library'
}
buildTypes {
@@ -26,15 +22,30 @@ android {
testOptions {
unitTests.returnDefaultValues = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
compile "com.android.support:support-annotations:${versions.android.support}"
compile "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.21'
implementation "androidx.annotation:annotation:${versions.android.annotation}"
implementation "androidx.exifinterface:exifinterface:${versions.android.exifinterface}"
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
testImplementation "junit:junit:${versions.test.junit}"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:${versions.kotlin}"
testImplementation "org.mockito:mockito-core:${versions.test.mockito}"
testImplementation "commons-io:commons-io:${versions.util.commons}"
}
}
ext {
bintrayName = 'fotoapparat'
libraryName = 'Fotoapparat'
artifact = 'fotoapparat'
libraryVersion = artifactVersion
libraryDescription = 'Camera library for Android with a more user-friendly API.'
}
apply from: '../deploy.gradle'
@@ -1,22 +1,19 @@
package io.fotoapparat
import android.content.Context
import android.support.annotation.FloatRange
import io.fotoapparat.capability.Capabilities
import io.fotoapparat.characteristic.LensPosition
import androidx.annotation.FloatRange
import io.fotoapparat.concurrent.CameraExecutor
import io.fotoapparat.concurrent.CameraExecutor.Operation
import io.fotoapparat.configuration.CameraConfiguration
import io.fotoapparat.configuration.Configuration
import io.fotoapparat.error.CameraErrorCallback
import io.fotoapparat.error.onMainThread
import io.fotoapparat.exception.camera.CameraException
import io.fotoapparat.hardware.Device
import io.fotoapparat.hardware.display.Display
import io.fotoapparat.hardware.execute
import io.fotoapparat.hardware.executeTask
import io.fotoapparat.hardware.orientation.OrientationSensor
import io.fotoapparat.log.Logger
import io.fotoapparat.log.none
import io.fotoapparat.parameter.ScaleType
import io.fotoapparat.parameter.camera.CameraParameters
import io.fotoapparat.result.*
import io.fotoapparat.routine.camera.bootStart
import io.fotoapparat.routine.camera.shutDown
@@ -27,13 +24,9 @@ import io.fotoapparat.routine.focus.focus
import io.fotoapparat.routine.parameter.getCurrentParameters
import io.fotoapparat.routine.photo.takePhoto
import io.fotoapparat.routine.zoom.updateZoomLevel
import io.fotoapparat.selector.back
import io.fotoapparat.selector.external
import io.fotoapparat.selector.firstAvailable
import io.fotoapparat.selector.front
import io.fotoapparat.selector.*
import io.fotoapparat.view.CameraRenderer
import io.fotoapparat.view.FocalPointSelector
import java.util.concurrent.FutureTask
/**
* Camera. Takes pictures.
@@ -43,14 +36,15 @@ class Fotoapparat
context: Context,
view: CameraRenderer,
focusView: FocalPointSelector? = null,
lensPosition: Collection<LensPosition>.() -> LensPosition? = firstAvailable(
lensPosition: LensPositionSelector = firstAvailable(
back(),
front(),
external()
),
scaleType: ScaleType = ScaleType.CenterCrop,
cameraConfiguration: CameraConfiguration = CameraConfiguration.default(),
cameraErrorCallback: ((CameraException) -> Unit) = {},
cameraErrorCallback: CameraErrorCallback = {},
private val executor: CameraExecutor = EXECUTOR,
private val logger: Logger = none()
) {
@@ -65,13 +59,16 @@ class Fotoapparat
display = display,
scaleType = scaleType,
initialLensPositionSelector = lensPosition,
initialConfiguration = cameraConfiguration
initialConfiguration = cameraConfiguration,
executor = executor
)
private val orientationSensor = OrientationSensor(
context = context,
device = device
)
private val orientationSensor by lazy {
OrientationSensor(
context = context,
device = device
)
}
init {
logger.recordMethod()
@@ -85,12 +82,12 @@ class Fotoapparat
fun start() {
logger.recordMethod()
execute {
executor.execute(Operation {
device.bootStart(
orientationSensor = orientationSensor,
mainThreadErrorCallback = mainThreadErrorCallback
)
}
})
}
/**
@@ -101,11 +98,12 @@ class Fotoapparat
fun stop() {
logger.recordMethod()
execute {
executor.cancelTasks()
executor.execute(Operation {
device.shutDown(
orientationSensor = orientationSensor
)
}
})
}
/**
@@ -116,13 +114,12 @@ class Fotoapparat
fun takePicture(): PhotoResult {
logger.recordMethod()
val takePictureTask = FutureTask<Photo> {
device.takePhoto()
}
val future = executor.execute(Operation(
cancellable = true,
function = device::takePhoto
))
executeTask(takePictureTask)
return PhotoResult.fromFuture(takePictureTask, logger)
return PhotoResult.fromFuture(future, logger)
}
/**
@@ -133,13 +130,12 @@ class Fotoapparat
fun getCapabilities(): CapabilitiesResult {
logger.recordMethod()
val getCapabilitiesTask = FutureTask<Capabilities> {
device.getCapabilities()
}
val future = executor.execute(Operation(
cancellable = true,
function = device::getCapabilities
))
executeTask(getCapabilitiesTask)
return PendingResult.fromFuture(getCapabilitiesTask, logger)
return PendingResult.fromFuture(future, logger)
}
/**
@@ -150,13 +146,12 @@ class Fotoapparat
fun getCurrentParameters(): ParametersResult {
logger.recordMethod()
val getCameraParametersTask = FutureTask<CameraParameters> {
device.getCurrentParameters()
}
val future = executor.execute(Operation(
cancellable = true,
function = device::getCurrentParameters
))
executeTask(getCameraParametersTask)
return PendingResult.fromFuture(getCameraParametersTask, logger)
return PendingResult.fromFuture(future, logger)
}
/**
@@ -164,11 +159,12 @@ class Fotoapparat
*
* @throws IllegalStateException If the current camera has not started.
*/
fun updateConfiguration(newConfiguration: Configuration) = execute {
logger.recordMethod()
fun updateConfiguration(newConfiguration: Configuration) = executor.execute(
Operation(cancellable = true) {
logger.recordMethod()
device.updateDeviceConfiguration(newConfiguration)
}
device.updateDeviceConfiguration(newConfiguration)
})
/**
* Asynchronously updates zoom level of the camera.
@@ -177,24 +173,23 @@ class Fotoapparat
* @param zoomLevel Zoom level of the camera. A value between 0 and 1.
* @throws IllegalStateException If the current camera has not started.
*/
fun setZoom(@FloatRange(from = 0.0, to = 1.0) zoomLevel: Float) = execute {
logger.recordMethod()
fun setZoom(@FloatRange(from = 0.0, to = 1.0) zoomLevel: Float) = executor.execute(
Operation(cancellable = true) {
logger.recordMethod()
device.updateZoomLevel(
zoomLevel = zoomLevel
)
}
device.updateZoomLevel(
zoomLevel = zoomLevel
)
})
/**
* Performs auto focus. If it is not available or not enabled, does nothing.
*
* @see Fotoapparat.focus
*/
fun autoFocus(): Fotoapparat {
fun autoFocus(): Fotoapparat = apply {
logger.recordMethod()
focus()
return this
}
/**
@@ -207,12 +202,12 @@ class Fotoapparat
fun focus(): PendingResult<FocusResult> {
logger.recordMethod()
val focusTask = FutureTask<FocusResult> {
device.focus()
}
executeTask(focusTask)
val future = executor.execute(Operation(
cancellable = true,
function = device::focus
))
return PendingResult.fromFuture(focusTask, logger)
return PendingResult.fromFuture(future, logger)
}
/**
@@ -220,31 +215,34 @@ class Fotoapparat
* stopped automatically and new will start.
*/
fun switchTo(
lensPosition: Collection<LensPosition>.() -> LensPosition?,
lensPosition: LensPositionSelector,
cameraConfiguration: CameraConfiguration
) {
logger.recordMethod()
execute {
executor.execute(Operation(cancellable = true) {
device.switchCamera(
orientationSensor = orientationSensor,
newLensPositionSelector = lensPosition,
newConfiguration = cameraConfiguration,
mainThreadErrorCallback = mainThreadErrorCallback
)
}
})
}
/**
* @return `true` if selected lens position is available. `false` if it is not available.
*/
fun isAvailable(
selector: (Collection<LensPosition>) -> LensPosition?
): Boolean {
return device.canSelectCamera(selector)
}
selector: LensPositionSelector
): Boolean = device.canSelectCamera(selector)
companion object {
private val EXECUTOR = CameraExecutor()
@JvmStatic
fun with(context: Context) = FotoapparatBuilder(context)
fun with(context: Context): FotoapparatBuilder = FotoapparatBuilder(context)
}
}
@@ -1,32 +1,33 @@
package io.fotoapparat
import android.content.Context
import io.fotoapparat.characteristic.LensPosition
import io.fotoapparat.configuration.CameraConfiguration
import io.fotoapparat.exception.camera.CameraException
import io.fotoapparat.error.CameraErrorCallback
import io.fotoapparat.error.CameraErrorListener
import io.fotoapparat.log.Logger
import io.fotoapparat.log.none
import io.fotoapparat.parameter.*
import io.fotoapparat.parameter.ScaleType
import io.fotoapparat.preview.Frame
import io.fotoapparat.selector.back
import io.fotoapparat.selector.external
import io.fotoapparat.selector.firstAvailable
import io.fotoapparat.selector.front
import io.fotoapparat.selector.*
import io.fotoapparat.util.FrameProcessor
import io.fotoapparat.view.CameraRenderer
import io.fotoapparat.view.CameraView
import io.fotoapparat.view.FocusView
import io.fotoapparat.preview.FrameProcessor as FrameProcessorJava
/**
* Builder for [Fotoapparat].
*/
class FotoapparatBuilder internal constructor(private var context: Context) {
internal var lensPositionSelector: Iterable<LensPosition>.() -> LensPosition? = firstAvailable(
internal var lensPositionSelector: LensPositionSelector = firstAvailable(
back(),
front(),
external()
)
internal var cameraErrorCallback: (CameraException) -> Unit = {}
internal var cameraErrorCallback: CameraErrorCallback = {}
internal var renderer: CameraRenderer? = null
internal var focusView: FocusView? = null
internal var scaleType: ScaleType = ScaleType.CenterCrop
internal var logger: Logger = none()
@@ -35,127 +36,142 @@ class FotoapparatBuilder internal constructor(private var context: Context) {
/**
* @param selector camera sensor position from list of available positions.
*/
fun lensPosition(selector: Iterable<LensPosition>.() -> LensPosition?): FotoapparatBuilder {
lensPositionSelector = selector
return this
}
fun lensPosition(selector: LensPositionSelector): FotoapparatBuilder =
apply { lensPositionSelector = selector }
/**
* @param scaleType of preview inside the view.
*/
fun previewScaleType(scaleType: ScaleType): FotoapparatBuilder {
this.scaleType = scaleType
return this
}
fun previewScaleType(scaleType: ScaleType): FotoapparatBuilder =
apply { this.scaleType = scaleType }
/**
* @param selector selects resolution of the photo (in pixels) from list of available resolutions.
*/
fun photoResolution(selector: Iterable<Resolution>.() -> Resolution?): FotoapparatBuilder {
fun photoResolution(selector: ResolutionSelector): FotoapparatBuilder = apply {
configuration = configuration.copy(
pictureResolution = selector
)
return this
}
/**
* @param selector selects size of preview stream (in pixels) from list of available resolutions.
*/
fun previewResolution(selector: Iterable<Resolution>.() -> Resolution?): FotoapparatBuilder {
fun previewResolution(selector: ResolutionSelector): FotoapparatBuilder = apply {
configuration = configuration.copy(
previewResolution = selector
)
return this
}
/**
* @param selector selects focus mode from list of available modes.
*/
fun focusMode(selector: Iterable<FocusMode>.() -> FocusMode?): FotoapparatBuilder {
fun focusMode(selector: FocusModeSelector): FotoapparatBuilder = apply {
configuration = configuration.copy(
focusMode = selector
)
return this
}
/**
* @param selector selects flash mode from list of available modes.
*/
fun flash(selector: Iterable<Flash>.() -> Flash?): FotoapparatBuilder {
fun flash(selector: FlashSelector): FotoapparatBuilder = apply {
configuration = configuration.copy(
flashMode = selector
)
return this
}
/**
* @param selector selects preview FPS range from list of available ranges.
*/
fun previewFpsRange(selector: Iterable<FpsRange>.() -> FpsRange?): FotoapparatBuilder {
fun previewFpsRange(selector: FpsRangeSelector): FotoapparatBuilder = apply {
configuration = configuration.copy(
previewFpsRange = selector
)
return this
}
/**
* @param selector selects ISO value from range of available values.
*/
fun sensorSensitivity(selector: Iterable<Int>.() -> Int?): FotoapparatBuilder {
fun sensorSensitivity(selector: SensorSensitivitySelector): FotoapparatBuilder = apply {
configuration = configuration.copy(
sensorSensitivity = selector
)
return this
}
/**
* @param selector of the Jpeg picture quality.
*/
fun jpegQuality(selector: IntRange.() -> Int?): FotoapparatBuilder {
fun jpegQuality(selector: QualitySelector): FotoapparatBuilder = apply {
configuration = configuration.copy(
jpegQuality = selector
)
return this
}
/**
* @param selector selects exposure compensation value from available range.
*/
fun exposureCompensation(selector: ExposureSelector): FotoapparatBuilder = apply {
configuration = configuration.copy(
exposureCompensation = selector
)
}
/**
* @param frameProcessor receives preview frames for processing.
* @see FrameProcessor
* @see FrameProcessorJava
*/
fun frameProcessor(frameProcessor: (Frame) -> Unit): FotoapparatBuilder {
fun frameProcessor(frameProcessor: FrameProcessor): FotoapparatBuilder = apply {
configuration = configuration.copy(
frameProcessor = frameProcessor
)
return this
}
/**
* @param frameProcessor receives preview frames for processing.
* @see FrameProcessorJava
*/
fun frameProcessor(frameProcessor: FrameProcessorJava?): FotoapparatBuilder = apply {
configuration = configuration.copy(
frameProcessor = frameProcessor?.let { it::process }
)
}
/**
* @param logger logger which will print logs. No logger is set by default.
* @see io.fotoapparat.log.Loggers
*/
fun logger(logger: Logger): FotoapparatBuilder {
this.logger = logger
return this
}
fun logger(logger: Logger): FotoapparatBuilder =
apply { this.logger = logger }
/**
* @param callback which will be notified when camera error happens in Fotoapparat.
* @see CameraErrorCallback
* @see CameraErrorListener
*/
fun cameraErrorCallback(callback: (CameraException) -> Unit): FotoapparatBuilder {
cameraErrorCallback = callback
return this
}
fun cameraErrorCallback(callback: CameraErrorListener): FotoapparatBuilder =
apply { cameraErrorCallback = { callback.onError(it) } }
/**
* @param callback which will be notified when camera error happens in Fotoapparat.
* @see CameraErrorListener
*/
fun cameraErrorCallback(callback: CameraErrorCallback): FotoapparatBuilder =
apply { cameraErrorCallback = callback }
/**
* @param renderer view which will draw the stream from the camera.
* @see CameraView
*/
fun into(renderer: CameraRenderer): FotoapparatBuilder {
this.renderer = renderer
return this
}
fun into(renderer: CameraRenderer): FotoapparatBuilder =
apply { this.renderer = renderer }
/**
* @param focusView view which will be used for touch to focus.
* @see FocusView
*/
fun focusView(focusView: FocusView): FotoapparatBuilder =
apply { this.focusView = focusView }
/**
* @return set up instance of [Fotoapparat].
@@ -175,6 +191,7 @@ class FotoapparatBuilder internal constructor(private var context: Context) {
return Fotoapparat(
context = context,
view = renderer,
focusView = focusView,
lensPosition = lensPositionSelector,
cameraConfiguration = configuration,
scaleType = scaleType,
@@ -183,4 +200,4 @@ class FotoapparatBuilder internal constructor(private var context: Context) {
)
}
}
}
@@ -10,13 +10,14 @@ import io.fotoapparat.util.wrap
* Sensor sensitivities is not guaranteed to always contain a value.
*/
data class Capabilities(
val canZoom: Boolean,
val zoom: Zoom,
val flashModes: Set<Flash>,
val focusModes: Set<FocusMode>,
val canSmoothZoom: Boolean,
val maxFocusAreas: Int,
val maxMeteringAreas: Int,
val jpegQualityRange: IntRange,
val exposureCompensationRange: IntRange,
val previewFpsRanges: Set<FpsRange>,
val antiBandingModes: Set<AntiBandingMode>,
val pictureResolutions: Set<Resolution>,
@@ -35,13 +36,14 @@ data class Capabilities(
override fun toString(): String {
return "Capabilities" + lineSeparator +
"canZoom:" + canZoom.wrap() +
"zoom:" + zoom.wrap() +
"flashModes:" + flashModes.wrap() +
"focusModes:" + focusModes.wrap() +
"canSmoothZoom:" + canSmoothZoom.wrap() +
"maxFocusAreas:" + maxFocusAreas.wrap() +
"maxMeteringAreas:" + maxMeteringAreas.wrap() +
"jpegQualityRange:" + jpegQualityRange.wrap() +
"exposureCompensationRange:" + exposureCompensationRange.wrap() +
"antiBandingModes:" + antiBandingModes.wrap() +
"previewFpsRanges:" + previewFpsRanges.wrap() +
"pictureResolutions:" + pictureResolutions.wrap() +
@@ -14,14 +14,15 @@ internal fun Camera.getCapabilities() = SupportedParameters(parameters).getCapab
private fun SupportedParameters.getCapabilities(): Capabilities {
return Capabilities(
canZoom = supportedZoom,
zoom = supportedZoom,
flashModes = flashModes.extract { it.toFlash() },
focusModes = focusModes.extract { it.toFocusMode() },
maxFocusAreas = maxNumFocusAreas,
canSmoothZoom = supportedSmoothZoom,
maxMeteringAreas = maxNumMeteringAreas,
jpegQualityRange = jpegQualityRange,
antiBandingModes = supportedAutoBandingModes.extract { it.toAntiBandingMode() },
exposureCompensationRange = exposureCompensationRange,
antiBandingModes = supportedAutoBandingModes.extract(String::toAntiBandingMode),
sensorSensitivities = sensorSensitivities.toSet(),
previewFpsRanges = supportedPreviewFpsRanges.extract { it.toFpsRange() },
pictureResolutions = pictureResolutions.mapSizes(),
@@ -3,6 +3,7 @@
package io.fotoapparat.characteristic
import android.hardware.Camera
import io.fotoapparat.hardware.orientation.toOrientation
/**
* Returns the [Characteristics] for the given `cameraId`.
@@ -14,7 +15,7 @@ internal fun getCharacteristics(cameraId: Int): Characteristics {
return Characteristics(
cameraId = cameraId,
lensPosition = lensPosition,
orientation = info.orientation,
cameraOrientation = info.orientation.toOrientation(),
isMirrored = lensPosition == LensPosition.Front
)
}
@@ -1,11 +1,13 @@
package io.fotoapparat.characteristic
import io.fotoapparat.hardware.orientation.Orientation
/**
* A set of information about the camera.
*/
internal data class Characteristics(
val cameraId: Int,
val lensPosition: LensPosition,
val orientation: Int,
val cameraOrientation: Orientation,
val isMirrored: Boolean
)
@@ -8,16 +8,22 @@ sealed class LensPosition : Characteristic {
/**
* The back camera.
*/
object Back : LensPosition()
object Back : LensPosition() {
override fun toString(): String = "LensPosition.Back"
}
/**
* The front camera.
*/
object Front : LensPosition()
object Front : LensPosition() {
override fun toString(): String = "LensPosition.Front"
}
/**
* An external camera.
*/
object External : LensPosition()
object External : LensPosition() {
override fun toString(): String = "LensPosition.External"
}
}
@@ -5,6 +5,11 @@ package io.fotoapparat.characteristic
import android.hardware.Camera
import io.fotoapparat.exception.camera.CameraException
/**
* On some devices with double cameras v1 still returns external lens position (as in v2).
*/
private const val CAMERA_FACING_EXTERNAL = 2
/**
* Maps between [LensPosition] and Camera v1 lens position code id.
*
@@ -12,13 +17,13 @@ import io.fotoapparat.exception.camera.CameraException
* @return [LensPosition] from the given lens position code id.
* `null` if position code id is not supported.
*/
internal fun Int.toLensPosition(): LensPosition {
return when (this) {
Camera.CameraInfo.CAMERA_FACING_FRONT -> LensPosition.Front
Camera.CameraInfo.CAMERA_FACING_BACK -> LensPosition.Back
else -> throw IllegalArgumentException("Lens position $this is not supported.")
}
}
internal fun Int.toLensPosition(): LensPosition =
when (this) {
Camera.CameraInfo.CAMERA_FACING_FRONT -> LensPosition.Front
Camera.CameraInfo.CAMERA_FACING_BACK -> LensPosition.Back
CAMERA_FACING_EXTERNAL -> LensPosition.External
else -> throw IllegalArgumentException("Lens position $this is not supported.")
}
/**
* Maps between [LensPosition] and Camera v1 code id.
@@ -26,10 +31,9 @@ internal fun Int.toLensPosition(): LensPosition {
* @receiver [LensPosition]
* @return code of the camera as in [Camera.CameraInfo].
*/
fun LensPosition.toCameraId(): Int {
return (0 until Camera.getNumberOfCameras())
.find {
this == getCharacteristics(it).lensPosition
}
?: throw CameraException("Device has no camera for the desired lens position(s).")
}
fun LensPosition.toCameraId(): Int =
(0 until Camera.getNumberOfCameras())
.find { cameraId ->
this == getCharacteristics(cameraId).lensPosition
}
?: throw CameraException("Device has no camera for the desired lens position(s).")
@@ -0,0 +1,78 @@
package io.fotoapparat.concurrent
import java.util.*
import java.util.concurrent.Callable
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Future
/**
* Executes camera related operations using a dedicated thread and provides a control over the queue
* of those operations.
*
* This class should be accessed only from a one thread at a time.
*/
class CameraExecutor(
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
) {
private val cancellableTasksQueue = LinkedList<Future<*>>()
/**
* Adds operation to the serial execution queue.
*/
fun <T> execute(operation: Operation<T>): Future<T> {
val future = executor.submit(Callable {
operation.function()
})
if (operation.cancellable) {
cancellableTasksQueue += future
}
cleanUpCancelledTasks()
return future
}
private fun cleanUpCancelledTasks() {
cancellableTasksQueue.removeAll {
!it.isPending
}
}
/**
* Cancels all cancellable tasks in the queue. Non-cancellable tasks would still be executed.
*
* After this operation is completed the executor is still in a valid state and can be used
* further.
*/
fun cancelTasks() {
cancellableTasksQueue
.filter { it.isPending }
.forEach {
it.cancel(true)
}
cancellableTasksQueue.clear()
}
private val Future<*>.isPending get() = !isCancelled && !isDone
/**
* Operation to be executed.
*/
data class Operation<out T>(
/**
* `true` if operation could be cancelled. `false` if operation can not be cancelled.
*/
val cancellable: Boolean = false,
/**
* Body of the operation.
*/
val function: () -> T
)
}
@@ -0,0 +1,12 @@
package io.fotoapparat.concurrent
import android.os.Looper
/**
* Throws [IllegalThreadStateException] if called from main thread.
*/
fun ensureBackgroundThread() {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw IllegalThreadStateException("Operation should not run from main thread.")
}
}
@@ -1,35 +1,36 @@
package io.fotoapparat.configuration
import io.fotoapparat.parameter.*
import io.fotoapparat.preview.Frame
import io.fotoapparat.preview.FrameProcessor
import io.fotoapparat.selector.*
import io.fotoapparat.util.FrameProcessor
import io.fotoapparat.preview.FrameProcessor as FrameProcessorJava
private const val DEFAULT_JPEG_QUALITY = 90
private const val DEFAULT_EXPOSURE_COMPENSATION = 0
/**
* A camera configuration which has all it's selectors defined.
*/
data class CameraConfiguration(
override val flashMode: Iterable<Flash>.() -> Flash? = off(),
override val focusMode: Iterable<FocusMode>.() -> FocusMode? = firstAvailable(
override val flashMode: FlashSelector = off(),
override val focusMode: FocusModeSelector = firstAvailable(
continuousFocusPicture(),
autoFocus(),
fixed()
fixed(),
infinity()
),
override val jpegQuality: (IntRange) -> Int? = manualJpegQuality(DEFAULT_JPEG_QUALITY),
override val frameProcessor: (Frame) -> Unit = {},
override val previewFpsRange: Iterable<FpsRange>.() -> FpsRange? = highestFps(),
override val antiBandingMode: Iterable<AntiBandingMode>.() -> AntiBandingMode? = firstAvailable(
override val jpegQuality: QualitySelector = manualJpegQuality(DEFAULT_JPEG_QUALITY),
override val exposureCompensation: ExposureSelector = manualExposure(DEFAULT_EXPOSURE_COMPENSATION),
override val frameProcessor: FrameProcessor? = null,
override val previewFpsRange: FpsRangeSelector = highestFps(),
override val antiBandingMode: AntiBandingModeSelector = firstAvailable(
auto(),
hz50(),
hz60(),
none()
),
override val sensorSensitivity: (Iterable<Int>.() -> Int?)? = null,
override val pictureResolution: Iterable<Resolution>.() -> Resolution? = highestResolution(),
override val previewResolution: Iterable<Resolution>.() -> Resolution? = highestResolution()
override val sensorSensitivity: SensorSensitivitySelector? = null,
override val pictureResolution: ResolutionSelector = highestResolution(),
override val previewResolution: ResolutionSelector = highestResolution()
) : Configuration {
/**
@@ -39,67 +40,64 @@ data class CameraConfiguration(
private var cameraConfiguration: CameraConfiguration = default()
fun flash(selector: (Iterable<Flash>.() -> Flash?)): Builder {
fun flash(selector: FlashSelector): Builder = apply {
cameraConfiguration = cameraConfiguration.copy(
flashMode = selector
)
return this
}
fun focusMode(selector: (Iterable<FocusMode>.() -> FocusMode?)): Builder {
fun focusMode(selector: FocusModeSelector): Builder = apply {
cameraConfiguration = cameraConfiguration.copy(
focusMode = selector
)
return this
}
fun previewFpsRange(selector: (Iterable<FpsRange>.() -> FpsRange?)): Builder {
fun previewFpsRange(selector: FpsRangeSelector): Builder = apply {
cameraConfiguration = cameraConfiguration.copy(
previewFpsRange = selector
)
return this
}
fun sensorSensitivity(selector: (Iterable<Int>.() -> Int?)): Builder {
fun exposureCompensation(selector: ExposureSelector): Builder = apply {
cameraConfiguration = cameraConfiguration.copy(
sensorSensitivity = selector
exposureCompensation = selector
)
return this
}
fun antiBandingMode(selector: Iterable<AntiBandingMode>.() -> AntiBandingMode?): Builder {
fun antiBandingMode(selector: AntiBandingModeSelector): Builder = apply {
cameraConfiguration.copy(
antiBandingMode = selector
)
return this
}
fun jpegQuality(selector: IntRange.() -> Int?): Builder {
fun jpegQuality(selector: QualitySelector): Builder = apply {
cameraConfiguration.copy(
jpegQuality = selector
)
return this
}
fun previewResolution(selector: (Iterable<Resolution>.() -> Resolution?)): Builder {
fun sensorSensitivity(selector: SensorSensitivitySelector): Builder = apply {
cameraConfiguration = cameraConfiguration.copy(
sensorSensitivity = selector
)
}
fun previewResolution(selector: ResolutionSelector): Builder = apply {
cameraConfiguration = cameraConfiguration.copy(
previewResolution = selector
)
return this
}
fun photoResolution(selector: (Iterable<Resolution>.() -> Resolution?)): Builder {
fun photoResolution(selector: ResolutionSelector): Builder = apply {
cameraConfiguration = cameraConfiguration.copy(
pictureResolution = selector
)
return this
}
fun frameProcessor(frameProcessor: FrameProcessor): Builder {
fun frameProcessor(frameProcessor: FrameProcessorJava?): Builder = apply {
cameraConfiguration = cameraConfiguration.copy(
frameProcessor = { frameProcessor.process(it) }
frameProcessor = frameProcessor?.let { it::process }
)
return this
}
/**
@@ -115,18 +113,18 @@ data class CameraConfiguration(
* Alias for [CameraConfiguration.default]
*/
@JvmStatic
fun standard() = default()
fun standard(): CameraConfiguration = default()
/**
* Default [CameraConfiguration].
*/
@JvmStatic
fun default() = CameraConfiguration()
fun default(): CameraConfiguration = CameraConfiguration()
/**
* Creates a new [CameraConfiguration.Builder].
*/
@JvmStatic
fun builder() = CameraConfiguration.Builder()
fun builder(): Builder = Builder()
}
}
@@ -1,16 +1,17 @@
package io.fotoapparat.configuration
import io.fotoapparat.parameter.*
import io.fotoapparat.preview.Frame
import io.fotoapparat.selector.*
import io.fotoapparat.util.FrameProcessor
interface Configuration {
val flashMode: (Iterable<Flash>.() -> Flash?)?
val focusMode: (Iterable<FocusMode>.() -> FocusMode?)?
val jpegQuality: ((IntRange) -> Int?)?
val frameProcessor: ((Frame) -> Unit)?
val previewFpsRange: (Iterable<FpsRange>.() -> FpsRange?)?
val antiBandingMode: (Iterable<AntiBandingMode>.() -> AntiBandingMode?)?
val sensorSensitivity: (Iterable<Int>.() -> Int?)?
val previewResolution: (Iterable<Resolution>.() -> Resolution?)?
val pictureResolution: (Iterable<Resolution>.() -> Resolution?)?
val flashMode: FlashSelector?
val focusMode: FocusModeSelector?
val jpegQuality: QualitySelector?
val exposureCompensation: ExposureSelector?
val frameProcessor: FrameProcessor?
val previewFpsRange: FpsRangeSelector?
val antiBandingMode: AntiBandingModeSelector?
val sensorSensitivity: SensorSensitivitySelector?
val previewResolution: ResolutionSelector?
val pictureResolution: ResolutionSelector?
}
@@ -1,21 +1,22 @@
package io.fotoapparat.configuration
import io.fotoapparat.parameter.*
import io.fotoapparat.preview.Frame
import io.fotoapparat.selector.*
import io.fotoapparat.util.FrameProcessor
/**
* A camera update configuration.
*/
data class UpdateConfiguration(
override val flashMode: (Iterable<Flash>.() -> Flash?)? = null,
override val focusMode: (Iterable<FocusMode>.() -> FocusMode?)? = null,
override val jpegQuality: ((IntRange) -> Int?)? = null,
override val frameProcessor: ((Frame) -> Unit)? = null,
override val previewFpsRange: (Iterable<FpsRange>.() -> FpsRange?)? = null,
override val antiBandingMode: (Iterable<AntiBandingMode>.() -> AntiBandingMode?)? = null,
override val sensorSensitivity: (Iterable<Int>.() -> Int?)? = null,
override val previewResolution: (Iterable<Resolution>.() -> Resolution?)? = null,
override val pictureResolution: (Iterable<Resolution>.() -> Resolution?)? = null
override val flashMode: FlashSelector? = null,
override val focusMode: FocusModeSelector? = null,
override val jpegQuality: QualitySelector? = null,
override val exposureCompensation: ExposureSelector? = null,
override val frameProcessor: FrameProcessor? = null,
override val previewFpsRange: FpsRangeSelector? = null,
override val antiBandingMode: AntiBandingModeSelector? = null,
override val sensorSensitivity: SensorSensitivitySelector? = null,
override val previewResolution: ResolutionSelector? = null,
override val pictureResolution: ResolutionSelector? = null
) : Configuration {
/**
@@ -25,67 +26,64 @@ data class UpdateConfiguration(
private var configuration = UpdateConfiguration()
fun flash(selector: Iterable<Flash>.() -> Flash?): Builder {
configuration.copy(
fun flash(selector: FlashSelector): Builder = apply {
configuration = configuration.copy(
flashMode = selector
)
return this
}
fun focusMode(selector: Iterable<FocusMode>.() -> FocusMode?): Builder {
configuration.copy(
fun focusMode(selector: FocusModeSelector): Builder = apply {
configuration = configuration.copy(
focusMode = selector
)
return this
}
fun previewFpsRange(selector: Iterable<FpsRange>.() -> FpsRange?): Builder {
configuration.copy(
fun previewFpsRange(selector: FpsRangeSelector): Builder = apply {
configuration = configuration.copy(
previewFpsRange = selector
)
return this
}
fun sensorSensitivity(selector: Iterable<Int>.() -> Int?): Builder {
configuration.copy(
fun sensorSensitivity(selector: SensorSensitivitySelector): Builder = apply {
configuration = configuration.copy(
sensorSensitivity = selector
)
return this
}
fun antiBandingMode(selector: Iterable<AntiBandingMode>.() -> AntiBandingMode?): Builder {
configuration.copy(
fun antiBandingMode(selector: AntiBandingModeSelector): Builder = apply {
configuration = configuration.copy(
antiBandingMode = selector
)
return this
}
fun jpegQuality(selector: IntRange.() -> Int?): Builder {
configuration.copy(
fun jpegQuality(selector: QualitySelector): Builder = apply {
configuration = configuration.copy(
jpegQuality = selector
)
return this
}
fun previewResolution(selector: Iterable<Resolution>.() -> Resolution?): Builder {
configuration.copy(
fun exposureCompensation(selector: ExposureSelector): Builder = apply {
configuration = configuration.copy(
exposureCompensation = selector
)
}
fun previewResolution(selector: ResolutionSelector): Builder = apply {
configuration = configuration.copy(
previewResolution = selector
)
return this
}
fun photoResolution(selector: Iterable<Resolution>.() -> Resolution?): Builder {
configuration.copy(
fun photoResolution(selector: ResolutionSelector): Builder = apply {
configuration = configuration.copy(
pictureResolution = selector
)
return this
}
fun frameProcessor(frameProcessor: (Frame) -> Unit): Builder {
configuration.copy(
fun frameProcessor(frameProcessor: FrameProcessor?): Builder = apply {
configuration = configuration.copy(
frameProcessor = frameProcessor
)
return this
}
/**
@@ -100,6 +98,6 @@ data class UpdateConfiguration(
* Creates a new [UpdateConfiguration.Builder].
*/
@JvmStatic
fun builder() = UpdateConfiguration.Builder()
fun builder(): Builder = Builder()
}
}
@@ -1,33 +1,43 @@
package io.fotoapparat.coroutines
import kotlinx.coroutines.experimental.CompletableDeferred
import kotlinx.coroutines.experimental.Deferred
import kotlinx.coroutines.experimental.channels.BroadcastChannel
import kotlinx.coroutines.experimental.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
/**
* A [ConflatedBroadcastChannel] which exposes a [getValue] which will [await] for at least one value.
*/
@ExperimentalCoroutinesApi
internal class AwaitBroadcastChannel<T>(
private val channel: ConflatedBroadcastChannel<T> = ConflatedBroadcastChannel(),
private val defered: CompletableDeferred<Boolean> = CompletableDeferred()
) : BroadcastChannel<T> by channel, Deferred<Boolean> by defered {
private val deferred: CompletableDeferred<Boolean> = CompletableDeferred()
) : BroadcastChannel<T> by channel, Deferred<Boolean> by deferred {
/**
* The most recently sent element to this channel.
*/
suspend fun getValue(): T {
defered.await()
deferred.await()
return channel.value
}
override fun offer(element: T): Boolean {
defered.complete(true)
deferred.complete(true)
return channel.offer(element)
}
suspend override fun send(element: T) {
defered.complete(true)
override suspend fun send(element: T) {
deferred.complete(true)
channel.send(element)
}
}
override fun cancel(cause: CancellationException?) {
channel.cancel(cause)
deferred.cancel(cause)
}
override fun cancel(cause: Throwable?): Boolean {
deferred.cancel(cause?.message ?:"", cause)
return channel.close(cause)
}
}
@@ -0,0 +1,16 @@
package io.fotoapparat.error
import io.fotoapparat.exception.camera.CameraException
/**
* Notified when an camera error happens within Fotoapparat.
*
* This method is always called from the main thread.
*/
interface CameraErrorListener {
/**
* Notified when a camera error happens within Fotoapparat.
*/
fun onError(e: CameraException)
}
@@ -4,13 +4,15 @@ import android.os.Looper
import io.fotoapparat.exception.camera.CameraException
import io.fotoapparat.hardware.executeMainThread
typealias CameraErrorCallback = (CameraException) -> Unit
/**
* @return CameraErrorCallback which will always move execution to the main thread.
*/
fun ((CameraException) -> Unit).onMainThread(): (CameraException) -> Unit = { cameraException ->
fun CameraErrorCallback.onMainThread(): CameraErrorCallback = { cameraException ->
if (Looper.myLooper() == Looper.getMainLooper()) {
this(cameraException)
} else {
executeMainThread { this(cameraException) }
}
}
}
@@ -1,6 +1,5 @@
package io.fotoapparat.exif
import io.fotoapparat.result.Photo
import java.io.File
/**
@@ -15,5 +14,5 @@ internal interface ExifOrientationWriter {
* @param photo Photo stored in the file.
* @throws FileSaveException If writing has failed.
*/
fun writeExifOrientation(file: File, photo: Photo)
fun writeExifOrientation(file: File, rotationDegrees: Int)
}
@@ -1,8 +1,7 @@
package io.fotoapparat.exif
import android.media.ExifInterface
import androidx.exifinterface.media.ExifInterface
import io.fotoapparat.exception.FileSaveException
import io.fotoapparat.result.Photo
import java.io.File
import java.io.IOException
@@ -11,16 +10,16 @@ import java.io.IOException
*/
internal object ExifWriter : ExifOrientationWriter {
@Throws(FileSaveException::class)
override fun writeExifOrientation(file: File, photo: Photo) {
override fun writeExifOrientation(file: File, rotationDegrees: Int) {
try {
val exifInterface = ExifInterface(file.path)
exifInterface.setAttribute(
ExifInterface.TAG_ORIENTATION,
toExifOrientation(photo.rotationDegrees).toString()
)
exifInterface.saveAttributes()
ExifInterface(file.path).apply {
setAttribute(
ExifInterface.TAG_ORIENTATION,
toExifOrientation(rotationDegrees).toString()
)
saveAttributes()
}
} catch (e: IOException) {
throw FileSaveException(e)
}
@@ -4,8 +4,8 @@ package io.fotoapparat.hardware
import android.hardware.Camera
import android.media.MediaRecorder
import android.support.annotation.FloatRange
import android.view.Surface
import androidx.annotation.FloatRange
import io.fotoapparat.capability.Capabilities
import io.fotoapparat.capability.provide.getCapabilities
import io.fotoapparat.characteristic.Characteristics
@@ -14,26 +14,25 @@ import io.fotoapparat.coroutines.AwaitBroadcastChannel
import io.fotoapparat.exception.camera.CameraException
import io.fotoapparat.hardware.metering.FocalRequest
import io.fotoapparat.hardware.metering.convert.toFocusAreas
import io.fotoapparat.hardware.orientation.computeDisplayOrientation
import io.fotoapparat.hardware.orientation.computeImageOrientation
import io.fotoapparat.hardware.orientation.*
import io.fotoapparat.log.Logger
import io.fotoapparat.parameter.FocusMode
import io.fotoapparat.parameter.Resolution
import io.fotoapparat.parameter.camera.CameraParameters
import io.fotoapparat.parameter.camera.apply.applyNewParameters
import io.fotoapparat.parameter.camera.apply.applyInto
import io.fotoapparat.parameter.camera.convert.toCode
import io.fotoapparat.preview.Frame
import io.fotoapparat.preview.PreviewStream
import io.fotoapparat.result.FocusResult
import io.fotoapparat.result.Photo
import io.fotoapparat.util.FrameProcessor
import io.fotoapparat.util.lineSeparator
import io.fotoapparat.view.Preview
import kotlinx.coroutines.experimental.CompletableDeferred
import kotlinx.coroutines.CompletableDeferred
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
typealias PreviewSize = io.fotoapparat.parameter.Resolution
/**
@@ -50,9 +49,10 @@ internal open class CameraDevice(
private lateinit var surface: Surface
private lateinit var camera: Camera
private var cachedZoomParameters: Camera.Parameters? = null
private var displayRotation = 0
var imageRotation = 0
private var cachedCameraParameters: Camera.Parameters? = null
private lateinit var displayOrientation: Orientation
private lateinit var imageOrientation: Orientation
private lateinit var previewOrientation: Orientation
/**
* Opens a connection to a camera.
@@ -80,7 +80,7 @@ internal open class CameraDevice(
*/
open fun close() {
logger.recordMethod()
surface.release()
camera.release()
}
@@ -136,7 +136,7 @@ internal open class CameraDevice(
open fun takePhoto(): Photo {
logger.recordMethod()
return camera.takePhoto(imageRotation)
return camera.takePhoto(imageOrientation.degrees)
}
/**
@@ -167,13 +167,15 @@ internal open class CameraDevice(
logger.log("New camera parameters are: $cameraParameters")
camera.updateParameters(cameraParameters)
cameraParameters.applyInto(cachedCameraParameters ?: camera.parameters)
.cacheLocally()
.setInCamera()
}
/**
* Updates the frame processor.
*/
open fun updateFrameProcessor(frameProcessor: ((Frame) -> Unit)?) {
open fun updateFrameProcessor(frameProcessor: FrameProcessor?) {
logger.recordMethod()
previewStream.updateProcessorSafely(frameProcessor)
@@ -182,23 +184,41 @@ internal open class CameraDevice(
/**
* Sets the current orientation of the display.
*/
open fun setDisplayOrientation(degrees: Int) {
open fun setDisplayOrientation(orientationState: OrientationState) {
logger.recordMethod()
imageRotation = computeImageOrientation(
degrees = degrees,
characteristics = characteristics
imageOrientation = computeImageOrientation(
deviceOrientation = orientationState.deviceOrientation,
cameraOrientation = characteristics.cameraOrientation,
cameraIsMirrored = characteristics.isMirrored
)
displayRotation = computeDisplayOrientation(
degrees = degrees,
characteristics = characteristics
displayOrientation = computeDisplayOrientation(
screenOrientation = orientationState.screenOrientation,
cameraOrientation = characteristics.cameraOrientation,
cameraIsMirrored = characteristics.isMirrored
)
logger.log("Image Rotation is: $imageRotation. Display rotation is: $displayRotation")
previewOrientation = computePreviewOrientation(
screenOrientation = orientationState.screenOrientation,
cameraOrientation = characteristics.cameraOrientation,
cameraIsMirrored = characteristics.isMirrored
)
previewStream.frameOrientation = imageRotation
camera.setDisplayOrientation(displayRotation)
logger.log("Orientations: $lineSeparator" +
"Screen orientation (preview) is: ${orientationState.screenOrientation}. " + lineSeparator +
"Camera sensor orientation is always at: ${characteristics.cameraOrientation}. " + lineSeparator +
"Camera is " + if (characteristics.isMirrored) "mirrored." else "not mirrored."
)
logger.log("Orientation adjustments: $lineSeparator" +
"Image orientation will be adjusted by: ${imageOrientation.degrees} degrees. " + lineSeparator +
"Display orientation will be adjusted by: ${displayOrientation.degrees} degrees. " + lineSeparator +
"Preview orientation will be adjusted by: ${previewOrientation.degrees} degrees."
)
previewStream.frameOrientation = previewOrientation
camera.setDisplayOrientation(displayOrientation.degrees)
}
/**
@@ -245,6 +265,7 @@ internal open class CameraDevice(
/**
* Sets the desired surface on which the camera's preview will be displayed.
*/
@Throws(IOException::class)
open fun setDisplaySurface(preview: Preview) {
logger.recordMethod()
@@ -266,14 +287,13 @@ internal open class CameraDevice(
open fun getPreviewResolution(): Resolution {
logger.recordMethod()
val previewResolution = camera.getPreviewResolution(imageRotation)
val previewResolution = camera.getPreviewResolution(previewOrientation)
logger.log("Preview resolution is: $previewResolution")
return previewResolution
}
private fun setZoomSafely(@FloatRange(from = 0.0, to = 1.0) level: Float) {
try {
setZoomUnsafe(level)
@@ -283,14 +303,20 @@ internal open class CameraDevice(
}
private fun setZoomUnsafe(@FloatRange(from = 0.0, to = 1.0) level: Float) {
(cachedZoomParameters ?: camera.parameters)
(cachedCameraParameters ?: camera.parameters)
.apply {
zoom = (maxZoom * level).toInt()
}
.let {
cachedZoomParameters = it
camera.parameters = it
}
.cacheLocally()
.setInCamera()
}
private fun Camera.Parameters.cacheLocally() = apply {
cachedCameraParameters = this
}
private fun Camera.Parameters.setInCamera() = apply {
camera.parameters = this
}
private fun Camera.focusSafely(): FocusResult {
@@ -315,7 +341,7 @@ internal open class CameraDevice(
private suspend fun Camera.updateFocusingAreas(focalRequest: FocalRequest) {
val focusingAreas = focalRequest.toFocusAreas(
displayOrientationDegrees = displayRotation,
displayOrientationDegrees = displayOrientation.degrees,
cameraIsMirrored = characteristics.isMirrored
)
@@ -337,16 +363,13 @@ internal open class CameraDevice(
private fun Camera.clearFocusingAreas() {
parameters = parameters.apply {
with(capabilities) {
meteringAreas = null
focusAreas = null
}
meteringAreas = null
focusAreas = null
}
}
}
private const val AUTOFOCUS_TIMEOUT_SECONDS = 3L
private fun Camera.takePhoto(imageRotation: Int): Photo {
@@ -371,73 +394,31 @@ private fun Camera.takePhoto(imageRotation: Int): Photo {
return photoReference.get()
}
private fun computeImageOrientation(
degrees: Int,
characteristics: Characteristics
) = computeImageOrientation(
screenRotationDegrees = degrees,
cameraRotationDegrees = characteristics.orientation,
cameraIsMirrored = characteristics.isMirrored
)
private fun computeDisplayOrientation(
degrees: Int,
characteristics: Characteristics
) = computeDisplayOrientation(
screenRotationDegrees = degrees,
cameraRotationDegrees = characteristics.orientation,
cameraIsMirrored = characteristics.isMirrored
)
private fun Camera.updateParameters(newParameters: CameraParameters) {
parameters = parameters.applyNewParameters(newParameters)
}
@Throws(IOException::class)
private fun Camera.setDisplaySurface(
preview: Preview
): Surface = when (preview) {
is Preview.Texture -> preview.surfaceTexture
.also {
setPreviewTexture(it)
}
.let {
Surface(it)
}
.also(this::setPreviewTexture)
.let(::Surface)
is Preview.Surface -> preview.surfaceHolder
.also {
setPreviewDisplay(it)
}
.also(this::setPreviewDisplay)
.surface
}
private fun Camera.getPreviewResolution(imageRotation: Int): Resolution {
val previewSize = parameters.previewSize
val size = PreviewSize(
previewSize.width,
previewSize.height
)
return size.run {
when (imageRotation) {
0, 180 -> this
else -> flipDimensions()
}
}
private fun Camera.getPreviewResolution(previewOrientation: Orientation): Resolution {
return parameters.previewSize
.run {
PreviewSize(width, height)
}
.run {
when (previewOrientation) {
is Orientation.Vertical -> this
is Orientation.Horizontal -> flipDimensions()
}
}
}
private fun PreviewStream.updateProcessorSafely(frameProcessor: ((Frame) -> Unit)?) {
clearProcessors()
when (frameProcessor) {
null -> stop()
else -> {
addProcessor(frameProcessor)
start()
}
}
}
private fun Capabilities.canSetFocusingAreas(): Boolean {
return maxMeteringAreas > 0 || maxFocusAreas > 0
}
private fun Capabilities.canSetFocusingAreas(): Boolean =
maxMeteringAreas > 0 || maxFocusAreas > 0
@@ -3,33 +3,35 @@
package io.fotoapparat.hardware
import android.hardware.Camera
import io.fotoapparat.characteristic.LensPosition
import io.fotoapparat.characteristic.getCharacteristics
import io.fotoapparat.concurrent.CameraExecutor
import io.fotoapparat.configuration.CameraConfiguration
import io.fotoapparat.configuration.Configuration
import io.fotoapparat.exception.camera.UnsupportedLensException
import io.fotoapparat.hardware.display.Display
import io.fotoapparat.hardware.orientation.Orientation
import io.fotoapparat.log.Logger
import io.fotoapparat.parameter.ScaleType
import io.fotoapparat.parameter.camera.CameraParameters
import io.fotoapparat.parameter.camera.provide.getCameraParameters
import io.fotoapparat.preview.Frame
import io.fotoapparat.selector.LensPositionSelector
import io.fotoapparat.util.FrameProcessor
import io.fotoapparat.view.CameraRenderer
import io.fotoapparat.view.FocalPointSelector
import kotlinx.coroutines.experimental.CompletableDeferred
import kotlinx.coroutines.CompletableDeferred
/**
* Phone.
*/
internal open class Device(
private val logger: Logger,
internal open val logger: Logger,
private val display: Display,
open val scaleType: ScaleType,
open val cameraRenderer: CameraRenderer,
val focusPointSelector: FocalPointSelector?,
internal open val scaleType: ScaleType,
internal open val cameraRenderer: CameraRenderer,
internal val focusPointSelector: FocalPointSelector?,
internal val executor: CameraExecutor,
numberOfCameras: Int = Camera.getNumberOfCameras(),
initialConfiguration: CameraConfiguration, initialLensPositionSelector: Collection<LensPosition>.() -> LensPosition?
initialConfiguration: CameraConfiguration, initialLensPositionSelector: LensPositionSelector
) {
private val cameras = (0 until numberOfCameras).map { cameraId ->
@@ -39,7 +41,7 @@ internal open class Device(
)
}
private var lensPositionSelector: Collection<LensPosition>.() -> LensPosition? = initialLensPositionSelector
private var lensPositionSelector: LensPositionSelector = initialLensPositionSelector
private var selectedCameraDevice = CompletableDeferred<CameraDevice>()
private var savedConfiguration = CameraConfiguration.default()
@@ -51,7 +53,7 @@ internal open class Device(
/**
* Selects a camera.
*/
open fun canSelectCamera(lensPositionSelector: (Collection<LensPosition>) -> LensPosition?): Boolean {
open fun canSelectCamera(lensPositionSelector: LensPositionSelector): Boolean {
val selectedCameraDevice = selectCamera(
availableCameras = cameras,
lensPositionSelector = lensPositionSelector
@@ -69,9 +71,7 @@ internal open class Device(
availableCameras = cameras,
lensPositionSelector = lensPositionSelector
)
?.let {
selectedCameraDevice.complete(it)
}
?.let(selectedCameraDevice::complete)
?: selectedCameraDevice.completeExceptionally(UnsupportedLensException())
}
@@ -85,9 +85,7 @@ internal open class Device(
/**
* Waits and returns the selected camera.
*/
open suspend fun awaitSelectedCamera(): CameraDevice {
return selectedCameraDevice.await()
}
open suspend fun awaitSelectedCamera(): CameraDevice = selectedCameraDevice.await()
/**
* Returns the selected camera.
@@ -95,12 +93,10 @@ internal open class Device(
* @throws IllegalStateException If no camera has been yet selected.
* @throws UnsupportedLensException If no camera could get selected.
*/
open fun getSelectedCamera(): CameraDevice {
return try {
selectedCameraDevice.getCompleted()
} catch (e: IllegalStateException) {
throw IllegalStateException("Camera has not started!")
}
open fun getSelectedCamera(): CameraDevice = try {
selectedCameraDevice.getCompleted()
} catch (e: IllegalStateException) {
throw IllegalStateException("Camera has not started!")
}
/**
@@ -109,16 +105,16 @@ internal open class Device(
open fun hasSelectedCamera() = selectedCameraDevice.isCompleted
/**
* @return rotation of the screen in degrees.
* @return Orientation of the screen.
*/
open fun getScreenRotation(): Int {
return display.getRotation()
open fun getScreenOrientation(): Orientation {
return display.getOrientation()
}
/**
* Updates the desired from the user camera lens position.
*/
open fun updateLensPositionSelector(newLensPosition: Collection<LensPosition>.() -> LensPosition?) {
open fun updateLensPositionSelector(newLensPosition: LensPositionSelector) {
logger.recordMethod()
lensPositionSelector = newLensPosition
@@ -139,33 +135,26 @@ internal open class Device(
/**
* @return The desired from the user selectors.
*/
open fun getConfiguration(): CameraConfiguration {
return savedConfiguration
}
open fun getConfiguration(): CameraConfiguration = savedConfiguration
/**
* @return The selected [CameraParameters] for the given [CameraDevice].
*/
open suspend fun getCameraParameters(cameraDevice: CameraDevice): CameraParameters {
return getCameraParameters(
cameraConfiguration = savedConfiguration,
capabilities = cameraDevice.getCapabilities()
)
}
open suspend fun getCameraParameters(cameraDevice: CameraDevice): CameraParameters =
getCameraParameters(
cameraConfiguration = savedConfiguration,
capabilities = cameraDevice.getCapabilities()
)
/**
* @return The frame processor.
*/
open fun getFrameProcessor(): (Frame) -> Unit {
return savedConfiguration.frameProcessor
}
open fun getFrameProcessor(): FrameProcessor? = savedConfiguration.frameProcessor
/**
* @return The desired from the user camera lens position.
*/
open fun getLensPositionSelector(): Collection<LensPosition>.() -> LensPosition? {
return lensPositionSelector
}
open fun getLensPositionSelector(): LensPositionSelector = lensPositionSelector
}
@@ -178,11 +167,16 @@ internal fun updateConfiguration(
) = CameraConfiguration(
flashMode = newConfiguration.flashMode ?: savedConfiguration.flashMode,
focusMode = newConfiguration.focusMode ?: savedConfiguration.focusMode,
exposureCompensation = newConfiguration.exposureCompensation
?: savedConfiguration.exposureCompensation,
frameProcessor = newConfiguration.frameProcessor ?: savedConfiguration.frameProcessor,
previewFpsRange = newConfiguration.previewFpsRange ?: savedConfiguration.previewFpsRange,
sensorSensitivity = newConfiguration.sensorSensitivity ?: savedConfiguration.sensorSensitivity,
pictureResolution = newConfiguration.pictureResolution ?: savedConfiguration.pictureResolution,
previewResolution = newConfiguration.previewResolution ?: savedConfiguration.previewResolution
sensorSensitivity = newConfiguration.sensorSensitivity
?: savedConfiguration.sensorSensitivity,
pictureResolution = newConfiguration.pictureResolution
?: savedConfiguration.pictureResolution,
previewResolution = newConfiguration.previewResolution
?: savedConfiguration.previewResolution
)
/**
@@ -190,11 +184,11 @@ internal fun updateConfiguration(
*/
internal fun selectCamera(
availableCameras: List<CameraDevice>,
lensPositionSelector: Collection<LensPosition>.() -> LensPosition?
lensPositionSelector: LensPositionSelector
): CameraDevice? {
val lensPositions = availableCameras.map { it.characteristics.lensPosition }.toSet()
val desiredPosition = lensPositionSelector(lensPositions)
return availableCameras.find { it.characteristics.lensPosition == desiredPosition }
}
}
@@ -4,13 +4,9 @@ import android.os.Handler
import android.os.Looper
import java.util.concurrent.Executors
private var taskExecutor = Executors.newSingleThreadExecutor()
get() = field.takeUnless { it.isShutdown } ?: Executors.newSingleThreadExecutor().also { field = it }
private val cameraExecutor = Executors.newSingleThreadExecutor()
private val loggingExecutor = Executors.newSingleThreadExecutor()
private val mainThreadHandler = Handler(Looper.getMainLooper())
/**
* [java.util.concurrent.Executor] operating the [io.fotoapparat.result.PendingResult].
*/
@@ -20,23 +16,6 @@ internal val pendingResultExecutor = Executors.newSingleThreadExecutor()
*/
internal val frameProcessingExecutor = Executors.newSingleThreadExecutor()
/**
* Shuts down all pending camera tasks.
*/
internal fun shutdownPendingTasks() {
taskExecutor.shutdownNow()
}
/**
* Executes a camera task.
*/
internal fun executeTask(function: Runnable) = taskExecutor.execute(function)
/**
* Executes a camera operation.
*/
internal fun execute(function: () -> Unit) = cameraExecutor.execute(function)
/**
* Executes an operation in the main thread.
*/
@@ -3,23 +3,28 @@ package io.fotoapparat.hardware.display
import android.content.Context
import android.view.Surface
import android.view.WindowManager
import io.fotoapparat.hardware.orientation.Orientation
import io.fotoapparat.hardware.orientation.Orientation.Horizontal.Landscape
import io.fotoapparat.hardware.orientation.Orientation.Horizontal.ReverseLandscape
import io.fotoapparat.hardware.orientation.Orientation.Vertical.Portrait
import io.fotoapparat.hardware.orientation.Orientation.Vertical.ReversePortrait
/**
* A phone's display.
*/
open internal class Display(context: Context) {
internal open class Display(context: Context) {
private val display = context.getDisplay()
/**
* Returns the rotation of the screen from its "natural" orientation in degrees.
* Returns the orientation of the screen.
*/
open fun getRotation(): Int = when (display.rotation) {
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
Surface.ROTATION_0 -> 0
else -> 0
open fun getOrientation(): Orientation = when (display.rotation) {
Surface.ROTATION_0 -> Portrait
Surface.ROTATION_90 -> Landscape
Surface.ROTATION_180 -> ReversePortrait
Surface.ROTATION_270 -> ReverseLandscape
else -> Portrait
}
}
@@ -1,6 +1,5 @@
package io.fotoapparat.hardware.metering
/**
* A point in arbitrary scale.
*/
@@ -1,61 +1,64 @@
package io.fotoapparat.hardware.orientation
typealias DeviceOrientation = Orientation
typealias ScreenOrientation = Orientation
/**
* @param screenRotationDegrees rotation of the display (as provided by system) in degrees.
* @param cameraRotationDegrees rotation of the camera sensor relatively to device natural
* orientation.
* @param cameraIsMirrored `true` if camera is mirrored (typically that is the case for
* front cameras). `false` if it is not mirrored.
*
* @return clockwise rotation of the image relatively to current device orientation.
* The device orientation.
*/
internal fun computeImageOrientation(
screenRotationDegrees: Int,
cameraRotationDegrees: Int,
cameraIsMirrored: Boolean
): Int {
val rotation = if (cameraIsMirrored) {
-(screenRotationDegrees + cameraRotationDegrees)
} else {
screenRotationDegrees - cameraRotationDegrees
sealed class Orientation(
val degrees: Int
) {
/**
* A vertical device orientation.
*/
sealed class Vertical(degrees: Int) : Orientation(degrees) {
/**
* A vertical, normal orientation.
*/
object Portrait : Vertical(0) {
override fun toString(): String = "Orientation.Vertical.Portrait"
}
/**
* A reversed (flipped phone) orientation.
*/
object ReversePortrait : Vertical(180) {
override fun toString(): String = "Orientation.Vertical.ReversePortrait"
}
}
return (rotation + 720) % 360
}
/**
* A horizontal device orientation.
*/
sealed class Horizontal(degrees: Int) : Orientation(degrees) {
/**
* @param screenRotationDegrees rotation of the display (as provided by system) in degrees.
* @param cameraRotationDegrees rotation of the camera sensor relatively to device natural
* orientation.
* @param cameraIsMirrored `true` if camera is mirrored (typically that is the case for
* front cameras). `false` if it is not mirrored.
*
* @return display orientation in which user will see the output camera in a correct rotation.
*/
internal fun computeDisplayOrientation(
screenRotationDegrees: Int,
cameraRotationDegrees: Int,
cameraIsMirrored: Boolean
): Int {
var degrees = toClosestRightAngle(screenRotationDegrees)
/**
* A 90 degrees clockwise from "normal", orientation.
*/
object Landscape : Horizontal(90) {
override fun toString(): String = "Orientation.Horizontal.Landscape"
}
/**
* A 90 degrees counter-clockwise from "normal", orientation.
*/
object ReverseLandscape : Horizontal(270) {
override fun toString(): String = "Orientation.Horizontal.ReverseLandscape"
}
return if (cameraIsMirrored) {
degrees = (cameraRotationDegrees + degrees) % 360
(360 - degrees) % 360
} else {
(cameraRotationDegrees - degrees + 360) % 360
}
}
/**
* @return closest right angle to given value. That is: 0, 90, 180, 270.
*/
internal fun toClosestRightAngle(degrees: Int): Int {
val roundUp = degrees % 90 > 45
val roundAppModifier = if (roundUp) 1 else 0
return (degrees / 90 + roundAppModifier) * 90 % 360
internal fun Int.toOrientation(): Orientation {
return when (this) {
0, 360 -> Orientation.Vertical.Portrait
90 -> Orientation.Horizontal.Landscape
180 -> Orientation.Vertical.ReversePortrait
270 -> Orientation.Horizontal.ReverseLandscape
else -> throw IllegalArgumentException("Cannot convert $this to absolute Orientation.")
}
}
@@ -0,0 +1,75 @@
package io.fotoapparat.hardware.orientation
/**
* @param screenOrientation Orientation of the display.
* @param cameraOrientation Orientation of the camera sensor.
* @param cameraIsMirrored `true` if camera is mirrored (typically that is the case
* for front cameras). `false` if it is not mirrored.
*
* @return rotation of the image relatively to current device orientation.
*/
fun computePreviewOrientation(
screenOrientation: Orientation,
cameraOrientation: Orientation,
cameraIsMirrored: Boolean
): Orientation {
val mirroredCameraModifier = if (cameraIsMirrored) -1 else 1
val rotation = (720
+ mirroredCameraModifier * screenOrientation.degrees
- cameraOrientation.degrees
) % 360
return rotation.toOrientation()
}
/**
* @param deviceOrientation Orientation of the device.
* @param cameraOrientation Orientation of the camera sensor.
* @param cameraIsMirrored `true` if camera is mirrored (typically that is the case
* for front cameras). `false` if it is not mirrored.
*
* @return clockwise rotation of the image relatively to current device orientation.
*/
internal fun computeImageOrientation(
deviceOrientation: Orientation,
cameraOrientation: Orientation,
cameraIsMirrored: Boolean
): Orientation {
val screenOrientationDegrees = deviceOrientation.degrees
val cameraRotationDegrees = cameraOrientation.degrees
val rotation = if (cameraIsMirrored) {
360 - (cameraRotationDegrees - screenOrientationDegrees + 360) % 360
} else {
360 - (cameraRotationDegrees + screenOrientationDegrees) % 360
}
return rotation.toOrientation()
}
/**
* @param screenOrientation Orientation of the display.
* @param cameraOrientation Orientation of the camera sensor.
* @param cameraIsMirrored `true` if camera is mirrored (typically that is the case for
* front cameras). `false` if it is not mirrored.
*
* @return display orientation in which user will see the output camera in a correct rotation.
*/
internal fun computeDisplayOrientation(
screenOrientation: Orientation,
cameraOrientation: Orientation,
cameraIsMirrored: Boolean
): Orientation {
val screenOrientationDegrees = screenOrientation.degrees
val cameraRotationDegrees = cameraOrientation.degrees
val rotation = if (cameraIsMirrored) {
(360 - (cameraRotationDegrees + screenOrientationDegrees) % 360) % 360
} else {
(cameraRotationDegrees - screenOrientationDegrees + 360) % 360
}
return rotation.toOrientation()
}
@@ -2,43 +2,55 @@ package io.fotoapparat.hardware.orientation
import android.content.Context
import io.fotoapparat.hardware.Device
import io.fotoapparat.hardware.orientation.Orientation.Vertical.Portrait
/**
* Monitors orientation of the device.
*/
open internal class OrientationSensor(
internal open class OrientationSensor(
private val rotationListener: RotationListener,
private val device: Device
) {
constructor(context: Context,
device: Device
private lateinit var listener: (OrientationState) -> Unit
private val onOrientationChanged: (DeviceRotationDegrees) -> Unit = { deviceRotation ->
deviceRotation.toClosestRightAngle()
.toOrientation()
.let { deviceOrientation ->
val screenOrientation = device.getScreenOrientation()
val newState = OrientationState(
deviceOrientation = deviceOrientation,
screenOrientation = screenOrientation
)
if (newState != lastKnownOrientationState) {
lastKnownOrientationState = newState
listener(newState)
}
}
}
open var lastKnownOrientationState: OrientationState = OrientationState(
deviceOrientation = Portrait,
screenOrientation = device.getScreenOrientation()
)
constructor(
context: Context,
device: Device
) : this(
RotationListener(context),
device
)
private val onOrientationChanged = {
device.getScreenRotation().let {
if (it != lastKnownRotation) {
listener(it)
lastKnownRotation = it
}
}
}
init {
rotationListener.orientationChanged = onOrientationChanged
}
private lateinit var listener: (Int) -> Unit
private var lastKnownRotation: Int = 0
/**
* Starts monitoring device's orientation.
* Starts monitoring device's orientation state.
*/
open fun start(listener: (Int) -> Unit) {
open fun start(listener: (OrientationState) -> Unit) {
this.listener = listener
rotationListener.enable()
}
@@ -50,4 +62,4 @@ open internal class OrientationSensor(
rotationListener.disable()
}
}
}
@@ -0,0 +1,16 @@
package io.fotoapparat.hardware.orientation
/**
* Phone orientation states.
*/
data class OrientationState(
/**
* The current orientation the device is being hold.
*/
val deviceOrientation: DeviceOrientation,
/**
* The current orientation of the screen.
*/
val screenOrientation: ScreenOrientation
)
@@ -0,0 +1,14 @@
package io.fotoapparat.hardware.orientation
typealias DeviceRotationDegrees = Int
/**
* @return closest right angle to given value. That is: 0, 90, 180, 270.
*/
internal fun Int.toClosestRightAngle(): Int {
val roundUp = this % 90 > 45
val roundAppModifier = if (roundUp) 1 else 0
return (this / 90 + roundAppModifier) * 90 % 360
}
@@ -3,20 +3,19 @@ package io.fotoapparat.hardware.orientation
import android.content.Context
import android.view.OrientationEventListener
/**
* Wrapper around [OrientationEventListener] to notify when the device's rotation has changed.
*/
open internal class RotationListener(
internal open class RotationListener(
context: Context
) : OrientationEventListener(context) {
lateinit var orientationChanged: () -> Unit
lateinit var orientationChanged: (DeviceRotationDegrees) -> Unit
override fun onOrientationChanged(orientation: Int) {
override fun onOrientationChanged(orientation: DeviceRotationDegrees) {
if (canDetectOrientation()) {
orientationChanged()
orientationChanged(orientation)
}
}
}
}
@@ -1,6 +1,5 @@
package io.fotoapparat.log
/**
* Logger which delegates messages to multiple other loggers.
*/
@@ -10,4 +9,4 @@ internal class CompositeLogger(private val loggers: List<Logger>) : Logger {
loggers.forEach { it.log(message) }
}
}
}
@@ -9,9 +9,7 @@ import java.io.IOException
*/
class FileLogger(private val file: File) : Logger {
private val writer: FileWriter by lazy {
FileWriter(file)
}
private val writer: FileWriter by lazy { FileWriter(file) }
override fun log(message: String) {
try {
@@ -19,11 +19,7 @@ fun logcat(): Logger = LogcatLogger()
*
* Note: if file is not writable, no errors will be produced.
*/
fun fileLogger(file: File): Logger {
return BackgroundThreadLogger(
FileLogger(file)
)
}
fun fileLogger(file: File): Logger = BackgroundThreadLogger(FileLogger(file))
/**
* @return logger which prints logs to file located at `context.getExternalFilesDir("logs")`.
@@ -8,21 +8,29 @@ sealed class AntiBandingMode : Parameter {
/**
* Auto adjust. This should be the default.
*/
object Auto : AntiBandingMode()
object Auto : AntiBandingMode() {
override fun toString(): String = "AntiBandingMode.Auto"
}
/**
* Anti Banding is set to 50Hz light frequency.
*/
object HZ50 : AntiBandingMode()
object HZ50 : AntiBandingMode() {
override fun toString(): String = "AntiBandingMode.HZ50"
}
/**
* Anti Banding is set to 60Hz light frequency.
*/
object HZ60 : AntiBandingMode()
object HZ60 : AntiBandingMode() {
override fun toString(): String = "AntiBandingMode.HZ60"
}
/**
* Anti Banding is not supported or ignored.
*/
object None : AntiBandingMode()
object None : AntiBandingMode() {
override fun toString(): String = "AntiBandingMode.None"
}
}
@@ -8,27 +8,37 @@ sealed class Flash : Parameter {
/**
* Camera flash will not fire.
*/
object Off : Flash()
object Off : Flash() {
override fun toString(): String = "Flash.Off"
}
/**
* Camera flash will always fire regardless light conditions.
*/
object On : Flash()
object On : Flash() {
override fun toString(): String = "Flash.On"
}
/**
* Camera flash will fire only in low light conditions.
*/
object Auto : Flash()
object Auto : Flash() {
override fun toString(): String = "Flash.Auto"
}
/**
* If deemed necessary by the camera device, a red eye reduction flash will fire during the
* precapture sequence in low light conditions.
*/
object AutoRedEye : Flash()
object AutoRedEye : Flash() {
override fun toString(): String = "Flash.AutoRedEye"
}
/**
* Transition flash to continuously on.
*/
object Torch : Flash()
object Torch : Flash() {
override fun toString(): String = "Flash.Torch"
}
}
@@ -8,40 +8,54 @@ sealed class FocusMode : Parameter {
/**
* Focus is not adjustable. It is always used by devices which do not support auto-focus.
*/
object Fixed : FocusMode()
object Fixed : FocusMode() {
override fun toString(): String = "FocusMode.Fixed"
}
/**
* Camera is focused at infinity.
*/
object Infinity : FocusMode()
object Infinity : FocusMode() {
override fun toString(): String = "FocusMode.Infinity"
}
/**
* Macro focus mode.
*/
object Macro : FocusMode()
object Macro : FocusMode() {
override fun toString(): String = "FocusMode.Macro"
}
/**
* Auto focus. Camera is trying to focus automatically when manually requested.
*/
object Auto : FocusMode()
object Auto : FocusMode() {
override fun toString(): String = "FocusMode.Auto"
}
/**
* Camera is constantly trying to stay in focus.
*
* The speed of focus change is more aggressive than [ContinuousFocusVideo].
*/
object ContinuousFocusPicture : FocusMode()
object ContinuousFocusPicture : FocusMode() {
override fun toString(): String = "FocusMode.ContinuousFocusPicture"
}
/**
* Camera is constantly trying to stay in focus.
*
* The speed of focus change is smoother than [ContinuousFocusPicture].
*/
object ContinuousFocusVideo : FocusMode()
object ContinuousFocusVideo : FocusMode() {
override fun toString(): String = "FocusMode.ContinuousFocusVideo"
}
/**
* The camera device will produce images with an extended depth of field.
*/
object Edof : FocusMode()
object Edof : FocusMode() {
override fun toString(): String = "FocusMode.Edof"
}
}
@@ -26,7 +26,5 @@ data class FpsRange(
/**
* `true` if the current range is fixed (min == max).
*/
val isFixed by lazy {
max == min
}
val isFixed get() = max == min
}
@@ -1,41 +1,33 @@
package io.fotoapparat.parameter
import android.support.annotation.IntRange
import androidx.annotation.IntRange
/**
* Resolution. Units in pixels.
*/
data class Resolution(
@JvmField
@IntRange(from = 0L, to = Long.MAX_VALUE)
val width: Int,
@JvmField
@IntRange(from = 0L, to = Long.MAX_VALUE)
val height: Int
@[JvmField IntRange(from = 0L)] val width: Int,
@[JvmField IntRange(from = 0L)] val height: Int
) : Parameter {
/**
* The total area this [Resolution] is covering.
*/
val area by lazy {
width * height
}
val area: Int get() = width * height
/**
* The aspect ratio for this size. [Float.NaN] if invalid dimensions.
*/
val aspectRatio by lazy {
when {
val aspectRatio: Float
get() = when {
width == 0 -> Float.NaN
height == 0 -> Float.NaN
else -> width.toFloat() / height
}
}
/**
* @return new instance of [Resolution] with width and height being swapped.
*/
fun flipDimensions() = Resolution(height, width)
fun flipDimensions(): Resolution = Resolution(height, width)
}
}
@@ -15,6 +15,13 @@ enum class ScaleType {
* The preview will be scaled so as its one dimensions will be equal and the other one equal or
* smaller than the corresponding dimension of the view
*/
CenterInside
CenterInside,
/**
* The preview will be scaled so as its one dimensions will be equal and the other one equal or
* larger than the corresponding dimension of the view with focus on the top part
*/
TopCrop,
}
@@ -56,10 +56,11 @@ internal class SupportedParameters(
}
/**
* @see Camera.Parameters.isZoomSupported
* @return [io.fotoapparat.parameter.Zoom.FixedZoom] if [Camera.Parameters.isZoomSupported] returns false,
* and [io.fotoapparat.parameter.Zoom.VariableZoom] with max zoom level otherwise.
*/
val supportedZoom by lazy {
cameraParameters.isZoomSupported
if (cameraParameters.isZoomSupported) Zoom.VariableZoom(cameraParameters.maxZoom, cameraParameters.zoomRatios) else Zoom.FixedZoom
}
/**
@@ -83,6 +84,13 @@ internal class SupportedParameters(
IntRange(0, 100)
}
/**
* @return A [IntRange] of exposure compensation values supported by the camera.
*/
val exposureCompensationRange by lazy {
IntRange(cameraParameters.minExposureCompensation, cameraParameters.maxExposureCompensation)
}
/**
* @see Camera.Parameters.getMaxNumFocusAreas
*/
@@ -0,0 +1,25 @@
package io.fotoapparat.parameter
/**
* Zoom modes which camera can have.
*/
sealed class Zoom {
/**
* The camera can only support one, fixed zoom level.
*/
object FixedZoom : Zoom() {
override fun toString(): String = "Zoom.FixedZoom"
}
/**
* The camera can only support a variable zoom level between (and including) 0 and [maxZoom] values.
* [zoomRatios] is a list of all zoom values. Ratios are in 1/100 increments. For example, zoom of
* 2.7x is returned as 270. The number of elements is [maxZoom] + 1. List is sorted from small to
* large. First element is always 100. The last element is the zoom ratio of the maximum zoom value.
*/
data class VariableZoom(val maxZoom: Int, val zoomRatios: List<Int>) : Zoom() {
override fun toString(): String = "Zoom.VariableZoom(maxZoom=$maxZoom, zoomRatios=$zoomRatios)"
}
}
@@ -5,7 +5,6 @@ import io.fotoapparat.parameter.*
import io.fotoapparat.util.lineSeparator
import io.fotoapparat.util.wrap
/**
* Parameters of [CameraDevice].
*/
@@ -13,6 +12,7 @@ data class CameraParameters(
val flashMode: Flash,
val focusMode: FocusMode,
val jpegQuality: Int,
val exposureCompensation: Int,
val previewFpsRange: FpsRange,
val antiBandingMode: AntiBandingMode,
val sensorSensitivity: Int?,
@@ -24,10 +24,11 @@ data class CameraParameters(
"flashMode:" + flashMode.wrap() +
"focusMode:" + focusMode.wrap() +
"jpegQuality:" + jpegQuality.wrap() +
"exposureCompensation:" + exposureCompensation.wrap() +
"previewFpsRange:" + previewFpsRange.wrap() +
"antiBandingMode:" + antiBandingMode.wrap() +
"sensorSensitivity:" + sensorSensitivity.wrap() +
"pictureResolution:" + pictureResolution.wrap() +
"previewResolution:" + previewResolution.wrap()
}
}
}
@@ -12,19 +12,20 @@ import io.fotoapparat.parameter.camera.convert.toCode
* Applies a new set of [CameraParameters] to existing [Camera.Parameters].
*
* @receiver The existing [Camera.Parameters]
* @param newParameters A new set of [CameraParameters].
* @param parameters A new set of [CameraParameters].
*
* @return Same [Camera.Parameters] object which was passed, but filled with new parameters.
*/
internal fun Camera.Parameters.applyNewParameters(newParameters: CameraParameters): Camera.Parameters {
newParameters tryApplyInto this
return this
internal fun CameraParameters.applyInto(parameters: Camera.Parameters): Camera.Parameters {
this tryApplyInto parameters
return parameters
}
private infix fun CameraParameters.tryApplyInto(parameters: Camera.Parameters) {
flashMode applyInto parameters
focusMode applyInto parameters
jpegQuality applyInto parameters
jpegQuality applyJpegQualityInto parameters
exposureCompensation applyExposureCompensationInto parameters
antiBandingMode applyInto parameters
previewFpsRange applyInto parameters
previewResolution applyPreviewInto parameters
@@ -40,10 +41,14 @@ private infix fun FocusMode.applyInto(parameters: Camera.Parameters) {
parameters.focusMode = toCode()
}
private infix fun Int.applyInto(parameters: Camera.Parameters) {
private infix fun Int.applyJpegQualityInto(parameters: Camera.Parameters) {
parameters.jpegQuality = this
}
private infix fun Int.applyExposureCompensationInto(parameters: Camera.Parameters) {
parameters.exposureCompensation = this
}
private infix fun AntiBandingMode.applyInto(parameters: Camera.Parameters) {
parameters.antibanding = toCode()
}
@@ -11,15 +11,14 @@ import io.fotoapparat.parameter.AntiBandingMode
* @receiver Code of the anti banding mode as in [Camera.Parameters].
* @return The [io.fotoapparat.Fotoapparat]'s camera [AntiBandingMode]. `null` if camera code is not supported.
*/
fun String.toAntiBandingMode(): AntiBandingMode? {
return when (this) {
Camera.Parameters.ANTIBANDING_AUTO -> AntiBandingMode.Auto
Camera.Parameters.ANTIBANDING_50HZ -> AntiBandingMode.HZ50
Camera.Parameters.ANTIBANDING_60HZ -> AntiBandingMode.HZ60
Camera.Parameters.ANTIBANDING_OFF -> AntiBandingMode.None
else -> null
}
}
fun String.toAntiBandingMode(): AntiBandingMode? =
when (this) {
Camera.Parameters.ANTIBANDING_AUTO -> AntiBandingMode.Auto
Camera.Parameters.ANTIBANDING_50HZ -> AntiBandingMode.HZ50
Camera.Parameters.ANTIBANDING_60HZ -> AntiBandingMode.HZ60
Camera.Parameters.ANTIBANDING_OFF -> AntiBandingMode.None
else -> null
}
/**
* Converts a [AntiBandingMode] to a antiBandingMode mode code as in [Camera.Parameters].
@@ -27,11 +26,10 @@ fun String.toAntiBandingMode(): AntiBandingMode? {
* @receiver The [io.fotoapparat.Fotoapparat]'s camera [AntiBandingMode].
* @return anti banding mode code as in [Camera.Parameters].
*/
fun AntiBandingMode.toCode(): String {
return when (this) {
AntiBandingMode.Auto -> Camera.Parameters.ANTIBANDING_AUTO
AntiBandingMode.HZ50 -> Camera.Parameters.ANTIBANDING_50HZ
AntiBandingMode.HZ60 -> Camera.Parameters.ANTIBANDING_60HZ
AntiBandingMode.None -> Camera.Parameters.ANTIBANDING_OFF
}
}
fun AntiBandingMode.toCode(): String =
when (this) {
AntiBandingMode.Auto -> Camera.Parameters.ANTIBANDING_AUTO
AntiBandingMode.HZ50 -> Camera.Parameters.ANTIBANDING_50HZ
AntiBandingMode.HZ60 -> Camera.Parameters.ANTIBANDING_60HZ
AntiBandingMode.None -> Camera.Parameters.ANTIBANDING_OFF
}
@@ -11,16 +11,15 @@ import io.fotoapparat.parameter.Flash
* @receiver Code of flash mode as in [Camera.Parameters].
* @return [Flash] from given camera code. `null` if camera code is not supported.
*/
internal fun String.toFlash(): Flash? {
return when (this) {
Camera.Parameters.FLASH_MODE_ON -> Flash.On
Camera.Parameters.FLASH_MODE_OFF -> Flash.Off
Camera.Parameters.FLASH_MODE_AUTO -> Flash.Auto
Camera.Parameters.FLASH_MODE_TORCH -> Flash.Torch
Camera.Parameters.FLASH_MODE_RED_EYE -> Flash.AutoRedEye
else -> null
}
}
internal fun String.toFlash(): Flash? =
when (this) {
Camera.Parameters.FLASH_MODE_ON -> Flash.On
Camera.Parameters.FLASH_MODE_OFF -> Flash.Off
Camera.Parameters.FLASH_MODE_AUTO -> Flash.Auto
Camera.Parameters.FLASH_MODE_TORCH -> Flash.Torch
Camera.Parameters.FLASH_MODE_RED_EYE -> Flash.AutoRedEye
else -> null
}
/**
* Maps between [Flash] and Camera v1 flash codes.
@@ -28,12 +27,11 @@ internal fun String.toFlash(): Flash? {
* @receiver Flash mode.
* @return code of the flash mode as in [Camera.Parameters].
*/
internal fun Flash.toCode(): String {
return when (this) {
Flash.On -> Camera.Parameters.FLASH_MODE_ON
Flash.Off -> Camera.Parameters.FLASH_MODE_OFF
Flash.Auto -> Camera.Parameters.FLASH_MODE_AUTO
Flash.Torch -> Camera.Parameters.FLASH_MODE_TORCH
Flash.AutoRedEye -> Camera.Parameters.FLASH_MODE_RED_EYE
}
}
internal fun Flash.toCode(): String =
when (this) {
Flash.On -> Camera.Parameters.FLASH_MODE_ON
Flash.Off -> Camera.Parameters.FLASH_MODE_OFF
Flash.Auto -> Camera.Parameters.FLASH_MODE_AUTO
Flash.Torch -> Camera.Parameters.FLASH_MODE_TORCH
Flash.AutoRedEye -> Camera.Parameters.FLASH_MODE_RED_EYE
}
@@ -12,18 +12,17 @@ import io.fotoapparat.parameter.FocusMode
* @receiver Code of focus mode as in [Camera.Parameters].
* @return [FocusMode] from given camera code. `null` if camera code is not supported.
*/
internal fun String.toFocusMode(): FocusMode? {
return when (this) {
Camera.Parameters.FOCUS_MODE_EDOF -> FocusMode.Edof
Camera.Parameters.FOCUS_MODE_AUTO -> FocusMode.Auto
Camera.Parameters.FOCUS_MODE_MACRO -> FocusMode.Macro
Camera.Parameters.FOCUS_MODE_FIXED -> FocusMode.Fixed
Camera.Parameters.FOCUS_MODE_INFINITY -> FocusMode.Infinity
Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO -> FocusMode.ContinuousFocusVideo
Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE -> FocusMode.ContinuousFocusPicture
else -> null
}
}
internal fun String.toFocusMode(): FocusMode? =
when (this) {
Camera.Parameters.FOCUS_MODE_EDOF -> FocusMode.Edof
Camera.Parameters.FOCUS_MODE_AUTO -> FocusMode.Auto
Camera.Parameters.FOCUS_MODE_MACRO -> FocusMode.Macro
Camera.Parameters.FOCUS_MODE_FIXED -> FocusMode.Fixed
Camera.Parameters.FOCUS_MODE_INFINITY -> FocusMode.Infinity
Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO -> FocusMode.ContinuousFocusVideo
Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE -> FocusMode.ContinuousFocusPicture
else -> null
}
/**
* Maps between [FocusMode] and Camera v1 focus codes.
@@ -31,14 +30,13 @@ internal fun String.toFocusMode(): FocusMode? {
* @receiver FocusMode mode.
* @return code of the focus mode as in [Camera.Parameters].
*/
internal fun FocusMode.toCode(): String {
return when (this) {
FocusMode.Edof -> Camera.Parameters.FOCUS_MODE_EDOF
FocusMode.Auto -> Camera.Parameters.FOCUS_MODE_AUTO
FocusMode.Macro -> Camera.Parameters.FOCUS_MODE_MACRO
FocusMode.Fixed -> Camera.Parameters.FOCUS_MODE_FIXED
FocusMode.Infinity -> Camera.Parameters.FOCUS_MODE_INFINITY
FocusMode.ContinuousFocusVideo -> Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO
FocusMode.ContinuousFocusPicture -> Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE
}
}
internal fun FocusMode.toCode(): String =
when (this) {
FocusMode.Edof -> Camera.Parameters.FOCUS_MODE_EDOF
FocusMode.Auto -> Camera.Parameters.FOCUS_MODE_AUTO
FocusMode.Macro -> Camera.Parameters.FOCUS_MODE_MACRO
FocusMode.Fixed -> Camera.Parameters.FOCUS_MODE_FIXED
FocusMode.Infinity -> Camera.Parameters.FOCUS_MODE_INFINITY
FocusMode.ContinuousFocusVideo -> Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO
FocusMode.ContinuousFocusPicture -> Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE
}
@@ -8,9 +8,8 @@ import io.fotoapparat.parameter.FpsRange
/**
* Converts a [IntArray] into a [FpsRange].
*/
internal fun IntArray.toFpsRange(): FpsRange {
return FpsRange(
this[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
this[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]
)
}
internal fun IntArray.toFpsRange(): FpsRange =
FpsRange(
this[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
this[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]
)
@@ -8,9 +8,4 @@ import io.fotoapparat.parameter.Resolution
/**
* Converts [Camera.Size] to [Resolution].
*/
fun Camera.Size.toResolution(): Resolution {
return Resolution(
width,
height
)
}
fun Camera.Size.toResolution(): Resolution = Resolution(width, height)
@@ -8,10 +8,7 @@ import io.fotoapparat.hardware.CameraDevice
import io.fotoapparat.parameter.Parameter
import io.fotoapparat.parameter.Resolution
import io.fotoapparat.parameter.camera.CameraParameters
import io.fotoapparat.selector.aspectRatio
import io.fotoapparat.selector.filtered
import io.fotoapparat.selector.firstAvailable
import io.fotoapparat.selector.*
/**
* @return [CameraParameters] which will be used by [CameraDevice].
@@ -34,6 +31,7 @@ internal fun getCameraParameters(
flashMode = flashMode selectFrom flashModes,
focusMode = focusMode selectFrom focusModes,
jpegQuality = jpegQuality selectFrom jpegQualityRange,
exposureCompensation = exposureCompensation selectFrom exposureCompensationRange,
previewFpsRange = previewFpsRange selectFrom previewFpsRanges,
antiBandingMode = antiBandingMode selectFrom antiBandingModes,
pictureResolution = selectedPictureResolution,
@@ -46,7 +44,7 @@ internal fun getCameraParameters(
private fun validPreviewSizeSelector(
resolution: Resolution,
original: Iterable<Resolution>.() -> Resolution?
original: ResolutionSelector
) = firstAvailable(
filtered(
selector = aspectRatio(
@@ -60,30 +58,28 @@ private fun validPreviewSizeSelector(
original
)
private infix fun <T> (Collection<T>.() -> T?)?.selectOptionalFrom(supportedParameters: Set<T>): T? {
return this?.run { this(supportedParameters) }
}
private infix fun <T> (Collection<T>.() -> T?)?.selectOptionalFrom(supportedParameters: Set<T>): T? =
this?.run { this(supportedParameters) }
private inline infix fun <reified T : Parameter> (Collection<T>.() -> T?).selectFrom(supportedParameters: Set<T>): T {
return this(supportedParameters)
.ensureSelected(supportedParameters)
.ensureInCollection(supportedParameters)
}
private inline infix fun <reified T : Parameter> (Collection<T>.() -> T?).selectFrom(
supportedParameters: Set<T>
): T = this(supportedParameters)
.ensureSelected(supportedParameters)
.ensureInCollection(supportedParameters)
private infix fun (IntRange.() -> Int?).selectFrom(supportedParameters: IntRange): Int {
return this(supportedParameters)
.ensureSelected(
supportedParameters = supportedParameters,
configurationName = "Jpeg quality"
)
.ensureInCollection(supportedParameters)
}
private infix fun QualitySelector.selectFrom(supportedParameters: IntRange): Int =
this(supportedParameters)
.ensureSelected(
supportedParameters = supportedParameters,
configurationName = "Jpeg quality"
)
.ensureInCollection(supportedParameters)
private inline fun <reified Param : Parameter> Param.ensureInCollection(supportedParameters: Set<Param>): Param {
return if (supportedParameters.contains(this)) {
this
} else {
private inline fun <reified Param : Parameter> Param.ensureInCollection(
supportedParameters: Set<Param>
): Param = apply {
if (this !in supportedParameters) {
throw InvalidConfigurationException(
value = this,
klass = Param::class.java,
@@ -92,10 +88,10 @@ private inline fun <reified Param : Parameter> Param.ensureInCollection(supporte
}
}
private inline fun <reified Param : Comparable<Param>> Param.ensureInCollection(supportedRange: ClosedRange<Param>): Param {
return if (supportedRange.contains(this)) {
this
} else {
private inline fun <reified Param : Comparable<Param>> Param.ensureInCollection(
supportedRange: ClosedRange<Param>
): Param = apply {
if (this !in supportedRange) {
throw InvalidConfigurationException(
value = this,
klass = Param::class.java,
@@ -105,19 +101,17 @@ private inline fun <reified Param : Comparable<Param>> Param.ensureInCollection(
}
private inline fun <reified Param : Parameter> Param?.ensureSelected(supportedParameters: Collection<Parameter>): Param {
return this ?: throw UnsupportedConfigurationException(
klass = Param::class.java,
supportedParameters = supportedParameters
)
}
private inline fun <reified Param : Parameter> Param?.ensureSelected(
supportedParameters: Collection<Parameter>
): Param = this ?: throw UnsupportedConfigurationException(
klass = Param::class.java,
supportedParameters = supportedParameters
)
private inline fun <reified Param : Comparable<Param>> Param?.ensureSelected(
supportedParameters: ClosedRange<Param>,
configurationName: String
): Param {
return this ?: throw UnsupportedConfigurationException(
configurationName = configurationName,
supportedRange = supportedParameters
)
}
): Param = this ?: throw UnsupportedConfigurationException(
configurationName = configurationName,
supportedRange = supportedParameters
)
@@ -10,14 +10,12 @@ import android.hardware.Camera
* Empty list of values will be returned
*/
internal fun Camera.Parameters.extractRawCameraValues(keys: List<String>): List<String> {
keys.forEach {
getValuesForKey(key = it)
?.let { return it }
keys.forEach { key ->
getValuesForKey(key)?.let { params -> return params }
}
return emptyList()
}
private fun Camera.Parameters.getValuesForKey(key: String): List<String>? {
return get(key)?.split(regex = ",".toRegex())
}
private fun Camera.Parameters.getValuesForKey(key: String): List<String>? =
get(key)?.split(regex = ",".toRegex())
@@ -2,9 +2,9 @@ package io.fotoapparat.preview
/**
* Performs processing on preview frames.
* <p>
*
* Frame processors are called from worker thread (aka non-UI thread). After
* {@link #processFrame(Frame)} completes the frame is returned back to the pool where it is reused
* [.processFrame] completes the frame is returned back to the pool where it is reused
* afterwards. This means that implementations should take special care to not do any operations on
* frame after method completes.
*/
@@ -14,7 +14,7 @@ interface FrameProcessor {
* Performs processing on preview frames. Read class description for more details.
*
* @param frame frame of the preview. Do not cache it as it will eventually be reused by the
* camera.
* camera.
*/
fun process(frame: Frame)
}
@@ -5,7 +5,9 @@ package io.fotoapparat.preview
import android.graphics.ImageFormat
import android.hardware.Camera
import io.fotoapparat.hardware.frameProcessingExecutor
import io.fotoapparat.hardware.orientation.Orientation
import io.fotoapparat.parameter.Resolution
import io.fotoapparat.util.FrameProcessor
import java.util.*
/**
@@ -13,19 +15,19 @@ import java.util.*
*/
internal class PreviewStream(private val camera: Camera) {
private val frameProcessors = LinkedHashSet<(Frame) -> Unit>()
private val frameProcessors = LinkedHashSet<FrameProcessor>()
private var previewResolution: Resolution? = null
/**
* CW rotation of frames in degrees.
* CW orientation.
*/
var frameOrientation = 0
var frameOrientation: Orientation = Orientation.Vertical.Portrait
/**
* Clears all processors.
*/
fun clearProcessors() {
private fun clearProcessors() {
synchronized(frameProcessors) {
frameProcessors.clear()
}
@@ -34,7 +36,7 @@ internal class PreviewStream(private val camera: Camera) {
/**
* Registers new processor. If processor was already added before, does nothing.
*/
fun addProcessor(processor: (Frame) -> Unit) {
private fun addProcessor(processor: FrameProcessor) {
synchronized(frameProcessors) {
frameProcessors.add(processor)
}
@@ -43,7 +45,7 @@ internal class PreviewStream(private val camera: Camera) {
/**
* Starts preview stream. After preview is started frame processors will start receiving frames.
*/
fun start() {
private fun start() {
camera.addFrameToBuffer()
camera.setPreviewCallbackWithBuffer { data, _ -> dispatchFrameOnBackgroundThread(data) }
@@ -52,10 +54,23 @@ internal class PreviewStream(private val camera: Camera) {
/**
* Stops preview stream.
*/
fun stop() {
private fun stop() {
camera.setPreviewCallbackWithBuffer(null)
}
/**
* Updates the frame processor safely.
*/
fun updateProcessorSafely(frameProcessor: FrameProcessor?) {
clearProcessors()
if (frameProcessor == null) {
stop()
} else {
addProcessor(frameProcessor)
start()
}
}
private fun Camera.addFrameToBuffer() {
addCallbackBuffer(parameters.allocateBuffer())
}
@@ -85,19 +100,19 @@ internal class PreviewStream(private val camera: Camera) {
val frame = Frame(
size = previewResolution,
image = image,
rotation = frameOrientation
rotation = frameOrientation.degrees
)
frameProcessors.forEach {
it(frame)
it.invoke(frame)
}
returnFrameToBuffer(frame)
}
private fun ensurePreviewSizeAvailable(): Resolution {
return previewResolution ?: throw IllegalStateException("previewSize is null. Frame was not added?")
}
private fun ensurePreviewSizeAvailable(): Resolution =
previewResolution
?: throw IllegalStateException("previewSize is null. Frame was not added?")
private fun returnFrameToBuffer(frame: Frame) {
camera.addCallbackBuffer(
@@ -107,9 +122,8 @@ internal class PreviewStream(private val camera: Camera) {
}
private fun Camera.Size.bytesPerFrame(): Int {
return width * height * ImageFormat.getBitsPerPixel(ImageFormat.NV21) / 8
}
private fun Camera.Size.bytesPerFrame(): Int =
width * height * ImageFormat.getBitsPerPixel(ImageFormat.NV21) / 8
private fun Camera.Parameters.ensureNv21Format() {
if (previewFormat != ImageFormat.NV21) {
@@ -8,11 +8,15 @@ sealed class FocusResult {
/**
* Camera is unable to focus for some reason.
*/
object UnableToFocus : FocusResult()
object UnableToFocus : FocusResult() {
override fun toString(): String = "FocusResult.UnableToFocus"
}
/**
* Camera is focused successfully.
*/
object Focused : FocusResult()
object Focused : FocusResult() {
override fun toString(): String = "FocusResult.Focused"
}
}
@@ -1,7 +1,7 @@
package io.fotoapparat.result
import io.fotoapparat.capability.Capabilities
import io.fotoapparat.exception.RecoverableRuntimeException
import io.fotoapparat.concurrent.ensureBackgroundThread
import io.fotoapparat.exception.UnableToDecodeBitmapException
import io.fotoapparat.hardware.executeMainThread
import io.fotoapparat.hardware.pendingResultExecutor
@@ -9,7 +9,6 @@ import io.fotoapparat.log.Logger
import io.fotoapparat.parameter.camera.CameraParameters
import java.util.concurrent.*
/**
* Result which might not be readily available at the given moment but will be available in the
* future.
@@ -22,13 +21,8 @@ internal constructor(
) {
private val resultUnsafe: T
get() {
return try {
future.get()
} catch (e: InterruptedException) {
throw RecoverableRuntimeException(e)
} catch (e: ExecutionException) {
throw RecoverableRuntimeException(e)
}
ensureBackgroundThread()
return future.get()
}
/**
@@ -39,9 +33,7 @@ internal constructor(
* @return [PendingResult] of another type.
*/
fun <R> transform(transformer: (T) -> R): PendingResult<R> {
val transformTask = FutureTask(Callable<R> {
transformer(future.get())
})
val transformTask = FutureTask { transformer(future.get()) }
executor.execute(transformTask)
@@ -60,9 +52,7 @@ internal constructor(
* @throws InterruptedException if the thread has been interrupted.
*/
@Throws(ExecutionException::class, InterruptedException::class)
fun await(): T {
return future.get()
}
fun await(): T = future.get()
/**
* Adapts the resulting object to a different type.
@@ -71,9 +61,7 @@ internal constructor(
* type.
* @return result adapted to a new type.
*/
fun <R> adapt(adapter: (Future<T>) -> R): R {
return adapter(future)
}
fun <R> adapt(adapter: (Future<T>) -> R): R = adapter(future)
/**
* Notifies given callback as soon as result is available. Callback will always be notified on
@@ -84,12 +72,24 @@ internal constructor(
fun whenAvailable(callback: (T?) -> Unit) {
executor.execute {
try {
resultUnsafe.notifyCallbackOnMainThread(callback)
val result = resultUnsafe
notifyOnMainThread {
callback(result)
}
} catch (e: UnableToDecodeBitmapException) {
logger.log("Couldn't decode bitmap from byte array")
} catch (e: RecoverableRuntimeException) {
logger.log("Couldn't deliver pending result.")
callback(null)
notifyOnMainThread {
callback(null)
}
} catch (e: InterruptedException) {
logger.log("Couldn't deliver pending result: Camera stopped before delivering result.")
} catch (e: CancellationException) {
logger.log("Couldn't deliver pending result: Camera operation was cancelled.")
} catch (e: ExecutionException) {
logger.log("Couldn't deliver pending result: Operation failed internally.")
notifyOnMainThread {
callback(null)
}
}
}
}
@@ -98,7 +98,7 @@ internal constructor(
* Alias for [PendingResult.whenAvailable] for java.
*/
fun whenDone(callback: WhenDoneListener<T>) {
whenAvailable { callback.whenDone(it) }
whenAvailable(callback::whenDone)
}
companion object {
@@ -117,9 +117,9 @@ internal constructor(
}
private fun <T> T.notifyCallbackOnMainThread(callback: (T) -> Unit) {
private fun notifyOnMainThread(function: () -> Unit) {
executeMainThread {
callback(this)
function()
}
}
@@ -1,7 +1,7 @@
package io.fotoapparat.result
import android.graphics.BitmapFactory
import java.util.*
import java.util.Arrays
/**
* Taken photo.
@@ -24,29 +24,23 @@ data class Photo(
val rotationDegrees: Int
) {
private val decodedBounds by lazy {
BitmapFactory.decodeByteArray(
encodedImage,
0,
encodedImage.size,
BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
)
}
/**
* The height of the photo.
*/
val height by lazy {
decodedBounds.height
}
val height: Int
get() = decodedBounds.outHeight
/**
* The width of the photo.
*/
val width by lazy {
decodedBounds.width
val width: Int
get() = decodedBounds.outWidth
private val decodedBounds by lazy {
BitmapFactory.Options().apply {
inJustDecodeBounds = true
BitmapFactory.decodeByteArray(encodedImage, 0, encodedImage.size, this)
}
}
override fun equals(other: Any?): Boolean {
@@ -67,17 +61,14 @@ data class Photo(
return result
}
companion object {
override fun toString(): String =
"Photo(encodedImage=ByteArray(${encodedImage.size}) rotationDegrees=$rotationDegrees)"
companion object {
/**
* @return empty [Photo].
*/
internal fun empty(): Photo {
return Photo(
encodedImage = ByteArray(0),
rotationDegrees = 0
)
}
internal fun empty(): Photo = Photo(encodedImage = ByteArray(0), rotationDegrees = 0)
}
}
}
@@ -4,8 +4,8 @@ import android.graphics.Bitmap
import io.fotoapparat.exception.FileSaveException
import io.fotoapparat.exif.ExifWriter
import io.fotoapparat.log.Logger
import io.fotoapparat.parameter.Resolution
import io.fotoapparat.result.transformer.BitmapPhotoTransformer
import io.fotoapparat.result.transformer.ResolutionTransformer
import io.fotoapparat.result.transformer.SaveToFileTransformer
import io.fotoapparat.result.transformer.originalResolution
import java.io.File
@@ -25,9 +25,10 @@ class PhotoResult internal constructor(private val pendingResult: PendingResult<
* future.
*/
@JvmOverloads
fun toBitmap(sizeTransformer: (Resolution) -> Resolution = originalResolution()): PendingResult<BitmapPhoto> {
return pendingResult.transform(BitmapPhotoTransformer(sizeTransformer))
}
fun toBitmap(
sizeTransformer: ResolutionTransformer = originalResolution()
): PendingResult<BitmapPhoto> =
pendingResult.transform(BitmapPhotoTransformer(sizeTransformer))
/**
* Saves result to file.
@@ -35,19 +36,16 @@ class PhotoResult internal constructor(private val pendingResult: PendingResult<
* @return pending operation which completes when photo is saved to file.
* @throws FileSaveException If the file cannot be saved.
*/
fun saveToFile(file: File): PendingResult<Unit> {
return pendingResult.transform(SaveToFileTransformer(
file = file,
exifOrientationWriter = ExifWriter
))
}
fun saveToFile(file: File): PendingResult<Unit> =
pendingResult.transform(SaveToFileTransformer(
file = file,
exifOrientationWriter = ExifWriter
))
/**
* @return result as [PendingResult].
*/
fun toPendingResult(): PendingResult<Photo> {
return pendingResult
}
fun toPendingResult(): PendingResult<Photo> = pendingResult
companion object {
@@ -11,7 +11,7 @@ import io.fotoapparat.result.Photo
* Creates [BitmapPhoto] out of [Photo].
*/
internal class BitmapPhotoTransformer(
private val sizeTransformer: (Resolution) -> Resolution
private val sizeTransformer: ResolutionTransformer
) : (Photo) -> BitmapPhoto {
override fun invoke(input: Photo): BitmapPhoto {
@@ -23,9 +23,8 @@ internal class BitmapPhotoTransformer(
desiredResolution = desiredResolution
)
val decodedBitmap = input.decodeBitmap(scaleFactor)
decodedBitmap ?: throw UnableToDecodeBitmapException()
val decodedBitmap = input.decodeBitmap(scaleFactor, originalResolution, desiredResolution)
?: throw UnableToDecodeBitmapException()
val bitmap = if (decodedBitmap.width == desiredResolution.width && decodedBitmap.height == desiredResolution.height) {
decodedBitmap
@@ -46,14 +45,28 @@ internal class BitmapPhotoTransformer(
}
private fun Photo.decodeBitmap(scaleFactor: Float): Bitmap? {
private fun Photo.decodeBitmap(
scaleFactor: Float,
originalResolution: Resolution,
desiredResolution: Resolution
): Bitmap? {
val options = BitmapFactory.Options()
options.inSampleSize = scaleFactor.toInt()
options.inScaled = true
if (desiredResolution.width > desiredResolution.height) {
options.inDensity = originalResolution.width
options.inTargetDensity = desiredResolution.width * options.inSampleSize
} else {
options.inDensity = originalResolution.height
options.inTargetDensity = desiredResolution.height * options.inSampleSize
}
return BitmapFactory.decodeByteArray(
encodedImage,
0,
encodedImage.size
encodedImage.size,
options
)
}
@@ -77,9 +90,7 @@ private fun Photo.readResolution(): Resolution {
private fun computeScaleFactor(
originalResolution: Resolution,
desiredResolution: Resolution
): Float {
return Math.min(
originalResolution.width / desiredResolution.width.toFloat(),
originalResolution.height / desiredResolution.height.toFloat()
)
}
): Float = Math.min(
originalResolution.width / desiredResolution.width.toFloat(),
originalResolution.height / desiredResolution.height.toFloat()
)
@@ -2,21 +2,20 @@ package io.fotoapparat.result.transformer
import io.fotoapparat.parameter.Resolution
typealias ResolutionTransformer = (Resolution) -> Resolution
/**
* @return Transforming function which always returns the same size as it receives.
*/
fun originalResolution(): (Resolution) -> Resolution = {
it
}
fun originalResolution(): ResolutionTransformer = { it }
/**
* @param scaleFactor scale factor which would be applied to image.
* @return Transforming function which returns size scaled by given factor.
*/
fun scaled(scaleFactor: Float): (Resolution) -> Resolution = { input ->
fun scaled(scaleFactor: Float): ResolutionTransformer = { input ->
Resolution(
width = (input.width * scaleFactor).toInt(),
height = (input.height * scaleFactor).toInt()
)
}
}
@@ -23,7 +23,7 @@ internal class SaveToFileTransformer(
try {
saveImage(input, outputStream)
exifOrientationWriter.writeExifOrientation(file, input)
exifOrientationWriter.writeExifOrientation(file, input.rotationDegrees)
} catch (e: IOException) {
throw FileSaveException(e)
}
@@ -1,25 +1,29 @@
package io.fotoapparat.routine.camera
import io.fotoapparat.concurrent.CameraExecutor.Operation
import io.fotoapparat.error.CameraErrorCallback
import io.fotoapparat.exception.camera.CameraException
import io.fotoapparat.hardware.Device
import io.fotoapparat.hardware.executeTask
import io.fotoapparat.hardware.orientation.OrientationSensor
import io.fotoapparat.routine.focus.focusOnPoint
import io.fotoapparat.routine.orientation.startOrientationMonitoring
import java.io.IOException
/**
* Starts the camera from idle.
*/
internal fun Device.bootStart(
orientationSensor: OrientationSensor,
mainThreadErrorCallback: (CameraException) -> Unit
mainThreadErrorCallback: CameraErrorCallback
) {
if (hasSelectedCamera()) {
throw IllegalStateException("Camera has already started!")
}
try {
start()
start(
orientationSensor = orientationSensor
)
startOrientationMonitoring(
orientationSensor = orientationSensor
)
@@ -31,7 +35,7 @@ internal fun Device.bootStart(
/**
* Starts the camera.
*/
internal fun Device.start() {
internal fun Device.start(orientationSensor: OrientationSensor) {
selectCamera()
val cameraDevice = getSelectedCamera().apply {
@@ -40,9 +44,7 @@ internal fun Device.start() {
updateCameraConfiguration(
cameraDevice = this
)
setDisplayOrientation(
degrees = getScreenRotation()
)
setDisplayOrientation(orientationSensor.lastKnownOrientationState)
}
val previewResolution = cameraDevice.getPreviewResolution()
@@ -57,17 +59,21 @@ internal fun Device.start() {
)
}
focusPointSelector?.setFocalPointListener {
executeTask(Runnable {
focusOnPoint(it)
focusPointSelector?.setFocalPointListener { focalRequest ->
executor.execute(Operation(cancellable = true) {
focusOnPoint(focalRequest)
})
}
cameraDevice.run {
setDisplaySurface(
preview = cameraRenderer.getPreview()
)
with(cameraDevice) {
try {
setDisplaySurface(
preview = cameraRenderer.getPreview()
)
startPreview()
startPreview()
} catch (e: IOException) {
logger.log("Can't start preview because of the exception: $e")
}
}
}
@@ -3,19 +3,15 @@ package io.fotoapparat.routine.camera
import io.fotoapparat.hardware.CameraDevice
import io.fotoapparat.hardware.Device
import io.fotoapparat.hardware.orientation.OrientationSensor
import io.fotoapparat.hardware.shutdownPendingTasks
import io.fotoapparat.routine.orientation.stopMonitoring
/**
* Stops the camera completely.
*/
internal fun Device.shutDown(
orientationSensor: OrientationSensor
) {
shutdownPendingTasks()
focusPointSelector?.setFocalPointListener { }
orientationSensor.stopMonitoring()
val cameraDevice = getSelectedCamera()
@@ -32,4 +28,4 @@ internal fun Device.stop(cameraDevice: CameraDevice) {
cameraDevice.close()
clearSelectedCamera()
}
}
@@ -2,9 +2,12 @@ package io.fotoapparat.routine.camera
import io.fotoapparat.characteristic.LensPosition
import io.fotoapparat.configuration.CameraConfiguration
import io.fotoapparat.error.CameraErrorCallback
import io.fotoapparat.exception.camera.CameraException
import io.fotoapparat.hardware.CameraDevice
import io.fotoapparat.hardware.Device
import io.fotoapparat.hardware.orientation.OrientationSensor
import io.fotoapparat.selector.LensPositionSelector
/**
* Switches to a new [LensPosition] camera. Will do nothing if [LensPosition] is same.
@@ -12,9 +15,10 @@ import io.fotoapparat.hardware.Device
* Will restart preview automatically if existing camera has started its preview.
*/
internal fun Device.switchCamera(
newLensPositionSelector: Collection<LensPosition>.() -> LensPosition?,
newLensPositionSelector: LensPositionSelector,
newConfiguration: CameraConfiguration,
mainThreadErrorCallback: (CameraException) -> Unit
mainThreadErrorCallback: CameraErrorCallback,
orientationSensor: OrientationSensor
) {
val oldCameraDevice = try {
getSelectedCamera()
@@ -31,6 +35,7 @@ internal fun Device.switchCamera(
restartPreview(
oldCameraDevice,
orientationSensor,
mainThreadErrorCallback
)
}
@@ -41,12 +46,13 @@ internal fun Device.switchCamera(
*/
internal fun Device.restartPreview(
oldCameraDevice: CameraDevice,
mainThreadErrorCallback: (CameraException) -> Unit
orientationSensor: OrientationSensor,
mainThreadErrorCallback: CameraErrorCallback
) {
stop(oldCameraDevice)
try {
start()
start(orientationSensor)
} catch (e: CameraException) {
mainThreadErrorCallback(e)
}
@@ -3,8 +3,7 @@ package io.fotoapparat.routine.camera
import io.fotoapparat.configuration.Configuration
import io.fotoapparat.hardware.CameraDevice
import io.fotoapparat.hardware.Device
import kotlinx.coroutines.experimental.runBlocking
import kotlinx.coroutines.runBlocking
/**
* Updates [Device] configuration.
@@ -33,4 +32,4 @@ internal fun Device.updateCameraConfiguration(
cameraDevice.updateFrameProcessor(
frameProcessor = frameProcessor
)
}
}
@@ -2,7 +2,7 @@ package io.fotoapparat.routine.capability
import io.fotoapparat.capability.Capabilities
import io.fotoapparat.hardware.Device
import kotlinx.coroutines.experimental.runBlocking
import kotlinx.coroutines.runBlocking
/**
* Returns the camera [Capabilities].
@@ -3,7 +3,7 @@ package io.fotoapparat.routine.focus
import io.fotoapparat.hardware.Device
import io.fotoapparat.hardware.metering.FocalRequest
import io.fotoapparat.result.FocusResult
import kotlinx.coroutines.experimental.runBlocking
import kotlinx.coroutines.runBlocking
/**
* Focuses the camera on a particular point.
@@ -14,4 +14,4 @@ internal fun Device.focusOnPoint(focalRequest: FocalRequest): FocusResult = runB
setFocalPoint(focalRequest)
autoFocus()
}
}
}
@@ -2,7 +2,7 @@ package io.fotoapparat.routine.focus
import io.fotoapparat.hardware.Device
import io.fotoapparat.result.FocusResult
import kotlinx.coroutines.experimental.runBlocking
import kotlinx.coroutines.runBlocking
/**
* Focuses the camera.
@@ -11,4 +11,4 @@ internal fun Device.focus(): FocusResult = runBlocking {
val cameraDevice = awaitSelectedCamera()
cameraDevice.autoFocus()
}
}
@@ -1,8 +1,9 @@
package io.fotoapparat.routine.orientation
import io.fotoapparat.concurrent.CameraExecutor.Operation
import io.fotoapparat.hardware.Device
import io.fotoapparat.hardware.execute
import io.fotoapparat.hardware.orientation.OrientationSensor
import io.fotoapparat.hardware.orientation.OrientationState
/**
* Starts orientation monitoring routine.
@@ -10,10 +11,10 @@ import io.fotoapparat.hardware.orientation.OrientationSensor
internal fun Device.startOrientationMonitoring(
orientationSensor: OrientationSensor
) {
orientationSensor.start { degrees ->
execute {
orientationSensor.start { orientationState: OrientationState ->
executor.execute(Operation(cancellable = true) {
val cameraDevice = getSelectedCamera()
cameraDevice.setDisplayOrientation(degrees)
}
cameraDevice.setDisplayOrientation(orientationState)
})
}
}
@@ -2,7 +2,7 @@ package io.fotoapparat.routine.parameter
import io.fotoapparat.hardware.Device
import io.fotoapparat.parameter.camera.CameraParameters
import kotlinx.coroutines.experimental.runBlocking
import kotlinx.coroutines.runBlocking
/**
* Returns the current [CameraParameters].
@@ -11,4 +11,4 @@ internal fun Device.getCurrentParameters(): CameraParameters = runBlocking {
val cameraDevice = awaitSelectedCamera()
cameraDevice.getParameters()
}
}
@@ -4,7 +4,7 @@ import io.fotoapparat.exception.camera.CameraException
import io.fotoapparat.hardware.CameraDevice
import io.fotoapparat.hardware.Device
import io.fotoapparat.result.Photo
import kotlinx.coroutines.experimental.runBlocking
import kotlinx.coroutines.runBlocking
/**
* Takes a photo.
@@ -20,7 +20,6 @@ internal fun Device.takePhoto(): Photo = runBlocking {
private fun CameraDevice.startPreviewSafely() {
try {
startPreview()
} catch (e: CameraException) {
//Nothing
} catch (ignore: CameraException) {
}
}
@@ -1,10 +1,10 @@
package io.fotoapparat.routine.zoom
import android.support.annotation.FloatRange
import androidx.annotation.FloatRange
import io.fotoapparat.exception.LevelOutOfRangeException
import io.fotoapparat.hardware.Device
import kotlinx.coroutines.experimental.runBlocking
import io.fotoapparat.parameter.Zoom
import kotlinx.coroutines.runBlocking
/**
* Updates zoom level of the camera. If zoom is not supported - does nothing.
@@ -17,13 +17,13 @@ internal fun Device.updateZoomLevel(
zoomLevel.ensureInBounds()
val cameraDevice = awaitSelectedCamera()
if (cameraDevice.getCapabilities().canZoom) {
if (cameraDevice.getCapabilities().zoom is Zoom.VariableZoom) {
cameraDevice.setZoom(zoomLevel)
}
}
private fun Float.ensureInBounds() {
if (this < 0f || this > 1f) {
if (this !in 0f..1f) {
throw LevelOutOfRangeException(this)
}
}
}
@@ -2,36 +2,30 @@ package io.fotoapparat.selector
import io.fotoapparat.parameter.AntiBandingMode
typealias AntiBandingModeSelector = Iterable<AntiBandingMode>.() -> AntiBandingMode?
/**
* @return Selector function which provides an auto anti banding mode if available.
* Otherwise provides `null`.
*/
fun auto(): Iterable<AntiBandingMode>.() -> AntiBandingMode? {
return single(AntiBandingMode.Auto)
}
fun auto(): AntiBandingModeSelector = single(AntiBandingMode.Auto)
/**
* @return Selector function which provides a 50hz banding mode if available.
* Otherwise provides `null`.
*/
fun hz50(): Iterable<AntiBandingMode>.() -> AntiBandingMode? {
return single(AntiBandingMode.HZ50)
}
fun hz50(): AntiBandingModeSelector = single(AntiBandingMode.HZ50)
/**
* @return Selector function which provides a 60hz anti banding mode if available.
* Otherwise provides `null`.
*/
fun hz60(): Iterable<AntiBandingMode>.() -> AntiBandingMode? {
return single(AntiBandingMode.HZ60)
}
fun hz60(): AntiBandingModeSelector = single(AntiBandingMode.HZ60)
/**
* @return Selector function which provides a disabled anti banding mode if available.
* Otherwise provides `null`.
*/
fun none(): Iterable<AntiBandingMode>.() -> AntiBandingMode? {
return single(AntiBandingMode.None)
}
fun none(): AntiBandingModeSelector = single(AntiBandingMode.None)
@@ -1,7 +1,6 @@
package io.fotoapparat.selector
import android.support.annotation.FloatRange
import io.fotoapparat.parameter.Resolution
import androidx.annotation.FloatRange
/**
* @param selector Input selector
@@ -11,15 +10,13 @@ import io.fotoapparat.parameter.Resolution
*/
@JvmOverloads
fun standardRatio(
selector: Iterable<Resolution>.() -> Resolution?,
selector: ResolutionSelector,
@FloatRange(from = 0.0, to = 1.0) tolerance: Double = 0.0
): Iterable<Resolution>.() -> Resolution? {
return aspectRatio(
aspectRatio = 4f / 3f,
selector = selector,
tolerance = tolerance
)
}
): ResolutionSelector = aspectRatio(
aspectRatio = 4f / 3f,
selector = selector,
tolerance = tolerance
)
/**
* @param selector Input sizes, selected by provided selector function
@@ -29,15 +26,13 @@ fun standardRatio(
*/
@JvmOverloads
fun wideRatio(
selector: Iterable<Resolution>.() -> Resolution?,
selector: ResolutionSelector,
@FloatRange(from = 0.0, to = 1.0) tolerance: Double = 0.0
): Iterable<Resolution>.() -> Resolution? {
return aspectRatio(
aspectRatio = 16f / 9f,
selector = selector,
tolerance = tolerance
)
}
): ResolutionSelector = aspectRatio(
aspectRatio = 16f / 9f,
selector = selector,
tolerance = tolerance
)
/**
* Select sizes with desired aspect ratio. This selector can
@@ -51,9 +46,9 @@ fun wideRatio(
@JvmOverloads
fun aspectRatio(
aspectRatio: Float,
selector: Iterable<Resolution>.() -> Resolution?,
selector: ResolutionSelector,
@FloatRange(from = 0.0, to = 1.0) tolerance: Double = 0.0
): Iterable<Resolution>.() -> Resolution? {
): ResolutionSelector {
if (tolerance !in 0.0..1.0) {
throw IllegalArgumentException("Tolerance must be between 0.0 and 1.0.")
}
@@ -0,0 +1,24 @@
package io.fotoapparat.selector
typealias ExposureSelector = IntRange.() -> Int?
/**
* @param exposure The specified exposure compensation value
* @return Selector function which selects the specified exposure compensation value.
*/
fun manualExposure(exposure: Int): ExposureSelector = single(exposure)
/**
* @return Selector function which always provides the highest exposure.
*/
fun highestExposure(): ExposureSelector = highest()
/**
* @return Selector function which always provides the lowest exposure.
*/
fun lowestExposure(): ExposureSelector = lowest()
/**
* @return Selector function which always provides the default exposure.
*/
fun defaultExposure(): ExposureSelector = single(0)
@@ -2,45 +2,36 @@ package io.fotoapparat.selector
import io.fotoapparat.parameter.Flash
typealias FlashSelector = Iterable<Flash>.() -> Flash?
/**
* @return Selector function which provides a disabled flash firing mode if available.
* Otherwise provides `null`.
*/
fun off(): Iterable<Flash>.() -> Flash? {
return single(Flash.Off)
}
fun off(): FlashSelector = single(Flash.Off)
/**
* @return Selector function which provides a forced on flash firing mode if available.
* Otherwise provides `null`.
*/
fun on(): Iterable<Flash>.() -> Flash? {
return single(Flash.On)
}
fun on(): FlashSelector = single(Flash.On)
/**
* @return Selector function which provides an auto flash firing mode if available.
* Otherwise provides `null`.
*/
fun autoFlash(): Iterable<Flash>.() -> Flash? {
return single(Flash.Auto)
}
fun autoFlash(): FlashSelector = single(Flash.Auto)
/**
* @return Selector function which provides an auto flash firing mode with red eye
* reduction if available.
* Otherwise provides `null`.
*/
fun autoRedEye(): Iterable<Flash>.() -> Flash? {
return single(Flash.AutoRedEye)
}
fun autoRedEye(): FlashSelector = single(Flash.AutoRedEye)
/**
* @return Selector function which provides a torch (continuous on) flash firing mode if
* available.
* Otherwise provides `null`.
*/
fun torch(): Iterable<Flash>.() -> Flash? {
return single(Flash.Torch)
}
fun torch(): FlashSelector = single(Flash.Torch)
@@ -2,63 +2,50 @@ package io.fotoapparat.selector
import io.fotoapparat.parameter.FocusMode
typealias FocusModeSelector = Iterable<FocusMode>.() -> FocusMode?
/**
* @return Selector function which provides a non-adjustable focus mode if available.
* Otherwise provides `null`.
*/
fun fixed(): Iterable<FocusMode>.() -> FocusMode? {
return single(FocusMode.Fixed)
}
fun fixed(): FocusModeSelector = single(FocusMode.Fixed)
/**
* @return Selector function which provides a focus mode targeting infinity if available.
* Otherwise provides `null`.
*/
fun infinity(): Iterable<FocusMode>.() -> FocusMode? {
return single(FocusMode.Infinity)
}
fun infinity(): FocusModeSelector = single(FocusMode.Infinity)
/**
* @return Selector function which provides a macro focus mode if available.
* Otherwise provides `null`.
*/
fun macro(): Iterable<FocusMode>.() -> FocusMode? {
return single(FocusMode.Macro)
}
fun macro(): FocusModeSelector = single(FocusMode.Macro)
/**
* @return Selector function which provides an auto focus mode if available.
* Otherwise provides `null`.
*/
fun autoFocus(): Iterable<FocusMode>.() -> FocusMode? {
return single(FocusMode.Auto)
}
fun autoFocus(): FocusModeSelector = single(FocusMode.Auto)
/**
* @return Selector function which provides a focus mode which constantly tries to stay
* in focus if available. The speed of focus change is aggressive.
* Otherwise provides `null`.
*/
fun continuousFocusPicture(): Iterable<FocusMode>.() -> FocusMode? {
return single(FocusMode.ContinuousFocusPicture)
}
fun continuousFocusPicture(): FocusModeSelector = single(FocusMode.ContinuousFocusPicture)
/**
* @return Selector function which provides a focus mode which constantly tries to stay
* in focus if available. The speed of focus change is smooth.
* Otherwise provides `null`.
*/
fun continuousFocusVideo(): Iterable<FocusMode>.() -> FocusMode? {
return single(FocusMode.ContinuousFocusVideo)
}
fun continuousFocusVideo(): FocusModeSelector = single(FocusMode.ContinuousFocusVideo)
/**
* @return Selector function which provides a focus mode which will produce images with
* an extended depth of field if available.
* Otherwise provides `null`.
*/
fun edof(): Iterable<FocusMode>.() -> FocusMode? {
return single(FocusMode.Edof)
}
fun edof(): FocusModeSelector = single(FocusMode.Edof)
@@ -1,17 +1,19 @@
package io.fotoapparat.selector
typealias QualitySelector = IntRange.() -> Int?
/**
* @param quality The specified Jpeq quality value
* @return Selector function which selects the specified Jpeq quality value.
*/
fun manualJpegQuality(quality: Int): IntRange.() -> Int? = single(quality)
fun manualJpegQuality(quality: Int): QualitySelector = single(quality)
/**
* @return Selector function which always provides the highest quality.
*/
fun highestQuality(): IntRange.() -> Int? = highest()
fun highestQuality(): QualitySelector = highest()
/**
* @return Selector function which always provides the lowest quality.
*/
fun lowestQuality(): IntRange.() -> Int? = lowest()
fun lowestQuality(): QualitySelector = lowest()
@@ -2,27 +2,22 @@ package io.fotoapparat.selector
import io.fotoapparat.characteristic.LensPosition
typealias LensPositionSelector = Iterable<LensPosition>.() -> LensPosition?
/**
* @return Selector function which provides the front camera if it is available.
* Otherwise provides `null`.
*/
fun front(): Iterable<LensPosition>.() -> LensPosition? {
return single(LensPosition.Front)
}
fun front(): LensPositionSelector = single(LensPosition.Front)
/**
* @return Selector function which provides the back camera if it is available.
* Otherwise provides `null`.
*/
fun back(): Iterable<LensPosition>.() -> LensPosition? {
return single(LensPosition.Back)
}
fun back(): LensPositionSelector = single(LensPosition.Back)
/**
* @return Selector function which provides the external camera if it is available.
* Otherwise provides `null`.
*/
fun external(): Iterable<LensPosition>.() -> LensPosition? {
return single(LensPosition.External)
}
fun external(): LensPositionSelector = single(LensPosition.External)
@@ -3,18 +3,18 @@ package io.fotoapparat.selector
import io.fotoapparat.parameter.FpsRange
import io.fotoapparat.util.CompareFpsRangeByBounds
typealias FpsRangeSelector = Iterable<FpsRange>.() -> FpsRange?
/**
* @param fps The specified FPS
* @return Selector function which selects FPS range that contains the specified FPS.
* Prefers fixed rates over non fixed ones.
*/
fun containsFps(fps: Float): Iterable<FpsRange>.() -> FpsRange? = firstAvailable(
fun containsFps(fps: Float): FpsRangeSelector = firstAvailable(
exactFixedFps(fps),
filtered(
selector = highestNonFixedFps(),
predicate = {
it.contains(fps.toFpsIntRepresentation())
}
predicate = { range -> fps.toFpsIntRepresentation() in range }
)
)
@@ -22,18 +22,16 @@ fun containsFps(fps: Float): Iterable<FpsRange>.() -> FpsRange? = firstAvailable
* @param fps The specified FPS
* @return Selector function which selects FPS range that contains only the specified FPS.
*/
fun exactFixedFps(fps: Float): Iterable<FpsRange>.() -> FpsRange? = filtered(
fun exactFixedFps(fps: Float): FpsRangeSelector = filtered(
selector = highestFixedFps(),
predicate = {
it.min == fps.toFpsIntRepresentation()
}
predicate = { it.min == fps.toFpsIntRepresentation() }
)
/**
* @return Selector function which selects FPS range with max FPS.
* Prefers non fixed rates over fixed ones.
*/
fun highestFps(): Iterable<FpsRange>.() -> FpsRange? = firstAvailable(
fun highestFps(): FpsRangeSelector = firstAvailable(
highestNonFixedFps(),
highestFixedFps()
)
@@ -41,7 +39,7 @@ fun highestFps(): Iterable<FpsRange>.() -> FpsRange? = firstAvailable(
/**
* @return Selector function which selects FPS range with max FPS and non fixed rate.
*/
fun highestNonFixedFps(): Iterable<FpsRange>.() -> FpsRange? = filtered(
fun highestNonFixedFps(): FpsRangeSelector = filtered(
selector = highestRangeFps(),
predicate = { !it.isFixed }
)
@@ -49,7 +47,7 @@ fun highestNonFixedFps(): Iterable<FpsRange>.() -> FpsRange? = filtered(
/**
* @return Selector function which selects FPS range with max FPS and fixed rate.
*/
fun highestFixedFps(): Iterable<FpsRange>.() -> FpsRange? = filtered(
fun highestFixedFps(): FpsRangeSelector = filtered(
selector = highestRangeFps(),
predicate = { it.isFixed }
)
@@ -58,7 +56,7 @@ fun highestFixedFps(): Iterable<FpsRange>.() -> FpsRange? = filtered(
* @return Selector function which selects FPS range with min FPS.
* Prefers non fixed rates over fixed ones.
*/
fun lowestFps(): Iterable<FpsRange>.() -> FpsRange? = firstAvailable(
fun lowestFps(): FpsRangeSelector = firstAvailable(
lowestNonFixedFps(),
lowestFixedFps()
)
@@ -66,29 +64,21 @@ fun lowestFps(): Iterable<FpsRange>.() -> FpsRange? = firstAvailable(
/**
* @return Selector function which selects FPS range with min FPS and non fixed rate.
*/
fun lowestNonFixedFps(): Iterable<FpsRange>.() -> FpsRange? {
return filtered(
selector = lowestRangeFps(),
predicate = { !it.isFixed }
)
}
fun lowestNonFixedFps(): FpsRangeSelector = filtered(
selector = lowestRangeFps(),
predicate = { !it.isFixed }
)
/**
* @return Selector function which selects FPS range with min FPS and fixed rate.
*/
fun lowestFixedFps(): Iterable<FpsRange>.() -> FpsRange? {
return filtered(
selector = lowestRangeFps(),
predicate = { it.isFixed }
)
}
fun lowestFixedFps(): FpsRangeSelector = filtered(
selector = lowestRangeFps(),
predicate = { it.isFixed }
)
private fun highestRangeFps(): Iterable<FpsRange>.() -> FpsRange? = {
maxWith(CompareFpsRangeByBounds)
}
private fun highestRangeFps(): FpsRangeSelector = { maxWith(CompareFpsRangeByBounds) }
private fun lowestRangeFps(): Iterable<FpsRange>.() -> FpsRange? = {
minWith(CompareFpsRangeByBounds)
}
private fun lowestRangeFps(): FpsRangeSelector = { minWith(CompareFpsRangeByBounds) }
private fun Float.toFpsIntRepresentation() = (this * 1000).toInt()
@@ -2,16 +2,14 @@ package io.fotoapparat.selector
import io.fotoapparat.parameter.Resolution
typealias ResolutionSelector = Iterable<Resolution>.() -> Resolution?
/**
* @return Selector function which always provides the biggest resolution.
*/
fun highestResolution(): Iterable<Resolution>.() -> Resolution? = {
maxBy { it.area }
}
fun highestResolution(): ResolutionSelector = { maxBy(Resolution::area) }
/**
* @return Selector function which always provides the smallest resolution.
*/
fun lowestResolution(): Iterable<Resolution>.() -> Resolution? = {
minBy { it.area }
}
fun lowestResolution(): ResolutionSelector = { minBy(Resolution::area) }
@@ -1,6 +1,5 @@
package io.fotoapparat.selector
/**
* @return Selector function which always returns `null`.
*/
@@ -20,22 +19,19 @@ fun <T> single(preference: T): Iterable<T>.() -> T? = {
/**
* @return Selector function which selects highest [Comparable] value.
*/
fun <T : Comparable<T>> highest(): Iterable<T>.() -> T? = {
max()
}
fun <T : Comparable<T>> highest(): Iterable<T>.() -> T? = Iterable<T>::max
/**
* @return Selector function which selects lowest [Comparable] value.
*/
fun <T : Comparable<T>> lowest(): Iterable<T>.() -> T? = {
min()
}
fun <T : Comparable<T>> lowest(): Iterable<T>.() -> T? = Iterable<T>::min
/**
* @param functions functions in order of importance.
* @return Selector function which returns first non-null result from given selectors.
* If there are no non-null results, returns `null`.
*/
@SafeVarargs
fun <Input, Output> firstAvailable(
vararg functions: Input.() -> Output?
): Input.() -> Output? = {
@@ -1,19 +1,20 @@
package io.fotoapparat.selector
typealias SensorSensitivitySelector = Iterable<Int>.() -> Int?
/**
* @param iso The specified ISO value
* @return Selector function which selects the specified ISO value. If there
* is no specified value - selects default or previous ISO value.
*/
fun manualSensorSensitivity(iso: Int): Iterable<Int>.() -> Int? = single(iso)
fun manualSensorSensitivity(iso: Int): SensorSensitivitySelector = single(iso)
/**
* @return Selector function which selects highest ISO value.
*/
fun highestSensorSensitivity(): Iterable<Int>.() -> Int? = highest()
fun highestSensorSensitivity(): SensorSensitivitySelector = highest()
/**
* @return Selector function which selects lowest ISO value.
*/
fun lowestSensorSensitivity(): Iterable<Int>.() -> Int? = lowest()
fun lowestSensorSensitivity(): SensorSensitivitySelector = lowest()
@@ -1,6 +1,5 @@
package io.fotoapparat.util
/**
* System line separator.
*/
@@ -0,0 +1,16 @@
package io.fotoapparat.util
import io.fotoapparat.preview.Frame
/**
* Performs processing on preview frames.
*
* This function is called from worker thread (aka non-UI thread). After
* the function completes the frame is returned back to the pool where it is reused
* afterwards. Thus, implementations should take special care to not do any operations on
* frame after method completes.
*
* @param frame frame of the preview. Do not cache it as it will eventually be reused by the
* camera.
*/
typealias FrameProcessor = (frame: Frame) -> Unit
@@ -12,7 +12,6 @@ import io.fotoapparat.parameter.Resolution
import io.fotoapparat.parameter.ScaleType
import java.util.concurrent.CountDownLatch
/**
* Uses [android.view.TextureView] as an output for camera.
*/
@@ -83,6 +82,7 @@ private fun ViewGroup.layoutTextureView(
) = when (scaleType) {
ScaleType.CenterInside -> previewResolution?.centerInside(this)
ScaleType.CenterCrop -> previewResolution?.centerCrop(this)
ScaleType.TopCrop -> previewResolution?.topCrop(this)
else -> null
}
@@ -130,6 +130,28 @@ private fun Resolution.centerCrop(view: ViewGroup) {
view.layoutChildrenAt(rect)
}
private fun Resolution.topCrop(view: ViewGroup) {
val scale = Math.max(
view.measuredWidth / width.toFloat(),
view.measuredHeight / height.toFloat()
)
val width = (width * scale).toInt()
val height = (height * scale).toInt()
val extraX = Math.max(0, width - view.measuredWidth)
val rect = Rect(
-extraX / 2,
0,
width - extraX / 2,
height
)
view.layoutChildrenAt(rect)
}
private fun ViewGroup.layoutChildrenAt(rect: Rect) {
(0 until childCount).forEach {
getChildAt(it).layout(
@@ -3,15 +3,14 @@ package io.fotoapparat.view
import android.animation.AnimatorInflater.loadAnimator
import android.animation.AnimatorSet
import android.content.Context
import android.support.annotation.AnimatorRes
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.annotation.AnimatorRes
import io.fotoapparat.R
/**
* A circle which gives feedback to the user about a click.
*/
@@ -81,4 +80,4 @@ internal class FeedbackCircleView
private fun newAnimator(@AnimatorRes id: Int, target: View) = loadAnimator(context, id).apply {
setTarget(target)
}
}
}
@@ -3,7 +3,9 @@ package io.fotoapparat.view
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.widget.FrameLayout
import io.fotoapparat.hardware.metering.FocalRequest
import io.fotoapparat.hardware.metering.PointF
@@ -14,7 +16,7 @@ import io.fotoapparat.parameter.Resolution
*
* If the camera doesn't support focus metering on specific area this will only display a visual feedback.
*/
class FocusView
open class FocusView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@@ -24,6 +26,15 @@ class FocusView
private val visualFeedbackCircle = FeedbackCircleView(context, attrs, defStyleAttr)
private var focusMeteringListener: ((FocalRequest) -> Unit)? = null
var scaleListener: ((Float) -> Unit)? = null
var ptrListener: ((Int) -> Unit)? = null
private var mPtrCount: Int = 0
set(value) {
field = value
ptrListener?.invoke(value)
}
init {
clipToPadding = false
clipChildren = false
@@ -36,27 +47,50 @@ class FocusView
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
focusMeteringListener?.let {
it(FocalRequest(
point = PointF(
x = event.x,
y = event.y
),
previewResolution = Resolution(
width = width,
height = height
)
))
visualFeedbackCircle.showAt(
x = event.x - visualFeedbackCircle.width / 2,
y = event.y - visualFeedbackCircle.height / 2
)
performClick()
return true
}
tapDetector.onTouchEvent(event)
scaleDetector.onTouchEvent(event)
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_POINTER_DOWN -> mPtrCount++
MotionEvent.ACTION_POINTER_UP -> mPtrCount--
MotionEvent.ACTION_DOWN -> mPtrCount++
MotionEvent.ACTION_UP -> mPtrCount--
}
return super.onTouchEvent(event)
return true
}
private val gestureDetectorListener = object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
return focusMeteringListener
?.let {
it(FocalRequest(
point = PointF(
x = e.x,
y = e.y),
previewResolution = Resolution(
width = width,
height = height)))
visualFeedbackCircle.showAt(
x = e.x - visualFeedbackCircle.width / 2,
y = e.y - visualFeedbackCircle.height / 2)
performClick()
true
}
?: super.onSingleTapUp(e)
}
}
private val tapDetector = GestureDetector(context, gestureDetectorListener)
private val scaleGestureDetector = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
return scaleListener
?.let {
it(detector.scaleFactor)
true
}?: super.onScale(detector)
}
}
private val scaleDetector = ScaleGestureDetector(context, scaleGestureDetector)
}
@@ -1,3 +0,0 @@
<resources>
<string name="app_name">Fotoapparat</string>
</resources>
@@ -3,10 +3,12 @@ package io.fotoapparat
import android.content.Context
import android.view.WindowManager
import io.fotoapparat.characteristic.LensPosition
import io.fotoapparat.exception.camera.CameraException
import io.fotoapparat.error.CameraErrorCallback
import io.fotoapparat.log.Logger
import io.fotoapparat.parameter.*
import io.fotoapparat.preview.Frame
import io.fotoapparat.parameter.Resolution
import io.fotoapparat.parameter.ScaleType
import io.fotoapparat.selector.*
import io.fotoapparat.util.FrameProcessor
import io.fotoapparat.view.CameraRenderer
import junit.framework.Assert.*
import org.junit.Before
@@ -28,27 +30,27 @@ class FotoapparatBuilderTest {
@Mock
lateinit var photoResolutionSelector: Iterable<Resolution>.() -> Resolution
@Mock
lateinit var previewResolutionSelector: Iterable<Resolution>.() -> Resolution?
lateinit var previewResolutionSelector: ResolutionSelector
@Mock
lateinit var lensPositionSelector: Iterable<LensPosition>.() -> LensPosition
@Mock
lateinit var focusModeSelector: Iterable<FocusMode>.() -> FocusMode?
lateinit var focusModeSelector: FocusModeSelector
@Mock
lateinit var flashSelector: Iterable<Flash>.() -> Flash?
lateinit var flashSelector: FlashSelector
@Mock
lateinit var previewFpsRangeSelector: Iterable<FpsRange>.() -> FpsRange?
lateinit var previewFpsRangeSelector: FpsRangeSelector
@Mock
lateinit var sensorSensitivitySelector: Iterable<Int>.() -> Int?
lateinit var sensorSensitivitySelector: SensorSensitivitySelector
@Mock
lateinit var jpegQualitySelector: IntRange.() -> Int?
lateinit var jpegQualitySelector: QualitySelector
@Mock
lateinit var frameProcessor: (Frame) -> Unit
lateinit var frameProcessor: FrameProcessor
@Mock
lateinit var logger: Logger
@Mock
lateinit var cameraErrorCallback: (CameraException) -> Unit
lateinit var cameraErrorCallback: CameraErrorCallback
@Before
fun setUp() {
@@ -235,12 +237,12 @@ class FotoapparatBuilderTest {
}
@Test
fun `frameProcessor empty by default`() {
fun `frameProcessor null by default`() {
// When
val builder = builderWithMandatoryArguments()
// Then
assertNotNull(builder.configuration.frameProcessor)
assertNull(builder.configuration.frameProcessor)
}
@Test
@@ -0,0 +1,218 @@
package io.fotoapparat.hardware.orientation
import io.fotoapparat.hardware.orientation.Orientation.Horizontal.Landscape
import io.fotoapparat.hardware.orientation.Orientation.Horizontal.ReverseLandscape
import io.fotoapparat.hardware.orientation.Orientation.Vertical.Portrait
import io.fotoapparat.hardware.orientation.Orientation.Vertical.ReversePortrait
import org.junit.Test
import kotlin.test.assertEquals
class OrientationResolverTest {
@Test
fun `Portait & front camera at 270 degrees return 90 display orientation`() {
// Given
val screenOrientation = Portrait
val cameraOrientation = ReverseLandscape
// When
val result = computeDisplayOrientation(
screenOrientation,
cameraOrientation,
true
)
// Then
assertEquals(Landscape, result)
}
@Test
fun `Landscape & front camera at 270 degrees return 180 display orientation`() {
// Given
val screenOrientation = ReverseLandscape
val cameraOrientation = ReverseLandscape
// When
val result = computeDisplayOrientation(
screenOrientation,
cameraOrientation,
true
)
// Then
assertEquals(ReversePortrait, result)
}
@Test
fun `Portrait & back camera at 90 degrees return 90 display orientation`() {
// Given
val screenOrientation = Portrait
val cameraOrientation = Landscape
// When
val result = computeDisplayOrientation(
screenOrientation,
cameraOrientation,
false
)
// Then
assertEquals(Landscape, result)
}
@Test
fun `Landscape & back camera at 90 degrees return 180 display orientation`() {
// Given
val screenOrientation = ReverseLandscape
val cameraOrientation = Landscape
// When
val result = computeDisplayOrientation(
screenOrientation,
cameraOrientation,
false
)
// Then
assertEquals(ReversePortrait, result)
}
@Test
fun `Portrait & front camera at 270 degrees return 90 preview orientation`() {
// Given
val screenOrientation = Portrait
val cameraOrientation = ReverseLandscape
// When
val result = computePreviewOrientation(
screenOrientation,
cameraOrientation,
true
)
// Then
assertEquals(Landscape, result)
}
@Test
fun `Landscape & front camera at 270 degrees return 180 preview orientation`() {
// Given
val screenOrientation = ReverseLandscape
val cameraOrientation = ReverseLandscape
// When
val result = computePreviewOrientation(
screenOrientation,
cameraOrientation,
true
)
// Then
assertEquals(ReversePortrait, result)
}
@Test
fun `Portrait & back camera at 90 degrees return 270 preview orientation`() {
// Given
val screenOrientation = Portrait
val cameraOrientation = Landscape
// When
val result = computePreviewOrientation(
screenOrientation,
cameraOrientation,
false
)
// Then
assertEquals(ReverseLandscape, result)
}
@Test
fun `Landscape & back camera at 90 degrees return 180 preview orientation`() {
// Given
val screenOrientation = ReverseLandscape
val cameraOrientation = Landscape
// When
val result = computePreviewOrientation(
screenOrientation,
cameraOrientation,
false
)
// Then
assertEquals(ReversePortrait, result)
}
@Test
fun `Portrait & front camera at 270 degrees return 90 image orientation`() {
// Given
val screenOrientation = Portrait
val cameraOrientation = ReverseLandscape
// When
val result = computeImageOrientation(
screenOrientation,
cameraOrientation,
true
)
// Then
assertEquals(Landscape, result)
}
@Test
fun `Landscape & front camera at 270 degrees return 0 image orientation`() {
// Given
val screenOrientation = ReverseLandscape
val cameraOrientation = ReverseLandscape
// When
val result = computeImageOrientation(
screenOrientation,
cameraOrientation,
true
)
// Then
assertEquals(Portrait, result)
}
@Test
fun `Portrait & back camera at 90 degrees return 270 image orientation`() {
// Given
val screenOrientation = Portrait
val cameraOrientation = Landscape
// When
val result = computeImageOrientation(
screenOrientation,
cameraOrientation,
false
)
// Then
assertEquals(ReverseLandscape, result)
}
@Test
fun `Landscape & back camera at 90 degrees return 180 image orientation`() {
// Given
val screenOrientation = ReverseLandscape
val cameraOrientation = Landscape
// When
val result = computeImageOrientation(
screenOrientation,
cameraOrientation,
false
)
// Then
assertEquals(Portrait, result)
}
}
@@ -1,6 +1,9 @@
package io.fotoapparat.hardware.orientation
import io.fotoapparat.hardware.Device
import io.fotoapparat.hardware.orientation.Orientation.Horizontal.Landscape
import io.fotoapparat.hardware.orientation.Orientation.Vertical.Portrait
import io.fotoapparat.hardware.orientation.Orientation.Vertical.ReversePortrait
import io.fotoapparat.test.willReturn
import org.junit.Before
import org.junit.Test
@@ -8,7 +11,7 @@ import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnitRunner
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import kotlin.test.assertEquals
@RunWith(MockitoJUnitRunner::class)
@@ -23,6 +26,7 @@ internal class OrientationSensorTest {
@Before
fun setUp() {
device.getScreenOrientation() willReturn Portrait
testee = OrientationSensor(
rotationListener,
device
@@ -34,7 +38,7 @@ internal class OrientationSensorTest {
// Given
// When
testee.start({})
testee.start {}
testee.stop()
// Then
@@ -45,21 +49,24 @@ internal class OrientationSensorTest {
@Test
fun singleEvent() {
// Given
device.getScreenRotation() willReturn 90
device.getScreenOrientation() willReturn Landscape
val atomicInteger = AtomicInteger()
val atomicReference = AtomicReference<OrientationState>()
testee.start { degrees ->
atomicInteger.set(degrees)
testee.start { orientationState: OrientationState ->
atomicReference.set(orientationState)
}
// When
rotationListener.orientationChanged()
rotationListener.orientationChanged(ReversePortrait.degrees)
// Then
assertEquals(
expected = 90,
actual = atomicInteger.get()
expected = OrientationState(
deviceOrientation = ReversePortrait,
screenOrientation = Landscape
),
actual = atomicReference.get()
)
}
}
@@ -1,185 +0,0 @@
package io.fotoapparat.hardware.orientation
import org.junit.Test
import kotlin.test.assertEquals
class OrientationTest {
@Test
fun `When angle is 5, return 0`() {
assertEquals(
expected = 0,
actual = toClosestRightAngle(5)
)
}
@Test
fun `When angle is 60, return 90`() {
assertEquals(
expected = 90,
actual = toClosestRightAngle(60)
)
}
@Test
fun `When angle is 190, return 180`() {
assertEquals(
180,
toClosestRightAngle(190)
)
}
@Test
fun `When angle is 269, return 270`() {
assertEquals(
270,
toClosestRightAngle(269)
)
}
@Test
fun `When angle is 359, return 0`() {
assertEquals(
expected = 0,
actual = toClosestRightAngle(359)
)
}
@Test
fun `Portait & front camera at 270 degrees return 90 display orientation`() {
// Given
val screenOrientation = 0
val cameraOrientation = 270
// When
val result = computeDisplayOrientation(
screenOrientation,
cameraOrientation,
true
)
// Then
assertEquals(90, result)
}
@Test
fun `Landscape & front camera at 270 degrees return 180 display orientation`() {
// Given
val screenOrientation = 270
val cameraOrientation = 270
// When
val result = computeDisplayOrientation(
screenOrientation,
cameraOrientation,
true
)
// Then
assertEquals(180, result)
}
@Test
fun `Portrait & back camera at 90 degrees return 90 display orientation`() {
// Given
val screenOrientation = 0
val cameraOrientation = 90
// When
val result = computeDisplayOrientation(
screenOrientation,
cameraOrientation,
false
)
// Then
assertEquals(90, result)
}
@Test
fun `Landscape & back camera at 90 degrees return 180 display orientation`() {
// Given
val screenOrientation = 270
val cameraOrientation = 90
// When
val result = computeDisplayOrientation(
screenOrientation,
cameraOrientation,
false
)
// Then
assertEquals(180, result)
}
@Test
fun `Portrait & front camera at 270 degrees return 90 image orientation`() {
// Given
val screenOrientation = 0
val cameraOrientation = 270
// When
val result = computeImageOrientation(
screenOrientation,
cameraOrientation,
true
)
// Then
assertEquals(90, result)
}
@Test
fun `Landscape & front camera at 270 degrees return 180 image orientation`() {
// Given
val screenOrientation = 270
val cameraOrientation = 270
// When
val result = computeImageOrientation(
screenOrientation,
cameraOrientation,
true
)
// Then
assertEquals(180, result)
}
@Test
fun `Portrait & back camera at 90 degrees return 270 image orientation`() {
// Given
val screenOrientation = 0
val cameraOrientation = 90
// When
val result = computeImageOrientation(
screenOrientation,
cameraOrientation,
false
)
// Then
assertEquals(270, result)
}
@Test
fun `Landscape & back camera at 90 degrees return 180 image orientation`() {
// Given
val screenOrientation = 270
val cameraOrientation = 90
// When
val result = computeImageOrientation(
screenOrientation,
cameraOrientation,
false
)
// Then
assertEquals(180, result)
}
}
@@ -0,0 +1,48 @@
package io.fotoapparat.hardware.orientation
import org.junit.Test
import kotlin.test.assertEquals
class RotationTest {
@Test
fun `When angle is 5, return 0`() {
assertEquals(
expected = 0,
actual = 5.toClosestRightAngle()
)
}
@Test
fun `When angle is 60, return 90`() {
assertEquals(
expected = 90,
actual = 60.toClosestRightAngle()
)
}
@Test
fun `When angle is 190, return 180`() {
assertEquals(
180,
190.toClosestRightAngle()
)
}
@Test
fun `When angle is 269, return 270`() {
assertEquals(
270,
269.toClosestRightAngle()
)
}
@Test
fun `When angle is 359, return 0`() {
assertEquals(
expected = 0,
actual = 359.toClosestRightAngle()
)
}
}

Some files were not shown because too many files have changed in this diff Show More