diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
similarity index 100%
rename from CODE_OF_CONDUCT.md
rename to .github/CODE_OF_CONDUCT.md
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 00000000..df5e5abc
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1 @@
+Contributing guidelines are [hosted here](https://natario1.github.io/CameraView/extra/contributing).
\ No newline at end of file
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..bc2dc68f
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: [natario1]
+patreon: # Replace with a single Patreon username
+open_collective: cameraview
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index dd6b4428..68208e70 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -8,26 +8,39 @@ assignees: ''
---
### Describe the bug
-A clear and concise description of what the bug is.
-
+Please add a clear description of what the bug is, **and** fill the list below.
- CameraView version: *version number*
+- Camera engine used: *camera1/camera2/both*
- Reproducible in official demo app: *yes/no*
- Device / Android version: *Pixel, API 28*
+- I have read the [FAQ page](https://natario1.github.io/CameraView/about/faq): *yes/no*
### To Reproduce
Steps to reproduce the behavior, possibly in the demo app:
1. Go to '...'
-2. Click on '....'
+2. Click on '...'
3. See error
### Expected behavior
A clear and concise description of what you expected to happen.
+### XML layout
+Part of the XML layout with the CameraView declaration, so we can read its attributes.
+
+```xml
+
+
+```
+
### Screenshots
If applicable, add screenshots to help explain your problem.
### Logs
-Use `CameraLogger.setLogLevel(LEVEL_VERBOSE)` to see all logs into LogCat.
+Use `CameraLogger.setLogLevel(LEVEL_INFO)` to see all logs into LogCat.
+
Use `CameraLogger.registerLogger()` to export to file or crash reporting service.
### APK
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index a56b5e59..f9e085a7 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -8,5 +8,8 @@ assignees: ''
---
### How do I?
-Describe your problem here. Please, read the docs first.
+Describe your problem here. Please, read the [docs](https://natario1.github.io/CameraView) and [FAQ page](https://natario1.github.io/CameraView/about/faq) first.
Questions not strictly related to CameraView should be asked elsewhere.
+
+### Version used
+CameraView exact version.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 62379506..96593347 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,10 +1,14 @@
### Before you go
-Unless this is a simple fix (typos, bugs with obvious solution), please open an issue first.
-If the edited files were covered by tests, updated tests are required for merging. Please look into the tests folders and make sure you cover new code.
+Unless this is a simple fix (typos, bugs with obvious solution), please open an issue first so that
+we can discuss the best approach to address the problem. Without a reference issue and discussion,
+unfortunately, this PR will likely be ignored.
+
+If the edited files were covered by tests, updated tests are required for merging.
+Please look into the tests folders and make sure you cover new code.
- Fixes ... (*issue number*)
- Tests: ... (*yes/no*)
- Docs updated: ... (*yes/no*)
### Solution
-If applicable, briefly describe how the issue was addressed.
+If applicable, describe briefly how the issue was addressed.
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..395539b5
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,113 @@
+# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
+# Renaming ? Change the README badge.
+name: Build
+on:
+ push:
+ branches:
+ - master
+ - main
+ pull_request:
+jobs:
+ ANDROID_BASE_CHECKS:
+ name: Base Checks
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+ - name: Perform base checks
+ run: ./gradlew demo:assembleDebug cameraview:publishToDirectory --stacktrace
+ ANDROID_UNIT_TESTS:
+ name: Unit Tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+ - name: Execute unit tests
+ run: ./gradlew cameraview:runUnitTests --stacktrace
+ - name: Upload unit tests artifact
+ uses: actions/upload-artifact@v1
+ with:
+ name: unit_tests
+ path: ./cameraview/build/coverage_input/unit_tests
+ ANDROID_EMULATOR_TESTS:
+ name: Emulator Tests
+ runs-on: macOS-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ # TODO 29 fails due to Mockito issues, probably reproducible locally
+ # 22-28 work (some of them, with SdkExclude restrictions)
+ EMULATOR_API: [22, 23, 24, 25, 26, 27, 28]
+ include:
+ - EMULATOR_API: 28
+ EMULATOR_ARCH: x86_64
+ - EMULATOR_API: 27
+ EMULATOR_ARCH: x86_64
+ - EMULATOR_API: 26
+ EMULATOR_ARCH: x86_64
+ - EMULATOR_API: 25
+ EMULATOR_ARCH: x86
+ - EMULATOR_API: 24
+ EMULATOR_ARCH: x86
+ - EMULATOR_API: 23
+ EMULATOR_ARCH: x86
+ - EMULATOR_API: 22
+ EMULATOR_ARCH: x86
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+ - name: Execute emulator tests
+ timeout-minutes: 30
+ uses: reactivecircus/android-emulator-runner@v2.2.0
+ with:
+ api-level: ${{ matrix.EMULATOR_API }}
+ arch: ${{ matrix.EMULATOR_ARCH }}
+ disable-animations: true
+ profile: Nexus 5X
+ emulator-options: -no-snapshot -no-window -no-boot-anim -camera-back emulated -camera-front emulated -gpu swiftshader_indirect
+ emulator-build: 6110076
+ script: ./.github/workflows/emulator_script.sh
+ - name: Upload emulator tests artifact
+ uses: actions/upload-artifact@v1
+ with:
+ name: emulator_tests_${{ matrix.EMULATOR_API }}
+ path: ./cameraview/build/coverage_input/android_tests
+ CODE_COVERAGE:
+ name: Code Coverage Report
+ runs-on: ubuntu-latest
+ needs: [ANDROID_UNIT_TESTS, ANDROID_EMULATOR_TESTS]
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+ - name: Download unit tests artifact
+ uses: actions/download-artifact@v1
+ with:
+ name: unit_tests
+ path: ./cameraview/build/coverage_input/unit_tests
+ - name: Download emulator tests artifact
+ uses: actions/download-artifact@v1
+ with:
+ # 27 is the EMULATOR_API with less SdkExclude annotations, and should have
+ # the best possible coverage.
+ name: emulator_tests_27
+ path: ./cameraview/build/coverage_input/android_tests
+ - name: Create merged coverage report
+ run: ./gradlew cameraview:computeCoverage
+ - name: Upload merged coverage report (GitHub)
+ uses: actions/upload-artifact@v1
+ with:
+ name: report
+ path: ./cameraview/build/coverage_output/xml
+ - name: Upload merged coverage report (Codecov)
+ uses: codecov/codecov-action@v1
+ with:
+ file: ./cameraview/build/coverage_output/xml/*
+ fail_ci_if_error: true
\ No newline at end of file
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 00000000..7eed30cd
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,20 @@
+# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
+name: Deploy
+on:
+ release:
+ types: [published]
+jobs:
+ BINTRAY_UPLOAD:
+ name: Bintray Upload
+ runs-on: ubuntu-latest
+ env:
+ BINTRAY_USER: ${{ secrets.BINTRAY_USER }}
+ BINTRAY_KEY: ${{ secrets.BINTRAY_KEY }}
+ BINTRAY_REPO: ${{ secrets.BINTRAY_REPO }}
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+ - name: Perform bintray upload
+ run: ./gradlew cameraview:publishToBintray
diff --git a/.github/workflows/emulator_script.sh b/.github/workflows/emulator_script.sh
new file mode 100755
index 00000000..3ffe9cdf
--- /dev/null
+++ b/.github/workflows/emulator_script.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+# Core
+ADB_TAGS="CameraView:I CameraCallbacks:I CameraOrchestrator:I CameraEngine:I"
+ADB_TAGS="$ADB_TAGS CameraUtils:I WorkerHandler:I"
+# Recorders
+ADB_TAGS="$ADB_TAGS VideoRecorder:I FullVideoRecorder:I SnapshotVideoRecorder:I"
+ADB_TAGS="$ADB_TAGS FullPictureRecorder:I SnapshotPictureRecorder:I DeviceEncoders:I"
+# Video encoders
+ADB_TAGS="$ADB_TAGS MediaEncoderEngine:I MediaEncoder:I AudioMediaEncoder:I VideoMediaEncoder:I TextureMediaEncoder:I"
+# Debugging
+ADB_TAGS="$ADB_TAGS CameraIntegrationTest:I MessageQueue:W MPEG4Writer:I"
+adb logcat -c
+adb logcat $ADB_TAGS *:E -v color &
+./gradlew cameraview:runAndroidTests
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 828a1704..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,86 +0,0 @@
-# https://github.com/andstatus/andstatus/blob/master/.travis.yml
-
-language: android
-
-branches:
- only:
- - master
- - /^v\d+\.\d+\.\d+$/
-
-sudo: false
-
-jdk:
- - oraclejdk8
-
-env:
- global:
- # Where to run androidTests
- - EMULATOR_API=22 # 24 has some issues, probably some overlayed window
- - EMULATOR_ABI=armeabi-v7a
- - EMULATOR_TAG=default
- - PATH=$ANDROID_HOME:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$PATH
-
-android:
- components:
- - tools
- - platform-tools
- - build-tools-28.0.2
- - android-28
- - doc-28
-
-install:
- # Setup
- - echo $ANDROID_HOME # We assume this is correctly set when setting path
- - sdkmanager --list || true # Look at the packages
- - echo yes | sdkmanager "tools" # Ensure tools is updated
- - echo yes | sdkmanager "emulator" # Ensure emulator is present
-
- # Install emulator
- - export EMULATOR="system-images;android-$EMULATOR_API;$EMULATOR_TAG;$EMULATOR_ABI"
- - echo yes | sdkmanager "platforms;android-$EMULATOR_API" # Install sdk
- - echo yes | sdkmanager "$EMULATOR" # Install system image
- - sdkmanager --list || true # Check everything is updated
-
- # Create adn start emulator
- - echo no | avdmanager create avd -n test -k "$EMULATOR" -f # Create emulator
- - which emulator # ensure we are using the right emulator (home/emulator/)
- - emulator -avd test -no-window -camera-back emulated -camera-front emulated -memory 2048 -writable-system & # Launch
- - adb wait-for-device # Wait for adb process
- - adb remount # Mount as writable
-
-before_script:
- # Wait for emulator
- - android-wait-for-emulator # Wait for emulator ready to interact
- - adb shell settings put global window_animation_scale 0 & # Disable animations
- - adb shell settings put global transition_animation_scale 0 & # Disable animations
- - adb shell settings put global animator_duration_scale 0 & # Disable animations
-
- # Unlock and configure logs.
- # Would be great to use -v color to adb logcat but looks not supported on travis.
- - sleep 20 # Sleep 20 seconds just in case
- - adb shell input keyevent 82 & # Dispatch unlock event
- - adb logcat --help # See if this version supports color
- - adb logcat -c # Clear logcat
- - adb logcat Test:V TestRunner:V CameraView:V CameraController:V Camera1:V WorkerHandler:V THREAD_STATE:S *:E &
- # - export LOGCAT_PID=$! # Save PID of the logcat process. Should kill later with kill $LOGCAT_PID
-
-
-
-script:
- - ./gradlew clean testDebugUnitTest connectedCheck mergedCoverageReport
-
-after_success:
- - bash <(curl -s https://codecov.io/bash) -s "*/build/reports/mergedCoverageReport/"
-
-cache:
- directories:
- - $HOME/.gradle
- - $HOME/.m2/repository
-
-deploy:
- provider: script
- script: ./gradlew bintrayUpload
- skip_cleanup: true
- on:
- branch: master
- tags: true
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index aa691b89..00000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,156 +0,0 @@
-## v1.6.1
-
-This is the last release before v2.
-
-- Fixed: crash when using TextureView in API 28, thanks to [@Keyrillanskiy][Keyrillanskiy] ([#297][297])
-- Fixed: restore Frame Processor callbacks after taking videos, thanks to [@stefanJi][stefanJi] ([#344][344])
-- Enhancement: when horizontal, camera now uses the last available orientation, thanks to [@aartikov][aartikov] ([#290][290])
-- Changed: we now swallow exceptions during autoFocus that were happening unpredictably on some devices, thanks to [@mahdi-ninja][mahdi-ninja] ([#332][332])
-https://github.com/natario1/CameraView/compare/v1.6.0...v1.6.1
-
-## v1.6.0
-
-- Lifecycle support. Use `setLifecycleOwner` instead of calling start, stop and destroy ([#265][265])
-- Enhancement: provide synchronous version of CameraUtils.decodeBitmap thanks to [@athornz][athornz] ([#224][224])
-- Enhancement: prevent possible context leak thanks to [@MatFl][MatFl] ([#245][245])
-- Bug: fix crash when using default VideoCodec thanks to [@Namazed][Namazed] ([#264][264])
-- Enhancement: CameraException.getReason() gives some insight about the error ([#265][265])
-- Enhancement: Common crashes are now being posted to the error callback instead of crashing the app ([#265][265])
-
-https://github.com/natario1/CameraView/compare/v1.5.1...v1.6.0
-
-### v1.5.1
-
-- Bug: byte array length for Frames was incorrect thanks to [@ssakhavi][ssakhavi] ([#205][205])
-- Bug: gestures were crashing in some conditions ([#222][222])
-- Bug: import correctly the ExifInterface library ([#222][222])
-- Updated dependencies thanks to [@caleb-allen][caleb-allen] ([#190][190])
-
-https://github.com/natario1/CameraView/compare/v1.5.0...v1.5.1
-
-## v1.5.0
-
-- New: set encoder for video recordings with `cameraVideoCodec` ([#174][174])
-- New: set max duration for videos with `cameraVideoMaxDuration` ([#172][172])
-- Enhancement: reduced lag with continuous gestures (ev, zoom) ([#170][170])
-- Bug: tap to focus was crashing on some devices ([#167][167])
-- Bug: capturePicture was breaking if followed by another event soon after ([#173][173])
-
-https://github.com/natario1/CameraView/compare/v1.4.2...v1.5.0
-
-### v1.4.2
-
-- Add prefix to XML resources so they don't collide, thanks to [@RocketRider][RocketRider] ([#162][162])
-- Add `videoMaxSize` API and XML attribute, to set max size video in bytes, thanks to [@chaitanyaraghav][chaitanyaraghav] ([#104][104])
-- Improved the preview size selection, thanks to [@YeungKC][YeungKC] ([#133][133])
-- Improved the playSounds attribute, was playing incorrectly, thanks to [@xp-vit][xp-vit] ([#143][143])
-
-https://github.com/natario1/CameraView/compare/v1.4.1...v1.4.2
-
-### v1.4.1
-
-- Fixed a bug that would flip the front camera preview on some devices ([#112][112])
-- Two new `CameraOptions` APIs: `o.getSupportedPictureSizes()` and `o.getSupportedPictureAspectRatios()` ([#101][101])
-- Most controls (video quality, hdr, grid, session type, audio, white balance, flash, facing) now inherit
- from a base `Control` class ([#105][105]). This let us add new APIs:
-
- - `CameraView.set(Control)`: sets the control to the given value, e.g. `set(Flash.AUTO)`
- - `CameraOptions.supports(Control)`: returns true if the control is supported
- - `CameraOptions.getSupportedControls(Class extends Control>)`: returns list of supported controls of a given kind
-
-https://github.com/natario1/CameraView/compare/v1.4.0...v1.4.1
-
-## v1.4.0
-
-- CameraView is now completely thread-safe. All actions are asynchronous. ([#97][97])
- This has some breaking drawbacks. Specifically, the `get` methods (e.g., `getWhiteBalance`) might
- not return the correct value while it is being changed. So don't trust them right after you have changed the value.
- Instead, always check the `CameraOptions` to see if the value you want is supported.
-- Added error handling ([#97][97]) in `CameraListener.onCameraError(CameraException)`.
- At the moment, all exceptions there are unrecoverable. When the method is called, the camera is showing
- a black preview. This is a good moment to show an error dialog to the user.
- You can also try to `start()` again but that is not guaranteed to work.
-- Long requested ability to set the picture output size ([#99][99]). Can be done through
- `CameraView.setPictureSize()` or through new XML attributes starting with `cameraPictureSize`.
- Please refer to docs about it.
-- Deprecated `toggleFacing`. It was unreliable and will be removed.
-- Deprecated `getCaptureSize`. Use `getPictureSize` instead.
-- Fixed bugs.
-
-https://github.com/natario1/CameraView/compare/v1.3.2...v1.4.0
-
-### v1.3.2
-
-- Fixed a memory leak thanks to [@andrewmunn][andrewmunn] ([#92][92])
-- Reduced memory usage when using cropOutput thanks to [@RobertoMorelos][RobertoMorelos] ([#93][93])
-- Improved efficiency for Frame processors, recycle buffers and Frames ([#94][94])
-
-https://github.com/natario1/CameraView/compare/v1.3.1...v1.3.2
-
-### v1.3.1
-
-- Fixed a bug that would make setFacing and other APIs freeze the camera ([#86][86])
-- Fixed ConcurrentModificationExceptions during CameraListener callbacks ([#88][88])
-
-https://github.com/natario1/CameraView/compare/v1.3.0...v1.3.1
-
-## v1.3.0
-
-- Ability to inject frame processors to do your own visual tasks (barcodes, facial recognition etc.) ([#82][82])
-- Ability to inject external loggers (e.g. Crashlytics) to listen for internal logging events ([#80][80])
-- Improved CameraUtils.decodeBitmap, you can now pass maxWidth and maxHeight to avoid OOM ([#83][83])
-- Updated dependencies thanks to [@v-gar][v-gar] ([#73][73])
-
-https://github.com/natario1/CameraView/compare/v1.2.3...v1.3.0
-
-[aartikov]: https://github.com/aartikov
-[athornz]: https://github.com/athornz
-[v-gar]: https://github.com/v-gar
-[andrewmunn]: https://github.com/andrewmunn
-[chaitanyaraghav]: https://github.com/chaitanyaraghav
-[YeungKC]: https://github.com/YeungKC
-[RobertoMorelos]: https://github.com/RobertoMorelos
-[RocketRider]: https://github.com/RocketRider
-[xp-vit]: https://github.com/xp-vit
-[caleb-allen]: https://github.com/caleb-allen
-[ssakhavi]: https://github.com/ssakhavi
-[MatFl]: https://github.com/MatFl
-[Namazed]: https://github.com/Namazed
-[Keyrillanskiy]: https://github.com/Keyrillanskiy
-[mahdi-ninja]: https://github.com/mahdi-ninja
-[stefanJi]: https://github.com/stefanJi
-
-[73]: https://github.com/natario1/CameraView/pull/73
-[80]: https://github.com/natario1/CameraView/pull/80
-[82]: https://github.com/natario1/CameraView/pull/82
-[83]: https://github.com/natario1/CameraView/pull/83
-[86]: https://github.com/natario1/CameraView/pull/86
-[88]: https://github.com/natario1/CameraView/pull/88
-[92]: https://github.com/natario1/CameraView/pull/92
-[93]: https://github.com/natario1/CameraView/pull/93
-[94]: https://github.com/natario1/CameraView/pull/94
-[97]: https://github.com/natario1/CameraView/pull/97
-[99]: https://github.com/natario1/CameraView/pull/99
-[101]: https://github.com/natario1/CameraView/pull/101
-[104]: https://github.com/natario1/CameraView/pull/104
-[105]: https://github.com/natario1/CameraView/pull/105
-[112]: https://github.com/natario1/CameraView/pull/112
-[133]: https://github.com/natario1/CameraView/pull/133
-[143]: https://github.com/natario1/CameraView/pull/143
-[162]: https://github.com/natario1/CameraView/pull/162
-[167]: https://github.com/natario1/CameraView/pull/167
-[170]: https://github.com/natario1/CameraView/pull/170
-[172]: https://github.com/natario1/CameraView/pull/172
-[173]: https://github.com/natario1/CameraView/pull/173
-[174]: https://github.com/natario1/CameraView/pull/174
-[190]: https://github.com/natario1/CameraView/pull/190
-[205]: https://github.com/natario1/CameraView/pull/205
-[222]: https://github.com/natario1/CameraView/pull/222
-[224]: https://github.com/natario1/CameraView/pull/224
-[245]: https://github.com/natario1/CameraView/pull/245
-[264]: https://github.com/natario1/CameraView/pull/264
-[265]: https://github.com/natario1/CameraView/pull/265
-[290]: https://github.com/natario1/CameraView/pull/290
-[297]: https://github.com/natario1/CameraView/pull/297
-[332]: https://github.com/natario1/CameraView/pull/332
-[334]: https://github.com/natario1/CameraView/pull/334
diff --git a/README.md b/README.md
index 77e65ccd..c3aee6ec 100644
--- a/README.md
+++ b/README.md
@@ -1,782 +1,166 @@
-[](https://travis-ci.org/natario1/CameraView)
+[](https://github.com/natario1/CameraView/actions)
[](https://codecov.io/gh/natario1/CameraView)
+[](https://github.com/natario1/CameraView/releases)
+[](https://github.com/natario1/CameraView/issues)
+[](https://natario1.github.io/CameraView/extra/donate)
+
+⠀
-
+
+*Post-processing videos or want to reduce video size before uploading? Take a look at our [Transcoder](https://github.com/natario1/Transcoder).*
+
+*Like the project, make profit from it, or simply want to thank back? Please consider [sponsoring me](https://github.com/sponsors/natario1) or [donating](https://natario1.github.io/CameraView/extra/donate)!*
+
+*Need support, consulting, or have any other business-related question? Feel free to get in touch .*
+
# CameraView
CameraView is a well documented, high-level library that makes capturing pictures and videos easy,
addressing most of the common issues and needs, and still leaving you with flexibility where needed.
-See [CHANGELOG](https://github.com/natario1/CameraView/blob/master/CHANGELOG.md).
```groovy
-compile 'com.otaliastudios:cameraview:1.6.1'
-```
-
-Make sure your project repositories include the `jcenter()`:
-
-```groovy
-allprojects {
- repositories {
- jcenter()
- }
-}
-```
-
-
-
-
-
-
-
-
-*This was a fork of [CameraKit-Android](https://github.com/gogopop/CameraKit-Android), originally a
-fork of [Google's CameraView](https://github.com/google/cameraview), but has been
-[completely rewritten](https://github.com/natario1/CameraView/graphs/contributors?type=d).
-See below for a [list of what was done](#roadmap) and [licensing info](#contributing-and-licenses).*
-
-### Features
-
-- Seamless image and video capturing
-- **Gestures** support (tap to focus, pinch to zoom and much more)
-- System permission handling
-- **Smart sizing** behavior
- - Preview: Create a `CameraView` of **any size**
- - Preview: Center inside or center crop behaviors
- - Output: Handy utilities to set the output size
- - Output: Automatic cropping to match your `CameraView` preview bounds
-- Built-in **grid drawing**
-- Multiple capture methods
- - Take high-resolution pictures with `capturePicture`
- - Take **quick snapshots** as a freeze frame of the preview with `captureSnapshot`
-- Control HDR, flash, zoom, white balance, exposure correction and more
-- **Frame processing** support
-- **Metadata** support for pictures and videos
- - Automatically detected orientation tags
- - Plug in **location tags** with `setLocation()` API
-- `CameraUtils` to help with Bitmaps and orientations
-- Error handling
-- Thread safe, **well tested**
-- **Lightweight**, no dependencies, just support `ExifInterface`
+api 'com.otaliastudios:cameraview:2.7.0'
+```
+
+- Fast & reliable
+- Gestures support [[docs]](https://natario1.github.io/CameraView/docs/gestures)
+- Real-time filters [[docs]](https://natario1.github.io/CameraView/docs/filters)
+- Camera1 or Camera2 powered engine [[docs]](https://natario1.github.io/CameraView/docs/previews)
+- Frame processing support [[docs]](https://natario1.github.io/CameraView/docs/frame-processing)
+- Watermarks & animated overlays [[docs]](https://natario1.github.io/CameraView/docs/watermarks-and-overlays)
+- OpenGL powered preview [[docs]](https://natario1.github.io/CameraView/docs/previews)
+- Take high-quality content with `takePicture` and `takeVideo` [[docs]](https://natario1.github.io/CameraView/docs/capturing-media)
+- Take super-fast snapshots with `takePictureSnapshot` and `takeVideoSnapshot` [[docs]](https://natario1.github.io/CameraView/docs/capturing-media)
+- Smart sizing: create a `CameraView` of any size [[docs]](https://natario1.github.io/CameraView/docs/preview-size)
+- Control HDR, flash, zoom, white balance, exposure, location, grid drawing & more [[docs]](https://natario1.github.io/CameraView/docs/controls)
+- RAW pictures support [[docs]](https://natario1.github.io/CameraView/docs/controls)
+- Lightweight
- Works down to API level 15
+- Well tested
-# Docs
-
-- [Usage](#usage)
- - [Capturing Images](#capturing-images)
- - [Capturing Video](#capturing-video)
- - [Other camera events](#other-camera-events)
-- [Gestures](#gestures)
-- [Sizing Behavior](#sizing-behavior)
- - [Preview Size](#preview-size)
- - [Picture Size](#picture-size)
-- [Camera Controls](#camera-controls)
-- [Frame Processing](#frame-processing)
-- [Other APIs](#other-apis)
-- [Permissions Behavior](#permissions-behavior)
-- [Logging](#logging)
-- [Device-specific issues](#device-specific-issues)
-- [Roadmap](#roadmap)
-
-## Usage
-
-To use the CameraView engine, simply add a `CameraView` to your layout:
-
-```xml
-
-```
-
-`CameraView` is a component bound to your activity or fragment lifecycle. This means that you must pass the
-lifecycle owner using `setLifecycleOwner`:
-
-```java
-@Override
-protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- CameraView camera = findViewById(R.id.camera);
- camera.setLifecycleOwner(this);
- // From fragments, use fragment.viewLifecycleOwner instead of this!
-}
-```
-
-For those who are not using the support libraries and the lifecycle implementation, make sure you override `onResume`,
-`onPause` and `onDestroy` in your component, and call `CameraView.start()`, `stop()`
-and `destroy()`.
-
-```java
-@Override
-protected void onResume() {
- super.onResume();
- cameraView.start();
-}
-
-@Override
-protected void onPause() {
- super.onPause();
- cameraView.stop();
-}
-
-@Override
-protected void onDestroy() {
- super.onDestroy();
- cameraView.destroy();
-}
-```
-
-### Capturing Images
-
-To capture an image just call `CameraView.capturePicture()`. Make sure you setup a `CameraListener`
-to handle the image callback.
-
-```java
-camera.addCameraListener(new CameraListener() {
- @Override
- public void onPictureTaken(byte[] picture) {
- // Create a bitmap or a file...
- // CameraUtils will read EXIF orientation for you, in a worker thread.
- CameraUtils.decodeBitmap(picture, ...);
- }
-});
-
-camera.capturePicture();
-```
-
-You can also use `camera.captureSnapshot()` to capture a preview frame. This is faster, though will
-ensure lower quality output.
-
-### Capturing Video
-
-To capture video just call `CameraView.startCapturingVideo(file)` to start, and
-`CameraView.stopCapturingVideo()` to finish. Make sure you setup a `CameraListener` to handle
-the video callback.
-
-```java
-camera.addCameraListener(new CameraListener() {
- @Override
- public void onVideoTaken(File video) {
- // The File is the same you passed before.
- // Now it holds a MP4 video.
- }
-});
-
-// Select output file. Make sure you have write permissions.
-File file = ...;
-camera.startCapturingVideo(file);
-
-// Later... stop recording. This will trigger onVideoTaken().
-camera.stopCapturingVideo();
-
-// You can also use one of the video constraints:
-// videoMaxSize and videoMaxDuration will automatically stop recording when satisfied.
-camera.setVideoMaxSize(100000);
-camera.setVideoMaxDuration(5000);
-camera.startCapturingVideo(file);
-```
-
-### Other camera events
-
-Make sure you can react to different camera events by setting up one or more `CameraListener`
-instances. All these are executed on the UI thread.
-
-```java
-camera.addCameraListener(new CameraListener() {
-
- /**
- * Notifies that the camera was opened.
- * The options object collects all supported options by the current camera.
- */
- @Override
- public void onCameraOpened(CameraOptions options) {}
-
- /**
- * Notifies that the camera session was closed.
- */
- @Override
- public void onCameraClosed() {}
-
- /**
- * Notifies about an error during the camera setup or configuration.
- * At the moment, errors that are passed here are unrecoverable. When this is called,
- * the camera has been released and is presumably showing a black preview.
- *
- * This is the right moment to show an error dialog to the user.
- */
- @Override
- public void onCameraError(CameraException error) {}
+⠀
- /**
- * Notifies that a picture previously captured with capturePicture()
- * or captureSnapshot() is ready to be shown or saved.
- *
- * If planning to get a bitmap, you can use CameraUtils.decodeBitmap()
- * to decode the byte array taking care about orientation.
- */
- @Override
- public void onPictureTaken(byte[] picture) {}
-
- /**
- * Notifies that a video capture has just ended. The file parameter is the one that
- * was passed to startCapturingVideo(File), or a fallback video file.
- */
- @Override
- public void onVideoTaken(File video) {}
-
- /**
- * Notifies that the device was tilted or the window offset changed.
- * The orientation passed can be used to align views (e.g. buttons) to the current
- * camera viewport so they will appear correctly oriented to the user.
- */
- @Override
- public void onOrientationChanged(int orientation) {}
-
- /**
- * Notifies that user interacted with the screen and started focus with a gesture,
- * and the autofocus is trying to focus around that area.
- * This can be used to draw things on screen.
- */
- @Override
- public void onFocusStart(PointF point) {}
-
- /**
- * Notifies that a gesture focus event just ended, and the camera converged
- * to a new focus (and possibly exposure and white balance).
- */
- @Override
- public void onFocusEnd(boolean successful, PointF point) {}
-
- /**
- * Noitifies that a finger gesture just caused the camera zoom
- * to be changed. This can be used, for example, to draw a seek bar.
- */
- @Override
- public void onZoomChanged(float newValue, float[] bounds, PointF[] fingers) {}
-
- /**
- * Noitifies that a finger gesture just caused the camera exposure correction
- * to be changed. This can be used, for example, to draw a seek bar.
- */
- @Override
- public void onExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers) {}
-
-});
-```
-
-## Gestures
-
-`CameraView` listen to lots of different gestures inside its bounds. You have the chance to map
-these gestures to particular actions or camera controls, using `mapGesture()`.
-This lets you emulate typical behaviors in a single line:
-
-```java
-cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM); // Pinch to zoom!
-cameraView.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER); // Tap to focus!
-cameraView.mapGesture(Gesture.LONG_TAP, GestureAction.CAPTURE); // Long tap to shoot!
-```
+
+
+
-Simple as that. More gestures are coming. There are two things to be noted:
+⠀
-- Not every mapping is valid. For example, you can't control zoom with long taps, or start focusing by pinching.
-- Some actions might not be supported by the sensor. Check out `CameraOptions` to know what's legit and what's not.
+## Support
-|Gesture (XML)|Description|Can be mapped to|
-|-------------|-----------|----------------|
-|`PINCH` (`cameraGesturePinch`)|Pinch gesture, typically assigned to the zoom control.|`zoom` `exposureCorrection` `none`|
-|`TAP` (`cameraGestureTap`)|Single tap gesture, typically assigned to the focus control.|`focus` `focusWithMarker` `capture` `none`|
-|`LONG_TAP` (`cameraGestureLongTap`)|Long tap gesture.|`focus` `focusWithMarker` `capture` `none`|
-|`SCROLL_HORIZONTAL` (`cameraGestureScrollHorizontal`)|Horizontal movement gesture.|`zoom` `exposureCorrection` `none`|
-|`SCROLL_VERTICAL` (`cameraGestureScrollVertical`)|Vertical movement gesture.|`zoom` `exposureCorrection` `none`|
+If you like the project, make profit from it, or simply want to thank back, please consider
+[sponsoring me](https://github.com/sponsors/natario1) through the GitHub Sponsors program! You can
+have your company logo here, get private support hours or simply help me push this forward.
+If you prefer, you can also [donate](https://natario1.github.io/CameraView/extra/donate)
+to our OpenCollective page.
+CameraView is trusted and supported by [ShareChat](https://sharechat.com/), a social media app with over 100 million downloads.
-## Sizing Behavior
+
+
+
-### Preview Size
+Feel free to contact me for support, consulting or any other business-related question.
-`CameraView` has a smart measuring behavior that will let you do what you want with a few flags.
-Measuring is controlled simply by `layout_width` and `layout_height` attributes, with this meaning:
+Thanks to all our project backers... [[become a backer]](https://opencollective.com/cameraview#backer)
-- `WRAP_CONTENT` : try to stretch this dimension to respect the preview aspect ratio.
-- `MATCH_PARENT` : fill this dimension, even if this means ignoring the aspect ratio.
-- Fixed values (e.g. `500dp`) : respect this dimension.
+
-You can have previews of all sizes, not just the supported presets. Whaterever you do,
-the preview will never be distorted.
+...and to all our project sponsors! [[become a sponsor]](https://opencollective.com/cameraview#sponsor)
-#### Center Inside
+
+
+
+
+
+
+
+
+
+
-You can emulate a **center inside** behavior (like the `ImageView` scaletype) by setting
-both dimensions to `wrap_content`. The camera will get the biggest possible size that fits
-into your bounds, just like what happens with image views.
+## Setup
+Please read the [official website](https://natario1.github.io/CameraView) for setup instructions and documentation.
+You might also be interested in our [changelog](https://natario1.github.io/CameraView/about/changelog)
+or in the [v1 migration guide](https://natario1.github.io/CameraView/extra/v1-migration-guide).
+Using CameraView is extremely simple:
```xml
-```
-
-This means that the whole preview is visible, and the image output matches what was visible
-during the capture.
-
-#### Center Crop
-
-You can emulate a **center crop** behavior by setting both dimensions to fixed values or to
-`MATCH_PARENT`. The camera view will fill the rect. If your dimensions don't match the aspect ratio
-of the internal preview surface, the surface will be cropped to fill the view,
-just like `android:scaleType="centerCrop"` on an `ImageView`.
-
-```xml
-
-```
-
-This means that part of the preview is hidden, and the image output will contain parts of the scene
-that were not visible during the capture. If this is a problem, see [cameraCropOutput](#cameracropoutput).
-
-### Picture Size
-
-On top of this, you can control the actual size of the output picture, among the list of available sizes.
-It is the size of the final JPEG picture. This can be achieved directly through XML, or
-using the `SizeSelector` class:
-
-```java
-cameraView.setPictureSize(new SizeSelector() {
- @Override
- public List select(List source) {
- // Receives a list of available sizes.
- // Must return a list of acceptable sizes.
- }
-});
-```
-
-In practice, this is way easier using XML attributes or leveraging the `SizeSelectors` utilities:
-
-|Constraint|XML attr|SizeSelector|
-|----------|--------|------------|
-|min. width|`app:cameraPictureSizeMinWidth="100"`|`SizeSelectors.minWidth(100)`|
-|min. height|`app:cameraPictureSizeMinHeight="100"`|`SizeSelectors.minHeight(100)`|
-|max. width|`app:cameraPictureSizeMaxWidth="3000"`|`SizeSelectors.maxWidth(3000)`|
-|max. height|`app:cameraPictureSizeMaxHeight="3000"`|`SizeSelectors.maxHeight(3000)`|
-|min. area|`app:cameraPictureSizeMinArea="1000000"`|`SizeSelectors.minArea(1000000)`|
-|max. area|`app:cameraPictureSizeMaxArea="5000000"`|`SizeSelectors.maxArea(5000000)`|
-|aspect ratio|`app:cameraPictureSizeAspectRatio="1:1"`|`SizeSelectors.aspectRatio(AspectRatio.of(1,1), 0)`|
-|smallest|`app:cameraPictureSizeSmallest="true"`|`SizeSelectors.smallest()`|
-|biggest (**default**)|`app:cameraPictureSizeBiggest="true"`|`SizeSelectors.biggest()`|
-
-If you declare more than one XML constraint, the resulting selector will try to match **all** the
-constraints. Be careful - it is very likely that applying lots of constraints will give
-empty results.
-
-#### SizeSelectors utilities
-
-For more versatility, or to address selection issues with multiple constraints,
-we encourage you to use `SizeSelectors` utilities, that will let you merge different selectors.
-
-This selector will try to find square sizes bigger than 1000x2000. If none is found, it falls back
-to just square sizes:
-
-```java
-SizeSelector width = SizeSelectors.minWidth(1000);
-SizeSelector height = SizeSelectors.minHeight(2000);
-SizeSelector dimensions = SizeSelectors.and(width, height); // Matches sizes bigger than 1000x2000.
-SizeSelector ratio = SizeSelectors.aspectRatio(AspectRatio.of(1, 1), 0); // Matches 1:1 sizes.
-
-SizeSelector result = SizeSelectors.or(
- SizeSelectors.and(ratio, dimensions), // Try to match both constraints
- ratio, // If none is found, at least try to match the aspect ratio
- SizeSelectors.biggest() // If none is found, take the biggest
-);
-camera.setPictureSize(result);
-```
-
-## Camera controls
-
-Most camera parameters can be controlled through XML attributes or linked methods.
-
-```xml
-
-```
-
-|XML Attribute|Method|Values|Default Value|
-|-------------|------|------|-------------|
-|[`cameraSessionType`](#camerasessiontype)|`setSessionType()`|`picture` `video`|`picture`|
-|[`cameraFacing`](#camerafacing)|`setFacing()`|`back` `front`|`back`|
-|[`cameraFlash`](#cameraflash)|`setFlash()`|`off` `on` `auto` `torch`|`off`|
-|[`cameraGrid`](#cameragrid)|`setGrid()`|`off` `draw3x3` `draw4x4` `drawPhi`|`off`|
-|[`cameraCropOutput`](#cameracropoutput)|`setCropOutput()`|`true` `false`|`false`|
-|[`cameraJpegQuality`](#camerajpegquality)|`setJpegQuality()`|`0 < n <= 100`|`100`|
-|[`cameraVideoQuality`](#cameravideoquality)|`setVideoQuality()`|`lowest` `highest` `maxQvga` `max480p` `max720p` `max1080p` `max2160p`|`max480p`|
-|[`cameraVideoCodec`](#cameravideocodec)|`setVideoCodec()`|`deviceDefault` `h263` `h264`|`deviceDefault`|
-|[`cameraWhiteBalance`](#camerawhitebalance)|`setWhiteBalance()`|`auto` `incandescent` `fluorescent` `daylight` `cloudy`|`auto`|
-|[`cameraHdr`](#camerahdr)|`setHdr()`|`off` `on`|`off`|
-|[`cameraAudio`](#cameraaudio)|`setAudio()`|`off` `on`|`on`|
-|[`cameraPlaySounds`](#cameraplaysounds)|`setPlaySounds()`|`true` `false`|`true`|
-|[`cameraVideoMaxSize`](#cameravideomaxsize)|`setVideoMaxSize()`|number|`0`|
-|[`cameraVideoMaxDuration`](#cameravideomaxduration)|`setVideoMaxDuration()`|number|`0`|
-
-#### cameraSessionType
-
-What to capture - either picture or video. This has a couple of consequences:
-
-- Sizing: picture and preview size are chosen among the available picture or video sizes,
- depending on the flag. The picture size is chosen according to the given [size selector](#picture-size).
- When `video`, in addition, we try to match the `videoQuality` aspect ratio.
-- Picture capturing: due to sizing behavior, capturing pictures in `video` mode might lead to
- inconsistent results. In this case it is encouraged to use `captureSnapshot` instead, which will
- capture preview frames. This is fast and thus works well with slower camera sensors.
-- Picture capturing: while recording a video, image capturing might work, but it is not guaranteed
- (it's device dependent)
-- Permission behavior: when requesting a `video` session, the record audio permission will be requested.
- If this is needed, the audio permission should be added to your manifest or the app will crash.
-
-```java
-cameraView.setSessionType(SessionType.PICTURE);
-cameraView.setSessionType(SessionType.VIDEO);
-```
-
-#### cameraFacing
-
-Which camera to use, either back facing or front facing.
-
-```java
-cameraView.setFacing(Facing.BACK);
-cameraView.setFacing(Facing.FRONT);
-```
-
-#### cameraFlash
-
-Flash mode, either off, on, auto or *torch*.
-
-```java
-cameraView.setFlash(Flash.OFF);
-cameraView.setFlash(Flash.ON);
-cameraView.setFlash(Flash.AUTO);
-cameraView.setFlash(Flash.TORCH);
-```
-
-#### cameraGrid
-
-Lets you draw grids over the camera preview. Supported values are `off`, `draw3x3` and `draw4x4`
-for regular grids, and `drawPhi` for a grid based on the golden ratio constant, often used in photography.
-
-```java
-cameraView.setGrid(Grid.OFF);
-cameraView.setGrid(Grid.DRAW_3X3);
-cameraView.setGrid(Grid.DRAW_4X4);
-cameraView.setGrid(Grid.DRAW_PHI);
-```
-
-#### cameraCropOutput
-
-Whether the output picture should be cropped to fit the aspect ratio of the preview surface.
-This can guarantee consistency between what the user sees and the final output, if you fixed
-the camera view dimensions. This does not support videos.
-
-#### cameraJpegQuality
-
-Sets the JPEG quality of pictures.
-
-```java
-cameraView.setJpegQuality(100);
-cameraView.setJpegQuality(50);
-```
-
-#### cameraVideoQuality
-
-Sets the desired video quality.
-
-```java
-cameraView.setVideoQuality(VideoQuality.LOWEST);
-cameraView.setVideoQuality(VideoQuality.HIGHEST);
-cameraView.setVideoQuality(VideoQuality.MAX_QVGA);
-cameraView.setVideoQuality(VideoQuality.MAX_480P);
-cameraView.setVideoQuality(VideoQuality.MAX_720P);
-cameraView.setVideoQuality(VideoQuality.MAX_1080P);
-cameraView.setVideoQuality(VideoQuality.MAX_2160P);
-```
-
-#### cameraVideoCodec
-
-Sets the encoder for video recordings.
-
-```java
-cameraView.setVideoCodec(VideoCodec.DEVICE_DEFAULT);
-cameraView.setVideoCodec(VideoCodec.H_263);
-cameraView.setVideoCodec(VideoCodec.H_264);
-```
-
-#### cameraWhiteBalance
-
-Sets the desired white balance for the current session.
-
-```java
-cameraView.setWhiteBalance(WhiteBalance.AUTO);
-cameraView.setWhiteBalance(WhiteBalance.INCANDESCENT);
-cameraView.setWhiteBalance(WhiteBalance.FLUORESCENT);
-cameraView.setWhiteBalance(WhiteBalance.DAYLIGHT);
-cameraView.setWhiteBalance(WhiteBalance.CLOUDY);
-```
-
-#### cameraHdr
-
-Turns on or off HDR captures.
-
-```java
-cameraView.setHdr(Hdr.OFF);
-cameraView.setHdr(Hdr.ON);
-```
-
-#### cameraAudio
-
-Turns on or off audio stream while recording videos.
-
-```java
-cameraView.setAudio(Audio.OFF);
-cameraView.setAudio(Audio.ON);
-```
-
-#### cameraPlaySounds
-
-Controls whether we should play platform-provided sounds during certain events
-(shutter click, focus completed). Please note that:
-
-- on API < 16, this flag is always set to `false`
-- the Camera1 engine will always play shutter sounds regardless of this flag
-
-```java
-cameraView.setPlaySounds(true);
-cameraView.setPlaySounds(false);
-```
-
-#### cameraVideoMaxSize
-
-Defines the maximum size in bytes for recorded video files.
-Once this size is reached, the recording will automatically stop.
-Defaults to unlimited size. Use 0 or negatives to disable.
-
-```java
-cameraView.setVideoMaxSize(100000);
-cameraView.setVideoMaxSize(0); // Disable
-```
-
-#### cameraVideoMaxDuration
-
-Defines the maximum duration in milliseconds for video recordings.
-Once this duration is reached, the recording will automatically stop.
-Defaults to unlimited duration. Use 0 or negatives to disable.
-
-```java
-cameraView.setVideoMaxDuration(100000);
-cameraView.setVideoMaxDuration(0); // Disable
-```
-
-## Frame Processing
-
-We support frame processors that will receive data from the camera preview stream:
-
-```java
-cameraView.addFrameProcessor(new FrameProcessor() {
- @Override
- @WorkerThread
- public void process(Frame frame) {
- byte[] data = frame.getData();
- int rotation = frame.getRotation();
- long time = frame.getTime();
- Size size = frame.getSize();
- int format = frame.getFormat();
- // Process...
- }
-}
-```
-
-For your convenience, the `FrameProcessor` method is run in a background thread so you can do your job
-in a synchronous fashion. Once the process method returns, internally we will re-use the `Frame` instance and
-apply new data to it. So:
-
-- you can do your job synchronously in the `process()` method
-- if you must hold the `Frame` instance longer, use `frame = frame.freeze()` to get a frozen instance
- that will not be affected
-
-|Frame API|Type|Description|
-|---------|----|-----------|
-|`frame.getData()`|`byte[]`|The current preview frame, in its original orientation.|
-|`frame.getTime()`|`long`|The preview timestamp, in `System.currentTimeMillis()` reference.|
-|`frame.getRotation()`|`int`|The rotation that should be applied to the byte array in order to see what the user sees.|
-|`frame.getSize()`|`Size`|The frame size, before any rotation is applied, to access data.|
-|`frame.getFormat()`|`int`|The frame `ImageFormat`. This will always be `ImageFormat.NV21` for now.|
-|`frame.freeze()`|`Frame`|Clones this frame and makes it immutable. Can be expensive because requires copying the byte array.|
-|`frame.release()`|`-`|Disposes the content of this frame. Should be used on frozen frames to release memory.|
-
-## Other APIs
-
-Other APIs not mentioned above are provided, and are well documented and commented in code.
-
-|Method|Description|
-|------|-----------|
-|`isStarted()`|Returns true if `start()` was called succesfully. This does not mean that camera is open or showing preview.|
-|`mapGesture(Gesture, GestureAction)`|Maps a certain gesture to a certain action. No-op if the action is not supported.|
-|`getGestureAction(Gesture)`|Returns the action currently mapped to the given gesture.|
-|`clearGesture(Gesture)`|Clears any action mapped to the given gesture.|
-|`getCameraOptions()`|If camera was started, returns non-null object with information about what is supported.|
-|`getExtraProperties()`|If camera was started, returns non-null object with extra information about the camera sensor. Not very useful at the moment.|
-|`setZoom(float)`, `getZoom()`|Sets a zoom value, where 0 means camera zoomed out and 1 means zoomed in. No-op if zoom is not supported, or camera not started.|
-|`setExposureCorrection(float)`, `getExposureCorrection()`|Sets exposure compensation EV value, in camera stops. No-op if this is not supported. Should be between the bounds returned by CameraOptions.|
-|`toggleFacing()`|Toggles the facing value between `Facing.FRONT` and `Facing.BACK`.|
-|`setLocation(Location)`|Sets location data to be appended to picture/video metadata.|
-|`setLocation(double, double)`|Sets latitude and longitude to be appended to picture/video metadata.|
-|`getLocation()`|Retrieves location data previously applied with setLocation().|
-|`startAutoFocus(float, float)`|Starts an autofocus process at the given coordinates, with respect to the view dimensions.|
-|`getPreviewSize()`|Returns the size of the preview surface. If CameraView was not constrained in its layout phase (e.g. it was `wrap_content`), this will return the same aspect ratio of CameraView.|
-|`getSnapshotSize()`|Returns `getPreviewSize()`, since a snapshot is a preview frame.|
-|`getPictureSize()`|Returns the size of the output picture. The aspect ratio is consistent with `getPreviewSize()`.|
-
-Take also a look at public methods in `CameraUtils`, `CameraOptions`, `ExtraProperties`.
-
-## Permissions behavior
-
-`CameraView` needs two permissions:
-
-- `android.permission.CAMERA` : required for capturing pictures and videos
-- `android.permission.RECORD_AUDIO` : required for capturing videos with `Audio.ON` (the default)
-
-### Declaration
-
-The library manifest file declares the `android.permission.CAMERA` permission, but not the audio one.
-This means that:
-
-- If you wish to record videos with `Audio.ON` (the default), you should also add
- `android.permission.RECORD_AUDIO` to required permissions
-
-```xml
-
-```
-
-- If you want your app to be installed only on devices that have a camera, you should add:
-
-```xml
-
-```
-
-If you don't request this feature, you can use `CameraUtils.hasCameras()` to detect if current
-device has cameras, and then start the camera view.
-
-### Handling
-
-On Marshmallow+, the user must explicitly approve our permissions. You can
-
-- handle permissions yourself and then call `cameraView.start()` once they are acquired
-- or call `cameraView.start()` anyway: `CameraView` will present a permission request to the user based on
- whether they are needed or not with the current configuration.
-
-In the second case, you should restart the camera if you have a successful response from `onRequestPermissionResults()`.
-
-## Logging
-
-`CameraView` will log a lot of interesting events related to the camera lifecycle. These are important
-to identify bugs. The default logger will simply use Android `Log` methods posting to logcat.
-
-You can attach and detach external loggers using `CameraLogger.registerLogger()`:
-
-```java
-CameraLogger.registerLogger(new Logger() {
- @Override
- public void log(@LogLevel int level, String tag, String message, @Nullable Throwable throwable) {
- // For example...
- Crashlytics.log(message);
- }
-});
+ app:cameraPictureSizeMinWidth="@integer/picture_min_width"
+ app:cameraPictureSizeMinHeight="@integer/picture_min_height"
+ app:cameraPictureSizeMaxWidth="@integer/picture_max_width"
+ app:cameraPictureSizeMaxHeight="@integer/picture_max_height"
+ app:cameraPictureSizeMinArea="@integer/picture_min_area"
+ app:cameraPictureSizeMaxArea="@integer/picture_max_area"
+ app:cameraPictureSizeSmallest="false|true"
+ app:cameraPictureSizeBiggest="false|true"
+ app:cameraPictureSizeAspectRatio="@string/video_ratio"
+ app:cameraVideoSizeMinWidth="@integer/video_min_width"
+ app:cameraVideoSizeMinHeight="@integer/video_min_height"
+ app:cameraVideoSizeMaxWidth="@integer/video_max_width"
+ app:cameraVideoSizeMaxHeight="@integer/video_max_height"
+ app:cameraVideoSizeMinArea="@integer/video_min_area"
+ app:cameraVideoSizeMaxArea="@integer/video_max_area"
+ app:cameraVideoSizeSmallest="false|true"
+ app:cameraVideoSizeBiggest="false|true"
+ app:cameraVideoSizeAspectRatio="@string/video_ratio"
+ app:cameraSnapshotMaxWidth="@integer/snapshot_max_width"
+ app:cameraSnapshotMaxHeight="@integer/snapshot_max_height"
+ app:cameraFrameProcessingMaxWidth="@integer/processing_max_width"
+ app:cameraFrameProcessingMaxHeight="@integer/processing_max_height"
+ app:cameraFrameProcessingFormat="@integer/processing_format"
+ app:cameraFrameProcessingPoolSize="@integer/processing_pool_size"
+ app:cameraFrameProcessingExecutors="@integer/processing_executors"
+ app:cameraVideoBitRate="@integer/video_bit_rate"
+ app:cameraAudioBitRate="@integer/audio_bit_rate"
+ app:cameraGestureTap="none|autoFocus|takePicture"
+ app:cameraGestureLongTap="none|autoFocus|takePicture"
+ app:cameraGesturePinch="none|zoom|exposureCorrection|filterControl1|filterControl2"
+ app:cameraGestureScrollHorizontal="none|zoom|exposureCorrection|filterControl1|filterControl2"
+ app:cameraGestureScrollVertical="none|zoom|exposureCorrection|filterControl1|filterControl2"
+ app:cameraEngine="camera1|camera2"
+ app:cameraPreview="glSurface|surface|texture"
+ app:cameraPreviewFrameRate="@integer/preview_frame_rate"
+ app:cameraPreviewFrameRateExact="false|true"
+ app:cameraFacing="back|front"
+ app:cameraHdr="on|off"
+ app:cameraFlash="on|auto|torch|off"
+ app:cameraWhiteBalance="auto|cloudy|daylight|fluorescent|incandescent"
+ app:cameraMode="picture|video"
+ app:cameraAudio="on|off|mono|stereo"
+ app:cameraGrid="draw3x3|draw4x4|drawPhi|off"
+ app:cameraGridColor="@color/grid_color"
+ app:cameraPlaySounds="true|false"
+ app:cameraVideoMaxSize="@integer/video_max_size"
+ app:cameraVideoMaxDuration="@integer/video_max_duration"
+ app:cameraVideoCodec="deviceDefault|h264|h263"
+ app:cameraAutoFocusResetDelay="@integer/autofocus_delay"
+ app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker"
+ app:cameraUseDeviceOrientation="true|false"
+ app:cameraFilter="@string/real_time_filter"
+ app:cameraPictureMetering="true|false"
+ app:cameraPictureSnapshotMetering="false|true"
+ app:cameraPictureFormat="jpeg|dng"
+ app:cameraRequestPermissions="true|false"
+ app:cameraExperimental="false|true">
+
+
+
+
+
```
-
-Make sure you enable the logger using `CameraLogger.setLogLevel(@LogLevel int)`. The default will only
-log error events.
-
-## Device-specific issues
-
-There are a couple of known issues if you are working with certain devices. The emulator is one of
-the most tricky in this sense.
-
-- Devices, or activities, with hardware acceleration turned off: this can be the case with emulators.
- In this case we will use SurfaceView as our surface provider. That is intrinsically flawed and can't
- deal with all we want to do here (runtime layout changes, scaling, etc.). So, nothing to do in this case.
-- Devices with no support for MediaRecorder: the emulator does not support it, officially. This means
- that video/audio recording is flawed. Again, not our fault.
-
-## Roadmap
-
-This is what was done since the library was forked. I have kept the original structure, but practically
-all the code was changed.
-
-- *a huge number of serious bugs fixed*
-- *decent orientation support for both pictures and videos*
-- *less dependencies*
-- *EXIF support*
-- *real tap-to-focus support*
-- *pinch-to-zoom support*
-- *simpler APIs, docs and heavily commented code*
-- *new `captureSnapshot` API*
-- *new `setLocation` and `setWhiteBalance` APIs*
-- *new `setGrid` APIs, to draw 3x3, 4x4 or golden ratio grids*
-- *option to pass a `File` when recording a video*
-- *other minor API additions*
-- *replacing Method and Permissions stuff with simpler `sessionType`*
-- *smart measuring and sizing behavior, replacing bugged `adjustViewBounds`*
-- *measure `CameraView` as center crop or center inside*
-- *add multiple `CameraListener`s for events*
-- *gesture framework support, map gestures to camera controls*
-- *pinch gesture support*
-- *tap & long tap gesture support*
-- *scroll gestures support*
-- *MediaActionSound support*
-- *Hdr controls*
-- *zoom and exposure correction controls*
-- *Tests!*
-- *`CameraLogger` APIs for logging and bug reports*
-- *Better threading, start() in worker thread and callbacks in UI*
-- *Frame processor support*
-- *inject external loggers*
-- *error handling*
-- *capture size selectors*
-
-These are still things that need to be done, off the top of my head:
-
-- [ ] `Camera2` integration
-- [ ] animate grid lines similar to stock camera app
-- [ ] add onRequestPermissionResults for easy permission callback
-- [ ] decent code coverage
-
-# Contributing and licenses
-
-The original project which served as a starting point for this library,
-[CameraKit-Android](https://github.com/wonderkiln/CameraKit-Android), is licensed under the
-[MIT](https://github.com/wonderkiln/CameraKit-Android/blob/master/LICENSE) license.
-Additional work is now licensed under the [MIT](https://github.com/natario1/CameraView/blob/master/LICENSE)
-license as well.
-
-You are welcome to contribute with suggestions or pull requests, this is under active development.
-To contact me, send an email.
diff --git a/Readme.md b/Readme.md
new file mode 100644
index 00000000..a6e724ef
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,128 @@
+⠀
+
+
+
+¿Postprocesar videos o quieres reducir el tamaño del video antes de subirlo? Eche un vistazo a nuestro transcodificador .
+
+¿Le gusta el proyecto, sacar provecho de él o simplemente quiere agradecerle? ¡Considere patrocinarme o donar !
+
+¿Necesita soporte, consultoría o tiene alguna otra pregunta relacionada con el negocio? No dude en ponerse en contacto .
+
+Vista de cámara
+CameraView es una biblioteca de alto nivel bien documentada que facilita la captura de imágenes y videos, aborda la mayoría de los problemas y necesidades comunes, y aún lo deja con flexibilidad donde sea necesario.
+
+api ' com.otaliastudios: cameraview: 2.7.0 '
+Rápido y confiable
+Soporte de gestos [docs]
+Filtros en tiempo real [documentos]
+Motor impulsado por Camera1 o Camera2 [docs]
+Soporte de procesamiento de fotogramas [docs]
+Marcas de agua y superposiciones animadas [docs]
+Vista previa con tecnología OpenGL [docs]
+Toma contenido de alta calidad con takePicturey takeVideo [docs]
+Toma instantáneas súper rápidas con takePictureSnapshoty takeVideoSnapshot [docs]
+CameraViewTamaño inteligente: cree un tamaño de cualquier tamaño [docs]
+Controla HDR, flash, zoom, balance de blancos, exposición, ubicación, dibujo de cuadrícula y más [docs]
+Soporte de imágenes RAW [docs]
+Ligero
+Funciona hasta el nivel de API 15
+Bien probado
+⠀
+
+
+
+⠀
+
+Apoyo
+Si te gusta el proyecto, obtienes beneficios de él o simplemente quieres agradecer, ¡considera patrocinarme a través del programa de patrocinadores de GitHub! Puede tener el logotipo de su empresa aquí, obtener horas de soporte privado o simplemente ayudarme a impulsar esto. Si lo prefiere, también puede donar a nuestra página OpenCollective.
+
+CameraView cuenta con el respaldo de ShareChat , una aplicación de redes sociales con más de 100 millones de descargas.
+
+
+
+No dude en contactarme para soporte, consultoría o cualquier otra pregunta relacionada con el negocio.
+
+Gracias a todos los patrocinadores de nuestro proyecto ... [conviértete en patrocinador]
+
+
+
+... ¡ya todos nuestros patrocinadores de proyectos! [conviértete en patrocinador]
+
+
+
+Preparar
+Lea el sitio web oficial para obtener instrucciones de configuración y documentación. También puede estar interesado en nuestro registro de cambios o en la guía de migración v1 . Usar CameraView es extremadamente simple:
+
+< com .otaliastudios.cameraview.CameraView
+ xmlns : app = " http://schemas.android.com/apk/res-auto "
+ android : layout_width = " wrap_content "
+ android : layout_height = " wrap_content "
+ aplicación : cameraPictureSizeMinWidth = " @integer / picture_min_width "
+ aplicación : cameraPictureSizeMinHeight = " @ integer / picture_min_height "
+ aplicación : cameraPictureSizeMaxWidth= " @ Entero / picture_max_width "
+ aplicación : cameraPictureSizeMaxHeight = " @ entero / picture_max_height "
+ aplicación : cameraPictureSizeMinArea = " @ entero / picture_min_area "
+ aplicación : cameraPictureSizeMaxArea = " @ entero / picture_max_area "
+ aplicación : cameraPictureSizeSmallest = " falsa | verdadera "
+ aplicación : cameraPictureSizeBiggest = " falsa | verdadera "
+ aplicación :cameraPictureSizeAspectRatio = " @ string / video_ratio "
+ aplicación : cameraVideoSizeMinWidth = " @ número entero / video_min_width "
+ aplicación : cameraVideoSizeMinHeight = " @ número entero / video_min_height "
+ aplicación : cameraVideoSizeMaxWidth = " @ número entero / video_max_width "
+ aplicación : cameraVideoSizeMaxHeight = " @ número entero / video_max_height "
+ aplicación : cameraVideoSizeMinArea = " @ integer / video_min_area "
+ aplicación: CameraVideoSizeMaxArea = " @ entero / video_max_area "
+ aplicación : cameraVideoSizeSmallest = " falsa | verdadera "
+ aplicación : cameraVideoSizeBiggest = " falsa | verdadera "
+ aplicación : cameraVideoSizeAspectRatio = " @ string / video_ratio "
+ aplicación : cameraSnapshotMaxWidth = " @ entero / snapshot_max_width "
+ aplicación : cameraSnapshotMaxHeight = " @ número entero / snapshot_max_height "
+ aplicación :cameraFrameProcessingMaxWidth = " @ número entero / processing_max_width "
+ aplicación : cameraFrameProcessingMaxHeight = " @ número entero / processing_max_height "
+ aplicación : cameraFrameProcessingFormat = " @ número entero / processing_format "
+ aplicación : cameraFrameProcessingPoolSize = " @ número entero / processing_pool_size "
+ app : cameraFrameProcessingExecutors = " @ número entero / processing_executors "
+ aplicación : cameraVideoBitRate = "@ integer / video_bit_rate "
+ app : cameraAudioBitRate = " @ integer / audio_bit_rate "
+ app : cameraGestureTap = " none | autoFocus | takePicture "
+ aplicación : cameraGestureLongTap = " none | autoFocus | takePicture "
+ aplicación : cameraGesturePinch = " ninguno | zoom | exposiciónCorrección | filter filterControl2 "
+ aplicación : cameraGestureScrollHorizontal = " none | zoom | exposiciónCorrection | filterControl1 | filterControl2 "
+ aplicación: CameraGestureScrollVertical = " none | zoom | exposureCorrection | filterControl1 | filterControl2 "
+ aplicación : cameraEngine = " camera1 | camera2 "
+ aplicación : cameraPreview = " glSurface | superficie | textura "
+ aplicación : cameraPreviewFrameRate = " @ entero / preview_frame_rate "
+ aplicación : cameraPreviewFrameRateExact = " falsa | verdadero "
+ app : cameraFacing = " back | front "
+ aplicación :cameraHdr = " on | off "
+ aplicación : cameraFlash = " on | auto | antorcha | off "
+ aplicación : cameraWhiteBalance = " auto | nublado | luz del día | fluorescente | incandescente "
+ aplicación : cameraMode = " imagen | video "
+ aplicación : cameraAudio = " on | off | mono | stereo "
+ aplicación : cameraGrid = " draw3x3 | draw4x4 | drawPhi | off "
+ aplicación : cameraGridColor = "@ color / grid_color "
+ app : cameraPlaySounds = " true | false "
+ app : cameraVideoMaxSize = " @ entero / video_max_size "
+ aplicación : cameraVideoMaxDuration = " @ entero / video_max_duration "
+ aplicación : cameraVideoCodec = " deviceDefault | H264 | h263 "
+ aplicación : cameraAutoFocusResetDelay = " @ integer / autofocus_delay "
+ aplicación : cameraAutoFocusMarker = "@ string / cameraview_default_autofocus_marker "
+ app : cameraUseDeviceOrientation = " true | false "
+ app : cameraFilter = " @ string / real_time_filter "
+ app : cameraPictureMetering = " true | false "
+ app : cameraPictureSnapshotMetering = " false | true "
+ app : cameraPictureFormat = " jpeg | "
+ app : cameraRequestPermissions = " true | false"
+ app : cameraExperimental = " false | true " >
+
+
+ < ImageView
+ android : layout_width = " wrap_content "
+ android : layout_height = " wrap_content "
+ android : layout_gravity = " bottom | end "
+ android : src = " @ drawable / watermark "
+ app : layout_drawOnPreview = " true | false "
+ aplicación : layout_drawOnPictureSnapshot = " verdadero | falso"
+ aplicación : layout_drawOnVideoSnapshot = " true | false " />
+
+ com .otaliastudios.cameraview.CameraVi
diff --git a/art/icon.png b/art/icon.png
deleted file mode 100644
index 50c84432..00000000
Binary files a/art/icon.png and /dev/null differ
diff --git a/art/old_screen1.png b/art/old_screen1.png
deleted file mode 100644
index 72deb7c8..00000000
Binary files a/art/old_screen1.png and /dev/null differ
diff --git a/art/old_screen2.png b/art/old_screen2.png
deleted file mode 100644
index c0ae2a54..00000000
Binary files a/art/old_screen2.png and /dev/null differ
diff --git a/art/screen1.jpg b/art/screen1.jpg
deleted file mode 100644
index 4c132942..00000000
Binary files a/art/screen1.jpg and /dev/null differ
diff --git a/art/screen2.jpg b/art/screen2.jpg
deleted file mode 100644
index 2f0d3a29..00000000
Binary files a/art/screen2.jpg and /dev/null differ
diff --git a/art/screen3.jpg b/art/screen3.jpg
deleted file mode 100644
index 5abf710b..00000000
Binary files a/art/screen3.jpg and /dev/null differ
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index d26434d3..00000000
--- a/build.gradle
+++ /dev/null
@@ -1,35 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
-buildscript {
- repositories {
- jcenter()
- google()
- }
-
- dependencies {
- classpath 'com.android.tools.build:gradle:3.2.0-rc03'
- // https://inthecheesefactory.com/blog/how-to-upload-library-to-jcenter-maven-central-as-dependency/en
- // https://www.theguardian.com/technology/developer-blog/2016/dec/06/how-to-publish-an-android-library-a-mysterious-conversation
- classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'
- classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0'
- }
-}
-
-allprojects {
- repositories {
- jcenter()
- google()
- }
-}
-
-ext {
- compileSdkVersion = 28
- supportLibVersion = '28.0.0-rc02'
- lifecycleVersion = '1.1.1'
- minSdkVersion = 15
- targetSdkVersion = 28
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 00000000..de358275
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,32 @@
+
+buildscript {
+
+ extra["minSdkVersion"] = 15
+ extra["compileSdkVersion"] = 30
+ extra["targetSdkVersion"] = 30
+
+ repositories {
+ google()
+ mavenCentral()
+ jcenter()
+ }
+
+ dependencies {
+ classpath("com.android.tools.build:gradle:4.1.2")
+ classpath("io.deepmedia.tools:publisher:0.4.1")
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.21")
+
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ jcenter()
+ }
+}
+
+tasks.register("clean", Delete::class) {
+ delete(buildDir)
+}
\ No newline at end of file
diff --git a/cameraview/build.gradle b/cameraview/build.gradle
deleted file mode 100644
index 497eae1b..00000000
--- a/cameraview/build.gradle
+++ /dev/null
@@ -1,217 +0,0 @@
-apply plugin: 'com.android.library'
-apply plugin: 'com.github.dcendents.android-maven'
-apply plugin: 'com.jfrog.bintray'
-
-// Required by bintray
-version = '1.6.1'
-group = 'com.otaliastudios'
-
-//region android dependencies
-
-android {
- compileSdkVersion rootProject.ext.compileSdkVersion
- // buildToolsVersion rootProject.ext.buildToolsVersion
-
- defaultConfig {
- minSdkVersion rootProject.ext.minSdkVersion
- targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 1
- versionName project.version
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
- }
-
- buildTypes {
- debug {
- testCoverageEnabled true
- }
-
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
-
- sourceSets {
- main.java.srcDirs += 'src/main/options'
- main.java.srcDirs += 'src/main/views'
- main.java.srcDirs += 'src/main/utils'
- }
-}
-
-dependencies {
- testImplementation 'junit:junit:4.12'
- testImplementation 'org.mockito:mockito-core:1.10.19'
-
- androidTestImplementation 'com.android.support.test:runner:1.0.2'
- androidTestImplementation 'com.android.support.test:rules:1.0.2'
- androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
- androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
- androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
-
- api "com.android.support:exifinterface:$supportLibVersion"
- api "android.arch.lifecycle:common:$lifecycleVersion"
- implementation "com.android.support:support-annotations:$supportLibVersion"
-}
-
-//endregion
-
-//region bintray
-
-install {
- repositories.mavenInstaller {
- pom.project {
- name 'CameraView'
- description 'A well documented, high-level Android interface that makes capturing pictures ' +
- 'and videos easy, addressing most of the common issues and needs.'
- url 'https://github.com/natario1/CameraView'
-
- packaging 'aar'
- groupId project.group
- artifactId 'cameraview'
- version project.version
-
- licenses {
- license {
- name 'The Apache Software License, Version 2.0'
- url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
- }
- }
- scm {
- connection 'https://github.com/natario1/CameraView.git'
- developerConnection 'https://github.com/natario1/CameraView.git'
- url 'https://github.com/natario1/CameraView'
-
- }
- developers {
- developer {
- id = 'natario'
- name 'Natario'
- }
- }
- }
- }
-}
-
-def bintrayUser
-def bintrayKey
-def travis = System.getenv("TRAVIS")
-if (travis) {
- bintrayUser = System.getenv("BINTRAY_USER")
- bintrayKey = System.getenv("BINTRAY_KEY")
-} else {
- Properties props = new Properties()
- props.load(project.rootProject.file('local.properties').newDataInputStream())
- bintrayUser = props.getProperty('bintray.user')
- bintrayKey = props.get('bintray.key')
-}
-
-bintray {
- // https://github.com/bintray/gradle-bintray-plugin
- user = bintrayUser
- key = bintrayKey
- configurations = ['archives']
- pkg {
- repo = 'android'
- name = 'CameraView'
- licenses = ['Apache-2.0']
- vcsUrl = 'https://github.com/natario1/CameraView.git'
- publish = true
- override = true
- version {
- name = project.version
- desc = 'CameraView v. '+project.version
- released = new Date()
- vcsTag = 'v'+project.version
- }
- }
-}
-
-//endregion
-
-//region javadoc and sources
-
-// From official sample https://github.com/bintray/bintray-examples/blob/master/gradle-aar-example/build.gradle
-task sourcesJar(type: Jar) {
- classifier = 'sources'
- from android.sourceSets.main.java.sourceFiles
-}
-
-task javadoc(type: Javadoc) {
- source = android.sourceSets.main.java.srcDirs
- classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
- classpath += project.files("${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
- project.android.libraryVariants.all { variant ->
- if (variant.name == 'release') {
- classpath += files(variant.javaCompile.classpath)
- }
- }
- exclude '**/BuildConfig.java'
- exclude '**/R.java'
- exclude '**/internal/**'
-}
-
-task javadocJar(type: Jar, dependsOn: javadoc) {
- classifier = 'javadoc'
- from javadoc.destinationDir
-}
-
-artifacts {
- archives javadocJar
- archives sourcesJar
-}
-
-//endregion
-
-//region code coverage
-
-// 1. running androidTests with connectedCheck will generate an .ec file
-// in build/outputs/code-coverage/connected, plus the XML result in
-// in build/reports/coverage/debug/report.xml .
-
-// 2. running unit tests with testDebugUnitTest will just generate the .exec file.
-// The JacocoReport task from the jacoco plugin can create the XML report out of it.
-
-// to have a unified report, we just pass both the .exec and the .ec file
-// to the jacoco task, so we get a unified XML report with total coverage.
-// Reference: https://medium.com/@rafael_toledo/setting-up-an-unified-coverage-report-in-android-with-jacoco-robolectric-and-espresso-ffe239aaf3fa
-
-apply plugin: 'jacoco'
-
-def reportsDirectory = "$buildDir/reports/"
-jacoco {
- toolVersion = "0.8.1"
- reportsDir = file(reportsDirectory)
-}
-
-task mergedCoverageReport(type: JacocoReport) {
- dependsOn "testDebugUnitTest"
- dependsOn "connectedCheck"
-
- def testData = "jacoco/testDebugUnitTest.exec"
- def androidTestData = "outputs/code-coverage/connected/*coverage.ec"
- executionData = fileTree(dir: "$buildDir", includes: [testData, androidTestData])
-
- // Sources.
- sourceDirectories = android.sourceSets.main.java.sourceFiles
- // Add BuildConfig and R.
- additionalSourceDirs = files([
- "$buildDir/generated/source/buildConfig/debug",
- "$buildDir/generated/source/r/debug"
- ])
-
- // Classes.
- def debugDir = "$buildDir/intermediates/classes/debug"
- def filter = ['**/R.class', '**/R$*.class', '**/*$ViewInjector*.*',
- '**/BuildConfig.*', '**/Manifest*.*']
- classDirectories = fileTree(dir: debugDir, excludes: filter);
-
- reports.xml.enabled = true
- reports.html.enabled = true
- reports.xml.destination = "$reportsDirectory/mergedCoverageReport/report.xml"
-}
-
-//endregion
-
-// export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home
-// To deploy ./gradlew bintrayUpload
-
diff --git a/cameraview/build.gradle.kts b/cameraview/build.gradle.kts
new file mode 100644
index 00000000..f1e0ac8b
--- /dev/null
+++ b/cameraview/build.gradle.kts
@@ -0,0 +1,125 @@
+import io.deepmedia.tools.publisher.common.License
+import io.deepmedia.tools.publisher.common.Release
+
+plugins {
+ id("com.android.library")
+ id("kotlin-android")
+ id("io.deepmedia.tools.publisher")
+ id("jacoco")
+}
+
+android {
+ setCompileSdkVersion(property("compileSdkVersion") as Int)
+ defaultConfig {
+ setMinSdkVersion(property("minSdkVersion") as Int)
+ setTargetSdkVersion(property("targetSdkVersion") as Int)
+ versionCode = 1
+ versionName = "2.7.0"
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunnerArgument("filter", "" +
+ "com.otaliastudios.cameraview.tools.SdkExcludeFilter," +
+ "com.otaliastudios.cameraview.tools.SdkIncludeFilter")
+ }
+ buildTypes["debug"].isTestCoverageEnabled = true
+ buildTypes["release"].isMinifyEnabled = false
+}
+
+dependencies {
+ testImplementation("junit:junit:4.13")
+ testImplementation("org.mockito:mockito-inline:2.28.2")
+
+ androidTestImplementation("androidx.test:runner:1.3.0")
+ androidTestImplementation("androidx.test:rules:1.3.0")
+ androidTestImplementation("androidx.test.ext:junit:1.1.2")
+ androidTestImplementation("org.mockito:mockito-android:2.28.2")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.2.0")
+
+ api("androidx.exifinterface:exifinterface:1.3.2")
+ api("androidx.lifecycle:lifecycle-common:2.2.0")
+ api("com.google.android.gms:play-services-tasks:17.2.0")
+ implementation("androidx.annotation:annotation:1.1.0")
+ implementation("com.otaliastudios.opengl:egloo:0.5.3")
+}
+
+// Publishing
+
+publisher {
+ project.description = "A well documented, high-level Android interface that makes capturing " +
+ "pictures and videos easy, addressing all of the common issues and needs. " +
+ "Real-time filters, gestures, watermarks, frame processing, RAW, output of any size."
+ project.artifact = "cameraview"
+ project.group = "com.otaliastudios"
+ project.url = "https://github.com/natario1/CameraView"
+ project.addLicense(License.APACHE_2_0)
+ bintray {
+ release.sources = Release.SOURCES_AUTO
+ release.docs = Release.DOCS_AUTO
+ auth.user = "BINTRAY_USER"
+ auth.key = "BINTRAY_KEY"
+ auth.repo = "BINTRAY_REPO"
+ }
+ directory {
+ directory = file(repositories.mavenLocal().url).absolutePath
+ }
+}
+
+// Code Coverage
+val buildDir = project.buildDir.absolutePath
+val coverageInputDir = "$buildDir/coverage_input" // changing? change github workflow
+val coverageOutputDir = "$buildDir/coverage_output" // changing? change github workflow
+
+// Run unit tests, with coverage enabled in the android { } configuration.
+// Output will be an .exec file in build/jacoco.
+tasks.register("runUnitTests") { // changing name? change github workflow
+ dependsOn("testDebugUnitTest")
+ doLast {
+ copy {
+ from("$buildDir/jacoco/testDebugUnitTest.exec")
+ into("$coverageInputDir/unit_tests") // changing? change github workflow
+ }
+ }
+}
+
+// Run android tests with coverage.
+tasks.register("runAndroidTests") { // changing name? change github workflow
+ dependsOn("connectedDebugAndroidTest")
+ doLast {
+ copy {
+ from("$buildDir/outputs/code_coverage/debugAndroidTest/connected")
+ include("*coverage.ec")
+ into("$coverageInputDir/android_tests") // changing? change github workflow
+ }
+ }
+}
+
+// Merge the two with a jacoco task.
+jacoco { toolVersion = "0.8.5" }
+tasks.register("computeCoverage", JacocoReport::class) {
+ dependsOn("compileDebugSources") // Compile sources, needed below
+ executionData.from(fileTree(coverageInputDir))
+ sourceDirectories.from(android.sourceSets["main"].java.srcDirs)
+ additionalSourceDirs.from("$buildDir/generated/source/buildConfig/debug")
+ additionalSourceDirs.from("$buildDir/generated/source/r/debug")
+ classDirectories.from(fileTree("$buildDir/intermediates/javac/debug") {
+ // Not everything here is relevant for CameraView, but let's keep it generic
+ exclude(
+ "**/R.class",
+ "**/R$*.class",
+ "**/BuildConfig.*",
+ "**/Manifest*.*",
+ "android/**",
+ "androidx/**",
+ "com/google/**",
+ "**/*\$ViewInjector*.*",
+ "**/Dagger*Component.class",
+ "**/Dagger*Component\$Builder.class",
+ "**/*Module_*Factory.class",
+ // We don"t test OpenGL filters.
+ "**/com/otaliastudios/cameraview/filters/**.*"
+ )
+ })
+ reports.html.isEnabled = true
+ reports.xml.isEnabled = true
+ reports.html.destination = file("$coverageOutputDir/html")
+ reports.xml.destination = file("$coverageOutputDir/xml/report.xml")
+}
\ No newline at end of file
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseEglTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseEglTest.java
new file mode 100644
index 00000000..18865899
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseEglTest.java
@@ -0,0 +1,36 @@
+package com.otaliastudios.cameraview;
+
+import android.opengl.EGL14;
+
+import com.otaliastudios.opengl.core.EglCore;
+import com.otaliastudios.opengl.surface.EglOffscreenSurface;
+import com.otaliastudios.opengl.surface.EglSurface;
+
+import org.junit.After;
+import org.junit.Before;
+
+
+@SuppressWarnings("WeakerAccess")
+public abstract class BaseEglTest extends BaseTest {
+
+ protected final static int WIDTH = 100;
+ protected final static int HEIGHT = 100;
+
+ protected EglCore eglCore;
+ protected EglSurface eglSurface;
+
+ @Before
+ public void setUp() {
+ eglCore = new EglCore(EGL14.EGL_NO_CONTEXT, EglCore.FLAG_RECORDABLE);
+ eglSurface = new EglOffscreenSurface(eglCore, WIDTH, HEIGHT);
+ eglSurface.makeCurrent();
+ }
+
+ @After
+ public void tearDown() {
+ eglSurface.release();
+ eglSurface = null;
+ eglCore.release();
+ eglCore = null;
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java
index 7d2e3a59..3f447902 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java
@@ -1,63 +1,50 @@
package com.otaliastudios.cameraview;
-import android.annotation.SuppressLint;
import android.app.KeyguardManager;
import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Rect;
-import android.graphics.YuvImage;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.annotation.UiThreadTest;
-import android.support.test.espresso.core.internal.deps.guava.collect.ObjectArrays;
-import android.support.test.rule.ActivityTestRule;
-import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.GrantPermissionRule;
+
+import com.otaliastudios.cameraview.tools.Op;
import org.junit.After;
import org.junit.AfterClass;
-import org.junit.Before;
import org.junit.BeforeClass;
-import org.junit.Rule;
+import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.mockito.stubbing.Stubber;
-import java.io.ByteArrayOutputStream;
-import java.io.OutputStream;
import java.util.concurrent.CountDownLatch;
import static android.content.Context.KEYGUARD_SERVICE;
import static android.content.Context.POWER_SERVICE;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
public class BaseTest {
- public static CameraLogger LOG = CameraLogger.create("Test");
-
private static KeyguardManager.KeyguardLock keyguardLock;
private static PowerManager.WakeLock wakeLock;
// https://github.com/linkedin/test-butler/blob/bc2bb4df13d0a554d2e2b0ea710795017717e710/test-butler-app/src/main/java/com/linkedin/android/testbutler/ButlerService.java#L121
@BeforeClass
- @SuppressWarnings("MissingPermission")
- public static void wakeUp() {
+ public static void beforeClass_wakeUp() {
CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE);
// Acquire a keyguard lock to prevent the lock screen from randomly appearing and breaking tests
- KeyguardManager keyguardManager = (KeyguardManager) context().getSystemService(KEYGUARD_SERVICE);
+ KeyguardManager keyguardManager = (KeyguardManager) getContext().getSystemService(KEYGUARD_SERVICE);
keyguardLock = keyguardManager.newKeyguardLock("CameraViewLock");
keyguardLock.disableKeyguard();
// Acquire a wake lock to prevent the cpu from going to sleep and breaking tests
- PowerManager powerManager = (PowerManager) context().getSystemService(POWER_SERVICE);
+ PowerManager powerManager = (PowerManager) getContext().getSystemService(POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK
| PowerManager.ACQUIRE_CAUSES_WAKEUP
| PowerManager.ON_AFTER_RELEASE, "CameraViewLock");
@@ -65,109 +52,68 @@ public class BaseTest {
}
@AfterClass
- @SuppressWarnings("MissingPermission")
- public static void releaseWakeUp() {
+ public static void afterClass_releaseWakeUp() {
+ CameraLogger.setLogLevel(CameraLogger.LEVEL_ERROR);
+
wakeLock.release();
keyguardLock.reenableKeyguard();
}
- public static void ui(Runnable runnable) {
- InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
- }
-
- public static void uiAsync(Runnable runnable) {
- new Handler(Looper.getMainLooper()).post(runnable);
+ /**
+ * This will make mockito report the error when it should.
+ * Mockito reports failure on the next mockito invocation, which is terrible
+ * since it might be on the next test or even never happen.
+ */
+ @After
+ public void after_checkMockito() {
+ Object object = Mockito.mock(Object.class);
+ //noinspection ResultOfMethodCallIgnored
+ object.toString();
}
- public static Context context() {
+ @NonNull
+ protected static Context getContext() {
return InstrumentationRegistry.getInstrumentation().getContext();
}
- public static void uiRequestLayout(final View view) {
- ui(new Runnable() {
- @Override
- public void run() {
- view.requestLayout();
- }
- });
- }
-
- public static void idle() {
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- }
-
- public static void sleep(long time) {
- try {
- Thread.sleep(time);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- public static void grantPermissions() {
- grantPermission("android.permission.CAMERA");
- grantPermission("android.permission.RECORD_AUDIO");
- grantPermission("android.permission.WRITE_EXTERNAL_STORAGE");
- }
-
- public static void grantPermission(String permission) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return;
- String command = "pm grant " + context().getPackageName() + " " + permission;
- InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(command);
+ protected static void uiSync(Runnable runnable) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
}
- public static byte[] mockJpeg(int width, int height) {
- Bitmap source = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- ByteArrayOutputStream os = new ByteArrayOutputStream();
- source.compress(Bitmap.CompressFormat.JPEG, 100, os);
- return os.toByteArray();
+ @SuppressWarnings("unused")
+ protected static void uiAsync(Runnable runnable) {
+ new Handler(Looper.getMainLooper()).post(runnable);
}
- public static YuvImage mockYuv(int width, int height) {
- YuvImage y = mock(YuvImage.class);
- when(y.getWidth()).thenReturn(width);
- when(y.getHeight()).thenReturn(height);
- when(y.compressToJpeg(any(Rect.class), anyInt(), any(OutputStream.class))).thenAnswer(new Answer() {
- @Override
- public Boolean answer(InvocationOnMock invocation) throws Throwable {
- Rect rect = (Rect) invocation.getArguments()[0];
- OutputStream stream = (OutputStream) invocation.getArguments()[2];
- stream.write(mockJpeg(rect.width(), rect.height()));
- return true;
- }
- });
- return y;
+ @SuppressWarnings("unused")
+ protected static void waitUiIdle() {
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
- public static Stubber doCountDown(final CountDownLatch latch) {
+ @NonNull
+ protected static Stubber doCountDown(@NonNull final CountDownLatch latch) {
return doAnswer(new Answer() {
@Override
- public Object answer(InvocationOnMock invocation) throws Throwable {
+ public Object answer(InvocationOnMock invocation) {
latch.countDown();
return null;
}
});
}
- public static Stubber doEndTask(final Task task, final T response) {
+ @NonNull
+ protected static Stubber doEndOp(final Op op, final T response) {
return doAnswer(new Answer() {
@Override
- public Object answer(InvocationOnMock invocation) throws Throwable {
- task.end(response);
+ public Object answer(InvocationOnMock invocation) {
+ op.controller().end(response);
return null;
}
});
}
- public static Stubber doEndTask(final Task task, final int withReturnArgument) {
- return doAnswer(new Answer() {
- @Override
- public Object answer(InvocationOnMock invocation) throws Throwable {
- Object o = invocation.getArguments()[withReturnArgument];
- //noinspection unchecked
- task.end(o);
- return null;
- }
- });
+ @NonNull
+ protected static Stubber doEndOp(final Op op, final int withReturnArgument) {
+ return op.controller().from(withReturnArgument);
}
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraLoggerTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraLoggerTest.java
index 332cbfa8..d3d985a4 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraLoggerTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraLoggerTest.java
@@ -1,15 +1,15 @@
package com.otaliastudios.cameraview;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import com.otaliastudios.cameraview.tools.Op;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@@ -24,11 +24,13 @@ public class CameraLoggerTest extends BaseTest {
@Before
public void setUp() {
CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE);
+ CameraLogger.unregisterLogger(CameraLogger.sAndroidLogger); // Avoid writing into Logs during these tests
logger = CameraLogger.create(loggerTag);
}
@After
public void tearDown() {
+ CameraLogger.registerLogger(CameraLogger.sAndroidLogger);
logger = null;
}
@@ -107,27 +109,21 @@ public class CameraLoggerTest extends BaseTest {
CameraLogger.Logger mock = mock(CameraLogger.Logger.class);
CameraLogger.registerLogger(mock);
- final Task task = new Task<>();
- doAnswer(new Answer() {
- @Override
- public Object answer(InvocationOnMock invocation) throws Throwable {
- Object[] args = invocation.getArguments();
- Throwable throwable = (Throwable) args[3];
- task.end(throwable);
- return null;
- }
- }).when(mock).log(anyInt(), anyString(), anyString(), any(Throwable.class));
-
- task.listen();
+ final Op op = new Op<>(false);
+ doEndOp(op, 3)
+ .when(mock)
+ .log(anyInt(), anyString(), anyString(), any(Throwable.class));
+
+ op.listen();
logger.e("Got no error.");
- assertNull(task.await(100));
+ assertNull(op.await(100));
- task.listen();
+ op.listen();
logger.e("Got error:", new RuntimeException(""));
- assertNotNull(task.await(100));
+ assertNotNull(op.await(100));
- task.listen();
+ op.listen();
logger.e("Got", new RuntimeException(""), "while starting");
- assertNotNull(task.await(100));
+ assertNotNull(op.await(100));
}
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java
index 7961e376..8da82853 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java
@@ -1,21 +1,26 @@
package com.otaliastudios.cameraview;
-import android.annotation.TargetApi;
-import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Color;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.internal.runner.InstrumentationConnection;
-import android.support.test.runner.AndroidJUnit4;
+
+import com.otaliastudios.cameraview.tools.Op;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
@@ -29,44 +34,93 @@ public class CameraUtilsTest extends BaseTest {
Context context = mock(Context.class);
PackageManager pm = mock(PackageManager.class);
when(context.getPackageManager()).thenReturn(pm);
- when(pm.hasSystemFeature(anyString())).thenReturn(true);
+ when(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)).thenReturn(true);
+ when(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)).thenReturn(true);
assertTrue(CameraUtils.hasCameras(context));
-
- when(pm.hasSystemFeature(anyString())).thenReturn(false);
+ when(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)).thenReturn(false);
+ when(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)).thenReturn(true);
+ assertTrue(CameraUtils.hasCameras(context));
+ when(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)).thenReturn(false);
+ when(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)).thenReturn(false);
assertFalse(CameraUtils.hasCameras(context));
}
- // Encodes bitmap and decodes again using our utility.
- private Task encodeDecodeTask(Bitmap source) {
- return encodeDecodeTask(source, 0, 0);
+ @NonNull
+ private Op writeAndReadString(@NonNull String data) {
+ final File file = new File(getContext().getFilesDir(), "string.txt");
+ final byte[] bytes = data.getBytes(Charset.forName("UTF-8"));
+ final Op result = new Op<>();
+ final FileCallback callback = new FileCallback() {
+ @Override
+ public void onFileReady(@Nullable File file) {
+ if (file == null) {
+ result.controller().end(null);
+ } else {
+ // Read back the file.
+ try {
+ FileInputStream stream = new FileInputStream(file);
+ byte[] bytes = new byte[stream.available()];
+ stream.read(bytes);
+ result.controller().end(new String(bytes, Charset.forName("UTF-8")));
+ } catch (IOException e) {
+ result.controller().end(null);
+ }
+ }
+ }
+ };
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ CameraUtils.writeToFile(bytes, file, callback);
+ }
+ });
+ return result;
+ }
+
+ @Test
+ public void testWriteToFile() {
+ Op op = writeAndReadString("testString");
+ String result = op.await(2000);
+ assertEquals("testString", result);
}
+
// Encodes bitmap and decodes again using our utility.
- private Task encodeDecodeTask(Bitmap source, final int maxWidth, final int maxHeight) {
+ private Op encodeDecodeTask(@NonNull Bitmap source, final int maxWidth, final int maxHeight, boolean async) {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
// Using lossy JPG we can't have strict comparison of values after compression.
source.compress(Bitmap.CompressFormat.PNG, 100, os);
final byte[] data = os.toByteArray();
- final Task decode = new Task<>(true);
- final CameraUtils.BitmapCallback callback = new CameraUtils.BitmapCallback() {
- @Override
- public void onBitmapReady(Bitmap bitmap) {
- decode.end(bitmap);
- }
- };
-
- // Run on ui because it involves handlers.
- ui(new Runnable() {
- @Override
- public void run() {
- if (maxWidth > 0 && maxHeight > 0) {
- CameraUtils.decodeBitmap(data, maxWidth, maxHeight, callback);
- } else {
- CameraUtils.decodeBitmap(data, callback);
+ final Op decode = new Op<>();
+ if (async) {
+ final BitmapCallback callback = new BitmapCallback() {
+ @Override
+ public void onBitmapReady(Bitmap bitmap) {
+ decode.controller().end(bitmap);
+ }
+ };
+
+ // Run on ui because it involves handlers.
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ if (maxWidth > 0 && maxHeight > 0) {
+ CameraUtils.decodeBitmap(data, maxWidth, maxHeight, callback);
+ } else {
+ CameraUtils.decodeBitmap(data, callback);
+ }
}
+ });
+ } else {
+ Bitmap result;
+ if (maxWidth > 0 && maxHeight > 0) {
+ result = CameraUtils.decodeBitmap(data, maxWidth, maxHeight);
+ } else {
+ result = CameraUtils.decodeBitmap(data);
}
- });
+ decode.controller().end(result);
+ }
return decode;
}
@@ -76,7 +130,7 @@ public class CameraUtilsTest extends BaseTest {
Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
source.setPixel(0, 0, color);
- Task decode = encodeDecodeTask(source);
+ Op decode = encodeDecodeTask(source, 0, 0, true);
Bitmap other = decode.await(800);
assertNotNull(other);
assertEquals(100, w);
@@ -85,8 +139,23 @@ public class CameraUtilsTest extends BaseTest {
assertEquals(0, other.getPixel(0, h-1));
assertEquals(0, other.getPixel(w-1, 0));
assertEquals(0, other.getPixel(w-1, h-1));
+ }
+
+ @Test
+ public void testDecodeBitmapSync() {
+ int w = 100, h = 200, color = Color.WHITE;
+ Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ source.setPixel(0, 0, color);
- // TODO: improve when we add EXIF writing to byte arrays
+ Op decode = encodeDecodeTask(source, 0, 0, false);
+ Bitmap other = decode.await(800);
+ assertNotNull(other);
+ assertEquals(100, w);
+ assertEquals(200, h);
+ assertEquals(color, other.getPixel(0, 0));
+ assertEquals(0, other.getPixel(0, h-1));
+ assertEquals(0, other.getPixel(w-1, 0));
+ assertEquals(0, other.getPixel(w-1, h-1));
}
@@ -94,25 +163,25 @@ public class CameraUtilsTest extends BaseTest {
public void testDecodeDownscaledBitmap() {
int width = 1000, height = 2000;
Bitmap source = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- Task task;
+ Op op;
Bitmap other;
- task = encodeDecodeTask(source, 100, 100);
- other = task.await(800);
+ op = encodeDecodeTask(source, 100, 100, true);
+ other = op.await(800);
assertNotNull(other);
assertTrue(other.getWidth() <= 100);
assertTrue(other.getHeight() <= 100);
- task = encodeDecodeTask(source, Integer.MAX_VALUE, Integer.MAX_VALUE);
- other = task.await(800);
+ op = encodeDecodeTask(source, Integer.MAX_VALUE, Integer.MAX_VALUE, true);
+ other = op.await(800);
assertNotNull(other);
- assertTrue(other.getWidth() == width);
- assertTrue(other.getHeight() == height);
+ assertEquals(other.getWidth(), width);
+ assertEquals(other.getHeight(), height);
- task = encodeDecodeTask(source, 6000, 6000);
- other = task.await(800);
+ op = encodeDecodeTask(source, 6000, 6000, true);
+ other = op.await(800);
assertNotNull(other);
- assertTrue(other.getWidth() == width);
- assertTrue(other.getHeight() == height);
+ assertEquals(other.getWidth(), width);
+ assertEquals(other.getHeight(), height);
}
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java
index 5f5054e1..a63e2b54 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java
@@ -2,75 +2,95 @@ package com.otaliastudios.cameraview;
import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
import android.graphics.PointF;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
import android.view.ViewGroup;
+import com.otaliastudios.cameraview.controls.Audio;
+import com.otaliastudios.cameraview.controls.Engine;
+import com.otaliastudios.cameraview.controls.Preview;
+import com.otaliastudios.cameraview.engine.CameraEngine;
+import com.otaliastudios.cameraview.frame.Frame;
+import com.otaliastudios.cameraview.frame.FrameProcessor;
+import com.otaliastudios.cameraview.gesture.Gesture;
+import com.otaliastudios.cameraview.gesture.GestureAction;
+import com.otaliastudios.cameraview.tools.Op;
+import com.otaliastudios.cameraview.engine.MockCameraEngine;
+import com.otaliastudios.cameraview.markers.AutoFocusMarker;
+import com.otaliastudios.cameraview.markers.AutoFocusTrigger;
+import com.otaliastudios.cameraview.markers.MarkerLayout;
+import com.otaliastudios.cameraview.preview.MockCameraPreview;
+import com.otaliastudios.cameraview.preview.CameraPreview;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-import org.mockito.stubbing.Stubber;
import static junit.framework.Assert.assertNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyFloat;
import static org.mockito.Matchers.anyInt;
-import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+/**
+ * Tests {@link CameraView#mCameraCallbacks} dispatch functions.
+ */
@RunWith(AndroidJUnit4.class)
@MediumTest
public class CameraViewCallbacksTest extends BaseTest {
+ private final static long DELAY = 500;
+
private CameraView camera;
private CameraListener listener;
private FrameProcessor processor;
- private MockCameraController mockController;
+ private MockCameraEngine mockController;
private MockCameraPreview mockPreview;
- private Task task;
-
+ private Op op;
@Before
public void setUp() {
- ui(new Runnable() {
+ uiSync(new Runnable() {
@Override
public void run() {
- Context context = context();
+ Context context = getContext();
listener = mock(CameraListener.class);
processor = mock(FrameProcessor.class);
camera = new CameraView(context) {
+
+ @NonNull
@Override
- protected CameraController instantiateCameraController(CameraCallbacks callbacks) {
- mockController = new MockCameraController(callbacks);
+ protected CameraEngine instantiateCameraEngine(@NonNull Engine engine, @NonNull CameraEngine.Callback callback) {
+ mockController = new MockCameraEngine(callback);
return mockController;
}
+ @NonNull
@Override
- protected CameraPreview instantiatePreview(Context context, ViewGroup container) {
+ protected CameraPreview instantiatePreview(@NonNull Preview preview, @NonNull Context context, @NonNull ViewGroup container) {
mockPreview = new MockCameraPreview(context, container);
return mockPreview;
}
@Override
- protected boolean checkPermissions(SessionType sessionType, Audio audio) {
+ protected boolean checkPermissions(@NonNull Audio audio) {
return true;
}
};
- camera.instantiatePreview();
+ camera.doInstantiatePreview();
camera.addCameraListener(listener);
camera.addFrameProcessor(processor);
- task = new Task<>(true);
+ op = new Op<>();
}
});
}
@@ -83,211 +103,176 @@ public class CameraViewCallbacksTest extends BaseTest {
listener = null;
}
- // Completes our task.
- private Stubber completeTask() {
- return doAnswer(new Answer() {
- @Override
- public Object answer(InvocationOnMock invocation) throws Throwable {
- task.end(true);
- return null;
- }
- });
- }
-
@Test
public void testDontDispatchIfRemoved() {
camera.removeCameraListener(listener);
- completeTask().when(listener).onCameraOpened(null);
- camera.mCameraCallbacks.dispatchOnCameraOpened(null);
+ CameraOptions options = mock(CameraOptions.class);
+ doEndOp(op, true).when(listener).onCameraOpened(options);
+ camera.mCameraCallbacks.dispatchOnCameraOpened(options);
- assertNull(task.await(200));
- verify(listener, never()).onCameraOpened(null);
+ assertNull(op.await(DELAY));
+ verify(listener, never()).onCameraOpened(options);
}
@Test
public void testDontDispatchIfCleared() {
camera.clearCameraListeners();
- completeTask().when(listener).onCameraOpened(null);
- camera.mCameraCallbacks.dispatchOnCameraOpened(null);
+ CameraOptions options = mock(CameraOptions.class);
+ doEndOp(op, true).when(listener).onCameraOpened(options);
+ camera.mCameraCallbacks.dispatchOnCameraOpened(options);
- assertNull(task.await(200));
- verify(listener, never()).onCameraOpened(null);
+ assertNull(op.await(DELAY));
+ verify(listener, never()).onCameraOpened(options);
}
@Test
public void testDispatchOnCameraOpened() {
- completeTask().when(listener).onCameraOpened(null);
- camera.mCameraCallbacks.dispatchOnCameraOpened(null);
+ CameraOptions options = mock(CameraOptions.class);
+ doEndOp(op, true).when(listener).onCameraOpened(options);
+ camera.mCameraCallbacks.dispatchOnCameraOpened(options);
- assertNotNull(task.await(200));
- verify(listener, times(1)).onCameraOpened(null);
+ assertNotNull(op.await(DELAY));
+ verify(listener, times(1)).onCameraOpened(options);
}
@Test
public void testDispatchOnCameraClosed() {
- completeTask().when(listener).onCameraClosed();
+ doEndOp(op, true).when(listener).onCameraClosed();
camera.mCameraCallbacks.dispatchOnCameraClosed();
- assertNotNull(task.await(200));
+ assertNotNull(op.await(DELAY));
verify(listener, times(1)).onCameraClosed();
}
+ @Test
+ public void testDispatchOnVideoRecordingStart() {
+ doEndOp(op, true).when(listener).onVideoRecordingStart();
+ camera.mCameraCallbacks.dispatchOnVideoRecordingStart();
+
+ assertNotNull(op.await(DELAY));
+ verify(listener, times(1)).onVideoRecordingStart();
+ }
+
+ @Test
+ public void testDispatchOnVideoRecordingEnd() {
+ doEndOp(op, true).when(listener).onVideoRecordingEnd();
+ camera.mCameraCallbacks.dispatchOnVideoRecordingEnd();
+
+ assertNotNull(op.await(DELAY));
+ verify(listener, times(1)).onVideoRecordingEnd();
+ }
+
@Test
public void testDispatchOnVideoTaken() {
- completeTask().when(listener).onVideoTaken(null);
- camera.mCameraCallbacks.dispatchOnVideoTaken(null);
+ VideoResult.Stub stub = new VideoResult.Stub();
+ doEndOp(op, true).when(listener).onVideoTaken(any(VideoResult.class));
+ camera.mCameraCallbacks.dispatchOnVideoTaken(stub);
- assertNotNull(task.await(200));
- verify(listener, times(1)).onVideoTaken(null);
+ assertNotNull(op.await(DELAY));
+ verify(listener, times(1)).onVideoTaken(any(VideoResult.class));
+ }
+
+ @Test
+ public void testDispatchOnPictureTaken() {
+ PictureResult.Stub stub = new PictureResult.Stub();
+ doEndOp(op, true).when(listener).onPictureTaken(any(PictureResult.class));
+ camera.mCameraCallbacks.dispatchOnPictureTaken(stub);
+ assertNotNull(op.await(DELAY));
+ verify(listener, times(1)).onPictureTaken(any(PictureResult.class));
}
@Test
public void testDispatchOnZoomChanged() {
- completeTask().when(listener).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class));
+ doEndOp(op, true).when(listener).onZoomChanged(eq(0f), eq(new float[]{0, 1}), nullable(PointF[].class));
camera.mCameraCallbacks.dispatchOnZoomChanged(0f, null);
- assertNotNull(task.await(200));
- verify(listener, times(1)).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class));
+ assertNotNull(op.await(DELAY));
+ verify(listener, times(1)).onZoomChanged(eq(0f), eq(new float[]{0, 1}), nullable(PointF[].class));
}
@Test
public void testDispatchOnExposureCorrectionChanged() {
- completeTask().when(listener).onExposureCorrectionChanged(0f, null, null);
- camera.mCameraCallbacks.dispatchOnExposureCorrectionChanged(0f, null, null);
+ float[] bounds = new float[]{};
+ doEndOp(op, true).when(listener).onExposureCorrectionChanged(0f, bounds, null);
+ camera.mCameraCallbacks.dispatchOnExposureCorrectionChanged(0f, bounds, null);
- assertNotNull(task.await(200));
- verify(listener, times(1)).onExposureCorrectionChanged(0f, null, null);
+ assertNotNull(op.await(DELAY));
+ verify(listener, times(1)).onExposureCorrectionChanged(0f, bounds, null);
}
@Test
public void testDispatchOnFocusStart() {
// Enable tap gesture.
- // Can't mock package protected. camera.mTapGestureLayout = mock(TapGestureLayout.class);
- camera.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER);
+ // Can't mock package protected. camera.mTapGestureFinder = mock(TapGestureLayout.class);
+ camera.mapGesture(Gesture.TAP, GestureAction.AUTO_FOCUS);
+ AutoFocusMarker marker = mock(AutoFocusMarker.class);
+ MarkerLayout markerLayout = mock(MarkerLayout.class);
+ camera.setAutoFocusMarker(marker);
+ camera.mMarkerLayout = markerLayout;
PointF point = new PointF();
- completeTask().when(listener).onFocusStart(point);
+ doEndOp(op, true).when(listener).onAutoFocusStart(point);
camera.mCameraCallbacks.dispatchOnFocusStart(Gesture.TAP, point);
- assertNotNull(task.await(200));
- verify(listener, times(1)).onFocusStart(point);
- // Can't mock package protected. verify(camera.mTapGestureLayout, times(1)).onFocusStart(point);
+ assertNotNull(op.await(DELAY));
+ verify(listener, times(1)).onAutoFocusStart(point);
+ verify(marker, times(1)).onAutoFocusStart(AutoFocusTrigger.GESTURE, point);
+ verify(markerLayout, times(1)).onEvent(eq(MarkerLayout.TYPE_AUTOFOCUS), any(PointF[].class));
}
@Test
public void testDispatchOnFocusEnd() {
// Enable tap gesture.
- // Can't mock package protected. camera.mTapGestureLayout = mock(TapGestureLayout.class);
- camera.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER);
+ // Can't mock package protected. camera.mTapGestureFinder = mock(TapGestureLayout.class);
+ camera.mapGesture(Gesture.TAP, GestureAction.AUTO_FOCUS);
+ AutoFocusMarker marker = mock(AutoFocusMarker.class);
+ camera.setAutoFocusMarker(marker);
PointF point = new PointF();
boolean success = true;
- completeTask().when(listener).onFocusEnd(success, point);
+ doEndOp(op, true).when(listener).onAutoFocusEnd(success, point);
camera.mCameraCallbacks.dispatchOnFocusEnd(Gesture.TAP, success, point);
- assertNotNull(task.await(200));
- verify(listener, times(1)).onFocusEnd(success, point);
- // Can't mock package protected. verify(camera.mTapGestureLayout, times(1)).onFocusEnd(success);
+ assertNotNull(op.await(DELAY));
+ verify(listener, times(1)).onAutoFocusEnd(success, point);
+ verify(marker, times(1)).onAutoFocusEnd(AutoFocusTrigger.GESTURE, success, point);
+
+ // Can't mock package protected. verify(camera.mTapGestureFinder, times(1)).onAutoFocusEnd(success);
}
@Test
public void testOrientationCallbacks() {
- completeTask().when(listener).onOrientationChanged(anyInt());
+ doEndOp(op, true).when(listener).onOrientationChanged(anyInt());
camera.mCameraCallbacks.onDeviceOrientationChanged(90);
- assertNotNull(task.await(200));
+ assertNotNull(op.await(DELAY));
verify(listener, times(1)).onOrientationChanged(anyInt());
}
- // TODO: test onShutter, here or elsewhere
+ @Test
+ public void testOnShutter() {
+ doEndOp(op, true).when(listener).onPictureShutter();
+ camera.mCameraCallbacks.dispatchOnPictureShutter(true);
+ assertNotNull(op.await(DELAY));
+ verify(listener, times(1)).onPictureShutter();
+ }
@Test
public void testCameraError() {
CameraException error = new CameraException(new RuntimeException("Error"));
- completeTask().when(listener).onCameraError(error);
+ doEndOp(op, true).when(listener).onCameraError(error);
camera.mCameraCallbacks.dispatchError(error);
- assertNotNull(task.await(200));
+ assertNotNull(op.await(DELAY));
verify(listener, times(1)).onCameraError(error);
}
- @Test
- public void testProcessJpeg() {
- int[] viewDim = new int[]{ 200, 200 };
- int[] imageDim = new int[]{ 1000, 1600 };
-
- // With crop flag: expect a 1:1 ratio.
- int[] output = testProcessImage(true, true, viewDim, imageDim);
- LOG.i("testProcessJpeg", output);
- assertEquals(output[0], 1000);
- assertEquals(output[1], 1000);
-
- // Without crop flag: expect original ratio.
- output = testProcessImage(true, false, viewDim, imageDim);
- LOG.i("testProcessJpeg", output);
- assertEquals(output[0], imageDim[0]);
- assertEquals(output[1], imageDim[1]);
- }
-
- @Test
- public void testProcessYuv() {
- int[] viewDim = new int[]{ 200, 200 };
- int[] imageDim = new int[]{ 1000, 1600 };
-
- // With crop flag: expect a 1:1 ratio.
- int[] output = testProcessImage(false, true, viewDim, imageDim);
- LOG.i("testProcessYuv", output);
- assertEquals(output[0], 1000);
- assertEquals(output[1], 1000);
-
- // Without crop flag: expect original ratio.
- output = testProcessImage(false, false, viewDim, imageDim);
- LOG.i("testProcessYuv", output);
- assertEquals(output[0], imageDim[0]);
- assertEquals(output[1], imageDim[1]);
- }
-
- private int[] testProcessImage(boolean jpeg, boolean crop, int[] viewDim, int[] imageDim) {
- // End our task when onPictureTaken is called. Take note of the result.
- final Task jpegTask = new Task<>(true);
- doAnswer(new Answer() {
- @Override
- public Object answer(InvocationOnMock invocation) throws Throwable {
- jpegTask.end((byte[]) invocation.getArguments()[0]);
- return null;
- }
- }).when(listener).onPictureTaken(any(byte[].class));
-
- // Fake our own dimensions.
- camera.setTop(0);
- camera.setBottom(viewDim[1]);
- camera.setLeft(0);
- camera.setRight(viewDim[0]);
-
- // Ensure the image will (not) be cropped.
- camera.setCropOutput(crop);
- mockPreview.setIsCropping(crop);
-
- // Create fake JPEG array and trigger the process.
- if (jpeg) {
- camera.mCameraCallbacks.processImage(mockJpeg(imageDim[0], imageDim[1]), true, false);
- } else {
- camera.mCameraCallbacks.processSnapshot(mockYuv(imageDim[0], imageDim[1]), true, false);
- }
-
- // Wait for result and get out dimensions.
- byte[] result = jpegTask.await(3000);
- assertNotNull("Image was processed", result);
- Bitmap bitmap = BitmapFactory.decodeByteArray(result, 0, result.length);
- return new int[]{ bitmap.getWidth(), bitmap.getHeight() };
- }
-
@Test
public void testProcessFrame() {
Frame mock = mock(Frame.class);
- completeTask().when(processor).process(mock);
+ doEndOp(op, true).when(processor).process(mock);
camera.mCameraCallbacks.dispatchFrame(mock);
- assertNotNull(task.await(200));
+ assertNotNull(op.await(DELAY));
verify(processor, times(1)).process(mock);
}
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
index 7c539bf6..4ee81244 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
@@ -2,20 +2,65 @@ package com.otaliastudios.cameraview;
import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.ImageFormat;
+import android.graphics.PointF;
+import android.graphics.RectF;
import android.location.Location;
-import android.support.annotation.NonNull;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.annotation.NonNull;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
import android.view.MotionEvent;
+import android.view.View;
import android.view.ViewGroup;
+import com.otaliastudios.cameraview.controls.Audio;
+import com.otaliastudios.cameraview.controls.AudioCodec;
+import com.otaliastudios.cameraview.controls.ControlParser;
+import com.otaliastudios.cameraview.controls.Engine;
+import com.otaliastudios.cameraview.controls.Facing;
+import com.otaliastudios.cameraview.controls.Flash;
+import com.otaliastudios.cameraview.controls.PictureFormat;
+import com.otaliastudios.cameraview.controls.Preview;
+import com.otaliastudios.cameraview.engine.CameraEngine;
+import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
+import com.otaliastudios.cameraview.filter.Filter;
+import com.otaliastudios.cameraview.filter.Filters;
+import com.otaliastudios.cameraview.filters.DuotoneFilter;
+import com.otaliastudios.cameraview.frame.Frame;
+import com.otaliastudios.cameraview.frame.FrameProcessor;
+import com.otaliastudios.cameraview.gesture.Gesture;
+import com.otaliastudios.cameraview.gesture.GestureAction;
+import com.otaliastudios.cameraview.controls.Grid;
+import com.otaliastudios.cameraview.controls.Hdr;
+import com.otaliastudios.cameraview.controls.Mode;
+import com.otaliastudios.cameraview.controls.VideoCodec;
+import com.otaliastudios.cameraview.controls.WhiteBalance;
+import com.otaliastudios.cameraview.gesture.GestureParser;
+import com.otaliastudios.cameraview.gesture.PinchGestureFinder;
+import com.otaliastudios.cameraview.gesture.ScrollGestureFinder;
+import com.otaliastudios.cameraview.gesture.TapGestureFinder;
+import com.otaliastudios.cameraview.engine.MockCameraEngine;
+import com.otaliastudios.cameraview.tools.Op;
+import com.otaliastudios.cameraview.markers.AutoFocusMarker;
+import com.otaliastudios.cameraview.markers.DefaultAutoFocusMarker;
+import com.otaliastudios.cameraview.markers.MarkerLayout;
+import com.otaliastudios.cameraview.overlay.OverlayLayout;
+import com.otaliastudios.cameraview.preview.MockCameraPreview;
+import com.otaliastudios.cameraview.preview.CameraPreview;
+import com.otaliastudios.cameraview.size.Size;
+import com.otaliastudios.cameraview.size.SizeSelector;
+import com.otaliastudios.cameraview.size.SizeSelectors;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import java.util.List;
-
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@@ -28,36 +73,39 @@ import static android.view.ViewGroup.LayoutParams.*;
public class CameraViewTest extends BaseTest {
private CameraView cameraView;
- private MockCameraController mockController;
- private CameraPreview mockPreview;
+ private MockCameraEngine mockController;
+ private MockCameraPreview mockPreview;
private boolean hasPermissions;
@Before
public void setUp() {
- ui(new Runnable() {
+ uiSync(new Runnable() {
@Override
public void run() {
- Context context = context();
+ Context context = getContext();
cameraView = new CameraView(context) {
+
+ @NonNull
@Override
- protected CameraController instantiateCameraController(CameraCallbacks callbacks) {
- mockController = new MockCameraController(callbacks);
+ protected CameraEngine instantiateCameraEngine(@NonNull Engine engine, @NonNull CameraEngine.Callback callback) {
+ mockController = spy(new MockCameraEngine(callback));
return mockController;
}
+ @NonNull
@Override
- protected CameraPreview instantiatePreview(Context context, ViewGroup container) {
- mockPreview = new MockCameraPreview(context, container);
+ protected CameraPreview instantiatePreview(@NonNull Preview preview, @NonNull Context context, @NonNull ViewGroup container) {
+ mockPreview = spy(new MockCameraPreview(context, container));
return mockPreview;
}
@Override
- protected boolean checkPermissions(SessionType sessionType, Audio audio) {
+ protected boolean checkPermissions(@NonNull Audio audio) {
return hasPermissions;
}
};
// Instantiate preview now.
- cameraView.instantiatePreview();
+ cameraView.doInstantiatePreview();
}
});
}
@@ -70,45 +118,82 @@ public class CameraViewTest extends BaseTest {
hasPermissions = false;
}
+ //region testLifecycle
+
+ @Test
+ public void testOpen() {
+ cameraView.open();
+ verify(mockPreview, times(1)).onResume();
+ // Can't verify controller, depends on permissions.
+ // See to-do at the end.
+ }
+
+ @Test
+ public void testClose() {
+ cameraView.close();
+ verify(mockPreview, times(1)).onPause();
+ verify(mockController, times(1)).stop(false);
+ }
+
+ @Test
+ public void testDestroy() {
+ cameraView.destroy();
+ verify(mockPreview, times(1)).onDestroy();
+ verify(mockController, times(1)).destroy(true);
+ }
+
//region testDefaults
@Test
public void testNullBeforeStart() {
- assertFalse(cameraView.isStarted());
+ assertFalse(cameraView.isOpened());
assertNull(cameraView.getCameraOptions());
- assertNull(cameraView.getExtraProperties());
- assertNull(cameraView.getPreviewSize());
- assertNull(cameraView.getPictureSize());
assertNull(cameraView.getSnapshotSize());
+ assertNull(cameraView.getPictureSize());
+ assertNull(cameraView.getVideoSize());
}
@Test
public void testDefaults() {
- // CameraController
- assertEquals(cameraView.getFlash(), Flash.DEFAULT);
- assertEquals(cameraView.getFacing(), Facing.DEFAULT);
- assertEquals(cameraView.getGrid(), Grid.DEFAULT);
- assertEquals(cameraView.getWhiteBalance(), WhiteBalance.DEFAULT);
- assertEquals(cameraView.getSessionType(), SessionType.DEFAULT);
- assertEquals(cameraView.getHdr(), Hdr.DEFAULT);
- assertEquals(cameraView.getAudio(), Audio.DEFAULT);
- assertEquals(cameraView.getVideoQuality(), VideoQuality.DEFAULT);
- assertEquals(cameraView.getVideoCodec(), VideoCodec.DEFAULT);
+ // CameraEngine
+ TypedArray empty = getContext().obtainStyledAttributes(new int[]{});
+ ControlParser controls = new ControlParser(getContext(), empty);
+ assertEquals(cameraView.getFlash(), controls.getFlash());
+ assertEquals(cameraView.getFacing(), controls.getFacing());
+ assertEquals(cameraView.getGrid(), controls.getGrid());
+ assertEquals(cameraView.getWhiteBalance(), controls.getWhiteBalance());
+ assertEquals(cameraView.getMode(), controls.getMode());
+ assertEquals(cameraView.getHdr(), controls.getHdr());
+ assertEquals(cameraView.getAudio(), controls.getAudio());
+ assertEquals(cameraView.getVideoCodec(), controls.getVideoCodec());
+ assertEquals(cameraView.getAudioCodec(), controls.getAudioCodec());
+ assertEquals(cameraView.getPictureFormat(), controls.getPictureFormat());
+ //noinspection SimplifiableJUnitAssertion
assertEquals(cameraView.getLocation(), null);
assertEquals(cameraView.getExposureCorrection(), 0f, 0f);
assertEquals(cameraView.getZoom(), 0f, 0f);
- assertEquals(cameraView.getVideoMaxDuration(), 0, 0);
- assertEquals(cameraView.getVideoMaxSize(), 0, 0);
+ assertEquals(cameraView.getVideoMaxDuration(), 0);
+ assertEquals(cameraView.getVideoMaxSize(), 0);
+ assertEquals(cameraView.getSnapshotMaxWidth(), 0);
+ assertEquals(cameraView.getSnapshotMaxHeight(), 0);
+ assertEquals(cameraView.getFrameProcessingMaxWidth(), 0);
+ assertEquals(cameraView.getFrameProcessingMaxHeight(), 0);
+ assertEquals(cameraView.getFrameProcessingFormat(), 0);
+ assertFalse(cameraView.getPreviewFrameRateExact());
// Self managed
+ GestureParser gestures = new GestureParser(empty);
assertEquals(cameraView.getPlaySounds(), CameraView.DEFAULT_PLAY_SOUNDS);
- assertEquals(cameraView.getCropOutput(), CameraView.DEFAULT_CROP_OUTPUT);
- assertEquals(cameraView.getJpegQuality(), CameraView.DEFAULT_JPEG_QUALITY);
- assertEquals(cameraView.getGestureAction(Gesture.TAP), GestureAction.DEFAULT_TAP);
- assertEquals(cameraView.getGestureAction(Gesture.LONG_TAP), GestureAction.DEFAULT_LONG_TAP);
- assertEquals(cameraView.getGestureAction(Gesture.PINCH), GestureAction.DEFAULT_PINCH);
- assertEquals(cameraView.getGestureAction(Gesture.SCROLL_HORIZONTAL), GestureAction.DEFAULT_SCROLL_HORIZONTAL);
- assertEquals(cameraView.getGestureAction(Gesture.SCROLL_VERTICAL), GestureAction.DEFAULT_SCROLL_VERTICAL);
+ assertEquals(cameraView.getAutoFocusResetDelay(), CameraView.DEFAULT_AUTOFOCUS_RESET_DELAY_MILLIS);
+ assertEquals(cameraView.getUseDeviceOrientation(), CameraView.DEFAULT_USE_DEVICE_ORIENTATION);
+ assertEquals(cameraView.getPictureMetering(), CameraView.DEFAULT_PICTURE_METERING);
+ assertEquals(cameraView.getPictureSnapshotMetering(), CameraView.DEFAULT_PICTURE_SNAPSHOT_METERING);
+ assertEquals(cameraView.getFrameProcessingPoolSize(), CameraView.DEFAULT_FRAME_PROCESSING_POOL_SIZE);
+ assertEquals(cameraView.getGestureAction(Gesture.TAP), gestures.getTapAction());
+ assertEquals(cameraView.getGestureAction(Gesture.LONG_TAP), gestures.getLongTapAction());
+ assertEquals(cameraView.getGestureAction(Gesture.PINCH), gestures.getPinchAction());
+ assertEquals(cameraView.getGestureAction(Gesture.SCROLL_HORIZONTAL), gestures.getHorizontalScrollAction());
+ assertEquals(cameraView.getGestureAction(Gesture.SCROLL_VERTICAL), gestures.getVerticalScrollAction());
}
//endregion
@@ -122,7 +207,7 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getGestureAction(Gesture.PINCH), GestureAction.ZOOM);
// Not assignable: This is like clearing
- cameraView.mapGesture(Gesture.PINCH, GestureAction.CAPTURE);
+ cameraView.mapGesture(Gesture.PINCH, GestureAction.TAKE_PICTURE);
assertEquals(cameraView.getGestureAction(Gesture.PINCH), GestureAction.NONE);
// Test clearing
@@ -141,21 +226,21 @@ public class CameraViewTest extends BaseTest {
// PinchGestureLayout
cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM);
- assertTrue(cameraView.mPinchGestureLayout.enabled());
+ assertTrue(cameraView.mPinchGestureFinder.isActive());
cameraView.clearGesture(Gesture.PINCH);
- assertFalse(cameraView.mPinchGestureLayout.enabled());
+ assertFalse(cameraView.mPinchGestureFinder.isActive());
// TapGestureLayout
- cameraView.mapGesture(Gesture.TAP, GestureAction.CAPTURE);
- assertTrue(cameraView.mTapGestureLayout.enabled());
+ cameraView.mapGesture(Gesture.TAP, GestureAction.TAKE_PICTURE);
+ assertTrue(cameraView.mTapGestureFinder.isActive());
cameraView.clearGesture(Gesture.TAP);
- assertFalse(cameraView.mPinchGestureLayout.enabled());
+ assertFalse(cameraView.mPinchGestureFinder.isActive());
// ScrollGestureLayout
cameraView.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.ZOOM);
- assertTrue(cameraView.mScrollGestureLayout.enabled());
+ assertTrue(cameraView.mScrollGestureFinder.isActive());
cameraView.clearGesture(Gesture.SCROLL_HORIZONTAL);
- assertFalse(cameraView.mScrollGestureLayout.enabled());
+ assertFalse(cameraView.mScrollGestureFinder.isActive());
}
//endregion
@@ -164,120 +249,226 @@ public class CameraViewTest extends BaseTest {
@Test
public void testGestureAction_capture() {
- mockController.mockStarted(true);
+ CameraOptions o = mock(CameraOptions.class);
+ mockController.setMockCameraOptions(o);
+ mockController.setMockState(CameraState.PREVIEW);
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
- ui(new Runnable() {
+ uiSync(new Runnable() {
@Override
public void run() {
- cameraView.mTapGestureLayout = new TapGestureLayout(cameraView.getContext()) {
- public boolean onTouchEvent(MotionEvent event) { return true; }
-
+ cameraView.mTapGestureFinder = new TapGestureFinder(cameraView.mCameraCallbacks) {
+ protected boolean handleTouchEvent(@NonNull MotionEvent event) {
+ setGesture(Gesture.TAP);
+ return true;
+ }
};
- cameraView.mTapGestureLayout.setGestureType(Gesture.TAP);
}
});
- cameraView.mapGesture(Gesture.TAP, GestureAction.CAPTURE);
+ cameraView.mapGesture(Gesture.TAP, GestureAction.TAKE_PICTURE);
cameraView.dispatchTouchEvent(event);
assertTrue(mockController.mPictureCaptured);
}
@Test
public void testGestureAction_focus() {
- mockController.mockStarted(true);
+ CameraOptions o = mock(CameraOptions.class);
+ mockController.setMockCameraOptions(o);
+ mockController.setMockState(CameraState.PREVIEW);
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
- ui(new Runnable() {
+ uiSync(new Runnable() {
@Override
public void run() {
- cameraView.mTapGestureLayout = new TapGestureLayout(cameraView.getContext()) {
- public boolean onTouchEvent(MotionEvent event) { return true; }
+ cameraView.mTapGestureFinder = new TapGestureFinder(cameraView.mCameraCallbacks) {
+ protected boolean handleTouchEvent(@NonNull MotionEvent event) {
+ setGesture(Gesture.TAP);
+ return true;
+ }
};
- cameraView.mTapGestureLayout.setGestureType(Gesture.TAP);
}
});
mockController.mFocusStarted = false;
- cameraView.mapGesture(Gesture.TAP, GestureAction.FOCUS);
- cameraView.dispatchTouchEvent(event);
- assertTrue(mockController.mFocusStarted);
-
- // Try with FOCUS_WITH_MARKER
- mockController.mFocusStarted = false;
- cameraView.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER);
+ cameraView.mapGesture(Gesture.TAP, GestureAction.AUTO_FOCUS);
cameraView.dispatchTouchEvent(event);
assertTrue(mockController.mFocusStarted);
}
+ private class FactorHolder { float value; }
+
@Test
public void testGestureAction_zoom() {
- mockController.mockStarted(true);
+ CameraOptions o = mock(CameraOptions.class);
+ mockController.setMockCameraOptions(o);
+ mockController.setMockState(CameraState.PREVIEW);
mockController.mZoomChanged = false;
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
- ui(new Runnable() {
+ final FactorHolder factor = new FactorHolder();
+ uiSync(new Runnable() {
@Override
public void run() {
- cameraView.mPinchGestureLayout = new PinchGestureLayout(cameraView.getContext()) {
- public boolean onTouchEvent(MotionEvent event) { return true; }
+ cameraView.mPinchGestureFinder = new PinchGestureFinder(cameraView.mCameraCallbacks) {
+ @Override
+ protected boolean handleTouchEvent(@NonNull MotionEvent event) {
+ setGesture(Gesture.PINCH);
+ return true;
+ }
+
+ @Override
+ protected float getFactor() {
+ return factor.value;
+ }
};
- cameraView.mPinchGestureLayout.setGestureType(Gesture.PINCH);
cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM);
}
});
// If factor is 0, we return the same value. The controller should not be notified.
- cameraView.mPinchGestureLayout.mFactor = 0f;
+ factor.value = 0f;
cameraView.dispatchTouchEvent(event);
assertFalse(mockController.mZoomChanged);
// For larger factors, the value is scaled. The controller should be notified.
- cameraView.mPinchGestureLayout.mFactor = 1f;
+ factor.value = 1f;
cameraView.dispatchTouchEvent(event);
assertTrue(mockController.mZoomChanged);
}
@Test
public void testGestureAction_exposureCorrection() {
- // This needs a valid CameraOptions value.
- CameraOptions o = mock(CameraOptions.class);
- when(o.getExposureCorrectionMinValue()).thenReturn(-10f);
- when(o.getExposureCorrectionMaxValue()).thenReturn(10f);
+ CameraOptions o = new CameraOptions() {};
+ o.exposureCorrectionMaxValue = 10F;
+ o.exposureCorrectionMinValue = -10F;
mockController.setMockCameraOptions(o);
- mockController.mockStarted(true);
+ mockController.setMockState(CameraState.PREVIEW);
mockController.mExposureCorrectionChanged = false;
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
- ui(new Runnable() {
+ final FactorHolder factor = new FactorHolder();
+ uiSync(new Runnable() {
@Override
public void run() {
- cameraView.mScrollGestureLayout = new ScrollGestureLayout(cameraView.getContext()) {
- public boolean onTouchEvent(MotionEvent event) { return true; }
+ cameraView.mScrollGestureFinder = new ScrollGestureFinder(cameraView.mCameraCallbacks) {
+ @Override
+ protected boolean handleTouchEvent(@NonNull MotionEvent event) {
+ setGesture(Gesture.SCROLL_HORIZONTAL);
+ return true;
+ }
+
+ @Override
+ protected float getFactor() {
+ return factor.value;
+ }
};
- cameraView.mScrollGestureLayout.setGestureType(Gesture.SCROLL_HORIZONTAL);
cameraView.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.EXPOSURE_CORRECTION);
}
});
// If factor is 0, we return the same value. The controller should not be notified.
- cameraView.mScrollGestureLayout.mFactor = 0f;
+ factor.value = 0f;
cameraView.dispatchTouchEvent(event);
assertFalse(mockController.mExposureCorrectionChanged);
// For larger factors, the value is scaled. The controller should be notified.
- cameraView.mScrollGestureLayout.mFactor = 1f;
+ factor.value = 1f;
cameraView.dispatchTouchEvent(event);
assertTrue(mockController.mExposureCorrectionChanged);
}
+ @Test
+ public void testGestureAction_filterControl1() {
+ mockController.setMockState(CameraState.PREVIEW);
+ mockController.setMockCameraOptions(mock(CameraOptions.class));
+ DuotoneFilter filter = new DuotoneFilter(); // supports two parameters
+ filter.setParameter1(0F);
+ filter = spy(filter);
+ cameraView.setExperimental(true);
+ cameraView.setFilter(filter);
+ mockController.mExposureCorrectionChanged = false;
+ MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
+ final FactorHolder factor = new FactorHolder();
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ cameraView.mScrollGestureFinder = new ScrollGestureFinder(cameraView.mCameraCallbacks) {
+ @Override
+ protected boolean handleTouchEvent(@NonNull MotionEvent event) {
+ setGesture(Gesture.SCROLL_HORIZONTAL);
+ return true;
+ }
+
+ @Override
+ protected float getFactor() {
+ return factor.value;
+ }
+ };
+ cameraView.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.FILTER_CONTROL_1);
+ }
+ });
+
+ // If factor is 0, we return the same value. The filter should not be notified.
+ factor.value = 0f;
+ cameraView.dispatchTouchEvent(event);
+ verify(filter, never()).setParameter1(anyFloat());
+
+ // For larger factors, the value is scaled. The filter should be notified.
+ factor.value = 1f;
+ cameraView.dispatchTouchEvent(event);
+ verify(filter, times(1)).setParameter1(anyFloat());
+ }
+
+ @Test
+ public void testGestureAction_filterControl2() {
+ mockController.setMockState(CameraState.PREVIEW);
+ mockController.setMockCameraOptions(mock(CameraOptions.class));
+ DuotoneFilter filter = new DuotoneFilter(); // supports two parameters
+ filter.setParameter2(0F);
+ filter = spy(filter);
+ cameraView.setExperimental(true);
+ cameraView.setFilter(filter);
+ mockController.mExposureCorrectionChanged = false;
+ MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
+ final FactorHolder factor = new FactorHolder();
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ cameraView.mScrollGestureFinder = new ScrollGestureFinder(cameraView.mCameraCallbacks) {
+ @Override
+ protected boolean handleTouchEvent(@NonNull MotionEvent event) {
+ setGesture(Gesture.SCROLL_HORIZONTAL);
+ return true;
+ }
+
+ @Override
+ protected float getFactor() {
+ return factor.value;
+ }
+ };
+ cameraView.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.FILTER_CONTROL_2);
+ }
+ });
+
+ // If factor is 0, we return the same value. The filter should not be notified.
+ factor.value = 0f;
+ cameraView.dispatchTouchEvent(event);
+ verify(filter, never()).setParameter2(anyFloat());
+
+ // For larger factors, the value is scaled. The filter should be notified.
+ factor.value = 1f;
+ cameraView.dispatchTouchEvent(event);
+ verify(filter, times(1)).setParameter2(anyFloat());
+ }
+
//endregion
//region testMeasure
- private void mockPreviewSize() {
+ private void mockPreviewStreamSize() {
Size size = new Size(900, 1600);
- mockController.setMockPreviewSize(size);
+ mockController.setMockPreviewStreamSize(size);
}
@Test
public void testMeasure_early() {
- mockController.setMockPreviewSize(null);
+ mockController.setMockPreviewStreamSize(null);
cameraView.measure(
makeMeasureSpec(500, EXACTLY),
makeMeasureSpec(500, EXACTLY));
@@ -287,7 +478,7 @@ public class CameraViewTest extends BaseTest {
@Test
public void testMeasure_matchParentBoth() {
- mockPreviewSize();
+ mockPreviewStreamSize();
// Respect parent/layout constraints on both dimensions.
cameraView.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
@@ -313,7 +504,7 @@ public class CameraViewTest extends BaseTest {
@Test
public void testMeasure_wrapContentBoth() {
- mockPreviewSize();
+ mockPreviewStreamSize();
// Respect parent constraints, but fit aspect ratio.
// Fit into a 160x160 parent so we espect final width to be 90.
@@ -327,7 +518,7 @@ public class CameraViewTest extends BaseTest {
@Test
public void testMeasure_wrapContentSingle() {
- mockPreviewSize();
+ mockPreviewStreamSize();
// Respect MATCH_PARENT on height, change width to fit the aspect ratio.
cameraView.setLayoutParams(new ViewGroup.LayoutParams(WRAP_CONTENT, MATCH_PARENT));
@@ -348,7 +539,7 @@ public class CameraViewTest extends BaseTest {
@Test
public void testMeasure_scrollableContainer() {
- mockPreviewSize();
+ mockPreviewStreamSize();
// Assume a vertical scroll view. It will pass UNSPECIFIED as height.
// We respect MATCH_PARENT on width (160), and enlarge height to match the aspect ratio.
@@ -386,9 +577,9 @@ public class CameraViewTest extends BaseTest {
@Test
public void testExposureCorrection() {
// This needs a valid CameraOptions value.
- CameraOptions o = mock(CameraOptions.class);
- when(o.getExposureCorrectionMinValue()).thenReturn(-10f);
- when(o.getExposureCorrectionMaxValue()).thenReturn(10f);
+ CameraOptions o = new CameraOptions() {};
+ o.exposureCorrectionMaxValue = 10F;
+ o.exposureCorrectionMinValue = -10F;
mockController.setMockCameraOptions(o);
cameraView.setExposureCorrection(5f);
@@ -407,11 +598,11 @@ public class CameraViewTest extends BaseTest {
@Test
public void testSetLocation() {
cameraView.setLocation(50d, -50d);
- assertEquals(50d, mockController.mLocation.getLatitude(), 0);
- assertEquals(-50d, mockController.mLocation.getLongitude(), 0);
- assertEquals(0, mockController.mLocation.getAltitude(), 0);
- assertEquals("Unknown", mockController.mLocation.getProvider());
- assertEquals(System.currentTimeMillis(), mockController.mLocation.getTime(), 1000f);
+ assertEquals(50d, mockController.getLocation().getLatitude(), 0);
+ assertEquals(-50d, mockController.getLocation().getLongitude(), 0);
+ assertEquals(0, mockController.getLocation().getAltitude(), 0);
+ assertEquals("Unknown", mockController.getLocation().getProvider());
+ assertEquals(System.currentTimeMillis(), mockController.getLocation().getTime(), 1000f);
Location source = new Location("Provider");
source.setTime(5000);
@@ -445,6 +636,20 @@ public class CameraViewTest extends BaseTest {
cameraView.startAutoFocus(200, 200);
}
+ @Test(expected = IllegalArgumentException.class)
+ public void testStartAutoFocus_illegal3() {
+ cameraView.startAutoFocus(new RectF(-1, -1, 1, 1));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testStartAutoFocus_illegal4() {
+ cameraView.setLeft(0);
+ cameraView.setRight(100);
+ cameraView.setTop(0);
+ cameraView.setBottom(100);
+ cameraView.startAutoFocus(new RectF(-100, -100, 200, 200));
+ }
+
@Test
public void testStartAutoFocus() {
cameraView.setLeft(0);
@@ -455,118 +660,157 @@ public class CameraViewTest extends BaseTest {
assertTrue(mockController.mFocusStarted);
}
+ @Test
+ public void testStartAutoFocusRect() {
+ cameraView.setLeft(0);
+ cameraView.setRight(100);
+ cameraView.setTop(0);
+ cameraView.setBottom(100);
+ cameraView.startAutoFocus(new RectF(25, 25, 75, 75));
+ assertTrue(mockController.mFocusStarted);
+ }
+
//endregion
//region test setParameters
@Test
- public void testSetCropOutput() {
- cameraView.setCropOutput(true);
- assertTrue(cameraView.getCropOutput());
- cameraView.setCropOutput(false);
- assertFalse(cameraView.getCropOutput());
+ public void testSetPlaySounds() {
+ cameraView.setPlaySounds(true);
+ assertTrue(cameraView.getPlaySounds());
+ cameraView.setPlaySounds(false);
+ assertFalse(cameraView.getPlaySounds());
}
@Test
- public void testSetJpegQuality() {
- cameraView.setJpegQuality(10);
- assertEquals(cameraView.getJpegQuality(), 10);
- cameraView.setJpegQuality(100);
- assertEquals(cameraView.getJpegQuality(), 100);
+ public void testSetUseDeviceOrientation() {
+ cameraView.setUseDeviceOrientation(true);
+ assertTrue(cameraView.getUseDeviceOrientation());
+ cameraView.setUseDeviceOrientation(false);
+ assertFalse(cameraView.getUseDeviceOrientation());
}
@Test
- public void testSetPlaySounds() {
- cameraView.setPlaySounds(true);
- assertEquals(cameraView.getPlaySounds(), true);
- cameraView.setPlaySounds(false);
- assertEquals(cameraView.getPlaySounds(), false);
+ public void testSetPictureMetering() {
+ cameraView.setPictureMetering(true);
+ assertTrue(cameraView.getPictureMetering());
+ cameraView.setPictureMetering(false);
+ assertFalse(cameraView.getPictureMetering());
}
- @Test(expected = IllegalArgumentException.class)
- public void testSetJpegQuality_illegal() {
- cameraView.setJpegQuality(-10);
+ @Test
+ public void testSetPictureSnapshotMetering() {
+ cameraView.setPictureSnapshotMetering(true);
+ assertTrue(cameraView.getPictureSnapshotMetering());
+ cameraView.setPictureSnapshotMetering(false);
+ assertFalse(cameraView.getPictureSnapshotMetering());
}
@Test
public void testSetFlash() {
cameraView.set(Flash.TORCH);
- assertEquals(cameraView.getFlash(), Flash.TORCH);
+ assertEquals(cameraView.get(Flash.class), Flash.TORCH);
cameraView.set(Flash.OFF);
- assertEquals(cameraView.getFlash(), Flash.OFF);
+ assertEquals(cameraView.get(Flash.class), Flash.OFF);
}
@Test
public void testSetFacing() {
cameraView.set(Facing.FRONT);
- assertEquals(cameraView.getFacing(), Facing.FRONT);
+ assertEquals(cameraView.get(Facing.class), Facing.FRONT);
cameraView.set(Facing.BACK);
- assertEquals(cameraView.getFacing(), Facing.BACK);
+ assertEquals(cameraView.get(Facing.class), Facing.BACK);
}
@Test
public void testToggleFacing() {
cameraView.set(Facing.FRONT);
cameraView.toggleFacing();
- assertEquals(cameraView.getFacing(), Facing.BACK);
+ assertEquals(cameraView.get(Facing.class), Facing.BACK);
cameraView.toggleFacing();
- assertEquals(cameraView.getFacing(), Facing.FRONT);
+ assertEquals(cameraView.get(Facing.class), Facing.FRONT);
}
@Test
public void testSetGrid() {
cameraView.set(Grid.DRAW_3X3);
- assertEquals(cameraView.getGrid(), Grid.DRAW_3X3);
+ assertEquals(cameraView.get(Grid.class), Grid.DRAW_3X3);
cameraView.set(Grid.OFF);
- assertEquals(cameraView.getGrid(), Grid.OFF);
+ assertEquals(cameraView.get(Grid.class), Grid.OFF);
}
@Test
public void testSetWhiteBalance() {
cameraView.set(WhiteBalance.CLOUDY);
- assertEquals(cameraView.getWhiteBalance(), WhiteBalance.CLOUDY);
+ assertEquals(cameraView.get(WhiteBalance.class), WhiteBalance.CLOUDY);
cameraView.set(WhiteBalance.AUTO);
- assertEquals(cameraView.getWhiteBalance(), WhiteBalance.AUTO);
+ assertEquals(cameraView.get(WhiteBalance.class), WhiteBalance.AUTO);
}
@Test
- public void testSessionType() {
- cameraView.set(SessionType.VIDEO);
- assertEquals(cameraView.getSessionType(), SessionType.VIDEO);
- cameraView.set(SessionType.PICTURE);
- assertEquals(cameraView.getSessionType(), SessionType.PICTURE);
+ public void testMode() {
+ cameraView.set(Mode.VIDEO);
+ assertEquals(cameraView.get(Mode.class), Mode.VIDEO);
+ cameraView.set(Mode.PICTURE);
+ assertEquals(cameraView.get(Mode.class), Mode.PICTURE);
}
@Test
public void testHdr() {
cameraView.set(Hdr.ON);
- assertEquals(cameraView.getHdr(), Hdr.ON);
+ assertEquals(cameraView.get(Hdr.class), Hdr.ON);
cameraView.set(Hdr.OFF);
- assertEquals(cameraView.getHdr(), Hdr.OFF);
+ assertEquals(cameraView.get(Hdr.class), Hdr.OFF);
}
@Test
public void testAudio() {
cameraView.set(Audio.ON);
- assertEquals(cameraView.getAudio(), Audio.ON);
+ assertEquals(cameraView.get(Audio.class), Audio.ON);
cameraView.set(Audio.OFF);
- assertEquals(cameraView.getAudio(), Audio.OFF);
+ assertEquals(cameraView.get(Audio.class), Audio.OFF);
+ cameraView.set(Audio.MONO);
+ assertEquals(cameraView.get(Audio.class), Audio.MONO);
+ cameraView.set(Audio.STEREO);
+ assertEquals(cameraView.get(Audio.class), Audio.STEREO);
}
@Test
- public void testVideoQuality() {
- cameraView.set(VideoQuality.MAX_1080P);
- assertEquals(cameraView.getVideoQuality(), VideoQuality.MAX_1080P);
- cameraView.set(VideoQuality.LOWEST);
- assertEquals(cameraView.getVideoQuality(), VideoQuality.LOWEST);
+ public void testAudioCodec() {
+ cameraView.set(AudioCodec.DEVICE_DEFAULT);
+ assertEquals(cameraView.get(AudioCodec.class), AudioCodec.DEVICE_DEFAULT);
+ cameraView.set(AudioCodec.AAC);
+ assertEquals(cameraView.get(AudioCodec.class), AudioCodec.AAC);
+ cameraView.set(AudioCodec.HE_AAC);
+ assertEquals(cameraView.get(AudioCodec.class), AudioCodec.HE_AAC);
+ cameraView.set(AudioCodec.AAC_ELD);
+ assertEquals(cameraView.get(AudioCodec.class), AudioCodec.AAC_ELD);
}
+
@Test
public void testVideoCodec() {
cameraView.set(VideoCodec.H_263);
- assertEquals(cameraView.getVideoCodec(), VideoCodec.H_263);
+ assertEquals(cameraView.get(VideoCodec.class), VideoCodec.H_263);
cameraView.set(VideoCodec.H_264);
- assertEquals(cameraView.getVideoCodec(), VideoCodec.H_264);
+ assertEquals(cameraView.get(VideoCodec.class), VideoCodec.H_264);
+ }
+
+ @Test
+ public void testPictureFormat() {
+ cameraView.set(PictureFormat.JPEG);
+ assertEquals(cameraView.get(PictureFormat.class), PictureFormat.JPEG);
+ cameraView.set(PictureFormat.DNG);
+ assertEquals(cameraView.get(PictureFormat.class), PictureFormat.DNG);
+ }
+
+ @Test
+ public void testPreviewStreamSizeSelector() {
+ SizeSelector source = SizeSelectors.minHeight(50);
+ cameraView.setPreviewStreamSize(source);
+ SizeSelector result = mockController.getPreviewStreamSizeSelector();
+ assertNotNull(result);
+ assertEquals(result, source);
}
@Test
@@ -578,6 +822,15 @@ public class CameraViewTest extends BaseTest {
assertEquals(result, source);
}
+ @Test
+ public void testVideoSizeSelector() {
+ SizeSelector source = SizeSelectors.minHeight(50);
+ cameraView.setVideoSize(source);
+ SizeSelector result = mockController.getVideoSizeSelector();
+ assertNotNull(result);
+ assertEquals(result, source);
+ }
+
@Test
public void testVideoMaxSize() {
cameraView.setVideoMaxSize(5000);
@@ -590,11 +843,69 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getVideoMaxDuration(), 5000);
}
+ @Test
+ public void testPreviewFrameRate() {
+ cameraView.setPreviewFrameRate(60);
+ assertEquals(cameraView.getPreviewFrameRate(), 60, 0);
+ }
+
+ @Test
+ public void testPreviewFrameRateExact() {
+ cameraView.setPreviewFrameRateExact(true);
+ assertTrue(cameraView.getPreviewFrameRateExact());
+ cameraView.setPreviewFrameRateExact(false);
+ assertFalse(cameraView.getPreviewFrameRateExact());
+ }
+
+ @Test
+ public void testSnapshotMaxSize() {
+ cameraView.setSnapshotMaxWidth(500);
+ assertEquals(500, cameraView.getSnapshotMaxWidth());
+ cameraView.setSnapshotMaxHeight(700);
+ assertEquals(700, cameraView.getSnapshotMaxHeight());
+ }
+
+ @Test
+ public void testFrameProcessingMaxSize() {
+ cameraView.setFrameProcessingMaxWidth(500);
+ assertEquals(500, cameraView.getFrameProcessingMaxWidth());
+ cameraView.setFrameProcessingMaxHeight(700);
+ assertEquals(700, cameraView.getFrameProcessingMaxHeight());
+ }
+
+ @Test
+ public void testFrameProcessingFormat() {
+ cameraView.setFrameProcessingFormat(ImageFormat.YUV_420_888);
+ assertEquals(ImageFormat.YUV_420_888, cameraView.getFrameProcessingFormat());
+ cameraView.setFrameProcessingFormat(ImageFormat.YUV_422_888);
+ assertEquals(ImageFormat.YUV_422_888, cameraView.getFrameProcessingFormat());
+ }
+
+ @Test
+ public void testFrameProcessingPoolSize() {
+ cameraView.setFrameProcessingPoolSize(4);
+ assertEquals(4, cameraView.getFrameProcessingPoolSize());
+ cameraView.setFrameProcessingPoolSize(6);
+ assertEquals(6, cameraView.getFrameProcessingPoolSize());
+ }
+
+ @Test
+ public void testFrameProcessingExecutors() {
+ cameraView.setFrameProcessingExecutors(5);
+ assertEquals(5, cameraView.getFrameProcessingExecutors());
+ cameraView.setFrameProcessingExecutors(2);
+ assertEquals(2, cameraView.getFrameProcessingExecutors());
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testFrameProcessingExecutors_throws() {
+ cameraView.setFrameProcessingExecutors(0);
+ }
+
//endregion
//region Lists of listeners and processors
- @SuppressWarnings("UseBulkOperation")
@Test
public void testCameraListenerList() {
assertTrue(cameraView.mListeners.isEmpty());
@@ -622,7 +933,6 @@ public class CameraViewTest extends BaseTest {
}
}
- @SuppressWarnings({"NullableProblems", "UseBulkOperation"})
@Test
public void testFrameProcessorsList() {
assertTrue(cameraView.mFrameProcessors.isEmpty());
@@ -644,9 +954,9 @@ public class CameraViewTest extends BaseTest {
assertTrue(cameraView.mFrameProcessors.isEmpty());
// Ensure this does not throw a ConcurrentModificationException
- cameraView.addFrameProcessor(new FrameProcessor() { public void process(Frame f) {} });
- cameraView.addFrameProcessor(new FrameProcessor() { public void process(Frame f) {} });
- cameraView.addFrameProcessor(new FrameProcessor() { public void process(Frame f) {} });
+ cameraView.addFrameProcessor(new FrameProcessor() { public void process(@NonNull Frame f) {} });
+ cameraView.addFrameProcessor(new FrameProcessor() { public void process(@NonNull Frame f) {} });
+ cameraView.addFrameProcessor(new FrameProcessor() { public void process(@NonNull Frame f) {} });
for (FrameProcessor test : cameraView.mFrameProcessors) {
cameraView.mFrameProcessors.remove(test);
}
@@ -654,5 +964,130 @@ public class CameraViewTest extends BaseTest {
//endregion
- // TODO: test permissions
+ //region Snapshots
+
+ @Test
+ public void testSetSnapshotMaxSize() {
+ cameraView.setSnapshotMaxWidth(500);
+ cameraView.setSnapshotMaxHeight(1000);
+ assertEquals(mockController.getSnapshotMaxWidth(), 500);
+ assertEquals(mockController.getSnapshotMaxHeight(), 1000);
+ }
+
+ //endregion
+
+ //region MarkerLayout
+
+ @Test
+ public void testMarkerLayout_forAutoFocus_onMarker() {
+ MarkerLayout markerLayout = mock(MarkerLayout.class);
+ AutoFocusMarker marker = new DefaultAutoFocusMarker();
+ cameraView.mMarkerLayout = markerLayout;
+ cameraView.setAutoFocusMarker(marker);
+ verify(markerLayout, times(1)).onMarker(MarkerLayout.TYPE_AUTOFOCUS, marker);
+ }
+
+ @Test
+ public void testMarkerLayout_forAutoFocus_onEvent() {
+ MarkerLayout markerLayout = spy(cameraView.mMarkerLayout);
+ cameraView.mMarkerLayout = markerLayout;
+ final PointF point = new PointF(0, 0);
+ final PointF[] points = new PointF[]{ point };
+ final Op op = new Op<>();
+ doEndOp(op, true).when(markerLayout).onEvent(MarkerLayout.TYPE_AUTOFOCUS, points);
+ cameraView.mCameraCallbacks.dispatchOnFocusStart(Gesture.TAP, point);
+ assertNotNull(op.await(100));
+ }
+
+ //endregion
+
+ //region Overlays
+
+ @Test
+ public void testOverlays_generateLayoutParams() {
+ cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ View overlay = inflater.inflate(com.otaliastudios.cameraview.test.R.layout.overlay, cameraView, false);
+ assertTrue(overlay.getLayoutParams() instanceof OverlayLayout.LayoutParams);
+ verify(cameraView.mOverlayLayout, times(1)).isOverlay(any(AttributeSet.class));
+ verify(cameraView.mOverlayLayout, times(1)).generateLayoutParams(any(AttributeSet.class));
+
+ }
+
+ @Test
+ public void testOverlays_dontGenerateLayoutParams() {
+ cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ View overlay = inflater.inflate(com.otaliastudios.cameraview.test.R.layout.not_overlay, cameraView, false);
+ assertFalse(overlay.getLayoutParams() instanceof OverlayLayout.LayoutParams);
+ verify(cameraView.mOverlayLayout, times(1)).isOverlay(any(AttributeSet.class));
+ verify(cameraView.mOverlayLayout, never()).generateLayoutParams(any(AttributeSet.class));
+ }
+
+ @Test
+ public void testOverlays_addOverlayView() {
+ cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
+ View overlay = new View(getContext());
+ OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
+ int count = cameraView.getChildCount();
+ cameraView.addView(overlay, 0, params);
+ assertEquals(count, cameraView.getChildCount()); // Not added to CameraView
+ verify(cameraView.mOverlayLayout, times(1)).isOverlay(params);
+ verify(cameraView.mOverlayLayout, times(1)).addView(overlay, params);
+ }
+
+ @Test
+ public void testOverlays_dontAddOverlayView() {
+ cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
+ View overlay = new View(getContext());
+ ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(10, 10);
+ int count = cameraView.getChildCount();
+ cameraView.addView(overlay, 0, params);
+ assertEquals(count + 1, cameraView.getChildCount());
+ verify(cameraView.mOverlayLayout, times(1)).isOverlay(params);
+ verify(cameraView.mOverlayLayout, never()).addView(overlay, params);
+ }
+
+ @Test
+ public void testOverlays_removeOverlayView() {
+ // First add one.
+ cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
+ View overlay = new View(getContext());
+ OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
+ cameraView.addView(overlay, 0, params);
+ reset(cameraView.mOverlayLayout);
+ // Then remove.
+ cameraView.removeView(overlay);
+ verify(cameraView.mOverlayLayout, times(1)).isOverlay(params);
+ verify(cameraView.mOverlayLayout, times(1)).removeView(overlay);
+ }
+
+ @Test
+ public void testOverlays_dontRemoveOverlayView() {
+ // First add a view.
+ cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
+ View notOverlay = new View(getContext());
+ ViewGroup.LayoutParams notOverlayParams = new ViewGroup.LayoutParams(10, 10);
+ cameraView.addView(notOverlay, notOverlayParams);
+ reset(cameraView.mOverlayLayout);
+ // Then remove.
+ cameraView.removeView(notOverlay);
+ verify(cameraView.mOverlayLayout, never()).removeView(notOverlay);
+ }
+
+ //endregion
+
+ //region Filter
+
+ @Test
+ public void testSetFilter() {
+ Filter filter = Filters.AUTO_FIX.newInstance();
+ cameraView.setFilter(filter);
+ verify(mockPreview, times(1)).setFilter(filter);
+ assertEquals(filter, cameraView.getFilter());
+ //noinspection ResultOfMethodCallIgnored
+ verify(mockPreview, times(1)).getCurrentFilter();
+ }
+
+ //endregion
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CropHelperTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CropHelperTest.java
deleted file mode 100644
index 3ca017b8..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CropHelperTest.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.graphics.YuvImage;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import java.io.ByteArrayOutputStream;
-import java.io.OutputStream;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class CropHelperTest extends BaseTest {
-
- @Test
- public void testCropFromYuv() {
- testCropFromYuv(1600, 1600, AspectRatio.of(16, 9));
- testCropFromYuv(1600, 1600, AspectRatio.of(9, 16));
- }
-
- @Test
- public void testCropFromJpeg() {
- testCropFromJpeg(1600, 1600, AspectRatio.of(16, 9));
- testCropFromJpeg(1600, 1600, AspectRatio.of(9, 16));
- }
-
- private void testCropFromYuv(final int w, final int h, final AspectRatio target) {
- final boolean wider = target.toFloat() > ((float) w / (float) h);
- byte[] b = CropHelper.cropToJpeg(mockYuv(w, h), target, 100);
- Bitmap result = BitmapFactory.decodeByteArray(b, 0, b.length);
-
- // Assert.
- AspectRatio ratio = AspectRatio.of(result.getWidth(), result.getHeight());
- assertEquals(target, ratio);
- if (wider) { // width must match.
- assertEquals(result.getWidth(), w);
- } else {
- assertEquals(result.getHeight(), h);
- }
- }
-
- private void testCropFromJpeg(int w, int h, AspectRatio target) {
- final boolean wider = target.toFloat() > ((float) w / (float) h);
- byte[] b = CropHelper.cropToJpeg(mockJpeg(w, h), target, 100);
- Bitmap result = BitmapFactory.decodeByteArray(b, 0, b.length);
-
- // Assert.
- AspectRatio ratio = AspectRatio.of(result.getWidth(), result.getHeight());
- assertEquals(target, ratio);
- if (wider) { // width must match.
- assertEquals(result.getWidth(), w);
- } else {
- assertEquals(result.getHeight(), h);
- }
- }
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/ExtraProperties1Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/ExtraProperties1Test.java
deleted file mode 100644
index 02e497ca..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/ExtraProperties1Test.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.annotation.TargetApi;
-import android.hardware.Camera;
-import android.hardware.camera2.CameraCharacteristics;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.util.SizeF;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mockito;
-
-import static org.mockito.Mockito.*;
-import static org.junit.Assert.*;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class ExtraProperties1Test extends BaseTest {
-
- @Test
- public void testConstructor1() {
- Camera.Parameters params = mock(Camera.Parameters.class);
- when(params.getVerticalViewAngle()).thenReturn(10f);
- when(params.getHorizontalViewAngle()).thenReturn(5f);
- ExtraProperties e = new ExtraProperties(params);
- assertEquals(e.getVerticalViewingAngle(), 10f, 0f);
- assertEquals(e.getHorizontalViewingAngle(), 5f, 0f);
- }
-
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GestureLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GestureLayoutTest.java
deleted file mode 100644
index 84820b0f..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GestureLayoutTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.support.test.espresso.Espresso;
-import android.support.test.espresso.Root;
-import android.support.test.espresso.ViewAssertion;
-import android.support.test.espresso.ViewInteraction;
-import android.support.test.espresso.assertion.ViewAssertions;
-import android.support.test.espresso.matcher.RootMatchers;
-import android.support.test.espresso.matcher.ViewMatchers;
-import android.support.test.rule.ActivityTestRule;
-import android.view.MotionEvent;
-import android.view.View;
-
-import org.hamcrest.BaseMatcher;
-import org.hamcrest.Description;
-import org.hamcrest.Matchers;
-import org.junit.Before;
-import org.junit.Rule;
-
-import static android.support.test.espresso.Espresso.onView;
-import static org.hamcrest.Matchers.any;
-
-@TargetApi(17)
-public abstract class GestureLayoutTest extends BaseTest {
-
- protected abstract T create(Context context);
-
- @Rule
- public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class);
-
- protected T layout;
- protected Task touch;
-
- @Before
- public void setUp() {
- ui(new Runnable() {
- @Override
- public void run() {
- TestActivity a = rule.getActivity();
- layout = create(a);
- layout.enable(true);
- a.inflate(layout);
-
- touch = new Task<>();
- layout.setOnTouchListener(new View.OnTouchListener() {
- @Override
- public boolean onTouch(View view, MotionEvent motionEvent) {
- boolean found = layout.onTouchEvent(motionEvent);
- if (found) touch.end(layout.getGestureType());
- return true;
- }
- });
- }
- });
- }
-
- protected final ViewInteraction onLayout() {
- return onView(Matchers.is(layout))
- .inRoot(RootMatchers.withDecorView(
- Matchers.is(rule.getActivity().getWindow().getDecorView())));
- }
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
deleted file mode 100644
index 887bc85d..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
+++ /dev/null
@@ -1,644 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.graphics.Bitmap;
-import android.graphics.PointF;
-import android.hardware.Camera;
-import android.os.Build;
-import android.os.Handler;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Ignore;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.File;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.mock;
-
-
-/**
- * These tests work great on real devices, and are the only way to test actual CameraController
- * implementation - we really need to open the camera device.
- * Unfortunately they fail unreliably on emulated devices, due to some bug with the
- * emulated camera controller. Waiting for it to be fixed.
- */
-@RunWith(AndroidJUnit4.class)
-@MediumTest
-@Ignore
-public class IntegrationTest extends BaseTest {
-
- @Rule
- public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class);
-
- private CameraView camera;
- private Camera1 controller;
- private CameraListener listener;
- private Task uiExceptionTask;
-
- @BeforeClass
- public static void grant() {
- grantPermissions();
- }
-
- @Before
- public void setUp() {
- WorkerHandler.destroy();
-
- ui(new Runnable() {
- @Override
- public void run() {
- camera = new CameraView(rule.getActivity()) {
- @Override
- protected CameraController instantiateCameraController(CameraCallbacks callbacks) {
- controller = new Camera1(callbacks);
- return controller;
- }
- };
-
- listener = mock(CameraListener.class);
- camera.addCameraListener(listener);
- rule.getActivity().inflate(camera);
- }
- });
-
- // Ensure that controller exceptions are thrown on this thread (not on the UI thread).
- uiExceptionTask = new Task<>(true);
- WorkerHandler crashThread = WorkerHandler.get("CrashThread");
- crashThread.getThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
- @Override
- public void uncaughtException(Thread t, Throwable e) {
- uiExceptionTask.end(e);
- }
- });
- controller.mCrashHandler = crashThread.get();
- }
-
- @After
- public void tearDown() throws Exception {
- camera.stopCapturingVideo();
- camera.destroy();
- WorkerHandler.destroy();
- }
-
- private void waitForUiException() throws Throwable {
- Throwable throwable = uiExceptionTask.await(2500);
- if (throwable != null) throw throwable;
- }
-
- private CameraOptions waitForOpen(boolean expectSuccess) {
- camera.start();
- final Task open = new Task<>(true);
- doEndTask(open, 0).when(listener).onCameraOpened(any(CameraOptions.class));
- CameraOptions result = open.await(4000);
- if (expectSuccess) {
- assertNotNull("Can open", result);
- } else {
- assertNull("Should not open", result);
- }
- return result;
- }
-
- private void waitForClose(boolean expectSuccess) {
- camera.stop();
- final Task close = new Task<>(true);
- doEndTask(close, true).when(listener).onCameraClosed();
- Boolean result = close.await(4000);
- if (expectSuccess) {
- assertNotNull("Can close", result);
- } else {
- assertNull("Should not close", result);
- }
- }
-
- private void waitForVideoEnd(boolean expectSuccess) {
- final Task video = new Task<>(true);
- doEndTask(video, true).when(listener).onVideoTaken(any(File.class));
- Boolean result = video.await(8000);
- if (expectSuccess) {
- assertNotNull("Should end video", result);
- } else {
- assertNull("Should not end video", result);
- }
- }
-
- private byte[] waitForPicture(boolean expectSuccess) {
- final Task pic = new Task<>(true);
- doEndTask(pic, 0).when(listener).onPictureTaken(any(byte[].class));
- byte[] result = pic.await(5000);
- if (expectSuccess) {
- assertNotNull("Can take picture", result);
- } else {
- assertNull("Should not take picture", result);
- }
- return result;
- }
-
- private void waitForVideoStart() {
- controller.mStartVideoTask.listen();
- camera.startCapturingVideo(null);
- controller.mStartVideoTask.await(400);
- }
-
- private void waitForVideoQuality(VideoQuality quality) {
- controller.mVideoQualityTask.listen();
- camera.setVideoQuality(quality);
- controller.mVideoQualityTask.await(400);
- }
-
- //region test open/close
-
- @Test
- public void testOpenClose() throws Exception {
- // Starting and stopping are hard to get since they happen on another thread.
- assertEquals(controller.getState(), CameraController.STATE_STOPPED);
-
- waitForOpen(true);
- assertEquals(controller.getState(), CameraController.STATE_STARTED);
-
- waitForClose(true);
- assertEquals(controller.getState(), CameraController.STATE_STOPPED);
- }
-
- @Test
- public void testOpenTwice() {
- waitForOpen(true);
- waitForOpen(false);
- }
-
- @Test
- public void testCloseTwice() {
- waitForClose(false);
- }
-
- @Test
- // This works great on the device but crashes often on the emulator.
- // There must be something wrong with the emulated camera...
- // Like stopPreview() and release() are not really sync calls?
- public void testConcurrentCalls() throws Exception {
- final CountDownLatch latch = new CountDownLatch(4);
- doCountDown(latch).when(listener).onCameraOpened(any(CameraOptions.class));
- doCountDown(latch).when(listener).onCameraClosed();
-
- camera.start();
- camera.stop();
- camera.start();
- camera.stop();
-
- boolean did = latch.await(10, TimeUnit.SECONDS);
- assertTrue("Handles concurrent calls to start & stop, " + latch.getCount(), did);
- }
-
- @Test
- public void testStartInitializesOptions() {
- assertNull(camera.getCameraOptions());
- assertNull(camera.getExtraProperties());
- waitForOpen(true);
- assertNotNull(camera.getCameraOptions());
- assertNotNull(camera.getExtraProperties());
- }
-
- //endregion
-
- //region test Facing/SessionType
- // Test things that should reset the camera.
-
- @Test
- public void testSetFacing() throws Exception {
- CameraOptions o = waitForOpen(true);
- int size = o.getSupportedFacing().size();
- if (size > 1) {
- // set facing should call stop and start again.
- final CountDownLatch latch = new CountDownLatch(2);
- doCountDown(latch).when(listener).onCameraOpened(any(CameraOptions.class));
- doCountDown(latch).when(listener).onCameraClosed();
-
- camera.toggleFacing();
-
- boolean did = latch.await(2, TimeUnit.SECONDS);
- assertTrue("Handles setFacing while active", did);
- }
- }
-
- @Test
- public void testSetSessionType() throws Exception {
- camera.setSessionType(SessionType.PICTURE);
- waitForOpen(true);
-
- // set session type should call stop and start again.
- final CountDownLatch latch = new CountDownLatch(2);
- doCountDown(latch).when(listener).onCameraOpened(any(CameraOptions.class));
- doCountDown(latch).when(listener).onCameraClosed();
-
- camera.setSessionType(SessionType.VIDEO);
-
- boolean did = latch.await(2, TimeUnit.SECONDS);
- assertTrue("Handles setSessionType while active", did);
- assertEquals(camera.getSessionType(), SessionType.VIDEO);
- }
-
- //endregion
-
- //region test Set Parameters
- // When camera is open, parameters will be set only if supported.
-
- @Test
- public void testSetZoom() {
- CameraOptions options = waitForOpen(true);
-
- controller.mZoomTask.listen();
- float oldValue = camera.getZoom();
- float newValue = 0.65f;
- camera.setZoom(newValue);
- controller.mZoomTask.await(500);
-
- if (options.isZoomSupported()) {
- assertEquals(newValue, camera.getZoom(), 0f);
- } else {
- assertEquals(oldValue, camera.getZoom(), 0f);
- }
- }
-
- @Test
- public void testSetExposureCorrection() {
- CameraOptions options = waitForOpen(true);
-
- controller.mExposureCorrectionTask.listen();
- float oldValue = camera.getExposureCorrection();
- float newValue = options.getExposureCorrectionMaxValue();
- camera.setExposureCorrection(newValue);
- controller.mExposureCorrectionTask.await(300);
-
- if (options.isExposureCorrectionSupported()) {
- assertEquals(newValue, camera.getExposureCorrection(), 0f);
- } else {
- assertEquals(oldValue, camera.getExposureCorrection(), 0f);
- }
- }
-
- @Test
- public void testSetFlash() {
- CameraOptions options = waitForOpen(true);
- Flash[] values = Flash.values();
- Flash oldValue = camera.getFlash();
- for (Flash value : values) {
- controller.mFlashTask.listen();
- camera.setFlash(value);
- controller.mFlashTask.await(300);
- if (options.supports(value)) {
- assertEquals(camera.getFlash(), value);
- oldValue = value;
- } else {
- assertEquals(camera.getFlash(), oldValue);
- }
- }
- }
-
- @Test
- public void testSetWhiteBalance() {
- CameraOptions options = waitForOpen(true);
- WhiteBalance[] values = WhiteBalance.values();
- WhiteBalance oldValue = camera.getWhiteBalance();
- for (WhiteBalance value : values) {
- controller.mWhiteBalanceTask.listen();
- camera.setWhiteBalance(value);
- controller.mWhiteBalanceTask.await(300);
- if (options.supports(value)) {
- assertEquals(camera.getWhiteBalance(), value);
- oldValue = value;
- } else {
- assertEquals(camera.getWhiteBalance(), oldValue);
- }
- }
- }
-
- @Test
- public void testSetHdr() {
- CameraOptions options = waitForOpen(true);
- Hdr[] values = Hdr.values();
- Hdr oldValue = camera.getHdr();
- for (Hdr value : values) {
- controller.mHdrTask.listen();
- camera.setHdr(value);
- controller.mHdrTask.await(300);
- if (options.supports(value)) {
- assertEquals(camera.getHdr(), value);
- oldValue = value;
- } else {
- assertEquals(camera.getHdr(), oldValue);
- }
- }
- }
-
- @Test
- public void testSetAudio() {
- // TODO: when permissions are managed, check that Audio.ON triggers the audio permission
- waitForOpen(true);
- Audio[] values = Audio.values();
- for (Audio value : values) {
- camera.setAudio(value);
- assertEquals(camera.getAudio(), value);
- }
- }
-
- @Test
- public void testSetLocation() {
- waitForOpen(true);
- controller.mLocationTask.listen();
- camera.setLocation(10d, 2d);
- controller.mLocationTask.await(300);
- assertNotNull(camera.getLocation());
- assertEquals(camera.getLocation().getLatitude(), 10d, 0d);
- assertEquals(camera.getLocation().getLongitude(), 2d, 0d);
- // This also ensures there are no crashes when attaching it to camera parameters.
- }
-
- @Test
- public void testSetPlaySounds() {
- controller.mPlaySoundsTask.listen();
- boolean oldValue = camera.getPlaySounds();
- boolean newValue = !oldValue;
- camera.setPlaySounds(newValue);
- controller.mPlaySoundsTask.await(300);
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- Camera.CameraInfo info = new Camera.CameraInfo();
- Camera.getCameraInfo(camera.getCameraId(), info);
- if (info.canDisableShutterSound) {
- assertEquals(newValue, camera.getPlaySounds());
- }
- } else {
- assertEquals(oldValue, camera.getPlaySounds());
- }
- }
-
- //endregion
-
- //region testSetVideoQuality
- // This can be tricky because can trigger layout changes.
-
- @Test(expected = RuntimeException.class)
- public void testSetVideoQuality_whileRecording() throws Throwable {
- // Can't run on Travis, MediaRecorder not supported.
- // Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
- camera.setSessionType(SessionType.VIDEO);
- waitForVideoQuality(VideoQuality.HIGHEST);
- waitForOpen(true);
- waitForVideoStart();
- waitForVideoQuality(VideoQuality.LOWEST);
- waitForUiException();
- }
-
- @Test
- public void testSetVideoQuality_whileInPictureSessionType() {
- camera.setSessionType(SessionType.PICTURE);
- waitForVideoQuality(VideoQuality.HIGHEST);
- waitForOpen(true);
- waitForVideoQuality(VideoQuality.LOWEST);
- assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST);
- }
-
- @Test
- public void testSetVideoQuality_whileNotStarted() {
- waitForVideoQuality(VideoQuality.HIGHEST);
- assertEquals(camera.getVideoQuality(), VideoQuality.HIGHEST);
-
- waitForVideoQuality(VideoQuality.LOWEST);
- assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST);
- }
-
- @Test
- public void testSetVideoQuality_shouldRecompute() {
- // TODO:
- // If video quality changes bring to a new capture size,
- // this might bring to a new aspect ratio,
- // which might bring to a new preview size. No idea how to test.
- assertTrue(true);
- }
-
- //endregion
-
- //region test startVideo
-
- @Test(expected = RuntimeException.class)
- public void testStartVideo_whileInPictureMode() throws Throwable {
- // Fails on Travis. Some emulators can't deal with MediaRecorder
- // Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
- // as documented. This works locally though.
- camera.setSessionType(SessionType.PICTURE);
- waitForOpen(true);
- waitForVideoStart();
- waitForUiException();
- }
-
- @Test
- public void testStartEndVideo() {
- // Fails on Travis. Some emulators can't deal with MediaRecorder,
- // Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
- // as documented. This works locally though.
- camera.setSessionType(SessionType.VIDEO);
- waitForOpen(true);
- camera.startCapturingVideo(null, 4000);
- waitForVideoEnd(true);
- }
-
- @Test
- public void testEndVideo_withoutStarting() {
- camera.setSessionType(SessionType.VIDEO);
- waitForOpen(true);
- camera.stopCapturingVideo();
- waitForVideoEnd(false);
- }
-
- @Test
- public void testEndVideo_withMaxSize() {
- camera.setSessionType(SessionType.VIDEO);
- camera.setVideoMaxSize(500*1000); // 0.5 mb
- waitForOpen(true);
- waitForVideoStart();
- waitForVideoEnd(true);
- }
-
- @Test
- public void testEndVideo_withMaxDuration() {
- camera.setSessionType(SessionType.VIDEO);
- camera.setVideoMaxDuration(4000);
- waitForOpen(true);
- waitForVideoStart();
- waitForVideoEnd(true);
- }
-
- //endregion
-
- //region startAutoFocus
- // TODO: won't test onStopAutoFocus because that is not guaranteed to be called
-
- @Test
- public void testStartAutoFocus() {
- CameraOptions o = waitForOpen(true);
-
- final Task focus = new Task<>(true);
- doEndTask(focus, 0).when(listener).onFocusStart(any(PointF.class));
-
- camera.startAutoFocus(1, 1);
- PointF point = focus.await(300);
- if (o.isAutoFocusSupported()) {
- assertNotNull(point);
- assertEquals(point, new PointF(1, 1));
- } else {
- assertNull(point);
- }
- }
-
- //endregion
-
- //region capture
-
- @Test
- public void testCapturePicture_beforeStarted() {
- camera.capturePicture();
- waitForPicture(false);
- }
-
- @Test
- public void testCapturePicture_concurrentCalls() throws Exception {
- // Second take should fail.
- waitForOpen(true);
-
- CountDownLatch latch = new CountDownLatch(2);
- doCountDown(latch).when(listener).onPictureTaken(any(byte[].class));
-
- camera.capturePicture();
- camera.capturePicture();
- boolean did = latch.await(4, TimeUnit.SECONDS);
- assertFalse(did);
- assertEquals(latch.getCount(), 1);
- }
-
- @Test
- public void testCapturePicture_size() throws Exception {
- camera.setCropOutput(false);
- waitForOpen(true);
-
- Size size = camera.getPictureSize();
- camera.capturePicture();
- byte[] jpeg = waitForPicture(true);
- Bitmap b = CameraUtils.decodeBitmap(jpeg, Integer.MAX_VALUE, Integer.MAX_VALUE);
- // Result can actually have swapped dimensions
- // Which one, depends on factors including device physical orientation
- assertTrue(b.getWidth() == size.getHeight() || b.getWidth() == size.getWidth());
- assertTrue(b.getHeight() == size.getHeight() || b.getHeight() == size.getWidth());
- }
-
- @Test
- public void testCaptureSnapshot_beforeStarted() {
- camera.captureSnapshot();
- waitForPicture(false);
- }
-
- @Test
- public void testCaptureSnapshot_concurrentCalls() throws Exception {
- // Second take should fail.
- waitForOpen(true);
-
- CountDownLatch latch = new CountDownLatch(2);
- doCountDown(latch).when(listener).onPictureTaken(any(byte[].class));
-
- camera.captureSnapshot();
- camera.captureSnapshot();
- boolean did = latch.await(6, TimeUnit.SECONDS);
- assertFalse(did);
- assertEquals(1, latch.getCount());
- }
-
- @Test
- public void testCaptureSnapshot_size() throws Exception {
- camera.setCropOutput(false);
- waitForOpen(true);
-
- Size size = camera.getPreviewSize();
- camera.captureSnapshot();
- byte[] jpeg = waitForPicture(true);
- Bitmap b = CameraUtils.decodeBitmap(jpeg, Integer.MAX_VALUE, Integer.MAX_VALUE);
- // Result can actually have swapped dimensions
- // Which one, depends on factors including device physical orientation
- assertTrue(b.getWidth() == size.getHeight() || b.getWidth() == size.getWidth());
- assertTrue(b.getHeight() == size.getHeight() || b.getHeight() == size.getWidth());
- }
-
- //endregion
-
- //region Frame Processing
-
- private void assert30Frames(FrameProcessor mock) throws Exception {
- // Expect 30 frames
- CountDownLatch latch = new CountDownLatch(30);
- doCountDown(latch).when(mock).process(any(Frame.class));
- boolean did = latch.await(4, TimeUnit.SECONDS);
- assertTrue(did);
- }
-
- @Test
- public void testFrameProcessing_simple() throws Exception {
- FrameProcessor processor = mock(FrameProcessor.class);
- camera.addFrameProcessor(processor);
- waitForOpen(true);
-
- assert30Frames(processor);
- }
-
- @Test
- public void testFrameProcessing_afterSnapshot() throws Exception {
- FrameProcessor processor = mock(FrameProcessor.class);
- camera.addFrameProcessor(processor);
- waitForOpen(true);
-
- // In Camera1, snapshots will clear the preview callback
- // Ensure we restore correctly
- camera.captureSnapshot();
- waitForPicture(true);
-
- assert30Frames(processor);
- }
-
- @Test
- public void testFrameProcessing_afterRestart() throws Exception {
- FrameProcessor processor = mock(FrameProcessor.class);
- camera.addFrameProcessor(processor);
- waitForOpen(true);
- waitForClose(true);
- waitForOpen(true);
-
- assert30Frames(processor);
- }
-
-
- @Test
- public void testFrameProcessing_afterVideo() throws Exception {
- FrameProcessor processor = mock(FrameProcessor.class);
- camera.addFrameProcessor(processor);
- camera.setSessionType(SessionType.VIDEO);
- waitForOpen(true);
-
- camera.startCapturingVideo(null, 2000);
- waitForVideoEnd(true);
-
- assert30Frames(processor);
- }
-
- //endregion
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/Mapper1Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/Mapper1Test.java
deleted file mode 100644
index 4a04c12d..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/Mapper1Test.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.hardware.Camera;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.*;
-
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class Mapper1Test extends BaseTest {
-
- private Mapper mapper = new Mapper.Mapper1();
-
- @Test
- public void testMap() {
- assertEquals(mapper.map(Flash.OFF), Camera.Parameters.FLASH_MODE_OFF);
- assertEquals(mapper.map(Flash.ON), Camera.Parameters.FLASH_MODE_ON);
- assertEquals(mapper.map(Flash.AUTO), Camera.Parameters.FLASH_MODE_AUTO);
- assertEquals(mapper.map(Flash.TORCH), Camera.Parameters.FLASH_MODE_TORCH);
-
- assertEquals(mapper.map(Facing.BACK), Camera.CameraInfo.CAMERA_FACING_BACK);
- assertEquals(mapper.map(Facing.FRONT), Camera.CameraInfo.CAMERA_FACING_FRONT);
-
- assertEquals(mapper.map(Hdr.OFF), Camera.Parameters.SCENE_MODE_AUTO);
- assertEquals(mapper.map(Hdr.ON), Camera.Parameters.SCENE_MODE_HDR);
-
- assertEquals(mapper.map(WhiteBalance.AUTO), Camera.Parameters.WHITE_BALANCE_AUTO);
- assertEquals(mapper.map(WhiteBalance.DAYLIGHT), Camera.Parameters.WHITE_BALANCE_DAYLIGHT);
- assertEquals(mapper.map(WhiteBalance.CLOUDY), Camera.Parameters.WHITE_BALANCE_CLOUDY_DAYLIGHT);
- assertEquals(mapper.map(WhiteBalance.INCANDESCENT), Camera.Parameters.WHITE_BALANCE_INCANDESCENT);
- assertEquals(mapper.map(WhiteBalance.FLUORESCENT), Camera.Parameters.WHITE_BALANCE_FLUORESCENT);
- }
-
-
- @Test
- public void testUnmap() {
- assertEquals(Flash.OFF, mapper.unmapFlash(Camera.Parameters.FLASH_MODE_OFF));
- assertEquals(Flash.ON, mapper.unmapFlash(Camera.Parameters.FLASH_MODE_ON));
- assertEquals(Flash.AUTO, mapper.unmapFlash(Camera.Parameters.FLASH_MODE_AUTO));
- assertEquals(Flash.TORCH, mapper.unmapFlash(Camera.Parameters.FLASH_MODE_TORCH));
-
- assertEquals(Facing.BACK, mapper.unmapFacing(Camera.CameraInfo.CAMERA_FACING_BACK));
- assertEquals(Facing.FRONT, mapper.unmapFacing(Camera.CameraInfo.CAMERA_FACING_FRONT));
-
- assertEquals(Hdr.OFF, mapper.unmapHdr(Camera.Parameters.SCENE_MODE_AUTO));
- assertEquals(Hdr.ON, mapper.unmapHdr(Camera.Parameters.SCENE_MODE_HDR));
-
- assertEquals(WhiteBalance.AUTO, mapper.unmapWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO));
- assertEquals(WhiteBalance.DAYLIGHT, mapper.unmapWhiteBalance(Camera.Parameters.WHITE_BALANCE_DAYLIGHT));
- assertEquals(WhiteBalance.CLOUDY, mapper.unmapWhiteBalance(Camera.Parameters.WHITE_BALANCE_CLOUDY_DAYLIGHT));
- assertEquals(WhiteBalance.INCANDESCENT, mapper.unmapWhiteBalance(Camera.Parameters.WHITE_BALANCE_INCANDESCENT));
- assertEquals(WhiteBalance.FLUORESCENT, mapper.unmapWhiteBalance(Camera.Parameters.WHITE_BALANCE_FLUORESCENT));
- }
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MapperTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MapperTest.java
deleted file mode 100644
index 6a61c36d..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MapperTest.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.hardware.Camera;
-import android.media.MediaRecorder;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.assertEquals;
-
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class MapperTest extends BaseTest {
-
- private Mapper mapper = new Mapper() {
- T map(Flash flash) { return null; }
- T map(Facing facing) { return null; }
- T map(WhiteBalance whiteBalance) { return null; }
- T map(Hdr hdr) { return null; }
- Flash unmapFlash(T cameraConstant) { return null; }
- Facing unmapFacing(T cameraConstant) { return null; }
- WhiteBalance unmapWhiteBalance(T cameraConstant) { return null; }
- Hdr unmapHdr(T cameraConstant) { return null; }
- };
-
- @Test
- public void testMap() {
- assertEquals(mapper.map(VideoCodec.DEVICE_DEFAULT), MediaRecorder.VideoEncoder.DEFAULT);
- assertEquals(mapper.map(VideoCodec.H_263), MediaRecorder.VideoEncoder.H263);
- assertEquals(mapper.map(VideoCodec.H_264), MediaRecorder.VideoEncoder.H264);
- }
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java
deleted file mode 100644
index 5a4b8f08..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java
+++ /dev/null
@@ -1,132 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.graphics.PointF;
-import android.location.Location;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.io.File;
-
-public class MockCameraController extends CameraController {
-
- boolean mPictureCaptured;
- boolean mFocusStarted;
- boolean mZoomChanged;
- boolean mExposureCorrectionChanged;
-
- MockCameraController(CameraView.CameraCallbacks callback) {
- super(callback);
- }
-
- void setMockCameraOptions(CameraOptions options) {
- mCameraOptions = options;
- }
-
- void setMockPreviewSize(Size size) {
- mPreviewSize = size;
- }
-
- void mockStarted(boolean started) {
- mState = started ? STATE_STARTED : STATE_STOPPED;
- }
-
- @Override
- void onStart() {
- }
-
- @Override
- void onStop() {
- }
-
- @Override
- void setZoom(float zoom, PointF[] points, boolean notify) {
- mZoomValue = zoom;
- mZoomChanged = true;
- }
-
- @Override
- void setExposureCorrection(float EVvalue, float[] bounds, PointF[] points, boolean notify) {
- mExposureCorrectionValue = EVvalue;
- mExposureCorrectionChanged = true;
- }
-
- @Override
- void setFacing(Facing facing) {
- mFacing = facing;
- }
-
- @Override
- void setFlash(Flash flash) {
- mFlash = flash;
- }
-
- @Override
- void setWhiteBalance(WhiteBalance whiteBalance) {
- mWhiteBalance = whiteBalance;
- }
-
- @Override
- void setVideoQuality(VideoQuality videoQuality) {
- mVideoQuality = videoQuality;
- }
-
- @Override
- void setSessionType(SessionType sessionType) {
- mSessionType = sessionType;
- }
-
- @Override
- void setHdr(Hdr hdr) {
- mHdr = hdr;
- }
-
- @Override
- void setAudio(Audio audio) {
- mAudio = audio;
- }
-
- @Override
- void setLocation(Location location) {
- mLocation = location;
- }
-
- @Override
- void capturePicture() {
- mPictureCaptured = true;
- }
-
- @Override
- void captureSnapshot() {
- }
-
- @Override
- void startVideo(@NonNull File file) {
- }
-
- @Override
- void endVideo() {
- }
-
- @Override
- void startAutoFocus(@Nullable Gesture gesture, PointF point) {
- mFocusStarted = true;
- }
-
- @Override
- public void onSurfaceChanged() {
- }
-
- @Override
- public void onSurfaceAvailable() {
- }
-
- @Override
- public void onBufferAvailable(byte[] buffer) {
- }
-
- @Override
- void setPlaySounds(boolean playSounds) {
-
- }
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraPreview.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraPreview.java
deleted file mode 100644
index f3f8a4d7..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraPreview.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.view.Surface;
-import android.view.View;
-import android.view.ViewGroup;
-
-public class MockCameraPreview extends CameraPreview {
-
- MockCameraPreview(Context context, ViewGroup parent) {
- super(context, parent, null);
- }
-
- private boolean mCropping = false;
-
- public void setIsCropping(boolean crop) {
- mCropping = crop;
- }
-
- @Override
- boolean isCropping() {
- return mCropping;
- }
-
- @NonNull
- @Override
- protected View onCreateView(Context context, ViewGroup parent) {
- return new View(context);
- }
-
- @Override
- Surface getSurface() {
- return null;
- }
-
- @Override
- Class getOutputClass() {
- return null;
- }
-
- @Override
- Void getOutput() {
- return null;
- }
-
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java
deleted file mode 100644
index f4d66425..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.view.OrientationEventListener;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class OrientationHelperTest extends BaseTest {
-
- private OrientationHelper helper;
- private OrientationHelper.Callback callback;
-
- @Before
- public void setUp() {
- ui(new Runnable() {
- @Override
- public void run() {
- callback = mock(OrientationHelper.Callback.class);
- helper = new OrientationHelper(context(), callback);
- }
- });
- }
-
- @After
- public void tearDown() {
- callback = null;
- helper = null;
- }
-
- @Test
- public void testEnable() {
- assertNotNull(helper.mListener);
- assertEquals(helper.getDisplayOffset(), -1);
- assertEquals(helper.getDeviceOrientation(), -1);
-
- helper.enable(context());
- assertNotNull(helper.mListener);
- assertNotEquals(helper.getDisplayOffset(), -1); // Don't know about device orientation.
-
- // Ensure nothing bad if called twice.
- helper.enable(context());
- assertNotNull(helper.mListener);
- assertNotEquals(helper.getDisplayOffset(), -1);
-
- helper.disable();
- assertNotNull(helper.mListener);
- assertEquals(helper.getDisplayOffset(), -1);
- assertEquals(helper.getDeviceOrientation(), -1);
- }
-
- @Test
- public void testRotation() {
-
- // Sometimes (on some APIs) the helper will trigger an update to 0
- // right after enabling. But that's fine for us, times(1) will be OK either way.
- helper.enable(context());
- helper.mListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
- assertEquals(helper.getDeviceOrientation(), 0);
- helper.mListener.onOrientationChanged(10);
- assertEquals(helper.getDeviceOrientation(), 0);
- helper.mListener.onOrientationChanged(-10);
- assertEquals(helper.getDeviceOrientation(), 0);
- helper.mListener.onOrientationChanged(44);
- assertEquals(helper.getDeviceOrientation(), 0);
- helper.mListener.onOrientationChanged(360);
- assertEquals(helper.getDeviceOrientation(), 0);
-
- // Callback called just once.
- verify(callback, times(1)).onDeviceOrientationChanged(0);
-
- helper.mListener.onOrientationChanged(90);
- helper.mListener.onOrientationChanged(91);
- assertEquals(helper.getDeviceOrientation(), 90);
- verify(callback, times(1)).onDeviceOrientationChanged(90);
-
- helper.mListener.onOrientationChanged(180);
- assertEquals(helper.getDeviceOrientation(), 180);
- verify(callback, times(1)).onDeviceOrientationChanged(180);
-
- helper.mListener.onOrientationChanged(270);
- assertEquals(helper.getDeviceOrientation(), 270);
- verify(callback, times(1)).onDeviceOrientationChanged(270);
-
- // It is still 270 after ORIENTATION_UNKNOWN
- helper.mListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
- assertEquals(helper.getDeviceOrientation(), 270);
- verify(callback, times(1)).onDeviceOrientationChanged(270);
- }
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PictureResultTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PictureResultTest.java
new file mode 100644
index 00000000..8bd0cd65
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PictureResultTest.java
@@ -0,0 +1,55 @@
+package com.otaliastudios.cameraview;
+
+
+import android.location.Location;
+
+import com.otaliastudios.cameraview.controls.Facing;
+import com.otaliastudios.cameraview.controls.PictureFormat;
+import com.otaliastudios.cameraview.size.Size;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import static org.junit.Assert.assertEquals;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PictureResultTest extends BaseTest {
+
+ private PictureResult.Stub stub = new PictureResult.Stub();
+
+ @Test
+ public void testResult() {
+ PictureFormat format = PictureFormat.JPEG;
+ int rotation = 90;
+ Size size = new Size(20, 120);
+ byte[] jpeg = new byte[]{2, 4, 1, 5, 2};
+ Location location = Mockito.mock(Location.class);
+ boolean isSnapshot = true;
+ Facing facing = Facing.FRONT;
+
+ stub.format = format;
+ stub.rotation = rotation;
+ stub.size = size;
+ stub.data = jpeg;
+ stub.location = location;
+ stub.facing = facing;
+ //noinspection ConstantConditions
+ stub.isSnapshot = isSnapshot;
+
+ PictureResult result = new PictureResult(stub);
+ assertEquals(result.getFormat(), format);
+ assertEquals(result.getRotation(), rotation);
+ assertEquals(result.getSize(), size);
+ assertEquals(result.getData(), jpeg);
+ assertEquals(result.getLocation(), location);
+ //noinspection ConstantConditions
+ assertEquals(result.isSnapshot(), isSnapshot);
+ assertEquals(result.getFacing(), facing);
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PinchGestureLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PinchGestureLayoutTest.java
deleted file mode 100644
index 8205d356..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PinchGestureLayoutTest.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.content.Context;
-import android.support.test.espresso.ViewAction;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class PinchGestureLayoutTest extends GestureLayoutTest {
-
- @Override
- protected PinchGestureLayout create(Context context) {
- return new PinchGestureLayout(context);
- }
-
- @Test
- public void testDefaults() {
- assertEquals(layout.getGestureType(), Gesture.PINCH);
- assertEquals(layout.getPoints().length, 2);
- assertEquals(layout.getPoints()[0].x, 0, 0);
- assertEquals(layout.getPoints()[0].y, 0, 0);
- assertEquals(layout.getPoints()[1].x, 0, 0);
- assertEquals(layout.getPoints()[1].y, 0, 0);
- }
-
- // TODO: test pinch open
- // TODO: test pinch close
- // TODO: test pinch disabled
-
- // Possible approach: mimic pinch gesture and let espresso test.
- // Too lazy to do this now, but it's possible.
- // https://stackoverflow.com/questions/11523423/how-to-generate-zoom-pinch-gesture-for-testing-for-android
-
- public abstract class PinchViewAction implements ViewAction {
- }
-
- private void testPinch(ViewAction action, boolean increasing) {
- touch.listen();
- touch.start();
- onLayout().perform(action);
- Gesture found = touch.await(10000);
- assertNotNull(found);
-
- // How will this move our parameter?
- float curr = 0.5f, min = 0f, max = 1f;
- float newValue = layout.scaleValue(curr, min, max);
- if (increasing) {
- assertTrue(newValue > curr);
- assertTrue(newValue <= max);
- } else {
- assertTrue(newValue < curr);
- assertTrue(newValue >= min);
- }
- }
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/ScrollGestureLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/ScrollGestureLayoutTest.java
deleted file mode 100644
index 124a7e59..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/ScrollGestureLayoutTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.content.Context;
-import android.support.test.espresso.ViewAction;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static android.support.test.espresso.action.ViewActions.click;
-import static android.support.test.espresso.action.ViewActions.swipeDown;
-import static android.support.test.espresso.action.ViewActions.swipeLeft;
-import static android.support.test.espresso.action.ViewActions.swipeRight;
-import static android.support.test.espresso.action.ViewActions.swipeUp;
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static junit.framework.Assert.assertNotNull;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class ScrollGestureLayoutTest extends GestureLayoutTest {
-
- @Override
- protected ScrollGestureLayout create(Context context) {
- return new ScrollGestureLayout(context);
- }
-
- @Test
- public void testDefaults() {
- assertNull(layout.getGestureType());
- assertEquals(layout.getPoints().length, 2);
- assertEquals(layout.getPoints()[0].x, 0, 0);
- assertEquals(layout.getPoints()[0].y, 0, 0);
- assertEquals(layout.getPoints()[1].x, 0, 0);
- assertEquals(layout.getPoints()[1].y, 0, 0);
- }
-
- @Test
- public void testScrollDisabled() {
- layout.enable(false);
- touch.listen();
- touch.start();
- onLayout().perform(swipeUp());
- Gesture found = touch.await(500);
- assertNull(found);
- }
-
- private void testScroll(ViewAction scroll, Gesture expected, boolean increasing) {
- touch.listen();
- touch.start();
- onLayout().perform(scroll);
- Gesture found = touch.await(500);
- assertEquals(found, expected);
-
- // How will this move our parameter?
- float curr = 0.5f, min = 0f, max = 1f;
- float newValue = layout.scaleValue(curr, min, max);
- if (increasing) {
- assertTrue(newValue > curr);
- assertTrue(newValue <= max);
- } else {
- assertTrue(newValue < curr);
- assertTrue(newValue >= min);
- }
- }
-
- @Test
- public void testScrollLeft() {
- testScroll(swipeLeft(), Gesture.SCROLL_HORIZONTAL, false);
- }
-
- @Test
- public void testScrollRight() {
- testScroll(swipeRight(), Gesture.SCROLL_HORIZONTAL, true);
- }
-
- @Test
- public void testScrollUp() {
- testScroll(swipeUp(), Gesture.SCROLL_VERTICAL, true);
- }
-
- @Test
- public void testScrollDown() {
- testScroll(swipeDown(), Gesture.SCROLL_VERTICAL, false);
- }
-
-
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceCameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceCameraPreviewTest.java
deleted file mode 100644
index d3d0cdb6..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceCameraPreviewTest.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.content.Context;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.view.ViewGroup;
-
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class SurfaceCameraPreviewTest extends PreviewTest {
-
- @Override
- protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) {
- return new SurfaceCameraPreview(context, parent, callback);
- }
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TapGestureLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TapGestureLayoutTest.java
deleted file mode 100644
index 6498e572..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TapGestureLayoutTest.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.content.Context;
-import android.support.test.espresso.action.GeneralClickAction;
-import android.support.test.espresso.action.GeneralLocation;
-import android.support.test.espresso.action.Press;
-import android.support.test.espresso.action.Tap;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.view.InputDevice;
-import android.view.MotionEvent;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static android.support.test.espresso.action.ViewActions.*;
-import static org.junit.Assert.*;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class TapGestureLayoutTest extends GestureLayoutTest {
-
- @Override
- protected TapGestureLayout create(Context context) {
- return new TapGestureLayout(context);
- }
-
- @Test
- public void testDefaults() {
- assertNull(layout.getGestureType());
- assertEquals(layout.getPoints().length, 1);
- assertEquals(layout.getPoints()[0].x, 0, 0);
- assertEquals(layout.getPoints()[0].y, 0, 0);
- }
-
- @Test
- public void testTap() {
- touch.listen();
- touch.start();
- GeneralClickAction a = new GeneralClickAction(
- Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER,
- InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY);
- onLayout().perform(a);
- Gesture found = touch.await(500);
-
- assertEquals(found, Gesture.TAP);
- Size size = rule.getActivity().getContentSize();
- assertEquals(layout.getPoints()[0].x, (size.getWidth() / 2f), 1f);
- assertEquals(layout.getPoints()[0].y, (size.getHeight() / 2f), 1f);
- }
-
- @Test
- public void testTapWhileDisabled() {
- layout.enable(false);
- touch.listen();
- touch.start();
- onLayout().perform(click());
- Gesture found = touch.await(500);
- assertNull(found);
- }
-
- @Test
- public void testLongTap() {
- touch.listen();
- touch.start();
- GeneralClickAction a = new GeneralClickAction(
- Tap.LONG, GeneralLocation.CENTER, Press.FINGER,
- InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY);
- onLayout().perform(a);
- Gesture found = touch.await(500);
- assertEquals(found, Gesture.LONG_TAP);
- Size size = rule.getActivity().getContentSize();
- assertEquals(layout.getPoints()[0].x, (size.getWidth() / 2f), 1f);
- assertEquals(layout.getPoints()[0].y, (size.getHeight() / 2f), 1f);
- }
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TestActivity.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TestActivity.java
index c8707917..48637d7a 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TestActivity.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TestActivity.java
@@ -2,18 +2,18 @@ package com.otaliastudios.cameraview;
import android.app.Activity;
-import android.app.KeyguardManager;
-import android.content.Context;
import android.graphics.Point;
import android.os.Bundle;
-import android.os.PersistableBundle;
-import android.support.annotation.Nullable;
+
+import androidx.annotation.Nullable;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
+import com.otaliastudios.cameraview.size.Size;
+
import static android.view.ViewGroup.LayoutParams.*;
public class TestActivity extends Activity {
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java
new file mode 100644
index 00000000..e4ed22a4
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java
@@ -0,0 +1,146 @@
+package com.otaliastudios.cameraview;
+
+
+import android.location.Location;
+
+import com.otaliastudios.cameraview.controls.Audio;
+import com.otaliastudios.cameraview.controls.AudioCodec;
+import com.otaliastudios.cameraview.controls.Facing;
+import com.otaliastudios.cameraview.controls.VideoCodec;
+import com.otaliastudios.cameraview.size.Size;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.FileDescriptor;
+
+import static org.junit.Assert.assertEquals;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VideoResultTest extends BaseTest {
+
+ private VideoResult.Stub stub = new VideoResult.Stub();
+
+ @Test
+ public void testResultWithFile() {
+ File file = Mockito.mock(File.class);
+ int rotation = 90;
+ Size size = new Size(20, 120);
+ VideoCodec videoCodec = VideoCodec.H_263;
+ AudioCodec audioCodec = AudioCodec.DEVICE_DEFAULT;
+ Location location = Mockito.mock(Location.class);
+ boolean isSnapshot = true;
+ int maxDuration = 1234;
+ long maxFileSize = 500000;
+ int reason = VideoResult.REASON_MAX_DURATION_REACHED;
+ int videoFrameRate = 30;
+ int videoBitRate = 300000;
+ int audioBitRate = 30000;
+ Audio audio = Audio.ON;
+ Facing facing = Facing.FRONT;
+
+ stub.file = file;
+ stub.rotation = rotation;
+ stub.size = size;
+ stub.videoCodec = videoCodec;
+ stub.audioCodec = audioCodec;
+ stub.location = location;
+ stub.isSnapshot = isSnapshot;
+ stub.maxDuration = maxDuration;
+ stub.maxSize = maxFileSize;
+ stub.endReason = reason;
+ stub.videoFrameRate = videoFrameRate;
+ stub.videoBitRate = videoBitRate;
+ stub.audioBitRate = audioBitRate;
+ stub.audio = audio;
+ stub.facing = facing;
+
+ VideoResult result = new VideoResult(stub);
+ assertEquals(result.getFile(), file);
+ assertEquals(result.getRotation(), rotation);
+ assertEquals(result.getSize(), size);
+ assertEquals(result.getVideoCodec(), videoCodec);
+ assertEquals(result.getAudioCodec(), audioCodec);
+ assertEquals(result.getLocation(), location);
+ assertEquals(result.isSnapshot(), isSnapshot);
+ assertEquals(result.getMaxSize(), maxFileSize);
+ assertEquals(result.getMaxDuration(), maxDuration);
+ assertEquals(result.getTerminationReason(), reason);
+ assertEquals(result.getVideoFrameRate(), videoFrameRate);
+ assertEquals(result.getVideoBitRate(), videoBitRate);
+ assertEquals(result.getAudioBitRate(), audioBitRate);
+ assertEquals(result.getAudio(), audio);
+ assertEquals(result.getFacing(), facing);
+ }
+
+ @Test
+ public void testResultWithFileDescriptor() {
+ FileDescriptor fileDescriptor = FileDescriptor.in;
+ int rotation = 90;
+ Size size = new Size(20, 120);
+ VideoCodec videoCodec = VideoCodec.H_263;
+ AudioCodec audioCodec = AudioCodec.DEVICE_DEFAULT;
+ Location location = Mockito.mock(Location.class);
+ boolean isSnapshot = true;
+ int maxDuration = 1234;
+ long maxFileSize = 500000;
+ int reason = VideoResult.REASON_MAX_DURATION_REACHED;
+ int videoFrameRate = 30;
+ int videoBitRate = 300000;
+ int audioBitRate = 30000;
+ Audio audio = Audio.ON;
+ Facing facing = Facing.FRONT;
+
+ stub.fileDescriptor = fileDescriptor;
+ stub.rotation = rotation;
+ stub.size = size;
+ stub.videoCodec = videoCodec;
+ stub.audioCodec = audioCodec;
+ stub.location = location;
+ stub.isSnapshot = isSnapshot;
+ stub.maxDuration = maxDuration;
+ stub.maxSize = maxFileSize;
+ stub.endReason = reason;
+ stub.videoFrameRate = videoFrameRate;
+ stub.videoBitRate = videoBitRate;
+ stub.audioBitRate = audioBitRate;
+ stub.audio = audio;
+ stub.facing = facing;
+
+ VideoResult result = new VideoResult(stub);
+ assertEquals(result.getFileDescriptor(), fileDescriptor);
+ assertEquals(result.getRotation(), rotation);
+ assertEquals(result.getSize(), size);
+ assertEquals(result.getVideoCodec(), videoCodec);
+ assertEquals(result.getAudioCodec(), audioCodec);
+ assertEquals(result.getLocation(), location);
+ assertEquals(result.isSnapshot(), isSnapshot);
+ assertEquals(result.getMaxSize(), maxFileSize);
+ assertEquals(result.getMaxDuration(), maxDuration);
+ assertEquals(result.getTerminationReason(), reason);
+ assertEquals(result.getVideoFrameRate(), videoFrameRate);
+ assertEquals(result.getVideoBitRate(), videoBitRate);
+ assertEquals(result.getAudioBitRate(), audioBitRate);
+ assertEquals(result.getAudio(), audio);
+ assertEquals(result.getFacing(), facing);
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testResultWithNoFile() {
+ VideoResult result = new VideoResult(stub);
+ result.getFile();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testResultWithNoFileDescriptor() {
+ VideoResult result = new VideoResult(stub);
+ result.getFileDescriptor();
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/WorkerHandlerTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/WorkerHandlerTest.java
deleted file mode 100644
index 44e5e6b7..00000000
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/WorkerHandlerTest.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.otaliastudios.cameraview;
-
-
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.*;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class WorkerHandlerTest extends BaseTest {
-
- @Test
- public void testCache() {
- WorkerHandler w1 = WorkerHandler.get("handler1");
- WorkerHandler w1a = WorkerHandler.get("handler1");
- WorkerHandler w2 = WorkerHandler.get("handler2");
- assertTrue(w1 == w1a);
- assertFalse(w1 == w2);
- }
-
- @Test
- public void testStaticRun() {
- final Task task = new Task<>(true);
- Runnable action = new Runnable() {
- @Override
- public void run() {
- task.end(true);
- }
- };
- WorkerHandler.run(action);
- Boolean result = task.await(500);
- assertNotNull(result);
- assertTrue(result);
- }
-}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera1IntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera1IntegrationTest.java
new file mode 100644
index 00000000..2b03f522
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera1IntegrationTest.java
@@ -0,0 +1,51 @@
+package com.otaliastudios.cameraview.engine;
+
+import com.otaliastudios.cameraview.CameraLogger;
+import com.otaliastudios.cameraview.CameraOptions;
+import com.otaliastudios.cameraview.controls.Engine;
+import com.otaliastudios.cameraview.frame.Frame;
+import com.otaliastudios.cameraview.frame.FrameProcessor;
+import com.otaliastudios.cameraview.tools.Op;
+import com.otaliastudios.cameraview.tools.Retry;
+import com.otaliastudios.cameraview.tools.SdkExclude;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.RequiresDevice;
+
+import java.util.Collection;
+
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * These tests work great on real devices, and are the only way to test actual CameraEngine
+ * implementation - we really need to open the camera device.
+ * Unfortunately they fail unreliably on emulated devices, due to some bug with the
+ * emulated camera controller.
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+// @RequiresDevice
+public class Camera1IntegrationTest extends CameraIntegrationTest {
+
+ @NonNull
+ @Override
+ protected Engine getEngine() {
+ return Engine.CAMERA1;
+ }
+
+ @Override
+ protected long getMeteringTimeoutMillis() {
+ return Camera1Engine.AUTOFOCUS_END_DELAY_MILLIS;
+ }
+
+ @Override
+ public void testFrameProcessing_maxSize() {
+ // Camera1Engine does not support different sizes.
+ // super.testFrameProcessing_maxSize();
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera2IntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera2IntegrationTest.java
new file mode 100644
index 00000000..ecf3fd8d
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera2IntegrationTest.java
@@ -0,0 +1,86 @@
+package com.otaliastudios.cameraview.engine;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.TotalCaptureResult;
+import android.os.Handler;
+
+import com.otaliastudios.cameraview.controls.Engine;
+import com.otaliastudios.cameraview.engine.action.ActionHolder;
+import com.otaliastudios.cameraview.engine.action.BaseAction;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.RequiresDevice;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * These tests work great on real devices, and are the only way to test actual CameraEngine
+ * implementation - we really need to open the camera device.
+ * Unfortunately they fail unreliably on emulated devices, due to some bug with the
+ * emulated camera controller.
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+// @RequiresDevice
+public class Camera2IntegrationTest extends CameraIntegrationTest {
+
+ @NonNull
+ @Override
+ protected Engine getEngine() {
+ return Engine.CAMERA2;
+ }
+
+ /* @Override
+ protected void onOpenSync() {
+ super.onOpenSync();
+ // Extra wait for the first frame to be dispatched.
+ // This is because various classes require getLastResult to be non-null
+ // and that's typically the case in a real app.
+ final CountDownLatch latch = new CountDownLatch(1);
+ new BaseAction() {
+ @Override
+ public void onCaptureCompleted(@NonNull ActionHolder holder,
+ @NonNull CaptureRequest request,
+ @NonNull TotalCaptureResult result) {
+ super.onCaptureCompleted(holder, request, result);
+ latch.countDown();
+ setState(STATE_COMPLETED);
+ }
+ }.start(controller);
+ try { latch.await(); } catch (InterruptedException ignore) {}
+ } */
+
+ @Override
+ protected long getMeteringTimeoutMillis() {
+ return Camera2Engine.METER_TIMEOUT;
+ }
+
+ /**
+ * setMaxDuration can crash on legacy devices (most emulator are), and I don't see
+ * any way to fix this in code. They shouldn't use Camera2 at all.
+ * @return true if possible.
+ */
+ @Override
+ protected boolean canSetVideoMaxDuration() {
+ if (!super.canSetVideoMaxDuration()) return false;
+ boolean shouldOpen = !camera.isOpened();
+ if (shouldOpen) openSync(true);
+ boolean result = controller.readCharacteristic(
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1)
+ != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
+ if (shouldOpen) closeSync(true);
+ return result;
+ }
+
+ @Override
+ public void testFrameProcessing_freezeRelease() {
+ // Camera2 Frames are not freezable.
+ // super.testFrameProcessing_freezeRelease();
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
new file mode 100644
index 00000000..779e23e8
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
@@ -0,0 +1,1227 @@
+package com.otaliastudios.cameraview.engine;
+
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.PointF;
+import android.hardware.Camera;
+import android.os.Build;
+import android.os.Handler;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.CameraException;
+import com.otaliastudios.cameraview.CameraListener;
+import com.otaliastudios.cameraview.CameraLogger;
+import com.otaliastudios.cameraview.CameraOptions;
+import com.otaliastudios.cameraview.CameraUtils;
+import com.otaliastudios.cameraview.CameraView;
+import com.otaliastudios.cameraview.PictureResult;
+import com.otaliastudios.cameraview.TestActivity;
+import com.otaliastudios.cameraview.VideoResult;
+import com.otaliastudios.cameraview.controls.Audio;
+import com.otaliastudios.cameraview.controls.Engine;
+import com.otaliastudios.cameraview.controls.Flash;
+import com.otaliastudios.cameraview.controls.Hdr;
+import com.otaliastudios.cameraview.controls.Mode;
+import com.otaliastudios.cameraview.controls.PictureFormat;
+import com.otaliastudios.cameraview.controls.WhiteBalance;
+import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
+import com.otaliastudios.cameraview.frame.Frame;
+import com.otaliastudios.cameraview.frame.FrameProcessor;
+import com.otaliastudios.cameraview.size.SizeSelectors;
+import com.otaliastudios.cameraview.tools.Emulator;
+import com.otaliastudios.cameraview.tools.Op;
+import com.otaliastudios.cameraview.internal.WorkerHandler;
+import com.otaliastudios.cameraview.overlay.Overlay;
+import com.otaliastudios.cameraview.size.Size;
+import com.otaliastudios.cameraview.tools.Retry;
+import com.otaliastudios.cameraview.tools.RetryRule;
+import com.otaliastudios.cameraview.tools.SdkExclude;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.rule.GrantPermissionRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public abstract class CameraIntegrationTest extends BaseTest {
+
+ private final static CameraLogger LOG = CameraLogger.create(CameraIntegrationTest.class.getSimpleName());
+ private final static long DELAY = 8000;
+
+ @Rule
+ public ActivityTestRule activityRule = new ActivityTestRule<>(TestActivity.class);
+
+ @Rule
+ public RetryRule retryRule = new RetryRule(3);
+
+ @Rule
+ public GrantPermissionRule permissionRule = GrantPermissionRule.grant(
+ "android.permission.CAMERA",
+ "android.permission.RECORD_AUDIO",
+ "android.permission.WRITE_EXTERNAL_STORAGE"
+ );
+
+ protected CameraView camera;
+ protected E controller;
+ private CameraListener listener;
+ private Op error;
+
+ @NonNull
+ protected abstract Engine getEngine();
+
+ @Before
+ public void setUp() {
+ LOG.w("[TEST STARTED]", "Setting up camera.");
+ WorkerHandler.destroyAll();
+
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ camera = new CameraView(activityRule.getActivity()) {
+ @NonNull
+ @Override
+ protected CameraEngine instantiateCameraEngine(
+ @NonNull Engine engine,
+ @NonNull CameraEngine.Callback callback) {
+ //noinspection unchecked
+ controller = (E) super.instantiateCameraEngine(getEngine(), callback);
+ return controller;
+ }
+ };
+ camera.setExperimental(true);
+ camera.setEngine(getEngine());
+ activityRule.getActivity().inflate(camera);
+ }
+ });
+
+ listener = mock(CameraListener.class);
+ camera.addCameraListener(listener);
+
+ error = new Op<>();
+ WorkerHandler crashThread = WorkerHandler.get("CrashThread");
+ crashThread.getThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(@NonNull Thread thread, @NonNull Throwable exception) {
+ error.controller().end(exception);
+ }
+ });
+ controller.mCrashHandler = crashThread.getHandler();
+
+ camera.addCameraListener(new CameraListener() {
+ @Override
+ public void onCameraError(@NonNull CameraException exception) {
+ super.onCameraError(exception);
+ if (exception.isUnrecoverable()) {
+ LOG.e("[UNRECOVERABLE CAMERAEXCEPTION]",
+ "Got unrecoverable exception, not clear what to do in a test.");
+ }
+ }
+ });
+
+ }
+
+ @After
+ public void tearDown() {
+ LOG.w("[TEST ENDED]", "Tearing down camera.");
+ camera.destroy();
+ WorkerHandler.destroyAll();
+ LOG.w("[TEST ENDED]", "Torn down camera.");
+ }
+
+ protected final CameraOptions openSync(boolean expectSuccess) {
+ final Op open = new Op<>();
+ doEndOp(open, 0).when(listener).onCameraOpened(any(CameraOptions.class));
+ camera.open();
+ CameraOptions result = open.await(DELAY);
+ if (expectSuccess) {
+ LOG.i("[OPEN SYNC]", "Expecting success.");
+ assertNotNull("Can open", result);
+ onOpenSync();
+ } else {
+ LOG.i("[OPEN SYNC]", "Expecting failure.");
+ assertNull("Should not open", result);
+ }
+ return result;
+ }
+
+ @SuppressWarnings("StatementWithEmptyBody")
+ protected void onOpenSync() {
+ // Extra wait for the bind and preview state, so we run tests in a fully operational
+ // state. If we didn't do so, we could have null values, for example, in getPictureSize
+ // or in getSnapshotSize.
+ while (controller.getState() != CameraState.PREVIEW) {}
+ }
+
+ protected final void closeSync(boolean expectSuccess) {
+ final Op close = new Op<>();
+ doEndOp(close, true).when(listener).onCameraClosed();
+ camera.close();
+ Boolean result = close.await(DELAY);
+ if (expectSuccess) {
+ LOG.i("[CLOSE SYNC]", "Expecting success.");
+ assertNotNull("Can close", result);
+ } else {
+ LOG.i("[CLOSE SYNC]", "Expecting failure.");
+ assertNull("Should not close", result);
+ }
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ @Nullable
+ private VideoResult waitForVideoResult(boolean expectSuccess) {
+ Op wait1 = new Op<>();
+ Op wait2 = new Op<>();
+ doEndOp(wait1, true).when(listener).onVideoRecordingEnd();
+ doEndOp(wait1, false).when(listener).onCameraError(argThat(new ArgumentMatcher() {
+ @Override
+ public boolean matches(CameraException argument) {
+ return argument.getReason() == CameraException.REASON_VIDEO_FAILED;
+ }
+ }));
+ doEndOp(wait2, 0).when(listener).onVideoTaken(any(VideoResult.class));
+ doEndOp(wait2, null).when(listener).onCameraError(argThat(new ArgumentMatcher() {
+ @Override
+ public boolean matches(CameraException argument) {
+ return argument.getReason() == CameraException.REASON_VIDEO_FAILED;
+ }
+ }));
+ int maxLoops = 10;
+ int loops = 0;
+
+ // First wait for onVideoRecordingEnd().
+ // It seems that when running all the tests together, the device can go in some
+ // power saving mode which makes the CPU extremely slow. This is especially problematic
+ // with video snapshots where we do lots of processing. The videoEnd callback can return
+ // long after the actual stop() call, so if we're still processing, let's wait more.
+ LOG.i("[WAIT VIDEO]", "Waiting for onVideoRecordingEnd()...");
+ Boolean wait1Result = wait1.await(DELAY);
+ if (expectSuccess) {
+ while (wait1Result == null && loops <= maxLoops) {
+ LOG.w("[WAIT VIDEO]", "Waiting extra", DELAY, "milliseconds...");
+ wait1.listen();
+ wait1Result = wait1.await(DELAY);
+ loops++;
+ }
+ }
+
+ // Now wait for onVideoResult(). One cycle should be enough.
+ LOG.i("[WAIT VIDEO]", "Waiting for onVideoTaken()...");
+ VideoResult wait2Result = wait2.await(DELAY);
+ if (expectSuccess) {
+ while (wait2Result == null && loops <= maxLoops) {
+ LOG.w("[WAIT VIDEO]", "Waiting extra", DELAY, "milliseconds...");
+ wait2.listen();
+ wait2Result = wait2.await(DELAY);
+ loops++;
+ }
+ }
+
+ // Assert.
+ if (expectSuccess) {
+ assertNotNull("Should call onVideoRecordingEnd", wait1Result);
+ assertTrue("Should call onVideoRecordingEnd", wait1Result);
+ assertNotNull("Should call onVideoTaken", wait2Result);
+ } else {
+ assertTrue("Should not call onVideoRecordingEnd",
+ wait1Result == null || !wait1Result);
+ assertNull("Should not call onVideoTaken", wait2Result);
+ }
+ return wait2Result;
+ }
+
+ @Nullable
+ private PictureResult waitForPictureResult(boolean expectSuccess) {
+ final Op pic = new Op<>();
+ doEndOp(pic, 0).when(listener).onPictureTaken(any(PictureResult.class));
+ doEndOp(pic, null).when(listener).onCameraError(argThat(new ArgumentMatcher() {
+ @Override
+ public boolean matches(CameraException argument) {
+ return argument.getReason() == CameraException.REASON_PICTURE_FAILED;
+ }
+ }));
+ PictureResult result = pic.await(DELAY);
+ if (expectSuccess) {
+ LOG.i("[WAIT PICTURE]", "Expecting success.");
+ assertNotNull("Can take picture", result);
+ } else {
+ LOG.i("[WAIT PICTURE]", "Expecting failure.");
+ assertNull("Should not take picture", result);
+ }
+ return result;
+ }
+
+ /**
+ * Emulators do not respond well to setVideoMaxDuration() with full videos.
+ * The mediaRecorder.stop() call can hang forever.
+ * @return true if possible
+ */
+ protected boolean canSetVideoMaxDuration() {
+ return !Emulator.isEmulator();
+ }
+
+ protected void takeVideoSync(boolean expectSuccess) {
+ takeVideoSync(expectSuccess,0);
+ }
+
+ protected void takeVideoSync(boolean expectSuccess, int duration) {
+ final Op op = new Op<>();
+ doEndOp(op, true).when(listener).onVideoRecordingStart();
+ doEndOp(op, false).when(listener).onCameraError(argThat(new ArgumentMatcher() {
+ @Override
+ public boolean matches(CameraException argument) {
+ return argument.getReason() == CameraException.REASON_VIDEO_FAILED;
+ }
+ }));
+ File file = new File(getContext().getFilesDir(), "video.mp4");
+ if (duration > 0) {
+ if (canSetVideoMaxDuration()) {
+ camera.takeVideo(file, duration);
+ } else {
+ final int delay = Math.round(duration * 1.2F); // Compensate for thread jumps, ...
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ camera.stopVideo();
+ }
+ }, delay);
+ }
+ });
+ camera.takeVideo(file);
+ }
+ } else {
+ camera.takeVideo(file);
+ }
+ Boolean result = op.await(DELAY);
+ if (expectSuccess) {
+ LOG.i("[WAIT VIDEO START]", "Expecting success.");
+ assertNotNull("should start video recording or get CameraError", result);
+ assertTrue("should start video recording successfully", result);
+ } else {
+ LOG.i("[WAIT VIDEO START]", "Expecting failure.");
+ assertTrue("should not start video recording", result == null || !result);
+ }
+ }
+
+ @SuppressWarnings({"unused", "SameParameterValue"})
+ private void takeVideoSnapshotSync(boolean expectSuccess) {
+ takeVideoSnapshotSync(expectSuccess,0);
+ }
+
+ private void takeVideoSnapshotSync(boolean expectSuccess, int duration) {
+ final Op op = new Op<>();
+ doEndOp(op, true).when(listener).onVideoRecordingStart();
+ doEndOp(op, false).when(listener).onCameraError(argThat(new ArgumentMatcher() {
+ @Override
+ public boolean matches(CameraException argument) {
+ return argument.getReason() == CameraException.REASON_VIDEO_FAILED;
+ }
+ }));
+ File file = new File(getContext().getFilesDir(), "video.mp4");
+ if (duration > 0) {
+ camera.takeVideoSnapshot(file, duration);
+ } else {
+ camera.takeVideoSnapshot(file);
+ }
+ Boolean result = op.await(DELAY);
+ if (expectSuccess) {
+ LOG.i("[WAIT VIDEO SNAP START]", "Expecting success.");
+ assertNotNull("should start video recording or get CameraError", result);
+ assertTrue("should start video recording successfully", result);
+ } else {
+ LOG.i("[WAIT VIDEO SNAP START]", "Expecting failure.");
+ assertTrue("should not start video recording", result == null || !result);
+ }
+ }
+
+ private void waitForError() throws Throwable {
+ Throwable throwable = error.await(DELAY);
+ if (throwable != null) {
+ throw throwable;
+ }
+ }
+
+ //region test open/close
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testOpenClose() {
+ assertEquals(CameraState.OFF, controller.getState());
+ openSync(true);
+ assertTrue(controller.getState().isAtLeast(CameraState.ENGINE));
+ closeSync(true);
+ assertEquals(CameraState.OFF, controller.getState());
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testOpenTwice() {
+ openSync(true);
+ openSync(false);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testCloseTwice() {
+ closeSync(false);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ // This works great on the device but crashes often on the emulator.
+ // There must be something wrong with the emulated camera...
+ // Like stopPreview() and release() are not really sync calls?
+ public void testConcurrentCalls() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(4);
+ doCountDown(latch).when(listener).onCameraOpened(any(CameraOptions.class));
+ doCountDown(latch).when(listener).onCameraClosed();
+
+ camera.open();
+ camera.close();
+ camera.open();
+ camera.close();
+
+ boolean did = latch.await(10, TimeUnit.SECONDS);
+ assertTrue("Handles concurrent calls to start & stop, " + latch.getCount(), did);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testStartInitializesOptions() {
+ assertNull(camera.getCameraOptions());
+ openSync(true);
+ assertNotNull(camera.getCameraOptions());
+ }
+
+ //endregion
+
+ //region test Facing/SessionType
+ // Test things that should reset the camera.
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testSetFacing() throws Exception {
+ CameraOptions o = openSync(true);
+ int size = o.getSupportedFacing().size();
+ if (size > 1) {
+ // set facing should call stop and start again.
+ final CountDownLatch latch = new CountDownLatch(2);
+ doCountDown(latch).when(listener).onCameraOpened(any(CameraOptions.class));
+ doCountDown(latch).when(listener).onCameraClosed();
+
+ camera.toggleFacing();
+
+ boolean did = latch.await(2, TimeUnit.SECONDS);
+ assertTrue("Handles setFacing while active", did);
+ }
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testSetMode() throws Exception {
+ camera.setMode(Mode.PICTURE);
+ openSync(true);
+
+ // set session type should call stop and start again.
+ final CountDownLatch latch = new CountDownLatch(2);
+ doCountDown(latch).when(listener).onCameraOpened(any(CameraOptions.class));
+ doCountDown(latch).when(listener).onCameraClosed();
+
+ camera.setMode(Mode.VIDEO);
+
+ boolean did = latch.await(2, TimeUnit.SECONDS);
+ assertTrue("Handles setMode while active", did);
+ assertEquals(camera.getMode(), Mode.VIDEO);
+ }
+
+ //endregion
+
+ //region test Set Parameters
+ // When camera is open, parameters will be set only if supported.
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testSetZoom() {
+ CameraOptions options = openSync(true);
+ float oldValue = camera.getZoom();
+ float newValue = 0.65f;
+ camera.setZoom(newValue);
+ Op op = new Op<>(controller.mZoomTask);
+ op.await(500);
+
+ if (options.isZoomSupported()) {
+ assertEquals(newValue, camera.getZoom(), 0f);
+ } else {
+ assertEquals(oldValue, camera.getZoom(), 0f);
+ }
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testSetExposureCorrection() {
+ CameraOptions options = openSync(true);
+ float oldValue = camera.getExposureCorrection();
+ float newValue = options.getExposureCorrectionMaxValue();
+ camera.setExposureCorrection(newValue);
+ Op op = new Op<>(controller.mExposureCorrectionTask);
+ op.await(300);
+
+ if (options.isExposureCorrectionSupported()) {
+ assertEquals(newValue, camera.getExposureCorrection(), 0f);
+ } else {
+ assertEquals(oldValue, camera.getExposureCorrection(), 0f);
+ }
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testSetFlash() {
+ CameraOptions options = openSync(true);
+ Flash[] values = Flash.values();
+ Flash oldValue = camera.getFlash();
+ for (Flash value : values) {
+
+ camera.setFlash(value);
+ Op op = new Op<>(controller.mFlashTask);
+ op.await(300);
+ if (options.supports(value)) {
+ assertEquals(camera.getFlash(), value);
+ oldValue = value;
+ } else {
+ assertEquals(camera.getFlash(), oldValue);
+ }
+ }
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testSetWhiteBalance() {
+ CameraOptions options = openSync(true);
+ WhiteBalance[] values = WhiteBalance.values();
+ WhiteBalance oldValue = camera.getWhiteBalance();
+ for (WhiteBalance value : values) {
+ camera.setWhiteBalance(value);
+ Op op = new Op<>(controller.mWhiteBalanceTask);
+ op.await(300);
+ if (options.supports(value)) {
+ assertEquals(camera.getWhiteBalance(), value);
+ oldValue = value;
+ } else {
+ assertEquals(camera.getWhiteBalance(), oldValue);
+ }
+ }
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testSetHdr() {
+ CameraOptions options = openSync(true);
+ Hdr[] values = Hdr.values();
+ Hdr oldValue = camera.getHdr();
+ for (Hdr value : values) {
+ camera.setHdr(value);
+ Op op = new Op<>(controller.mHdrTask);
+ op.await(300);
+ if (options.supports(value)) {
+ assertEquals(camera.getHdr(), value);
+ oldValue = value;
+ } else {
+ assertEquals(camera.getHdr(), oldValue);
+ }
+ }
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testSetAudio() {
+ openSync(true);
+ Audio[] values = Audio.values();
+ for (Audio value : values) {
+ camera.setAudio(value);
+ assertEquals(camera.getAudio(), value);
+ }
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testSetLocation() {
+ openSync(true);
+ camera.setLocation(10d, 2d);
+ Op op = new Op<>(controller.mLocationTask);
+ op.await(300);
+ assertNotNull(camera.getLocation());
+ assertEquals(camera.getLocation().getLatitude(), 10d, 0d);
+ assertEquals(camera.getLocation().getLongitude(), 2d, 0d);
+ // This also ensures there are no crashes when attaching it to camera parameters.
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testSetPreviewFrameRate() {
+ CameraOptions options = openSync(true);
+ camera.setPreviewFrameRate(30);
+ Op op = new Op<>(controller.mPreviewFrameRateTask);
+ op.await(300);
+ assertEquals(camera.getPreviewFrameRate(),
+ Math.min(options.getPreviewFrameRateMaxValue(),
+ Math.max(options.getPreviewFrameRateMinValue(), 30)),
+ 0);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testSetPlaySounds() {
+ boolean oldValue = camera.getPlaySounds();
+ boolean newValue = !oldValue;
+ camera.setPlaySounds(newValue);
+ Op op = new Op<>(controller.mPlaySoundsTask);
+ op.await(300);
+
+ if (controller instanceof Camera1Engine) {
+ Camera1Engine camera1Engine = (Camera1Engine) controller;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ Camera.CameraInfo info = new Camera.CameraInfo();
+ Camera.getCameraInfo(camera1Engine.mCameraId, info);
+ if (info.canDisableShutterSound) {
+ assertEquals(newValue, camera.getPlaySounds());
+ }
+ } else {
+ assertEquals(oldValue, camera.getPlaySounds());
+ }
+ } else {
+ assertEquals(newValue, camera.getPlaySounds());
+ }
+ }
+
+ //endregion
+
+ //region test takeVideo
+
+ @Test(expected = RuntimeException.class)
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testStartVideo_whileInPictureMode() throws Throwable {
+ camera.setMode(Mode.PICTURE);
+ openSync(true);
+ takeVideoSync(false);
+ waitForError();
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 25, emulatorOnly = true)
+ public void testStartEndVideo() {
+ camera.setMode(Mode.VIDEO);
+ openSync(true);
+ takeVideoSync(true, 4000);
+ waitForVideoResult(true);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testStartEndVideoSnapshot() {
+ // TODO should check api level for snapshot?
+ openSync(true);
+ takeVideoSnapshotSync(true, 4000);
+ waitForVideoResult(true);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 25, emulatorOnly = true)
+ public void testStartEndVideo_withManualStop() {
+ camera.setMode(Mode.VIDEO);
+ openSync(true);
+ takeVideoSync(true);
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ camera.stopVideo();
+ }
+ }, 5000);
+ }
+ });
+ waitForVideoResult(true);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testStartEndVideoSnapshot_withManualStop() {
+ openSync(true);
+ takeVideoSnapshotSync(true);
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ camera.stopVideo();
+ }
+ }, 5000);
+ }
+ });
+ waitForVideoResult(true);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testEndVideo_withoutStarting() {
+ camera.setMode(Mode.VIDEO);
+ openSync(true);
+ camera.stopVideo();
+ waitForVideoResult(false);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 25, emulatorOnly = true)
+ public void testEndVideo_withMaxSize() {
+ camera.setMode(Mode.VIDEO);
+ camera.setVideoSize(SizeSelectors.maxArea(480 * 360));
+ openSync(true);
+ // Assuming video frame rate is 20...
+ //noinspection ConstantConditions
+ camera.setVideoBitRate((int) estimateVideoBitRate(camera.getVideoSize(), 20));
+ camera.setVideoMaxSize(estimateVideoBytes(camera.getVideoBitRate(), 5000));
+ takeVideoSync(true);
+ waitForVideoResult(true);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testEndVideoSnapshot_withMaxSize() {
+ openSync(true);
+ camera.setSnapshotMaxWidth(480);
+ camera.setSnapshotMaxHeight(480);
+ // We don't want a very low FPS or the video frames are too sparse and recording
+ // can fail (e.g. audio reaching completion while video still struggling to start)
+ camera.setPreviewFrameRate(30F);
+ //noinspection ConstantConditions
+ camera.setVideoBitRate((int) estimateVideoBitRate(camera.getSnapshotSize(),
+ (int) camera.getPreviewFrameRate()));
+ camera.setVideoMaxSize(estimateVideoBytes(camera.getVideoBitRate(), 5000));
+ takeVideoSnapshotSync(true);
+ waitForVideoResult(true);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 25, emulatorOnly = true)
+ public void testEndVideo_withMaxDuration() {
+ if (canSetVideoMaxDuration()) {
+ camera.setMode(Mode.VIDEO);
+ camera.setVideoMaxDuration(4000);
+ openSync(true);
+ takeVideoSync(true);
+ waitForVideoResult(true);
+ }
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testEndVideoSnapshot_withMaxDuration() {
+ camera.setVideoMaxDuration(4000);
+ openSync(true);
+ takeVideoSnapshotSync(true);
+ waitForVideoResult(true);
+ }
+
+ //endregion
+
+ //region startAutoFocus
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testStartAutoFocus() {
+ CameraOptions o = openSync(true);
+
+ final Op focus = new Op<>();
+ doEndOp(focus, 0).when(listener).onAutoFocusStart(any(PointF.class));
+
+ camera.startAutoFocus(1, 1);
+ PointF point = focus.await(300);
+ if (o.isAutoFocusSupported()) {
+ assertNotNull(point);
+ assertEquals(point, new PointF(1, 1));
+ } else {
+ assertNull(point);
+ }
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testStopAutoFocus() {
+ CameraOptions o = openSync(true);
+
+ final Op focus = new Op<>();
+ doEndOp(focus, 1).when(listener).onAutoFocusEnd(anyBoolean(), any(PointF.class));
+
+ camera.startAutoFocus(1, 1);
+ // Stop routine can fail, so engines use a timeout. So wait at least the timeout time.
+ PointF point = focus.await(1000 + getMeteringTimeoutMillis());
+ if (o.isAutoFocusSupported()) {
+ assertNotNull(point);
+ assertEquals(point, new PointF(1, 1));
+ } else {
+ assertNull(point);
+ }
+ }
+
+ protected abstract long getMeteringTimeoutMillis();
+
+ //endregion
+
+ //region capture
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testCapturePicture_beforeStarted() {
+ camera.takePicture();
+ waitForPictureResult(false);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testCapturePicture_concurrentCalls() throws Exception {
+ // Second take should fail.
+ openSync(true);
+
+ CountDownLatch latch = new CountDownLatch(2);
+ doCountDown(latch).when(listener).onPictureTaken(any(PictureResult.class));
+
+ camera.takePicture();
+ camera.takePicture();
+ boolean did = latch.await(4, TimeUnit.SECONDS);
+ assertFalse(did);
+ assertEquals(1, latch.getCount());
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testCapturePicture_size() {
+ // Decoding can fail for large bitmaps. set a small size.
+ // camera.setPictureSize(SizeSelectors.smallest());
+ openSync(true);
+ Size size = camera.getPictureSize();
+ assertNotNull(size);
+ camera.takePicture();
+ PictureResult result = waitForPictureResult(true);
+ assertNotNull(result);
+ assertNotNull(result.getData());
+ assertNull(result.getLocation());
+ assertFalse(result.isSnapshot());
+ assertEquals(result.getSize(), size);
+ Bitmap bitmap = CameraUtils.decodeBitmap(result.getData(),
+ Integer.MAX_VALUE, Integer.MAX_VALUE);
+ if (bitmap != null) {
+ assertNotNull(bitmap);
+ String message = LOG.i("[PICTURE SIZE]", "Desired:", size, "Bitmap:",
+ new Size(bitmap.getWidth(), bitmap.getHeight()));
+ if (!Emulator.isEmulator()) {
+ assertEquals(message, bitmap.getWidth(), size.getWidth());
+ assertEquals(message, bitmap.getHeight(), size.getHeight());
+ } else {
+ // Emulator implementation sometimes does not rotate the image correctly.
+ assertTrue(message, bitmap.getWidth() == size.getWidth()
+ || bitmap.getWidth() == size.getHeight());
+ assertTrue(message, bitmap.getHeight() == size.getWidth()
+ || bitmap.getHeight() == size.getHeight());
+ assertEquals(message, size.getWidth() * size.getHeight(),
+ bitmap.getWidth() * bitmap.getHeight());
+ }
+ }
+ }
+
+ @Test(expected = RuntimeException.class)
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testCapturePicture_whileInVideoMode() throws Throwable {
+ camera.setMode(Mode.VIDEO);
+ openSync(true);
+ camera.takePicture();
+ waitForError();
+ camera.takePicture();
+
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testCapturePicture_withMetering() {
+ openSync(true);
+ camera.setPictureMetering(true);
+ camera.takePicture();
+ waitForPictureResult(true);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testCapturePicture_withoutMetering() {
+ openSync(true);
+ camera.setPictureMetering(false);
+ camera.takePicture();
+ waitForPictureResult(true);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testCaptureSnapshot_beforeStarted() {
+ camera.takePictureSnapshot();
+ waitForPictureResult(false);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testCaptureSnapshot_concurrentCalls() throws Exception {
+ // Second take should fail.
+ openSync(true);
+
+ CountDownLatch latch = new CountDownLatch(2);
+ doCountDown(latch).when(listener).onPictureTaken(any(PictureResult.class));
+
+ camera.takePictureSnapshot();
+ camera.takePictureSnapshot();
+ boolean did = latch.await(6, TimeUnit.SECONDS);
+ assertFalse(did);
+ assertEquals(1, latch.getCount());
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testCaptureSnapshot_size() {
+ openSync(true);
+ Size size = camera.getSnapshotSize();
+ assertNotNull(size);
+ camera.takePictureSnapshot();
+
+ PictureResult result = waitForPictureResult(true);
+ assertNotNull(result);
+ assertNotNull(result.getData());
+ assertNull(result.getLocation());
+ assertTrue(result.isSnapshot());
+ assertEquals(result.getSize(), size);
+ Bitmap bitmap = CameraUtils.decodeBitmap(result.getData(),
+ Integer.MAX_VALUE, Integer.MAX_VALUE);
+ if (bitmap != null) {
+ String message = LOG.i("[PICTURE SIZE]", "Desired:", size, "Bitmap:",
+ new Size(bitmap.getWidth(), bitmap.getHeight()));
+ assertNotNull(bitmap);
+ assertEquals(message, bitmap.getWidth(), size.getWidth());
+ assertEquals(message, bitmap.getHeight(), size.getHeight());
+ }
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testCaptureSnapshot_withMetering() {
+ openSync(true);
+ camera.setPictureSnapshotMetering(true);
+ camera.takePictureSnapshot();
+ waitForPictureResult(true);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testCaptureSnapshot_withoutMetering() {
+ openSync(true);
+ camera.setPictureSnapshotMetering(false);
+ camera.takePictureSnapshot();
+ waitForPictureResult(true);
+ }
+
+ //endregion
+
+ //region Picture Formats
+
+ @SuppressWarnings("ConstantConditions")
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testPictureFormat_DNG() {
+ openSync(true);
+ if (camera.getCameraOptions().supports(PictureFormat.DNG)) {
+ Op op = new Op<>();
+ doEndOp(op, true).when(listener).onCameraOpened(any(CameraOptions.class));
+ camera.setPictureFormat(PictureFormat.DNG);
+ assertNotNull(op.await(2000));
+ camera.takePicture();
+ PictureResult result = waitForPictureResult(true);
+ // assert that result.getData() is a DNG file:
+ // We can use the first 4 bytes assuming they are the same as a TIFF file
+ // https://en.wikipedia.org/wiki/List_of_file_signatures 73, 73, 42, 0
+ byte[] b = result.getData();
+ boolean isII = b[0] == 'I' && b[1] == 'I' && b[2] == '*' && b[3] == 0;
+ boolean isMM = b[0] == 'M' && b[1] == 'M' && b[2] == 0 && b[3] == '*';
+ assertTrue(isII || isMM);
+ }
+ }
+
+ //endregion
+
+ //region Frame Processing
+
+ private void assert15Frames(@NonNull FrameProcessor mock) throws Exception {
+ // Expect 15 frames. Time is very high because currently Camera2 keeps a very low FPS.
+ CountDownLatch latch = new CountDownLatch(15);
+ doCountDown(latch).when(mock).process(any(Frame.class));
+ boolean did = latch.await(30, TimeUnit.SECONDS);
+ assertTrue("Latch count should be 0: " + latch.getCount(), did);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testFrameProcessing_simple() throws Exception {
+ FrameProcessor processor = mock(FrameProcessor.class);
+ camera.addFrameProcessor(processor);
+ openSync(true);
+
+ assert15Frames(processor);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testFrameProcessing_maxSize() {
+ final int max = 600;
+ camera.setFrameProcessingMaxWidth(max);
+ camera.setFrameProcessingMaxHeight(max);
+ final Op sizeOp = new Op<>();
+ camera.addFrameProcessor(new FrameProcessor() {
+ @Override
+ public void process(@NonNull Frame frame) {
+ sizeOp.controller().end(frame.getSize());
+ }
+ });
+ openSync(true);
+ Size size = sizeOp.await(2000);
+ assertNotNull(size);
+ assertTrue(size.getWidth() <= max);
+ assertTrue(size.getHeight() <= max);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testFrameProcessing_afterSnapshot() throws Exception {
+ FrameProcessor processor = mock(FrameProcessor.class);
+ camera.addFrameProcessor(processor);
+ openSync(true);
+
+ // In Camera1, snapshots will clear the preview callback
+ // Ensure we restore correctly
+ camera.takePictureSnapshot();
+ waitForPictureResult(true);
+
+ assert15Frames(processor);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testFrameProcessing_afterPicture() throws Exception {
+ FrameProcessor processor = mock(FrameProcessor.class);
+ camera.addFrameProcessor(processor);
+ openSync(true);
+
+ camera.takePicture();
+ waitForPictureResult(true);
+
+ assert15Frames(processor);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testFrameProcessing_afterRestart() throws Exception {
+ FrameProcessor processor = mock(FrameProcessor.class);
+ camera.addFrameProcessor(processor);
+ openSync(true);
+ closeSync(true);
+ openSync(true);
+
+ assert15Frames(processor);
+ }
+
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 25, emulatorOnly = true)
+ public void testFrameProcessing_afterVideo() throws Exception {
+ FrameProcessor processor = mock(FrameProcessor.class);
+ camera.addFrameProcessor(processor);
+ camera.setMode(Mode.VIDEO);
+ openSync(true);
+ takeVideoSync(true,4000);
+ waitForVideoResult(true);
+
+ assert15Frames(processor);
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testFrameProcessing_freezeRelease() throws Exception {
+ // Ensure that freeze/release cycles do not cause OOMs.
+ // There was a bug doing this and it might resurface for any improper
+ // disposal of the frames.
+ FrameProcessor source = new FreezeReleaseFrameProcessor();
+ FrameProcessor processor = spy(source);
+ camera.addFrameProcessor(processor);
+ openSync(true);
+
+ assert15Frames(processor);
+ }
+
+ public class FreezeReleaseFrameProcessor implements FrameProcessor {
+ @Override
+ public void process(@NonNull Frame frame) {
+ frame.freeze().release();
+ }
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testFrameProcessing_format() {
+ // We wouldn't need to open/close for each format, but we do because legacy devices can
+ // crash due to their bad internal implementation when we perform restartBind().
+ // And setFrameProcessorFormat can trigger such restart.
+ CameraOptions o = openSync(true);
+ Collection formats = o.getSupportedFrameProcessingFormats();
+ closeSync(true);
+ for (int format : formats) {
+ LOG.i("[TEST FRAME FORMAT]", "Testing", format, "...");
+ Op op = testFrameProcessorFormat(format);
+ assertNotNull(op.await(DELAY));
+ closeSync(true);
+ }
+ }
+
+ @NonNull
+ private Op testFrameProcessorFormat(final int format) {
+ final Op op = new Op<>();
+ camera.setFrameProcessingFormat(format);
+ camera.addFrameProcessor(new FrameProcessor() {
+ @Override
+ public void process(@NonNull Frame frame) {
+ if (frame.getFormat() == format) {
+ op.controller().start();
+ op.controller().end(true);
+ }
+ }
+ });
+ openSync(true);
+ return op;
+ }
+
+ //endregion
+
+ //region Overlays
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testOverlay_forPictureSnapshot() {
+ Overlay overlay = mock(Overlay.class);
+ when(overlay.drawsOn(any(Overlay.Target.class))).thenReturn(true);
+ controller.setOverlay(overlay);
+ openSync(true);
+ camera.takePictureSnapshot();
+ waitForPictureResult(true);
+ verify(overlay, atLeastOnce()).drawsOn(Overlay.Target.PICTURE_SNAPSHOT);
+ verify(overlay, times(1)).drawOn(eq(Overlay.Target.PICTURE_SNAPSHOT), any(Canvas.class));
+ }
+
+ @Test
+ @Retry(emulatorOnly = true)
+ @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
+ public void testOverlay_forVideoSnapshot() {
+ Overlay overlay = mock(Overlay.class);
+ when(overlay.drawsOn(any(Overlay.Target.class))).thenReturn(true);
+ controller.setOverlay(overlay);
+ openSync(true);
+ takeVideoSnapshotSync(true, 4000);
+ waitForVideoResult(true);
+ verify(overlay, atLeastOnce()).drawsOn(Overlay.Target.VIDEO_SNAPSHOT);
+ verify(overlay, atLeastOnce()).drawOn(eq(Overlay.Target.VIDEO_SNAPSHOT), any(Canvas.class));
+ }
+
+ //endregion
+
+ @SuppressWarnings("SameParameterValue")
+ private static long estimateVideoBitRate(@NonNull Size size, int frameRate) {
+ // Nasty estimate for a LQ video
+ return Math.round(0.05D * size.getWidth() * size.getHeight() * frameRate);
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static long estimateVideoBytes(long videoBitRate, long millis) {
+ // 1.3F accounts for audio.
+ return Math.round((videoBitRate * 1.3F) * (millis / 1000D) / 8D);
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java
new file mode 100644
index 00000000..e3d17c62
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java
@@ -0,0 +1,215 @@
+package com.otaliastudios.cameraview.engine;
+
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.location.Location;
+
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.tasks.Tasks;
+import com.otaliastudios.cameraview.CameraOptions;
+import com.otaliastudios.cameraview.PictureResult;
+import com.otaliastudios.cameraview.VideoResult;
+import com.otaliastudios.cameraview.controls.Facing;
+import com.otaliastudios.cameraview.controls.Flash;
+import com.otaliastudios.cameraview.controls.PictureFormat;
+import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
+import com.otaliastudios.cameraview.frame.ByteBufferFrameManager;
+import com.otaliastudios.cameraview.frame.FrameManager;
+import com.otaliastudios.cameraview.gesture.Gesture;
+import com.otaliastudios.cameraview.controls.Hdr;
+import com.otaliastudios.cameraview.controls.WhiteBalance;
+import com.otaliastudios.cameraview.metering.MeteringRegions;
+import com.otaliastudios.cameraview.size.AspectRatio;
+import com.otaliastudios.cameraview.size.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+public class MockCameraEngine extends CameraBaseEngine {
+
+ public boolean mPictureCaptured;
+ public boolean mFocusStarted;
+ public boolean mZoomChanged;
+ public boolean mExposureCorrectionChanged;
+
+ public MockCameraEngine(CameraEngine.Callback callback) {
+ super(callback);
+ }
+
+ @NonNull
+ @Override
+ protected Task onStartEngine() {
+ return Tasks.forResult(mCameraOptions);
+ }
+
+ @NonNull
+ @Override
+ protected Task onStopEngine() {
+ return Tasks.forResult(null);
+ }
+
+ @NonNull
+ @Override
+ protected Task onStartBind() {
+ return Tasks.forResult(null);
+ }
+
+ @NonNull
+ @Override
+ protected Task onStopBind() {
+ return Tasks.forResult(null);
+ }
+
+ @NonNull
+ @Override
+ protected Task onStartPreview() {
+ return Tasks.forResult(null);
+ }
+
+ @NonNull
+ @Override
+ protected Task onStopPreview() {
+ return Tasks.forResult(null);
+ }
+
+ public void setMockCameraOptions(CameraOptions options) {
+ mCameraOptions = options;
+ }
+
+ public void setMockPreviewStreamSize(Size size) {
+ mPreviewStreamSize = size;
+ }
+
+ public void setMockState(@NonNull CameraState state) {
+ Task change = getOrchestrator().scheduleStateChange(getState(),
+ state,
+ false,
+ new Callable>() {
+ @Override
+ public Task call() {
+ return Tasks.forResult(null);
+ }
+ });
+ try {
+ Tasks.await(change);
+ } catch (Exception ignore) {}
+ }
+
+ @Override
+ public void setZoom(float zoom, @Nullable PointF[] points, boolean notify) {
+ mZoomValue = zoom;
+ mZoomChanged = true;
+ }
+
+ @Override
+ public void setExposureCorrection(float EVvalue, @NonNull float[] bounds, @Nullable PointF[] points, boolean notify) {
+ mExposureCorrectionValue = EVvalue;
+ mExposureCorrectionChanged = true;
+ }
+
+ @Override
+ public void setFlash(@NonNull Flash flash) {
+ mFlash = flash;
+ }
+
+ @Override
+ public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) {
+ mWhiteBalance = whiteBalance;
+ }
+
+ @Override
+ public void setHdr(@NonNull Hdr hdr) {
+ mHdr = hdr;
+ }
+
+ @Override
+ public void setLocation(@Nullable Location location) {
+ mLocation = location;
+ }
+
+ @Override
+ public void setPictureFormat(@NonNull PictureFormat pictureFormat) {
+ mPictureFormat = pictureFormat;
+ }
+
+ @Override
+ public void setHasFrameProcessors(boolean hasFrameProcessors) {
+ mHasFrameProcessors = hasFrameProcessors;
+ }
+
+ @Override
+ public void setFrameProcessingFormat(int format) {
+ mFrameProcessingFormat = format;
+ }
+
+ @Override
+ public void takePicture(@NonNull PictureResult.Stub stub) {
+ super.takePicture(stub);
+ mPictureCaptured = true;
+ }
+
+ @Override
+ protected void onTakePicture(@NonNull PictureResult.Stub stub, boolean doMetering) {
+
+ }
+
+ @Override
+ protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio outputRatio, boolean doMetering) {
+
+ }
+
+ @Override
+ protected void onTakeVideo(@NonNull VideoResult.Stub stub) {
+
+ }
+
+ @Override
+ protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio outputRatio) {
+
+ }
+
+ @Override
+ protected void onPreviewStreamSizeChanged() {
+
+ }
+
+ @NonNull
+ @Override
+ protected List getPreviewStreamAvailableSizes() {
+ return new ArrayList<>();
+ }
+
+ @NonNull
+ @Override
+ protected List getFrameProcessingAvailableSizes() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void startAutoFocus(@Nullable Gesture gesture, @NonNull MeteringRegions regions, @NonNull PointF legacyPoint) {
+ mFocusStarted = true;
+ }
+
+ @NonNull
+ @Override
+ protected FrameManager instantiateFrameManager(int poolSize) {
+ return new ByteBufferFrameManager(poolSize, null);
+ }
+
+ @Override
+ public void setPlaySounds(boolean playSounds) { }
+
+ @Override
+ protected boolean collectCameraInfo(@NonNull Facing facing) {
+ return true;
+ }
+
+ @Override public void setPreviewFrameRate(float previewFrameRate) {
+ mPreviewFrameRate = previewFrameRate;
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/mappers/Camera1MapperTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/mappers/Camera1MapperTest.java
new file mode 100644
index 00000000..c2b42271
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/mappers/Camera1MapperTest.java
@@ -0,0 +1,68 @@
+package com.otaliastudios.cameraview.engine.mappers;
+
+
+import android.hardware.Camera;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.controls.Facing;
+import com.otaliastudios.cameraview.controls.Flash;
+import com.otaliastudios.cameraview.controls.Hdr;
+import com.otaliastudios.cameraview.controls.WhiteBalance;
+import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class Camera1MapperTest extends BaseTest {
+
+ private Camera1Mapper mapper = Camera1Mapper.get();
+
+ @Test
+ public void testMap() {
+ assertEquals(mapper.mapFlash(Flash.OFF), Camera.Parameters.FLASH_MODE_OFF);
+ assertEquals(mapper.mapFlash(Flash.ON), Camera.Parameters.FLASH_MODE_ON);
+ assertEquals(mapper.mapFlash(Flash.AUTO), Camera.Parameters.FLASH_MODE_AUTO);
+ assertEquals(mapper.mapFlash(Flash.TORCH), Camera.Parameters.FLASH_MODE_TORCH);
+
+ assertEquals(mapper.mapFacing(Facing.BACK), Camera.CameraInfo.CAMERA_FACING_BACK);
+ assertEquals(mapper.mapFacing(Facing.FRONT), Camera.CameraInfo.CAMERA_FACING_FRONT);
+
+ assertEquals(mapper.mapHdr(Hdr.OFF), Camera.Parameters.SCENE_MODE_AUTO);
+ assertEquals(mapper.mapHdr(Hdr.ON), Camera.Parameters.SCENE_MODE_HDR);
+
+ assertEquals(mapper.mapWhiteBalance(WhiteBalance.AUTO), Camera.Parameters.WHITE_BALANCE_AUTO);
+ assertEquals(mapper.mapWhiteBalance(WhiteBalance.DAYLIGHT), Camera.Parameters.WHITE_BALANCE_DAYLIGHT);
+ assertEquals(mapper.mapWhiteBalance(WhiteBalance.CLOUDY), Camera.Parameters.WHITE_BALANCE_CLOUDY_DAYLIGHT);
+ assertEquals(mapper.mapWhiteBalance(WhiteBalance.INCANDESCENT), Camera.Parameters.WHITE_BALANCE_INCANDESCENT);
+ assertEquals(mapper.mapWhiteBalance(WhiteBalance.FLUORESCENT), Camera.Parameters.WHITE_BALANCE_FLUORESCENT);
+ }
+
+
+ @Test
+ public void testUnmap() {
+ assertEquals(Flash.OFF, mapper.unmapFlash(Camera.Parameters.FLASH_MODE_OFF));
+ assertEquals(Flash.ON, mapper.unmapFlash(Camera.Parameters.FLASH_MODE_ON));
+ assertEquals(Flash.AUTO, mapper.unmapFlash(Camera.Parameters.FLASH_MODE_AUTO));
+ assertEquals(Flash.TORCH, mapper.unmapFlash(Camera.Parameters.FLASH_MODE_TORCH));
+
+ assertEquals(Facing.BACK, mapper.unmapFacing(Camera.CameraInfo.CAMERA_FACING_BACK));
+ assertEquals(Facing.FRONT, mapper.unmapFacing(Camera.CameraInfo.CAMERA_FACING_FRONT));
+
+ assertEquals(Hdr.OFF, mapper.unmapHdr(Camera.Parameters.SCENE_MODE_AUTO));
+ assertEquals(Hdr.ON, mapper.unmapHdr(Camera.Parameters.SCENE_MODE_HDR));
+
+ assertEquals(WhiteBalance.AUTO, mapper.unmapWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO));
+ assertEquals(WhiteBalance.DAYLIGHT, mapper.unmapWhiteBalance(Camera.Parameters.WHITE_BALANCE_DAYLIGHT));
+ assertEquals(WhiteBalance.CLOUDY, mapper.unmapWhiteBalance(Camera.Parameters.WHITE_BALANCE_CLOUDY_DAYLIGHT));
+ assertEquals(WhiteBalance.INCANDESCENT, mapper.unmapWhiteBalance(Camera.Parameters.WHITE_BALANCE_INCANDESCENT));
+ assertEquals(WhiteBalance.FLUORESCENT, mapper.unmapWhiteBalance(Camera.Parameters.WHITE_BALANCE_FLUORESCENT));
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/mappers/Camera2MapperTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/mappers/Camera2MapperTest.java
new file mode 100644
index 00000000..f0e895c7
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/mappers/Camera2MapperTest.java
@@ -0,0 +1,101 @@
+package com.otaliastudios.cameraview.engine.mappers;
+
+
+import android.hardware.Camera;
+import android.util.Pair;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.controls.Facing;
+import com.otaliastudios.cameraview.controls.Flash;
+import com.otaliastudios.cameraview.controls.Hdr;
+import com.otaliastudios.cameraview.controls.WhiteBalance;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Set;
+
+import static android.hardware.camera2.CameraMetadata.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class Camera2MapperTest extends BaseTest {
+
+ private Camera2Mapper mapper = Camera2Mapper.get();
+
+ @Test
+ public void testMap() {
+ List> values = mapper.mapFlash(Flash.OFF);
+ assertEquals(2, values.size());
+ assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_ON, FLASH_MODE_OFF)));
+ assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_OFF, FLASH_MODE_OFF)));
+ values = mapper.mapFlash(Flash.TORCH);
+ assertEquals(2, values.size());
+ assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_ON, FLASH_MODE_TORCH)));
+ assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_OFF, FLASH_MODE_TORCH)));
+ values = mapper.mapFlash(Flash.AUTO);
+ assertEquals(2, values.size());
+ assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_ON_AUTO_FLASH, FLASH_MODE_OFF)));
+ assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE, FLASH_MODE_OFF)));
+ values = mapper.mapFlash(Flash.ON);
+ assertEquals(1, values.size());
+ assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_ON_ALWAYS_FLASH, FLASH_MODE_OFF)));
+
+ assertEquals(mapper.mapFacing(Facing.BACK), LENS_FACING_BACK);
+ assertEquals(mapper.mapFacing(Facing.FRONT), LENS_FACING_FRONT);
+
+ assertEquals(mapper.mapHdr(Hdr.OFF), CONTROL_SCENE_MODE_DISABLED);
+ assertEquals(mapper.mapHdr(Hdr.ON), CONTROL_SCENE_MODE_HDR);
+
+ assertEquals(mapper.mapWhiteBalance(WhiteBalance.AUTO), CONTROL_AWB_MODE_AUTO);
+ assertEquals(mapper.mapWhiteBalance(WhiteBalance.DAYLIGHT), CONTROL_AWB_MODE_DAYLIGHT);
+ assertEquals(mapper.mapWhiteBalance(WhiteBalance.CLOUDY), CONTROL_AWB_MODE_CLOUDY_DAYLIGHT);
+ assertEquals(mapper.mapWhiteBalance(WhiteBalance.INCANDESCENT), CONTROL_AWB_MODE_INCANDESCENT);
+ assertEquals(mapper.mapWhiteBalance(WhiteBalance.FLUORESCENT), CONTROL_AWB_MODE_FLUORESCENT);
+ }
+
+
+ @Test
+ public void testUnmap() {
+ Set values;
+ values = mapper.unmapFlash(CONTROL_AE_MODE_OFF);
+ assertEquals(values.size(), 2);
+ assertTrue(values.contains(Flash.OFF));
+ assertTrue(values.contains(Flash.TORCH));
+ values = mapper.unmapFlash(CONTROL_AE_MODE_ON);
+ assertEquals(values.size(), 2);
+ assertTrue(values.contains(Flash.OFF));
+ assertTrue(values.contains(Flash.TORCH));
+ values = mapper.unmapFlash(CONTROL_AE_MODE_ON_ALWAYS_FLASH);
+ assertEquals(values.size(), 1);
+ assertTrue(values.contains(Flash.ON));
+ values = mapper.unmapFlash(CONTROL_AE_MODE_ON_AUTO_FLASH);
+ assertEquals(values.size(), 1);
+ assertTrue(values.contains(Flash.AUTO));
+ values = mapper.unmapFlash(CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE);
+ assertEquals(values.size(), 1);
+ assertTrue(values.contains(Flash.AUTO));
+ values = mapper.unmapFlash(CONTROL_AE_MODE_ON_EXTERNAL_FLASH);
+ assertEquals(values.size(), 0);
+
+ assertEquals(Facing.BACK, mapper.unmapFacing(LENS_FACING_BACK));
+ assertEquals(Facing.FRONT, mapper.unmapFacing(LENS_FACING_FRONT));
+
+ assertEquals(Hdr.OFF, mapper.unmapHdr(CONTROL_SCENE_MODE_DISABLED));
+ assertEquals(Hdr.ON, mapper.unmapHdr(CONTROL_SCENE_MODE_HDR));
+
+ assertEquals(WhiteBalance.AUTO, mapper.unmapWhiteBalance(CONTROL_AWB_MODE_AUTO));
+ assertEquals(WhiteBalance.DAYLIGHT, mapper.unmapWhiteBalance(CONTROL_AWB_MODE_DAYLIGHT));
+ assertEquals(WhiteBalance.CLOUDY, mapper.unmapWhiteBalance(CONTROL_AWB_MODE_CLOUDY_DAYLIGHT));
+ assertEquals(WhiteBalance.INCANDESCENT, mapper.unmapWhiteBalance(CONTROL_AWB_MODE_INCANDESCENT));
+ assertEquals(WhiteBalance.FLUORESCENT, mapper.unmapWhiteBalance(CONTROL_AWB_MODE_FLUORESCENT));
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/options/Camera1OptionsTest.java
similarity index 50%
rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java
rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/options/Camera1OptionsTest.java
index b240c7aa..8574c9ef 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/options/Camera1OptionsTest.java
@@ -1,9 +1,28 @@
-package com.otaliastudios.cameraview;
+package com.otaliastudios.cameraview.engine.options;
+import android.graphics.ImageFormat;
import android.hardware.Camera;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.CameraOptions;
+import com.otaliastudios.cameraview.controls.Audio;
+import com.otaliastudios.cameraview.controls.AudioCodec;
+import com.otaliastudios.cameraview.controls.Facing;
+import com.otaliastudios.cameraview.controls.Flash;
+import com.otaliastudios.cameraview.controls.PictureFormat;
+import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper;
+import com.otaliastudios.cameraview.gesture.GestureAction;
+import com.otaliastudios.cameraview.controls.Grid;
+import com.otaliastudios.cameraview.controls.Hdr;
+import com.otaliastudios.cameraview.controls.Mode;
+import com.otaliastudios.cameraview.controls.VideoCodec;
+import com.otaliastudios.cameraview.controls.WhiteBalance;
+import com.otaliastudios.cameraview.size.AspectRatio;
+import com.otaliastudios.cameraview.size.Size;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -23,22 +42,26 @@ import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
-public class CameraOptions1Test extends BaseTest {
+public class Camera1OptionsTest extends BaseTest {
@Test
public void testEmpty() {
- CameraOptions o = new CameraOptions(mock(Camera.Parameters.class), false);
+ CameraOptions o = new Camera1Options(mock(Camera.Parameters.class), 0, false);
assertTrue(o.getSupportedPictureAspectRatios().isEmpty());
assertTrue(o.getSupportedPictureSizes().isEmpty());
assertTrue(o.getSupportedWhiteBalance().isEmpty());
- assertTrue(o.getSupportedFlash().isEmpty());
- assertTrue(o.getSupportedHdr().isEmpty());
+ assertEquals(1, o.getSupportedFlash().size()); // Flash.OFF is always there
+ assertEquals(1, o.getSupportedHdr().size()); // Hdr.OFF is always there
assertFalse(o.isAutoFocusSupported());
assertFalse(o.isExposureCorrectionSupported());
- assertFalse(o.isVideoSnapshotSupported());
assertFalse(o.isZoomSupported());
assertEquals(o.getExposureCorrectionMaxValue(), 0f, 0);
assertEquals(o.getExposureCorrectionMinValue(), 0f, 0);
+ // Static
+ assertEquals(1, o.getSupportedPictureFormats().size());
+ assertTrue(o.getSupportedPictureFormats().contains(PictureFormat.JPEG));
+ assertEquals(1, o.getSupportedFrameProcessingFormats().size());
+ assertTrue(o.getSupportedFrameProcessingFormats().contains(ImageFormat.NV21));
}
private Camera.Size mockCameraSize(int width, int height) {
@@ -58,8 +81,8 @@ public class CameraOptions1Test extends BaseTest {
);
Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedPictureSizes()).thenReturn(sizes);
- CameraOptions o = new CameraOptions(params, false);
- Set supportedSizes = o.getSupportedPictureSizes();
+ CameraOptions o = new Camera1Options(params, 0, false);
+ Collection supportedSizes = o.getSupportedPictureSizes();
assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) {
Size internalSize = new Size(size.width, size.height);
@@ -77,8 +100,8 @@ public class CameraOptions1Test extends BaseTest {
);
Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedPictureSizes()).thenReturn(sizes);
- CameraOptions o = new CameraOptions(params, true);
- Set supportedSizes = o.getSupportedPictureSizes();
+ CameraOptions o = new Camera1Options(params, 0, true);
+ Collection supportedSizes = o.getSupportedPictureSizes();
assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) {
Size internalSize = new Size(size.width, size.height).flip();
@@ -102,8 +125,94 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedPictureSizes()).thenReturn(sizes);
- CameraOptions o = new CameraOptions(params, false);
- Set supportedRatios = o.getSupportedPictureAspectRatios();
+ CameraOptions o = new Camera1Options(params, 0, false);
+ Collection supportedRatios = o.getSupportedPictureAspectRatios();
+ assertEquals(supportedRatios.size(), expected.size());
+ for (AspectRatio ratio : expected) {
+ assertTrue(supportedRatios.contains(ratio));
+ }
+ }
+
+
+ @Test
+ public void testVideoSizes() {
+ // VideoSize are capped by CamcorderProfile.QUALITY_HIGH max size.
+ // This can be very small on an emulator, so use very small sizes to not hit that value.
+ List sizes = Arrays.asList(
+ mockCameraSize(10, 20),
+ mockCameraSize(5, 5),
+ mockCameraSize(16, 9),
+ mockCameraSize(20, 40)
+ );
+ Camera.Parameters params = mock(Camera.Parameters.class);
+ when(params.getSupportedVideoSizes()).thenReturn(sizes);
+ CameraOptions o = new Camera1Options(params, 0, false);
+ Collection supportedSizes = o.getSupportedVideoSizes();
+ assertEquals(supportedSizes.size(), sizes.size());
+ for (Camera.Size size : sizes) {
+ Size internalSize = new Size(size.width, size.height);
+ assertTrue(supportedSizes.contains(internalSize));
+ }
+ }
+
+ @Test
+ public void testVideoSizesNull() {
+ // When videoSizes is null, we take the preview sizes.
+ List sizes = Arrays.asList(
+ mockCameraSize(10, 20),
+ mockCameraSize(5, 5),
+ mockCameraSize(16, 9),
+ mockCameraSize(20, 40)
+ );
+ Camera.Parameters params = mock(Camera.Parameters.class);
+ when(params.getSupportedVideoSizes()).thenReturn(null);
+ when(params.getSupportedPreviewSizes()).thenReturn(sizes);
+ CameraOptions o = new Camera1Options(params, 0, false);
+ Collection supportedSizes = o.getSupportedVideoSizes();
+ assertEquals(supportedSizes.size(), sizes.size());
+ for (Camera.Size size : sizes) {
+ Size internalSize = new Size(size.width, size.height);
+ assertTrue(supportedSizes.contains(internalSize));
+ }
+ }
+
+ @Test
+ public void testVideoSizesFlip() {
+ List sizes = Arrays.asList(
+ mockCameraSize(10, 20),
+ mockCameraSize(5, 5),
+ mockCameraSize(16, 9),
+ mockCameraSize(20, 40)
+ );
+ Camera.Parameters params = mock(Camera.Parameters.class);
+ when(params.getSupportedVideoSizes()).thenReturn(sizes);
+ CameraOptions o = new Camera1Options(params, 0, true);
+ Collection supportedSizes = o.getSupportedVideoSizes();
+ assertEquals(supportedSizes.size(), sizes.size());
+ for (Camera.Size size : sizes) {
+ Size internalSize = new Size(size.width, size.height).flip();
+ assertTrue(supportedSizes.contains(internalSize));
+ }
+ }
+
+ @Test
+ public void testVideoAspectRatio() {
+ List sizes = Arrays.asList(
+ mockCameraSize(10, 20),
+ mockCameraSize(5, 5),
+ mockCameraSize(16, 9),
+ mockCameraSize(20, 40)
+ );
+
+ Set expected = new HashSet<>();
+ expected.add(AspectRatio.of(1, 2));
+ expected.add(AspectRatio.of(1, 1));
+ expected.add(AspectRatio.of(16, 9));
+
+ Camera.Parameters params = mock(Camera.Parameters.class);
+ when(params.getSupportedVideoSizes()).thenReturn(sizes);
+ CameraOptions o = new Camera1Options(params, 0, false);
+ Collection supportedRatios = o.getSupportedVideoAspectRatios();
assertEquals(supportedRatios.size(), expected.size());
for (AspectRatio ratio : expected) {
assertTrue(supportedRatios.contains(ratio));
@@ -118,12 +227,13 @@ public class CameraOptions1Test extends BaseTest {
when(params.getMaxExposureCompensation()).thenReturn(0);
when(params.getMinExposureCompensation()).thenReturn(0);
- CameraOptions o = new CameraOptions(params, false);
- assertFalse(o.supports(GestureAction.FOCUS));
- assertFalse(o.supports(GestureAction.FOCUS_WITH_MARKER));
- assertTrue(o.supports(GestureAction.CAPTURE));
+ CameraOptions o = new Camera1Options(params, 0, false);
+ assertFalse(o.supports(GestureAction.AUTO_FOCUS));
+ assertTrue(o.supports(GestureAction.TAKE_PICTURE));
assertTrue(o.supports(GestureAction.NONE));
assertTrue(o.supports(GestureAction.ZOOM));
+ assertTrue(o.supports(GestureAction.FILTER_CONTROL_1));
+ assertTrue(o.supports(GestureAction.FILTER_CONTROL_2));
assertFalse(o.supports(GestureAction.EXPOSURE_CORRECTION));
}
@@ -131,15 +241,17 @@ public class CameraOptions1Test extends BaseTest {
public void testAlwaysSupportedControls() {
// Grid, VideoQuality, SessionType and Audio are always supported.
Camera.Parameters params = mock(Camera.Parameters.class);
- CameraOptions o = new CameraOptions(params, false);
+ CameraOptions o = new Camera1Options(params, 0, false);
Collection grids = o.getSupportedControls(Grid.class);
- Collection video = o.getSupportedControls(VideoQuality.class);
- Collection sessions = o.getSupportedControls(SessionType.class);
+ Collection video = o.getSupportedControls(VideoCodec.class);
+ Collection audioCodecs = o.getSupportedControls(AudioCodec.class);
+ Collection sessions = o.getSupportedControls(Mode.class);
Collection audio = o.getSupportedControls(Audio.class);
assertEquals(grids.size(), Grid.values().length);
- assertEquals(video.size(), VideoQuality.values().length);
- assertEquals(sessions.size(), SessionType.values().length);
+ assertEquals(video.size(), VideoCodec.values().length);
+ assertEquals(audioCodecs.size(), AudioCodec.values().length);
+ assertEquals(sessions.size(), Mode.values().length);
assertEquals(audio.size(), Audio.values().length);
}
@@ -152,12 +264,12 @@ public class CameraOptions1Test extends BaseTest {
supported.add(cameraInfo.facing);
}
- CameraOptions o = new CameraOptions(mock(Camera.Parameters.class), false);
- Mapper m = new Mapper.Mapper1();
+ CameraOptions o = new Camera1Options(mock(Camera.Parameters.class), 0, false);
+ Camera1Mapper m = Camera1Mapper.get();
Collection s = o.getSupportedControls(Facing.class);
assertEquals(s.size(), supported.size());
for (Facing facing : s) {
- assertTrue(supported.contains(m.map(facing)));
+ assertTrue(supported.contains(m.mapFacing(facing)));
assertTrue(o.supports(facing));
}
}
@@ -171,7 +283,7 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters.WHITE_BALANCE_SHADE // Not supported
));
- CameraOptions o = new CameraOptions(params, false);
+ CameraOptions o = new Camera1Options(params, 0, false);
Collection w = o.getSupportedControls(WhiteBalance.class);
assertEquals(w.size(), 2);
assertTrue(w.contains(WhiteBalance.AUTO));
@@ -184,16 +296,20 @@ public class CameraOptions1Test extends BaseTest {
public void testFlash() {
Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedFlashModes()).thenReturn(Arrays.asList(
+ Camera.Parameters.FLASH_MODE_OFF, // Supported
Camera.Parameters.FLASH_MODE_AUTO, // Supported
Camera.Parameters.FLASH_MODE_TORCH, // Supported
Camera.Parameters.FLASH_MODE_RED_EYE // Not supported
));
- CameraOptions o = new CameraOptions(params, false);
+ CameraOptions o = new Camera1Options(params, 0, false);
Collection f = o.getSupportedControls(Flash.class);
- assertEquals(f.size(), 2);
+ assertEquals(f.size(), 3);
+ assertTrue(f.contains(Flash.OFF));
assertTrue(f.contains(Flash.AUTO));
assertTrue(f.contains(Flash.TORCH));
+
+ assertTrue(o.supports(Flash.OFF));
assertTrue(o.supports(Flash.AUTO));
assertTrue(o.supports(Flash.TORCH));
}
@@ -207,11 +323,12 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters.SCENE_MODE_FIREWORKS // Not supported
));
- CameraOptions o = new CameraOptions(params, false);
+ CameraOptions o = new Camera1Options(params, 0, false);
Collection h = o.getSupportedControls(Hdr.class);
assertEquals(h.size(), 2);
assertTrue(h.contains(Hdr.OFF));
assertTrue(h.contains(Hdr.ON));
+
assertTrue(o.supports(Hdr.OFF));
assertTrue(o.supports(Hdr.ON));
}
@@ -221,9 +338,9 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters params = mock(Camera.Parameters.class);
when(params.isVideoSnapshotSupported()).thenReturn(true);
when(params.isZoomSupported()).thenReturn(true);
+ //noinspection ArraysAsListWithZeroOrOneArgument
when(params.getSupportedFocusModes()).thenReturn(Arrays.asList(Camera.Parameters.FOCUS_MODE_AUTO));
- CameraOptions o = new CameraOptions(params, false);
- assertTrue(o.isVideoSnapshotSupported());
+ CameraOptions o = new Camera1Options(params, 0, false);
assertTrue(o.isZoomSupported());
assertTrue(o.isAutoFocusSupported());
}
@@ -234,10 +351,23 @@ public class CameraOptions1Test extends BaseTest {
when(params.getMaxExposureCompensation()).thenReturn(10);
when(params.getMinExposureCompensation()).thenReturn(-10);
when(params.getExposureCompensationStep()).thenReturn(0.5f);
- CameraOptions o = new CameraOptions(params, false);
+ CameraOptions o = new Camera1Options(params, 0, false);
assertTrue(o.isExposureCorrectionSupported());
assertEquals(o.getExposureCorrectionMinValue(), -10f * 0.5f, 0f);
assertEquals(o.getExposureCorrectionMaxValue(), 10f * 0.5f, 0f);
}
+ @Test
+ public void testPreviewFrameRate() {
+ Camera.Parameters params = mock(Camera.Parameters.class);
+ List result = Arrays.asList(
+ new int[]{20000, 30000},
+ new int[]{30000, 60000},
+ new int[]{60000, 120000}
+ );
+ when(params.getSupportedPreviewFpsRange()).thenReturn(result);
+ CameraOptions o = new Camera1Options(params, 0, false);
+ assertEquals(20F, o.getPreviewFrameRateMinValue(), 0.001F);
+ assertEquals(120F, o.getPreviewFrameRateMaxValue(), 0.001F);
+ }
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java
new file mode 100644
index 00000000..a3a7b672
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java
@@ -0,0 +1,172 @@
+package com.otaliastudios.cameraview.filter;
+
+
+import android.opengl.GLES20;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseEglTest;
+import com.otaliastudios.cameraview.internal.GlTextureDrawer;
+import com.otaliastudios.opengl.program.GlTextureProgram;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BaseFilterTest extends BaseEglTest {
+
+ public static class TestFilter extends BaseFilter implements TwoParameterFilter {
+
+ private float param1;
+ private float param2;
+
+ @NonNull
+ @Override
+ public String getFragmentShader() {
+ return createDefaultFragmentShader();
+ }
+
+ @Override
+ public void setParameter1(float value) {
+ param1 = value;
+ }
+
+ @Override
+ public void setParameter2(float value) {
+ param2 = value;
+ }
+
+ @Override
+ public float getParameter1() {
+ return param1;
+ }
+
+ @Override
+ public float getParameter2() {
+ return param2;
+ }
+ }
+
+ private TestFilter filter;
+
+ @Test
+ public void testCreateDefaultFragmentShader() {
+ filter = new TestFilter();
+ filter.fragmentTextureCoordinateName = "XXX";
+ String defaultFragmentShader = filter.createDefaultFragmentShader();
+ assertNotNull(defaultFragmentShader);
+ assertTrue(defaultFragmentShader.contains(filter.fragmentTextureCoordinateName));
+ }
+
+ @Test
+ public void testCreateDefaultVertexShader() {
+ filter = new TestFilter();
+ filter.vertexModelViewProjectionMatrixName = "AAA";
+ filter.vertexPositionName = "BBB";
+ filter.vertexTextureCoordinateName = "CCC";
+ filter.vertexTransformMatrixName = "DDD";
+ filter.fragmentTextureCoordinateName = "EEE";
+ String defaultVertexShader = filter.createDefaultVertexShader();
+ assertNotNull(defaultVertexShader);
+ assertTrue(defaultVertexShader.contains(filter.vertexModelViewProjectionMatrixName));
+ assertTrue(defaultVertexShader.contains(filter.vertexPositionName));
+ assertTrue(defaultVertexShader.contains(filter.vertexTextureCoordinateName));
+ assertTrue(defaultVertexShader.contains(filter.vertexTransformMatrixName));
+ assertTrue(defaultVertexShader.contains(filter.fragmentTextureCoordinateName));
+ }
+
+ @Test
+ public void testOnProgramCreate() {
+ filter = new TestFilter();
+ int handle = GlTextureProgram.create(filter.getVertexShader(), filter.getFragmentShader());
+ filter.onCreate(handle);
+ assertNotNull(filter.program);
+ filter.onDestroy();
+ assertNull(filter.program);
+ GLES20.glDeleteProgram(handle);
+ }
+
+ @Test
+ public void testDraw_whenInvalid() {
+ filter = spy(new TestFilter());
+ float[] matrix = new float[16];
+ filter.draw(0L, matrix);
+ verify(filter, never()).onPreDraw(0L, matrix);
+ verify(filter, never()).onDraw(0L);
+ verify(filter, never()).onPostDraw(0L);
+ }
+
+ @Test
+ public void testDraw() {
+ // Use a drawer which cares about GL setup.
+ filter = spy(new TestFilter());
+ GlTextureDrawer drawer = new GlTextureDrawer();
+ drawer.setFilter(filter);
+
+ float[] matrix = drawer.getTextureTransform();
+ drawer.draw(0L);
+ verify(filter, times(1)).onPreDraw(0L, matrix);
+ verify(filter, times(1)).onDraw(0L);
+ verify(filter, times(1)).onPostDraw(0L);
+
+ drawer.release();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testOnCopy_invalid() {
+ // Anonymous inner classes do not have a public constructor.
+ Filter filter = new BaseFilter() {
+ @NonNull
+ @Override
+ public String getFragmentShader() {
+ return "whatever";
+ }
+ };
+ filter.copy();
+ }
+
+ @Test
+ public void testOnCopy() {
+ filter = new TestFilter();
+ BaseFilter other = filter.copy();
+ assertTrue(other instanceof TestFilter);
+ }
+
+ @Test
+ public void testCopy_withSize() {
+ filter = new TestFilter();
+ filter.setSize(WIDTH, HEIGHT);
+ BaseFilter other = filter.copy();
+ assertEquals(WIDTH, other.size.getWidth());
+ assertEquals(HEIGHT, other.size.getHeight());
+ }
+
+ @Test
+ public void testCopy_withParameter1() {
+ filter = new TestFilter();
+ filter.setParameter1(0.5F);
+ TestFilter other = (TestFilter) filter.copy();
+ assertEquals(filter.getParameter1(), other.getParameter1(), 0.001F);
+ }
+
+ @Test
+ public void testCopy_withParameter2() {
+ filter = new TestFilter();
+ filter.setParameter2(0.5F);
+ TestFilter other = (TestFilter) filter.copy();
+ assertEquals(filter.getParameter2(), other.getParameter2(), 0.001F);
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FilterParserTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FilterParserTest.java
new file mode 100644
index 00000000..3a964b40
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FilterParserTest.java
@@ -0,0 +1,52 @@
+package com.otaliastudios.cameraview.filter;
+
+
+import android.content.res.TypedArray;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.R;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static junit.framework.TestCase.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class FilterParserTest extends BaseTest {
+
+ @Test
+ public void testFallback() {
+ TypedArray array = mock(TypedArray.class);
+ when(array.hasValue(R.styleable.CameraView_cameraFilter)).thenReturn(false);
+ when(array.getString(R.styleable.CameraView_cameraFilter)).thenReturn(null);
+ FilterParser parser = new FilterParser(array);
+ assertNotNull(parser.getFilter());
+ assertTrue(parser.getFilter() instanceof NoFilter);
+ }
+ @Test
+ public void testConstructor() {
+ TypedArray array = mock(TypedArray.class);
+ when(array.hasValue(R.styleable.CameraView_cameraFilter)).thenReturn(true);
+ when(array.getString(R.styleable.CameraView_cameraFilter)).thenReturn(MyFilter.class.getName());
+ FilterParser parser = new FilterParser(array);
+ assertNotNull(parser.getFilter());
+ assertTrue(parser.getFilter() instanceof MyFilter);
+ }
+
+ public static class MyFilter extends BaseFilter {
+ @NonNull
+ @Override
+ public String getFragmentShader() {
+ return createDefaultFragmentShader();
+ }
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FiltersTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FiltersTest.java
new file mode 100644
index 00000000..b251fd71
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FiltersTest.java
@@ -0,0 +1,30 @@
+package com.otaliastudios.cameraview.filter;
+
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class FiltersTest extends BaseTest {
+
+ @Test
+ public void testNewInstance() {
+ // At least tests that all our default filters have a no-args constructor.
+ Filters[] filtersArray = Filters.values();
+ for (Filters filters : filtersArray) {
+ Filter filter = filters.newInstance();
+ assertNotNull(filter);
+ }
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/MultiFilterTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/MultiFilterTest.java
new file mode 100644
index 00000000..b01469cb
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/MultiFilterTest.java
@@ -0,0 +1,212 @@
+package com.otaliastudios.cameraview.filter;
+
+
+import android.opengl.GLES20;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseEglTest;
+import com.otaliastudios.cameraview.filters.AutoFixFilter;
+import com.otaliastudios.cameraview.filters.BrightnessFilter;
+import com.otaliastudios.cameraview.filters.DuotoneFilter;
+import com.otaliastudios.cameraview.filters.VignetteFilter;
+import com.otaliastudios.cameraview.internal.GlTextureDrawer;
+import com.otaliastudios.opengl.program.GlProgram;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MultiFilterTest extends BaseEglTest {
+
+ @Test
+ public void testConstructor1() {
+ MultiFilter multiFilter = new MultiFilter(
+ new DuotoneFilter(),
+ new AutoFixFilter()
+ );
+ assertEquals(2, multiFilter.filters.size());
+ }
+
+ @Test
+ public void testConstructor2() {
+ List filters = new ArrayList<>();
+ filters.add(new DuotoneFilter());
+ filters.add(new AutoFixFilter());
+ MultiFilter multiFilter = new MultiFilter(filters);
+ assertEquals(2, multiFilter.filters.size());
+ }
+
+ @Test
+ public void testAddFilter() {
+ MultiFilter multiFilter = new MultiFilter();
+ assertEquals(0, multiFilter.filters.size());
+ multiFilter.addFilter(new DuotoneFilter());
+ assertEquals(1, multiFilter.filters.size());
+ multiFilter.addFilter(new AutoFixFilter());
+ assertEquals(2, multiFilter.filters.size());
+ }
+
+ @Test
+ public void testAddFilter_multi() {
+ MultiFilter multiFilter = new MultiFilter(new DuotoneFilter());
+ assertEquals(1, multiFilter.filters.size());
+ MultiFilter other = new MultiFilter(
+ new AutoFixFilter(),
+ new BrightnessFilter(),
+ new VignetteFilter());
+ multiFilter.addFilter(other);
+ assertEquals(4, multiFilter.filters.size());
+ }
+
+ @Test
+ public void testSetSize() {
+ DuotoneFilter filter = new DuotoneFilter();
+ MultiFilter multiFilter = new MultiFilter(filter);
+ MultiFilter.State state = multiFilter.states.get(filter);
+ assertNotNull(state);
+ assertNull(state.size);
+ multiFilter.setSize(WIDTH, HEIGHT);
+ assertNotNull(state.size);
+ }
+
+ @Test
+ public void testCopy() {
+ DuotoneFilter filter = spy(new DuotoneFilter());
+ MultiFilter multiFilter = new MultiFilter(filter);
+ MultiFilter multiFilterCopy = (MultiFilter) multiFilter.copy();
+ assertEquals(1, multiFilterCopy.filters.size());
+ verify(filter, times(1)).onCopy();
+ }
+
+ @Test
+ public void testParameter1() {
+ DuotoneFilter filter = new DuotoneFilter();
+ MultiFilter multiFilter = new MultiFilter(filter);
+ float desired = 0.21582F; // whatever
+ multiFilter.setParameter1(desired);
+ assertEquals(desired, multiFilter.getParameter1(), 0.001F);
+ assertEquals(desired, filter.getParameter1(), 0.001F);
+ }
+
+ @Test
+ public void testParameter2() {
+ DuotoneFilter filter = new DuotoneFilter();
+ MultiFilter multiFilter = new MultiFilter(filter);
+ float desired = 0.21582F; // whatever
+ multiFilter.setParameter2(desired);
+ assertEquals(desired, multiFilter.getParameter2(), 0.001F);
+ assertEquals(desired, filter.getParameter2(), 0.001F);
+ }
+
+ @Test
+ public void testOnCreate_isLazy() {
+ DuotoneFilter filter = spy(new DuotoneFilter());
+ MultiFilter multiFilter = new MultiFilter(filter);
+
+ int program = GlProgram.create(multiFilter.getVertexShader(),
+ multiFilter.getFragmentShader());
+ multiFilter.onCreate(program);
+ verify(filter, never()).onCreate(anyInt());
+
+ multiFilter.onDestroy();
+ GLES20.glDeleteProgram(program);
+ verify(filter, never()).onDestroy();
+ }
+
+ @Test
+ public void testDraw_simple() {
+ DuotoneFilter filter = spy(new DuotoneFilter());
+ MultiFilter multiFilter = new MultiFilter(filter);
+ multiFilter.setSize(WIDTH, HEIGHT);
+ GlTextureDrawer drawer = new GlTextureDrawer();
+ drawer.setFilter(multiFilter);
+ float[] matrix = drawer.getTextureTransform();
+ drawer.draw(0L);
+ drawer.release();
+
+ // The child should have experienced the whole lifecycle.
+ verify(filter, atLeastOnce()).getVertexShader();
+ verify(filter, atLeastOnce()).getFragmentShader();
+ verify(filter, atLeastOnce()).setSize(anyInt(), anyInt());
+ verify(filter, times(1)).onCreate(anyInt());
+ verify(filter, times(1)).draw(0L, matrix);
+ verify(filter, times(1)).onDestroy();
+ }
+
+ @Test
+ public void testDraw_multi() {
+ // Want to test that when filter1 is drawn, the current framebuffer is a
+ // non-default one. When filter2 is drawn, the current framebuffer is 0.
+ final DuotoneFilter filter1 = spy(new DuotoneFilter());
+ final DuotoneFilter filter2 = spy(new DuotoneFilter());
+ final MultiFilter multiFilter = new MultiFilter(filter1, filter2);
+ multiFilter.setSize(WIDTH, HEIGHT);
+ GlTextureDrawer drawer = new GlTextureDrawer();
+ drawer.setFilter(multiFilter);
+ float[] matrix = drawer.getTextureTransform();
+ final int[] result = new int[1];
+
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) {
+ MultiFilter.State state = multiFilter.states.get(filter1);
+ assertNotNull(state);
+ assertTrue(state.isProgramCreated);
+ assertTrue(state.isFramebufferCreated);
+
+ GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0);
+ // assertTrue(result[0] != 0);
+ return null;
+ }
+ }).when(filter1).draw(0L, matrix);
+
+ // Note: second filter is drawn with the identity matrix!
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) {
+ // The last filter has no FBO / texture.
+ MultiFilter.State state = multiFilter.states.get(filter2);
+ assertNotNull(state);
+ assertTrue(state.isProgramCreated);
+ assertFalse(state.isFramebufferCreated);
+
+ GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0);
+ assertEquals(0, result[0]);
+ return null;
+
+ }
+ }).when(filter2).draw(eq(0L), any(float[].class));
+
+ drawer.draw(0L);
+ drawer.release();
+
+ // Verify that both are drawn.
+ verify(filter1, times(1)).draw(0L, matrix);
+ verify(filter2, times(1)).draw(eq(0L), any(float[].class));
+ }
+
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/NoFilterTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/NoFilterTest.java
new file mode 100644
index 00000000..72fb14e6
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/NoFilterTest.java
@@ -0,0 +1,35 @@
+package com.otaliastudios.cameraview.filter;
+
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NoFilterTest extends BaseTest {
+
+ public static class DummyFilter extends BaseFilter {
+ @NonNull
+ @Override
+ public String getFragmentShader() {
+ return "whatever";
+ }
+ }
+
+ @Test
+ public void testGetFragmentShader() {
+ NoFilter filter = new NoFilter();
+ String defaultFragmentShader = new DummyFilter().createDefaultFragmentShader();
+ assertEquals(defaultFragmentShader, filter.getFragmentShader());
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/SimpleFilterTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/SimpleFilterTest.java
new file mode 100644
index 00000000..1673584d
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/SimpleFilterTest.java
@@ -0,0 +1,43 @@
+package com.otaliastudios.cameraview.filter;
+
+
+import android.content.res.TypedArray;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.R;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static junit.framework.TestCase.assertNotNull;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SimpleFilterTest extends BaseTest {
+
+ @Test
+ public void testGetFragmentShader() {
+ String shader = "shader";
+ SimpleFilter filter = new SimpleFilter(shader);
+ assertEquals(shader, filter.getFragmentShader());
+ }
+
+ @Test
+ public void testCopy() {
+ String shader = "shader";
+ SimpleFilter filter = new SimpleFilter(shader);
+ BaseFilter copy = filter.copy();
+ assertTrue(copy instanceof SimpleFilter);
+ SimpleFilter simpleCopy = (SimpleFilter) copy;
+ assertEquals(shader, simpleCopy.getFragmentShader());
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/ByteBufferFrameManagerTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/ByteBufferFrameManagerTest.java
new file mode 100644
index 00000000..36e7d3ac
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/ByteBufferFrameManagerTest.java
@@ -0,0 +1,112 @@
+package com.otaliastudios.cameraview.frame;
+
+
+import android.graphics.ImageFormat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.engine.offset.Angles;
+import com.otaliastudios.cameraview.size.Size;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ByteBufferFrameManagerTest extends BaseTest {
+
+ private final Angles angles = new Angles();
+ private ByteBufferFrameManager.BufferCallback callback;
+
+ @Before
+ public void setUp() {
+ callback = mock(ByteBufferFrameManager.BufferCallback.class);
+ }
+
+ @After
+ public void tearDown() {
+ callback = null;
+ }
+
+ @Test
+ public void testAllocate() {
+ ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
+ manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
+ verify(callback, times(1)).onBufferAvailable(any(byte[].class));
+ reset(callback);
+
+ manager = new ByteBufferFrameManager(5, callback);
+ manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
+ verify(callback, times(5)).onBufferAvailable(any(byte[].class));
+ }
+
+ @Test
+ public void testOnFrameReleased_alreadyFull() {
+ ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
+ manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
+ int length = manager.getFrameBytes();
+
+ Frame frame1 = manager.getFrame(new byte[length], 0);
+ assertNotNull(frame1);
+ // Since frame1 is already taken and poolSize = 1, getFrame() would return null.
+ // To create a new frame, freeze the first one.
+ Frame frame2 = frame1.freeze();
+ // Now release the first frame so it goes back into the pool.
+ manager.onFrameReleased(frame1, (byte[]) frame1.getData());
+ reset(callback);
+ // Release the second. The pool is already full, so onBufferAvailable should not be called
+ // since this Frame instance will NOT be reused.
+ manager.onFrameReleased(frame2, (byte[]) frame2.getData());
+ verify(callback, never()).onBufferAvailable((byte[]) frame2.getData());
+ }
+
+ @Test
+ public void testOnFrameReleased_sameLength() {
+ ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
+ manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
+ int length = manager.getFrameBytes();
+
+ // A camera preview frame comes. Request a frame.
+ byte[] picture = new byte[length];
+ Frame frame = manager.getFrame(picture, 0);
+ assertNotNull(frame);
+
+ // Release the frame and ensure that onBufferAvailable is called.
+ reset(callback);
+ manager.onFrameReleased(frame, (byte[]) frame.getData());
+ verify(callback, times(1)).onBufferAvailable(picture);
+ }
+
+ @Test
+ public void testOnFrameReleased_differentLength() {
+ ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
+ manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
+ int length = manager.getFrameBytes();
+
+ // A camera preview frame comes. Request a frame.
+ byte[] picture = new byte[length];
+ Frame frame = manager.getFrame(picture, 0);
+ assertNotNull(frame);
+
+ // Don't release the frame. Change the allocation size.
+ manager.setUp(ImageFormat.NV16, new Size(15, 15), angles);
+
+ // Now release the old frame and ensure that onBufferAvailable is NOT called,
+ // because the released data has wrong length.
+ manager.onFrameReleased(frame, (byte[]) frame.getData());
+ reset(callback);
+ verify(callback, never()).onBufferAvailable(picture);
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/FrameManagerTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/FrameManagerTest.java
new file mode 100644
index 00000000..1f48be2f
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/FrameManagerTest.java
@@ -0,0 +1,73 @@
+package com.otaliastudios.cameraview.frame;
+
+
+import android.graphics.ImageFormat;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.engine.offset.Angles;
+import com.otaliastudios.cameraview.size.Size;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class FrameManagerTest extends BaseTest {
+
+ private final Angles angles = new Angles();
+
+ @Test
+ public void testFrameRecycling() {
+ // A 1-pool manager will always recycle the same frame.
+ FrameManager manager = new FrameManager(1, String.class) {
+ @Override
+ protected void onFrameDataReleased(@NonNull String data, boolean recycled) { }
+
+ @NonNull
+ @Override
+ protected String onCloneFrameData(@NonNull String data) {
+ return data;
+ }
+ };
+ manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
+
+ Frame first = manager.getFrame("foo", 0);
+ assertNotNull(first);
+ first.release();
+ Frame second = manager.getFrame("bar", 0);
+ assertNotNull(second);
+ second.release();
+ assertEquals(first, second);
+ }
+
+ @Test
+ public void testGetFrame() {
+ FrameManager manager = new FrameManager(1, String.class) {
+ @Override
+ protected void onFrameDataReleased(@NonNull String data, boolean recycled) { }
+
+ @NonNull
+ @Override
+ protected String onCloneFrameData(@NonNull String data) {
+ return data;
+ }
+ };
+ manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
+
+ Frame first = manager.getFrame("foo", 0);
+ assertNotNull(first);
+ Frame second = manager.getFrame("bar", 0);
+ assertNull(second);
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/GestureFinderTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/GestureFinderTest.java
new file mode 100644
index 00000000..cdcc7e8f
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/GestureFinderTest.java
@@ -0,0 +1,90 @@
+package com.otaliastudios.cameraview.gesture;
+
+
+import android.annotation.TargetApi;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.test.espresso.ViewInteraction;
+import androidx.test.espresso.matcher.RootMatchers;
+import androidx.test.rule.ActivityTestRule;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.TestActivity;
+import com.otaliastudios.cameraview.tools.Op;
+
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Rule;
+
+import static androidx.test.espresso.Espresso.onView;
+
+@TargetApi(17)
+public abstract class GestureFinderTest extends BaseTest {
+
+ protected abstract T createFinder(@NonNull GestureFinder.Controller controller);
+
+ @Rule
+ public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class);
+
+ @SuppressWarnings("WeakerAccess")
+ protected T finder;
+ @SuppressWarnings("WeakerAccess")
+ protected Op touchOp;
+ @SuppressWarnings("WeakerAccess")
+ protected ViewGroup layout;
+
+ @Before
+ public void setUp() {
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ TestActivity a = rule.getActivity();
+ layout = new FrameLayout(a);
+ finder = createFinder(new Controller());
+ finder.setActive(true);
+ a.inflate(layout);
+
+ touchOp = new Op<>(false);
+ layout.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ boolean found = finder.onTouchEvent(motionEvent);
+ if (found) touchOp.controller().end(finder.getGesture());
+ return true;
+ }
+ });
+ }
+ });
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ protected final ViewInteraction onLayout() {
+ return onView(Matchers.is(layout))
+ .inRoot(RootMatchers.withDecorView(
+ Matchers.is(rule.getActivity().getWindow().getDecorView())));
+ }
+
+ private class Controller implements GestureFinder.Controller {
+
+ @NonNull
+ @Override
+ public Context getContext() {
+ return layout.getContext();
+ }
+
+ @Override
+ public int getWidth() {
+ return layout.getWidth();
+ }
+
+ @Override
+ public int getHeight() {
+ return layout.getHeight();
+ }
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/PinchGestureFinderTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/PinchGestureFinderTest.java
new file mode 100644
index 00000000..58bc5865
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/PinchGestureFinderTest.java
@@ -0,0 +1,71 @@
+package com.otaliastudios.cameraview.gesture;
+
+
+import androidx.annotation.NonNull;
+import androidx.test.espresso.ViewAction;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.tools.SdkExclude;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * On API 26 these tests fail during Espresso's inRoot() - the window never gains focus.
+ * This might be due to a system popup or something similar.
+ */
+@SdkExclude(minSdkVersion = 26, maxSdkVersion = 26)
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PinchGestureFinderTest extends GestureFinderTest {
+
+ @Override
+ protected PinchGestureFinder createFinder(@NonNull GestureFinder.Controller controller) {
+ return new PinchGestureFinder(controller);
+ }
+
+ @Test
+ public void testDefaults() {
+ assertEquals(finder.getGesture(), Gesture.PINCH);
+ assertEquals(finder.getPoints().length, 2);
+ assertEquals(finder.getPoints()[0].x, 0, 0);
+ assertEquals(finder.getPoints()[0].y, 0, 0);
+ assertEquals(finder.getPoints()[1].x, 0, 0);
+ assertEquals(finder.getPoints()[1].y, 0, 0);
+ }
+
+ // TODO: test pinch open
+ // TODO: test pinch close
+ // TODO: test pinch disabled
+
+ // Possible approach: mimic pinch gesture and let espresso test.
+ // Too lazy to do this now, but it's possible.
+ // https://stackoverflow.com/questions/11523423/how-to-generate-zoom-pinch-gesture-for-testing-for-android
+
+ public abstract class PinchViewAction implements ViewAction {
+ }
+
+ private void testPinch(ViewAction action, boolean increasing) {
+ touchOp.listen();
+ touchOp.controller().start();
+ onLayout().perform(action);
+ Gesture found = touchOp.await(10000);
+ assertNotNull(found);
+
+ // How will this move our parameter?
+ float curr = 0.5f, min = 0f, max = 1f;
+ float newValue = finder.computeValue(curr, min, max);
+ if (increasing) {
+ assertTrue(newValue > curr);
+ assertTrue(newValue <= max);
+ } else {
+ assertTrue(newValue < curr);
+ assertTrue(newValue >= min);
+ }
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinderTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinderTest.java
new file mode 100644
index 00000000..fd94f210
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinderTest.java
@@ -0,0 +1,98 @@
+package com.otaliastudios.cameraview.gesture;
+
+
+import androidx.annotation.NonNull;
+import androidx.test.espresso.ViewAction;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.tools.SdkExclude;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static androidx.test.espresso.action.ViewActions.swipeDown;
+import static androidx.test.espresso.action.ViewActions.swipeLeft;
+import static androidx.test.espresso.action.ViewActions.swipeRight;
+import static androidx.test.espresso.action.ViewActions.swipeUp;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * On API 26 these tests fail during Espresso's inRoot() - the window never gains focus.
+ * This might be due to a system popup or something similar.
+ */
+@SdkExclude(minSdkVersion = 26, maxSdkVersion = 26)
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ScrollGestureFinderTest extends GestureFinderTest {
+
+ private final static long WAIT = 2000; // 500 was too short
+
+ @Override
+ protected ScrollGestureFinder createFinder(@NonNull GestureFinder.Controller controller) {
+ return new ScrollGestureFinder(controller);
+ }
+
+ @Test
+ public void testDefaults() {
+ assertNull(finder.mType);
+ assertEquals(finder.getPoints().length, 2);
+ assertEquals(finder.getPoints()[0].x, 0, 0);
+ assertEquals(finder.getPoints()[0].y, 0, 0);
+ assertEquals(finder.getPoints()[1].x, 0, 0);
+ assertEquals(finder.getPoints()[1].y, 0, 0);
+ }
+
+ @Test
+ public void testScrollDisabled() {
+ finder.setActive(false);
+ touchOp.listen();
+ touchOp.controller().start();
+ onLayout().perform(swipeUp());
+ Gesture found = touchOp.await(WAIT);
+ assertNull(found);
+ }
+
+ private void testScroll(ViewAction scroll, Gesture expected, boolean increasing) {
+ touchOp.listen();
+ touchOp.controller().start();
+ onLayout().perform(scroll);
+ Gesture found = touchOp.await(WAIT);
+ assertEquals(found, expected);
+
+ // How will this move our parameter?
+ float curr = 0.5f, min = 0f, max = 1f;
+ float newValue = finder.computeValue(curr, min, max);
+ if (increasing) {
+ assertTrue(newValue >= curr);
+ assertTrue(newValue <= max);
+ } else {
+ assertTrue(newValue <= curr);
+ assertTrue(newValue >= min);
+ }
+ }
+
+ @Test
+ public void testScrollLeft() {
+ testScroll(swipeLeft(), Gesture.SCROLL_HORIZONTAL, false);
+ }
+
+ @Test
+ public void testScrollRight() {
+ testScroll(swipeRight(), Gesture.SCROLL_HORIZONTAL, true);
+ }
+
+ @Test
+ public void testScrollUp() {
+ testScroll(swipeUp(), Gesture.SCROLL_VERTICAL, true);
+ }
+
+ @Test
+ public void testScrollDown() {
+ testScroll(swipeDown(), Gesture.SCROLL_VERTICAL, false);
+ }
+
+
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/TapGestureFinderTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/TapGestureFinderTest.java
new file mode 100644
index 00000000..23d5deb2
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/TapGestureFinderTest.java
@@ -0,0 +1,85 @@
+package com.otaliastudios.cameraview.gesture;
+
+
+import androidx.annotation.NonNull;
+import androidx.test.espresso.action.GeneralClickAction;
+import androidx.test.espresso.action.GeneralLocation;
+import androidx.test.espresso.action.Press;
+import androidx.test.espresso.action.Tap;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+
+import com.otaliastudios.cameraview.tools.SdkExclude;
+import com.otaliastudios.cameraview.size.Size;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static androidx.test.espresso.action.ViewActions.*;
+import static org.junit.Assert.*;
+
+/**
+ * On API 26 these tests fail during Espresso's inRoot() - the window never gains focus.
+ * This might be due to a system popup or something similar.
+ */
+@SdkExclude(minSdkVersion = 26, maxSdkVersion = 26)
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TapGestureFinderTest extends GestureFinderTest {
+
+ @Override
+ protected TapGestureFinder createFinder(@NonNull GestureFinder.Controller controller) {
+ return new TapGestureFinder(controller);
+ }
+
+ @Test
+ public void testDefaults() {
+ assertNull(finder.mType);
+ assertEquals(finder.getPoints().length, 1);
+ assertEquals(finder.getPoints()[0].x, 0, 0);
+ assertEquals(finder.getPoints()[0].y, 0, 0);
+ }
+
+ @Test
+ public void testTap() {
+ touchOp.listen();
+ touchOp.controller().start();
+ GeneralClickAction a = new GeneralClickAction(
+ Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER,
+ InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY);
+ onLayout().perform(a);
+ Gesture found = touchOp.await(500);
+
+ assertEquals(found, Gesture.TAP);
+ Size size = rule.getActivity().getContentSize();
+ assertEquals(finder.getPoints()[0].x, (size.getWidth() / 2f), 1f);
+ assertEquals(finder.getPoints()[0].y, (size.getHeight() / 2f), 1f);
+ }
+
+ @Test
+ public void testTapWhileDisabled() {
+ finder.setActive(false);
+ touchOp.listen();
+ touchOp.controller().start();
+ onLayout().perform(click());
+ Gesture found = touchOp.await(500);
+ assertNull(found);
+ }
+
+ @Test
+ public void testLongTap() {
+ touchOp.listen();
+ touchOp.controller().start();
+ GeneralClickAction a = new GeneralClickAction(
+ Tap.LONG, GeneralLocation.CENTER, Press.FINGER,
+ InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY);
+ onLayout().perform(a);
+ Gesture found = touchOp.await(500);
+ assertEquals(found, Gesture.LONG_TAP);
+ Size size = rule.getActivity().getContentSize();
+ assertEquals(finder.getPoints()[0].x, (size.getWidth() / 2f), 1f);
+ assertEquals(finder.getPoints()[0].y, (size.getHeight() / 2f), 1f);
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CamcorderProfilesTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CamcorderProfilesTest.java
new file mode 100644
index 00000000..31057235
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CamcorderProfilesTest.java
@@ -0,0 +1,61 @@
+package com.otaliastudios.cameraview.internal;
+
+
+import android.media.CamcorderProfile;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.CameraUtils;
+import com.otaliastudios.cameraview.internal.CamcorderProfiles;
+import com.otaliastudios.cameraview.tools.SdkExclude;
+import com.otaliastudios.cameraview.size.Size;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CamcorderProfilesTest extends BaseTest {
+
+ private String getCameraId() {
+ if (CameraUtils.hasCameras(getContext())) {
+ return "0";
+ }
+ return null;
+ }
+
+ @Test
+ public void testInvalidCameraReturnsLowest() {
+ CamcorderProfile invalid = CamcorderProfiles.get("invalid", new Size(100, 100));
+ CamcorderProfile lowest = CamcorderProfile.get(CamcorderProfile.QUALITY_LOW);
+ assertEquals(lowest.videoFrameWidth, invalid.videoFrameWidth);
+ assertEquals(lowest.videoFrameHeight, invalid.videoFrameHeight);
+ }
+
+ /**
+ * For some reason this fails on emulator 26.
+ */
+ @SdkExclude(minSdkVersion = 26, maxSdkVersion = 26)
+ @Test
+ public void testGet() {
+ String cameraId = getCameraId();
+ if (cameraId == null) return;
+ int cameraIdInt = Integer.parseInt(cameraId);
+
+ // Not much we can test. Let's just ask for lowest and highest.
+ CamcorderProfile low = CamcorderProfiles.get(cameraId, new Size(1, 1));
+ CamcorderProfile high = CamcorderProfiles.get(cameraId, new Size(Integer.MAX_VALUE, Integer.MAX_VALUE));
+
+ // Compare with lowest
+ CamcorderProfile lowest = CamcorderProfile.get(cameraIdInt, CamcorderProfile.QUALITY_LOW);
+ CamcorderProfile highest = CamcorderProfile.get(cameraIdInt, CamcorderProfile.QUALITY_HIGH);
+ assertEquals(lowest.videoFrameWidth, low.videoFrameWidth);
+ assertEquals(lowest.videoFrameHeight, low.videoFrameHeight);
+ assertEquals(highest.videoFrameWidth, high.videoFrameWidth);
+ assertEquals(highest.videoFrameHeight, high.videoFrameHeight);
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CropHelperTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CropHelperTest.java
new file mode 100644
index 00000000..bb7291ba
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CropHelperTest.java
@@ -0,0 +1,53 @@
+package com.otaliastudios.cameraview.internal;
+
+
+import android.graphics.Rect;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.internal.CropHelper;
+import com.otaliastudios.cameraview.size.AspectRatio;
+import com.otaliastudios.cameraview.size.Size;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CropHelperTest extends BaseTest {
+
+ @Test
+ public void testCrop() {
+ testCrop(new Size(1600, 1600), AspectRatio.of(16, 16));
+ testCrop(new Size(1600, 1600), AspectRatio.of(16, 9));
+ testCrop(new Size(1600, 1600), AspectRatio.of(9, 16));
+ }
+
+ private void testCrop(final Size inSize, final AspectRatio outRatio) {
+ AspectRatio inRatio = AspectRatio.of(inSize.getWidth(), inSize.getHeight());
+ Rect out = CropHelper.computeCrop(inSize, outRatio);
+ Size outSize = new Size(out.width(), out.height());
+ assertTrue(outRatio.matches(outSize));
+
+ if (outRatio.matches(inSize)) {
+ // They are equal.
+ assertEquals(outSize.getWidth(), inSize.getWidth());
+ assertEquals(outSize.getHeight(), inSize.getHeight());
+ } else if (outRatio.toFloat() > inRatio.toFloat()) {
+ // Width must match.
+ assertEquals(outSize.getWidth(), inSize.getWidth());
+ assertNotEquals(outSize.getHeight(), inSize.getHeight());
+ } else {
+ // Height must match.
+ assertEquals(outSize.getHeight(), inSize.getHeight());
+ assertNotEquals(outSize.getWidth(), inSize.getWidth());
+ }
+ }
+
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/DeviceEncodersTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/DeviceEncodersTest.java
new file mode 100644
index 00000000..4355e3fe
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/DeviceEncodersTest.java
@@ -0,0 +1,219 @@
+package com.otaliastudios.cameraview.internal;
+
+
+import android.media.MediaCodecInfo;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.size.AspectRatio;
+import com.otaliastudios.cameraview.size.Size;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class DeviceEncodersTest extends BaseTest {
+
+ // This is guaranteed to work, see
+ // https://developer.android.com/guide/topics/media/media-formats
+ private final static Size GUARANTEED_SIZE = new Size(176, 144);
+
+ private boolean enabled;
+
+ @Before
+ public void setUp() {
+ enabled = DeviceEncoders.ENABLED;
+ }
+
+ @After
+ public void tearDown() {
+ DeviceEncoders.ENABLED = enabled;
+ }
+
+ @NonNull
+ private DeviceEncoders create() {
+ return new DeviceEncoders(DeviceEncoders.MODE_RESPECT_ORDER,
+ "video/avc",
+ "audio/mp4a-latm",
+ 0,
+ 0);
+ }
+
+ @Test
+ public void testGetDeviceEncoders() {
+ DeviceEncoders deviceEncoders = create();
+ if (DeviceEncoders.ENABLED) {
+ List infos = deviceEncoders.getDeviceEncoders();
+ for (MediaCodecInfo info : infos) {
+ assertTrue(info.isEncoder());
+ }
+ }
+ }
+
+ @Test
+ public void testIsHardwareEncoder() {
+ DeviceEncoders deviceEncoders = create();
+ if (DeviceEncoders.ENABLED) {
+ assertFalse(deviceEncoders.isHardwareEncoder("OMX.google.encoder"));
+ assertTrue(deviceEncoders.isHardwareEncoder("OMX.other.encoder"));
+ }
+ }
+
+ @Test
+ public void testFindDeviceEncoder() {
+ DeviceEncoders deviceEncoders = create();
+ if (DeviceEncoders.ENABLED) {
+ List allEncoders = deviceEncoders.getDeviceEncoders();
+ MediaCodecInfo encoder = deviceEncoders.findDeviceEncoder(allEncoders,
+ "video/avc", DeviceEncoders.MODE_RESPECT_ORDER, 0);
+ assertNotNull(encoder);
+ List encoderTypes = Arrays.asList(encoder.getSupportedTypes());
+ assertTrue(encoderTypes.contains("video/avc"));
+ }
+ }
+
+ @Test
+ public void testGetVideoEncoder() {
+ if (DeviceEncoders.ENABLED) {
+ DeviceEncoders deviceEncoders = create();
+ assertNotNull(deviceEncoders.getVideoEncoder());
+ }
+
+ DeviceEncoders.ENABLED = false;
+ DeviceEncoders deviceEncoders = create();
+ assertNull(deviceEncoders.getVideoEncoder());
+ }
+
+ @Test
+ public void testGetAudioEncoder() {
+ if (DeviceEncoders.ENABLED) {
+ DeviceEncoders deviceEncoders = create();
+ assertNotNull(deviceEncoders.getAudioEncoder());
+ }
+
+ DeviceEncoders.ENABLED = false;
+ DeviceEncoders deviceEncoders = create();
+ assertNull(deviceEncoders.getAudioEncoder());
+ }
+
+ @Test
+ public void testGetSupportedVideoSize_disabled() {
+ DeviceEncoders.ENABLED = false;
+ DeviceEncoders deviceEncoders = create();
+ Size input = new Size(GUARANTEED_SIZE.getWidth(), GUARANTEED_SIZE.getHeight());
+ Size output = deviceEncoders.getSupportedVideoSize(input);
+ assertSame(input, output);
+ }
+
+ @Test
+ public void testGetSupportedVideoSize_scalesDown() {
+ DeviceEncoders deviceEncoders = create();
+ if (DeviceEncoders.ENABLED) {
+ Size input = new Size(
+ GUARANTEED_SIZE.getWidth() * 1000,
+ GUARANTEED_SIZE.getHeight() * 1000);
+ try {
+ Size output = deviceEncoders.getSupportedVideoSize(input);
+ assertTrue(AspectRatio.of(input).matches(output, 0.01F));
+ } catch (RuntimeException e) {
+ // The scaled down size happens to be not supported.
+ // I see no way of testing this easily if we're not sure of supported ranges.
+ // This depends highly on the alignment since scaling down, while keeping AR,
+ // can change the alignment and require width / height changes.
+ }
+ }
+ }
+
+ @Test
+ public void testGetSupportedVideoSize_aligns() {
+ DeviceEncoders deviceEncoders = create();
+ if (DeviceEncoders.ENABLED) {
+ Size input = new Size(GUARANTEED_SIZE.getWidth() + 1,
+ GUARANTEED_SIZE.getHeight() + 1);
+ Size output = deviceEncoders.getSupportedVideoSize(input);
+ assertTrue(output.getWidth() <= input.getWidth());
+ assertTrue(output.getHeight() <= input.getHeight());
+ }
+ }
+
+ @Test
+ public void testGetSupportedVideoBitRate_disabled() {
+ DeviceEncoders.ENABLED = false;
+ DeviceEncoders deviceEncoders = create();
+ int input = 1000;
+ int output = deviceEncoders.getSupportedVideoBitRate(input);
+ assertEquals(input, output);
+ }
+
+ @Test
+ public void testGetSupportedVideoBitRate_enabled() {
+ DeviceEncoders deviceEncoders = create();
+ if (DeviceEncoders.ENABLED) {
+ // Ensure it's clamped: we can pass a negative value and check it's >= 0.
+ int input = -1000;
+ int output = deviceEncoders.getSupportedVideoBitRate(input);
+ assertNotEquals(input, output);
+ assertTrue(output >= 0);
+ }
+ }
+
+ @Test
+ public void testGetSupportedAudioBitRate_disabled() {
+ DeviceEncoders.ENABLED = false;
+ DeviceEncoders deviceEncoders = create();
+ int input = 1000;
+ int output = deviceEncoders.getSupportedAudioBitRate(input);
+ assertEquals(input, output);
+ }
+
+ @Test
+ public void testGetSupportedAudioBitRate_enabled() {
+ DeviceEncoders deviceEncoders = create();
+ if (DeviceEncoders.ENABLED) {
+ // Ensure it's clamped: we can pass a negative value and check it's >= 0.
+ int input = -1000;
+ int output = deviceEncoders.getSupportedAudioBitRate(input);
+ assertNotEquals(input, output);
+ assertTrue(output >= 0);
+ }
+ }
+
+ @Test
+ public void testGetSupportedFrameRate_disabled() {
+ DeviceEncoders.ENABLED = false;
+ DeviceEncoders deviceEncoders = create();
+ int input = 1000;
+ int output = deviceEncoders.getSupportedVideoFrameRate(GUARANTEED_SIZE, input);
+ assertEquals(input, output);
+ }
+
+ @Test
+ public void testGetSupportedFrameRate_enabled() {
+ DeviceEncoders deviceEncoders = create();
+ if (DeviceEncoders.ENABLED) {
+ // Ensure it's clamped: we can pass a negative value and check it's >= 0.
+ int input = -10;
+ Size inputSize = deviceEncoders.getSupportedVideoSize(GUARANTEED_SIZE);
+ int output = deviceEncoders.getSupportedVideoFrameRate(inputSize, input);
+ assertNotEquals(input, output);
+ assertTrue(output >= 0);
+ }
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GridLinesLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/GridLinesLayoutTest.java
similarity index 51%
rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/GridLinesLayoutTest.java
rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/GridLinesLayoutTest.java
index 30260606..2bb87665 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GridLinesLayoutTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/GridLinesLayoutTest.java
@@ -1,18 +1,25 @@
-package com.otaliastudios.cameraview;
+package com.otaliastudios.cameraview.internal;
-import android.graphics.Canvas;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.TestActivity;
+import com.otaliastudios.cameraview.controls.Grid;
+import com.otaliastudios.cameraview.tools.Op;
+import com.otaliastudios.cameraview.tools.Retry;
+import com.otaliastudios.cameraview.tools.RetryRule;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
@@ -21,54 +28,68 @@ public class GridLinesLayoutTest extends BaseTest {
@Rule
public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class);
+ @Rule
+ public RetryRule retryRule = new RetryRule(3);
+
private GridLinesLayout layout;
+ @NonNull
+ private Op getDrawOp() {
+ final Op op = new Op<>();
+ layout.callback = mock(GridLinesLayout.DrawCallback.class);
+ doEndOp(op, 0).when(layout.callback).onDraw(anyInt());
+ return op;
+ }
+
@Before
public void setUp() {
- ui(new Runnable() {
+ uiSync(new Runnable() {
@Override
public void run() {
TestActivity a = rule.getActivity();
layout = new GridLinesLayout(a);
layout.setGridMode(Grid.OFF);
- layout.drawTask.listen();
+ Op op = getDrawOp();
a.getContentView().addView(layout);
+ op.await(1000);
}
});
- // Wait for first draw.
- layout.drawTask.await(1000);
}
private int setGridAndWait(Grid value) {
- layout.drawTask.listen();
layout.setGridMode(value);
- Integer result = layout.drawTask.await(1000);
+ Op op = getDrawOp();
+ Integer result = op.await(1000);
assertNotNull(result);
return result;
}
+ @Retry
@Test
public void testOff() {
int linesDrawn = setGridAndWait(Grid.OFF);
- assertEquals(linesDrawn, 0);
+ assertEquals(0, linesDrawn);
}
+ @Retry
@Test
public void test3x3() {
int linesDrawn = setGridAndWait(Grid.DRAW_3X3);
- assertEquals(linesDrawn, 2);
+ assertEquals(2, linesDrawn);
}
+ @Retry
@Test
public void testPhi() {
int linesDrawn = setGridAndWait(Grid.DRAW_PHI);
- assertEquals(linesDrawn, 2);
+ assertEquals(2, linesDrawn);
}
+ @Retry
@Test
public void test4x4() {
int linesDrawn = setGridAndWait(Grid.DRAW_4X4);
- assertEquals(linesDrawn, 3);
+ assertEquals(3, linesDrawn);
}
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/OrientationHelperTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/OrientationHelperTest.java
new file mode 100644
index 00000000..5d20ae15
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/OrientationHelperTest.java
@@ -0,0 +1,109 @@
+package com.otaliastudios.cameraview.internal;
+
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import android.view.OrientationEventListener;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.internal.OrientationHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class OrientationHelperTest extends BaseTest {
+
+ private OrientationHelper helper;
+ private OrientationHelper.Callback callback;
+
+ @Before
+ public void setUp() {
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ callback = mock(OrientationHelper.Callback.class);
+ helper = new OrientationHelper(getContext(), callback);
+ }
+ });
+ }
+
+ @After
+ public void tearDown() {
+ callback = null;
+ helper = null;
+ }
+
+ @Test
+ public void testEnable() {
+ // On some API levels, enable() needs to be run on the UI thread.
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ assertEquals(helper.getLastDisplayOffset(), -1);
+ assertEquals(helper.getLastDeviceOrientation(), -1);
+
+ helper.enable();
+ assertNotEquals(helper.getLastDisplayOffset(), -1); // Don't know about device orientation.
+
+ // Ensure nothing bad if called twice.
+ helper.enable();
+ assertNotEquals(helper.getLastDisplayOffset(), -1);
+
+ helper.disable();
+ assertEquals(helper.getLastDisplayOffset(), -1);
+ assertEquals(helper.getLastDeviceOrientation(), -1);
+ }
+ });
+ }
+
+ @Test
+ public void testRotation() {
+ // On some API levels, enable() needs to be run on the UI thread.
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ // Sometimes (on some APIs) the helper will trigger an update to 0
+ // right after enabling. But that's fine for us, times(1) will be OK either way.
+ helper.enable();
+ helper.mDeviceOrientationListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
+ assertEquals(helper.getLastDeviceOrientation(), 0);
+ helper.mDeviceOrientationListener.onOrientationChanged(10);
+ assertEquals(helper.getLastDeviceOrientation(), 0);
+ helper.mDeviceOrientationListener.onOrientationChanged(-10);
+ assertEquals(helper.getLastDeviceOrientation(), 0);
+ helper.mDeviceOrientationListener.onOrientationChanged(44);
+ assertEquals(helper.getLastDeviceOrientation(), 0);
+ helper.mDeviceOrientationListener.onOrientationChanged(360);
+ assertEquals(helper.getLastDeviceOrientation(), 0);
+
+ // Callback called just once.
+ verify(callback, times(1)).onDeviceOrientationChanged(0);
+
+ helper.mDeviceOrientationListener.onOrientationChanged(90);
+ helper.mDeviceOrientationListener.onOrientationChanged(91);
+ assertEquals(helper.getLastDeviceOrientation(), 90);
+ verify(callback, times(1)).onDeviceOrientationChanged(90);
+
+ helper.mDeviceOrientationListener.onOrientationChanged(180);
+ assertEquals(helper.getLastDeviceOrientation(), 180);
+ verify(callback, times(1)).onDeviceOrientationChanged(180);
+
+ helper.mDeviceOrientationListener.onOrientationChanged(270);
+ assertEquals(helper.getLastDeviceOrientation(), 270);
+ verify(callback, times(1)).onDeviceOrientationChanged(270);
+
+ // It is still 270 after ORIENTATION_UNKNOWN
+ helper.mDeviceOrientationListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
+ assertEquals(helper.getLastDeviceOrientation(), 270);
+ verify(callback, times(1)).onDeviceOrientationChanged(270);
+ }
+ });
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/RotationHelperTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/RotationHelperTest.java
new file mode 100644
index 00000000..caaea54c
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/RotationHelperTest.java
@@ -0,0 +1,53 @@
+package com.otaliastudios.cameraview.internal;
+
+
+import android.graphics.ImageFormat;
+import android.graphics.YuvImage;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.internal.RotationHelper;
+import com.otaliastudios.cameraview.size.Size;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RotationHelperTest extends BaseTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidRotation1() {
+ RotationHelper.rotate(new byte[10], new Size(1, 1), -1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidRotation2() {
+ RotationHelper.rotate(new byte[10], new Size(1, 1), -90);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidRotation3() {
+ RotationHelper.rotate(new byte[10], new Size(1, 1), 360);
+ }
+
+ @Test
+ public void testRotate() {
+ // Just test that nothing happens.
+ Size inputSize = new Size(160, 90);
+ int inputSizeBits = inputSize.getWidth() * inputSize.getHeight() * ImageFormat.getBitsPerPixel(ImageFormat.NV21);
+ int inputSizeBytes = (int) Math.ceil(inputSizeBits / 8.0d);
+ byte[] input = new byte[inputSizeBytes];
+ byte[] output = RotationHelper.rotate(input, inputSize, 90);
+ assertEquals(input.length, output.length);
+
+ Size outputSize = inputSize.flip();
+ YuvImage image = new YuvImage(output, ImageFormat.NV21, outputSize.getWidth(), outputSize.getHeight(), null);
+ assertNotNull(image);
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/WorkerHandlerTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/WorkerHandlerTest.java
new file mode 100644
index 00000000..40dcbc69
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/WorkerHandlerTest.java
@@ -0,0 +1,242 @@
+package com.otaliastudios.cameraview.internal;
+
+
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.tasks.Tasks;
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.internal.WorkerHandler;
+import com.otaliastudios.cameraview.tools.Op;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+import static org.junit.Assert.*;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class WorkerHandlerTest extends BaseTest {
+
+ @Test
+ public void testGetFromCache() {
+ WorkerHandler first = WorkerHandler.get("first");
+ WorkerHandler second = WorkerHandler.get("first");
+ assertSame(first, second);
+ }
+
+ @Test
+ public void testGetAnother() {
+ WorkerHandler first = WorkerHandler.get("first");
+ WorkerHandler second = WorkerHandler.get("second");
+ assertNotSame(first, second);
+ }
+
+ @NonNull
+ private Runnable getRunnableForOp(final @NonNull Op op) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ op.controller().end(true);
+ }
+ };
+ }
+
+ @NonNull
+ private Callable getCallableForOp(final @NonNull Op op) {
+ return new Callable() {
+ @Override
+ public Boolean call() {
+ op.controller().end(true);
+ return true;
+ }
+ };
+ }
+
+ @NonNull
+ private Callable getThrowCallable() {
+ return new Callable() {
+ @Override
+ public Void call() {
+ throw new RuntimeException("Fake error");
+ }
+ };
+ }
+
+ private void waitOp(@NonNull Op op) {
+ Boolean result = op.await(500);
+ assertNotNull(result);
+ assertTrue(result);
+ }
+
+ @Test
+ public void testFallbackExecute() {
+ final Op op = new Op<>();
+ WorkerHandler.execute(getRunnableForOp(op));
+ waitOp(op);
+ }
+
+ @Test
+ public void testPostRunnable() {
+ WorkerHandler handler = WorkerHandler.get("handler");
+ final Op op = new Op<>();
+ handler.post(getRunnableForOp(op));
+ waitOp(op);
+ }
+
+ @Test
+ public void testPostCallable() {
+ WorkerHandler handler = WorkerHandler.get("handler");
+ final Op op = new Op<>();
+ handler.post(getCallableForOp(op));
+ waitOp(op);
+ }
+
+ @Test
+ public void testPostCallable_throws() {
+ WorkerHandler handler = WorkerHandler.get("handler");
+ Task task = handler.post(getThrowCallable());
+ try { Tasks.await(task); } catch (ExecutionException | InterruptedException ignore) {}
+ assertTrue(task.isComplete());
+ assertFalse(task.isSuccessful());
+ }
+
+ @Test
+ public void testRunRunnable_background() {
+ WorkerHandler handler = WorkerHandler.get("handler");
+ final Op op = new Op<>();
+ handler.run(getRunnableForOp(op));
+ waitOp(op);
+ }
+
+ @Test
+ public void testRunRunnable_sameThread() {
+ final WorkerHandler handler = WorkerHandler.get("handler");
+ final Op op1 = new Op<>();
+ final Op op2 = new Op<>();
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ handler.run(getRunnableForOp(op2));
+ assertTrue(op2.await(0)); // Do not wait.
+ op1.controller().end(true);
+ }
+ });
+ waitOp(op1);
+ }
+
+ @Test
+ public void testRunCallable_background() {
+ WorkerHandler handler = WorkerHandler.get("handler");
+ final Op op = new Op<>();
+ handler.run(getCallableForOp(op));
+ waitOp(op);
+ }
+
+ @Test
+ public void testRunCallable_sameThread() {
+ final WorkerHandler handler = WorkerHandler.get("handler");
+ final Op op1 = new Op<>();
+ final Op op2 = new Op<>();
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ handler.run(getCallableForOp(op2));
+ assertTrue(op2.await(0)); // Do not wait.
+ op1.controller().end(true);
+ }
+ });
+ waitOp(op1);
+ }
+
+ @Test
+ public void testRunCallable_sameThread_throws() {
+ final WorkerHandler handler = WorkerHandler.get("handler");
+ final Op op = new Op<>();
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ Task task = handler.run(getThrowCallable());
+ assertTrue(task.isComplete()); // Already complete
+ assertFalse(task.isSuccessful());
+ op.controller().end(true);
+ }
+ });
+ waitOp(op);
+ }
+
+ @Test
+ public void testPostDelayed_tooEarly() {
+ final WorkerHandler handler = WorkerHandler.get("handler");
+ final Op op = new Op<>();
+ handler.post(1000, getRunnableForOp(op));
+ assertNull(op.await(500));
+ }
+
+ @Test
+ public void testPostDelayed() {
+ final WorkerHandler handler = WorkerHandler.get("handler");
+ final Op op = new Op<>();
+ handler.post(1000, getRunnableForOp(op));
+ assertNotNull(op.await(2000));
+ }
+
+ @Test
+ public void testRemove() {
+ final WorkerHandler handler = WorkerHandler.get("handler");
+ final Op op = new Op<>();
+ Runnable runnable = getRunnableForOp(op);
+ handler.post(1000, runnable);
+ handler.remove(runnable);
+ assertNull(op.await(2000));
+ }
+
+ @Test
+ public void testGetters() {
+ final WorkerHandler handler = WorkerHandler.get("handler");
+ assertNotNull(handler.getExecutor());
+ assertNotNull(handler.getHandler());
+ assertNotNull(handler.getLooper());
+ assertNotNull(handler.getThread());
+ }
+
+ @Test
+ public void testExecutor() {
+ final WorkerHandler handler = WorkerHandler.get("handler");
+ Executor executor = handler.getExecutor();
+ final Op op = new Op<>();
+ executor.execute(getRunnableForOp(op));
+ waitOp(op);
+ }
+
+ @Test
+ public void testDestroy() {
+ final WorkerHandler handler = WorkerHandler.get("handler");
+ assertTrue(handler.getThread().isAlive());
+ handler.destroy();
+ WorkerHandler newHandler = WorkerHandler.get("handler");
+ assertNotSame(handler, newHandler);
+ assertTrue(newHandler.getThread().isAlive());
+ // Ensure old thread dies at some point.
+ try { handler.getThread().join(500); } catch (InterruptedException ignore) {}
+ assertFalse(handler.getThread().isAlive());
+ }
+
+ @Test
+ public void testDestroyAll() {
+ final WorkerHandler handler1 = WorkerHandler.get("handler1");
+ final WorkerHandler handler2 = WorkerHandler.get("handler2");
+ WorkerHandler.destroyAll();
+ WorkerHandler newHandler1 = WorkerHandler.get("handler1");
+ WorkerHandler newHandler2 = WorkerHandler.get("handler2");
+ assertNotSame(handler1, newHandler1);
+ assertNotSame(handler2, newHandler2);
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarkerTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarkerTest.java
new file mode 100644
index 00000000..76fe970a
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarkerTest.java
@@ -0,0 +1,104 @@
+package com.otaliastudios.cameraview.markers;
+
+
+import android.graphics.PointF;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.test.annotation.UiThreadTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+public class DefaultAutoFocusMarkerTest extends BaseTest {
+
+ private DefaultAutoFocusMarker marker;
+
+ @Before
+ public void setUp() {
+ marker = new DefaultAutoFocusMarker();
+ }
+
+ @After
+ public void tearDown() {
+ marker = null;
+ }
+
+ @Test
+ public void testOnAttach() {
+ assertNull(marker.mContainer);
+ assertNull(marker.mFill);
+ ViewGroup container = new FrameLayout(getContext());
+ View result = marker.onAttach(getContext(), container);
+ assertNotNull(result);
+ assertNotNull(marker.mContainer);
+ assertNotNull(marker.mFill);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testOnAutoFocusStart() {
+ View mockContainer = spy(new View(getContext()));
+ View mockFill = spy(new View(getContext()));
+ marker.mContainer = mockContainer;
+ marker.mFill = mockFill;
+ marker.onAutoFocusStart(AutoFocusTrigger.GESTURE, new PointF());
+ verify(mockContainer, atLeastOnce()).clearAnimation();
+ verify(mockFill, atLeastOnce()).clearAnimation();
+ verify(mockContainer, atLeastOnce()).animate();
+ verify(mockFill, atLeastOnce()).animate();
+ }
+
+ @UiThreadTest
+ @Test
+ public void testOnAutoFocusStart_fromMethod() {
+ View mockContainer = spy(new View(getContext()));
+ View mockFill = spy(new View(getContext()));
+ marker.mContainer = mockContainer;
+ marker.mFill = mockFill;
+ marker.onAutoFocusStart(AutoFocusTrigger.METHOD, new PointF());
+ verify(mockContainer, never()).clearAnimation();
+ verify(mockFill, never()).clearAnimation();
+ verify(mockContainer, never()).animate();
+ verify(mockFill, never()).animate();
+ }
+
+ @UiThreadTest
+ @Test
+ public void testOnAutoFocusEnd() {
+ View mockContainer = spy(new View(getContext()));
+ View mockFill = spy(new View(getContext()));
+ marker.mContainer = mockContainer;
+ marker.mFill = mockFill;
+ marker.onAutoFocusEnd(AutoFocusTrigger.GESTURE, true, new PointF());
+ verify(mockContainer, atLeastOnce()).animate();
+ verify(mockFill, atLeastOnce()).animate();
+ }
+
+ @UiThreadTest
+ @Test
+ public void testOnAutoFocusEnd_fromMethod() {
+ View mockContainer = spy(new View(getContext()));
+ View mockFill = spy(new View(getContext()));
+ marker.mContainer = mockContainer;
+ marker.mFill = mockFill;
+ marker.onAutoFocusEnd(AutoFocusTrigger.METHOD, true, new PointF());
+ verify(mockContainer, never()).animate();
+ verify(mockFill, never()).animate();
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerLayoutTest.java
new file mode 100644
index 00000000..1b8ee44c
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerLayoutTest.java
@@ -0,0 +1,128 @@
+package com.otaliastudios.cameraview.markers;
+
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.TestActivity;
+import com.otaliastudios.cameraview.tools.SdkExclude;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import androidx.annotation.NonNull;
+import androidx.test.rule.ActivityTestRule;
+
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Not clear why, but for some reason on API 28+ the UiThreadTests here crash for an internal NPE
+ * in FrameLayout.onMeasure.
+ */
+@SdkExclude(minSdkVersion = 28, maxSdkVersion = 29)
+@TargetApi(17)
+public class MarkerLayoutTest extends BaseTest {
+
+
+ @Rule
+ public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class);
+
+ private MarkerLayout markerLayout;
+ private AutoFocusMarker autoFocusMarker;
+
+ @Before
+ public void setUp() {
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ TestActivity a = rule.getActivity();
+ markerLayout = spy(new MarkerLayout(a));
+ a.inflate(markerLayout);
+ autoFocusMarker = spy(new DefaultAutoFocusMarker());
+ }
+ });
+ }
+
+ @Test
+ public void testOnMarker_callsOnAttach() {
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker);
+ Mockito.verify(autoFocusMarker, times(1)).onAttach(
+ Mockito.any(Context.class),
+ Mockito.eq(markerLayout));
+ }
+ });
+ }
+
+ @Test
+ public void testOnMarker_addsView() {
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ Assert.assertEquals(markerLayout.getChildCount(), 0);
+ markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker);
+ Assert.assertEquals(markerLayout.getChildCount(), 1);
+ }
+ });
+ }
+
+ @Test
+ public void testOnMarker_removesView() {
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker);
+ Assert.assertEquals(markerLayout.getChildCount(), 1);
+ markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker);
+ Assert.assertEquals(markerLayout.getChildCount(), 1);
+ markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, null);
+ Assert.assertEquals(markerLayout.getChildCount(), 0);
+
+ Mockito.verify(autoFocusMarker, times(2)).onAttach(
+ Mockito.any(Context.class),
+ Mockito.eq(markerLayout));
+ }
+ });
+ }
+
+ @Test
+ public void testOnEvent() {
+ uiSync(new Runnable() {
+ @Override
+ public void run() {
+ final View mockView = spy(new View(getContext()));
+ // These fail, however it's not really needed.
+ // when(mockView.getWidth()).thenReturn(50);
+ // when(mockView.getHeight()).thenReturn(50);
+ AutoFocusMarker mockMarker = new AutoFocusMarker() {
+ public void onAutoFocusStart(@NonNull AutoFocusTrigger trigger, @NonNull PointF point) { }
+ public void onAutoFocusEnd(@NonNull AutoFocusTrigger trigger, boolean successful, @NonNull PointF point) { }
+
+ @Override
+ public View onAttach(@NonNull Context context, @NonNull ViewGroup container) {
+ return mockView;
+ }
+ };
+ markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, mockMarker);
+ reset(mockView);
+ markerLayout.onEvent(MarkerLayout.TYPE_AUTOFOCUS, new PointF[]{new PointF(0, 0)});
+ verify(mockView, times(1)).clearAnimation();
+ verify(mockView, times(1)).setTranslationX(anyFloat());
+ verify(mockView, times(1)).setTranslationY(anyFloat());
+ }
+ });
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerParserTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerParserTest.java
new file mode 100644
index 00000000..30f71c95
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerParserTest.java
@@ -0,0 +1,79 @@
+package com.otaliastudios.cameraview.markers;
+
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.PointF;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.arch.core.util.Function;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.R;
+import com.otaliastudios.cameraview.size.Size;
+import com.otaliastudios.cameraview.size.SizeSelectorParser;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static junit.framework.TestCase.assertNotNull;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MarkerParserTest extends BaseTest {
+
+ @Test
+ public void testNullConstructor() {
+ TypedArray array = mock(TypedArray.class);
+ when(array.hasValue(R.styleable.CameraView_cameraAutoFocusMarker)).thenReturn(false);
+ when(array.getString(R.styleable.CameraView_cameraAutoFocusMarker)).thenReturn(null);
+ MarkerParser parser = new MarkerParser(array);
+ assertNull(parser.getAutoFocusMarker());
+ }
+ @Test
+ public void testConstructor() {
+ TypedArray array = mock(TypedArray.class);
+ when(array.hasValue(R.styleable.CameraView_cameraAutoFocusMarker)).thenReturn(true);
+ when(array.getString(R.styleable.CameraView_cameraAutoFocusMarker)).thenReturn(Marker.class.getName());
+ MarkerParser parser = new MarkerParser(array);
+ assertNotNull(parser.getAutoFocusMarker());
+ assertTrue(parser.getAutoFocusMarker() instanceof Marker);
+ }
+
+ public static class Marker implements AutoFocusMarker {
+
+ public Marker() { }
+
+ @Nullable
+ @Override
+ public View onAttach(@NonNull Context context, @NonNull ViewGroup container) {
+ return null;
+ }
+
+ @Override
+ public void onAutoFocusStart(@NonNull AutoFocusTrigger trigger, @NonNull PointF point) { }
+
+ @Override
+ public void onAutoFocusEnd(@NonNull AutoFocusTrigger trigger, boolean successful, @NonNull PointF point) { }
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/metering/MeteringRegionsTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/metering/MeteringRegionsTest.java
new file mode 100644
index 00000000..5853efde
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/metering/MeteringRegionsTest.java
@@ -0,0 +1,137 @@
+package com.otaliastudios.cameraview.metering;
+
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.R;
+import com.otaliastudios.cameraview.markers.AutoFocusMarker;
+import com.otaliastudios.cameraview.markers.AutoFocusTrigger;
+import com.otaliastudios.cameraview.markers.MarkerParser;
+import com.otaliastudios.cameraview.size.Size;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.List;
+
+import static junit.framework.TestCase.assertNotNull;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MeteringRegionsTest extends BaseTest {
+
+ private final Size bounds = new Size(1000, 1000);
+
+ private void checkRegion(@NonNull MeteringRegion region, @NonNull PointF center, int weight) {
+ assertEquals(center.x, region.mRegion.centerX(), 0.01F);
+ assertEquals(center.y, region.mRegion.centerY(), 0.01F);
+ assertEquals(weight, region.mWeight);
+ }
+
+ @Test
+ public void testFromPoint() {
+ PointF center = new PointF(500, 500);
+ MeteringRegions regions = MeteringRegions.fromPoint(bounds, center);
+ assertEquals(2, regions.mRegions.size());
+ MeteringRegion first = regions.mRegions.get(0);
+ MeteringRegion second = regions.mRegions.get(1);
+ checkRegion(first, center, MeteringRegion.MAX_WEIGHT);
+ checkRegion(second, center,
+ Math.round(MeteringRegions.BLUR_FACTOR_WEIGHT * MeteringRegion.MAX_WEIGHT));
+ }
+
+ @Test
+ public void testFromArea() {
+ RectF area = new RectF(400, 400, 600, 600);
+ MeteringRegions regions = MeteringRegions.fromArea(bounds, area);
+ assertEquals(1, regions.mRegions.size());
+ MeteringRegion region = regions.mRegions.get(0);
+ checkRegion(region, new PointF(area.centerX(), area.centerY()), MeteringRegion.MAX_WEIGHT);
+ }
+
+ @Test
+ public void testFromArea_withBlur() {
+ RectF area = new RectF(400, 400, 600, 600);
+ MeteringRegions regions = MeteringRegions.fromArea(bounds, area,
+ MeteringRegion.MAX_WEIGHT, true);
+ assertEquals(2, regions.mRegions.size());
+ MeteringRegion first = regions.mRegions.get(0);
+ MeteringRegion second = regions.mRegions.get(1);
+ PointF center = new PointF(area.centerX(), area.centerY());
+ checkRegion(first, center, MeteringRegion.MAX_WEIGHT);
+ checkRegion(second, center,
+ Math.round(MeteringRegions.BLUR_FACTOR_WEIGHT * MeteringRegion.MAX_WEIGHT));
+ }
+
+ @Test
+ public void testTransform() {
+ MeteringTransform transform = mock(MeteringTransform.class);
+ when(transform.transformMeteringPoint(any(PointF.class))).then(new Answer() {
+ @Override
+ public PointF answer(InvocationOnMock invocation) {
+ PointF in = invocation.getArgument(0);
+ // This will swap x and y coordinates
+ //noinspection SuspiciousNameCombination
+ return new PointF(in.y, in.x);
+ }
+ });
+ RectF area = new RectF(0, 0, 100, 500); // tall area
+ RectF expected = new RectF(0, 0, 500, 100); // wide area
+ MeteringRegions regions = MeteringRegions.fromArea(bounds, area);
+ MeteringRegions transformed = regions.transform(transform);
+ verify(transform, times(4)).transformMeteringPoint(any(PointF.class));
+ assertEquals(1, transformed.mRegions.size());
+ assertEquals(expected, transformed.mRegions.get(0).mRegion);
+ }
+
+
+ @Test
+ public void testGet() {
+ MeteringTransform transform = new MeteringTransform() {
+ @NonNull
+ @Override
+ public PointF transformMeteringPoint(@NonNull PointF point) {
+ return point;
+ }
+
+ @NonNull
+ @Override
+ public Integer transformMeteringRegion(@NonNull RectF region, int weight) {
+ return weight;
+ }
+ };
+ MeteringRegions regions = MeteringRegions.fromArea(bounds,
+ new RectF(400, 400, 600, 600),
+ 900,
+ true);
+ assertEquals(2, regions.mRegions.size());
+ List result = regions.get(1, transform);
+ assertEquals(1, result.size());
+ assertEquals(900, (int) result.get(0));
+ }
+
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java
new file mode 100644
index 00000000..eea3def7
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java
@@ -0,0 +1,78 @@
+package com.otaliastudios.cameraview.overlay;
+
+
+import android.graphics.Canvas;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseEglTest;
+import com.otaliastudios.cameraview.internal.GlTextureDrawer;
+import com.otaliastudios.cameraview.size.Size;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class OverlayDrawerTest extends BaseEglTest {
+
+ @Test
+ public void testDraw() {
+ Overlay overlay = mock(Overlay.class);
+ OverlayDrawer drawer = new OverlayDrawer(overlay, new Size(WIDTH, HEIGHT));
+ drawer.draw(Overlay.Target.PICTURE_SNAPSHOT);
+ verify(overlay, times(1)).drawOn(
+ eq(Overlay.Target.PICTURE_SNAPSHOT),
+ any(Canvas.class));
+ }
+
+ @Test
+ public void testGetTransform() {
+ // We'll check that the transform is not all zeros, which is highly unlikely
+ // (the default transform should be the identity matrix)
+ OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT));
+ drawer.draw(Overlay.Target.PICTURE_SNAPSHOT);
+ assertThat(drawer.getTransform(), new BaseMatcher() {
+ public void describeTo(Description description) { }
+ public boolean matches(Object item) {
+ float[] array = (float[]) item;
+ for (float value : array) {
+ if (value != 0.0F) return true;
+ }
+ return false;
+ }
+ });
+ }
+
+ @Test
+ public void testRender() {
+ OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT));
+ drawer.mTextureDrawer = spy(drawer.mTextureDrawer);
+
+ drawer.draw(Overlay.Target.PICTURE_SNAPSHOT);
+ drawer.render(0L);
+ verify(drawer.mTextureDrawer, times(1)).draw(0L);
+ }
+
+ @Test
+ public void testRelease() {
+ OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT));
+ GlTextureDrawer textureDrawer = spy(drawer.mTextureDrawer);
+ drawer.mTextureDrawer = textureDrawer;
+ drawer.release();
+ verify(textureDrawer, times(1)).release();
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayLayoutTest.java
new file mode 100644
index 00000000..02bc368a
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayLayoutTest.java
@@ -0,0 +1,174 @@
+package com.otaliastudios.cameraview.overlay;
+
+
+import android.content.res.XmlResourceParser;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.util.Xml;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class OverlayLayoutTest extends BaseTest {
+
+ private OverlayLayout overlayLayout;
+
+ @Before
+ public void setUp() {
+ overlayLayout = spy(new OverlayLayout(getContext()));
+ }
+
+ @After
+ public void tearDown() {
+ overlayLayout = null;
+ }
+
+ @Test
+ public void testIsOverlay_LayoutParams() {
+ ViewGroup.LayoutParams params;
+
+ params = new ViewGroup.LayoutParams(10, 10);
+ assertFalse(overlayLayout.isOverlay(params));
+
+ params = new OverlayLayout.LayoutParams(10, 10);
+ assertTrue(overlayLayout.isOverlay(params));
+ }
+
+ @Test
+ public void testIsOverlay_attributeSet() throws Exception {
+ int layout1 = com.otaliastudios.cameraview.test.R.layout.overlay;
+ int layout2 = com.otaliastudios.cameraview.test.R.layout.not_overlay;
+
+ AttributeSet set1 = getAttributeSet(layout1);
+ assertTrue(overlayLayout.isOverlay(set1));
+
+ AttributeSet set2 = getAttributeSet(layout2);
+ assertFalse(overlayLayout.isOverlay(set2));
+ }
+
+ @NonNull
+ private AttributeSet getAttributeSet(int layout) throws Exception {
+ // Get the attribute set in the correct state: use a parser and move to START_TAG
+ XmlResourceParser parser = getContext().getResources().getLayout(layout);
+ //noinspection StatementWithEmptyBody
+ while (parser.next() != XmlResourceParser.START_TAG) {}
+ return Xml.asAttributeSet(parser);
+ }
+
+ @Test
+ public void testLayoutParams_drawsOn() {
+ OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
+
+ assertFalse(params.drawsOn(Overlay.Target.PREVIEW));
+ assertFalse(params.drawsOn(Overlay.Target.PICTURE_SNAPSHOT));
+ assertFalse(params.drawsOn(Overlay.Target.VIDEO_SNAPSHOT));
+
+ params.drawOnPreview = true;
+ assertTrue(params.drawsOn(Overlay.Target.PREVIEW));
+ params.drawOnPictureSnapshot = true;
+ assertTrue(params.drawsOn(Overlay.Target.PICTURE_SNAPSHOT));
+ params.drawOnVideoSnapshot = true;
+ assertTrue(params.drawsOn(Overlay.Target.VIDEO_SNAPSHOT));
+ }
+
+ @Test
+ public void testLayoutParams_toString() {
+ OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
+ String string = params.toString();
+ assertTrue(string.contains("drawOnPreview"));
+ assertTrue(string.contains("drawOnPictureSnapshot"));
+ assertTrue(string.contains("drawOnVideoSnapshot"));
+ }
+
+ @Test
+ public void testDrawChild() {
+ Canvas canvas = new Canvas();
+ OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
+ View child = new View(getContext());
+ child.setLayoutParams(params);
+ when(overlayLayout.doDrawChild(canvas, child, 0)).thenReturn(true);
+
+ overlayLayout.currentTarget = Overlay.Target.PREVIEW;
+ assertFalse(overlayLayout.drawChild(canvas, child, 0));
+ params.drawOnPreview = true;
+ assertTrue(overlayLayout.drawChild(canvas, child, 0));
+
+ overlayLayout.currentTarget = Overlay.Target.PICTURE_SNAPSHOT;
+ assertFalse(overlayLayout.drawChild(canvas, child, 0));
+ params.drawOnPictureSnapshot = true;
+ assertTrue(overlayLayout.drawChild(canvas, child, 0));
+
+ overlayLayout.currentTarget = Overlay.Target.VIDEO_SNAPSHOT;
+ assertFalse(overlayLayout.drawChild(canvas, child, 0));
+ params.drawOnVideoSnapshot = true;
+ assertTrue(overlayLayout.drawChild(canvas, child, 0));
+ }
+
+ @UiThreadTest
+ @Test
+ public void testDraw() {
+ Canvas canvas = new Canvas();
+ when(overlayLayout.drawsOn(Overlay.Target.PREVIEW)).thenReturn(false);
+ overlayLayout.draw(canvas);
+ verify(overlayLayout, never()).drawOn(Overlay.Target.PREVIEW, canvas);
+
+ when(overlayLayout.drawsOn(Overlay.Target.PREVIEW)).thenReturn(true);
+ overlayLayout.draw(canvas);
+ verify(overlayLayout, times(1)).drawOn(Overlay.Target.PREVIEW, canvas);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testDrawOn() {
+ Canvas canvas = spy(new Canvas());
+ View child = new View(getContext());
+ OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
+ params.drawOnPreview = true;
+ params.drawOnPictureSnapshot = true;
+ params.drawOnVideoSnapshot = true;
+ overlayLayout.addView(child, params);
+
+ overlayLayout.drawOn(Overlay.Target.PREVIEW, canvas);
+ verify(canvas, never()).scale(anyFloat(), anyFloat());
+ verify(overlayLayout, times(1)).doDrawChild(eq(canvas), eq(child), anyLong());
+ reset(canvas);
+ reset(overlayLayout);
+
+ overlayLayout.drawOn(Overlay.Target.PICTURE_SNAPSHOT, canvas);
+ verify(canvas, times(1)).scale(anyFloat(), anyFloat());
+ verify(overlayLayout, times(1)).doDrawChild(eq(canvas), eq(child), anyLong());
+ reset(canvas);
+ reset(overlayLayout);
+
+ overlayLayout.drawOn(Overlay.Target.VIDEO_SNAPSHOT, canvas);
+ verify(canvas, times(1)).scale(anyFloat(), anyFloat());
+ verify(overlayLayout, times(1)).doDrawChild(eq(canvas), eq(child), anyLong());
+ reset(canvas);
+ reset(overlayLayout);
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/picture/PictureRecorderTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/picture/PictureRecorderTest.java
new file mode 100644
index 00000000..464cf471
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/picture/PictureRecorderTest.java
@@ -0,0 +1,43 @@
+package com.otaliastudios.cameraview.picture;
+
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.PictureResult;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.lang.reflect.Constructor;
+
+import static org.junit.Assert.assertNull;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PictureRecorderTest extends BaseTest {
+
+ @Test
+ public void testRecorder() throws Exception {
+ PictureResult.Stub result = createStub();
+ PictureRecorder.PictureResultListener listener = Mockito.mock(PictureRecorder.PictureResultListener.class);
+ PictureRecorder recorder = new PictureRecorder(result, listener) {
+ public void take() {
+ dispatchResult();
+ }
+ };
+ recorder.take();
+ Mockito.verify(listener, Mockito.times(1)).onPictureResult(result, null);
+ assertNull(recorder.mListener);
+ assertNull(recorder.mResult);
+ }
+
+ private PictureResult.Stub createStub() throws Exception {
+ Constructor constructor = PictureResult.Stub.class.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ return constructor.newInstance();
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java
similarity index 56%
rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java
rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java
index eb1fbf88..b2d3d064 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java
@@ -1,10 +1,16 @@
-package com.otaliastudios.cameraview;
+package com.otaliastudios.cameraview.preview;
import android.content.Context;
-import android.support.test.rule.ActivityTestRule;
+import androidx.test.rule.ActivityTestRule;
import android.view.ViewGroup;
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.TestActivity;
+import com.otaliastudios.cameraview.tools.Op;
+import com.otaliastudios.cameraview.size.AspectRatio;
+import com.otaliastudios.cameraview.size.Size;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -17,55 +23,71 @@ import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
-public abstract class PreviewTest extends BaseTest {
+public abstract class CameraPreviewTest extends BaseTest {
+
+ private final static long DELAY = 4000;
- protected abstract CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback);
+ protected abstract T createPreview(Context context, ViewGroup parent);
@Rule
public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class);
- protected CameraPreview preview;
+ protected T preview;
+ @SuppressWarnings("WeakerAccess")
protected Size surfaceSize;
private CameraPreview.SurfaceCallback callback;
- private Task availability;
+
+ private Op available;
+ private Op destroyed;
@Before
public void setUp() {
- availability = new Task<>(true);
+ available = new Op<>();
+ destroyed = new Op<>();
- ui(new Runnable() {
+ uiSync(new Runnable() {
@Override
public void run() {
TestActivity a = rule.getActivity();
surfaceSize = a.getContentSize();
-
callback = mock(CameraPreview.SurfaceCallback.class);
+
doAnswer(new Answer() {
@Override
- public Object answer(InvocationOnMock invocation) throws Throwable {
- if (availability != null) availability.end(true);
+ public Object answer(InvocationOnMock invocation) {
+ if (available != null) available.controller().end(true);
return null;
}
}).when(callback).onSurfaceAvailable();
- preview = createPreview(a, a.getContentView(), callback);
+
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) {
+ if (destroyed != null) destroyed.controller().end(true);
+ return null;
+ }
+ }).when(callback).onSurfaceDestroyed();
+
+ preview = createPreview(a, a.getContentView());
+ preview.setSurfaceCallback(callback);
}
});
}
// Wait for surface to be available.
protected void ensureAvailable() {
- assertNotNull(availability.await(2000));
+ assertNotNull(available.await(DELAY));
}
// Trigger a destroy.
protected void ensureDestroyed() {
- ui(new Runnable() {
+ uiSync(new Runnable() {
@Override
public void run() {
- rule.getActivity().getContentView().removeView(preview.getView());
+ rule.getActivity().getContentView().removeView(preview.getRootView());
}
});
- idle();
+ assertNotNull(destroyed.await(DELAY));
}
@After
@@ -73,22 +95,24 @@ public abstract class PreviewTest extends BaseTest {
preview = null;
callback = null;
surfaceSize = null;
- availability = null;
+ available = null;
+ destroyed = null;
}
@Test
public void testDefaults() {
ensureAvailable();
- assertTrue(preview.isReady());
+ assertTrue(preview.hasSurface());
assertNotNull(preview.getView());
+ assertNotNull(preview.getRootView());
assertNotNull(preview.getOutputClass());
}
@Test
public void testDesiredSize() {
- preview.setDesiredSize(160, 90);
- assertEquals(160, preview.getDesiredSize().getWidth());
- assertEquals(90, preview.getDesiredSize().getHeight());
+ preview.setStreamSize(160, 90);
+ assertEquals(160, preview.getStreamSize().getWidth());
+ assertEquals(90, preview.getStreamSize().getHeight());
}
@Test
@@ -103,12 +127,14 @@ public abstract class PreviewTest extends BaseTest {
public void testSurfaceDestroyed() {
ensureAvailable();
ensureDestroyed();
+ // This might be called twice in Texture because it overrides ensureDestroyed method
+ verify(callback, atLeastOnce()).onSurfaceDestroyed();
assertEquals(0, preview.getSurfaceSize().getWidth());
assertEquals(0, preview.getSurfaceSize().getHeight());
}
@Test
- public void testCropCenter() throws Exception {
+ public void testCropCenter() {
ensureAvailable();
// This is given by the activity, it's the fixed size.
@@ -127,17 +153,23 @@ public abstract class PreviewTest extends BaseTest {
// Since desired is 'desired', let's fake a new view size that is consistent with it.
// Ensure crop is not happening anymore.
- preview.mCropTask.listen();
- preview.onSurfaceSizeChanged((int) (50f * desired), 50); // Wait...
- preview.mCropTask.await();
+ preview.mCropCallback = mock(CameraPreview.CropCallback.class);
+ Op op = new Op<>();
+ doEndOp(op, null).when(preview.mCropCallback).onCrop();
+ preview.dispatchOnSurfaceSizeChanged((int) (50f * desired), 50);
+
+ op.await(); // Wait...
assertEquals(desired, getViewAspectRatioWithScale(), 0.01f);
assertFalse(preview.isCropping());
}
private void setDesiredAspectRatio(float desiredAspectRatio) {
- preview.mCropTask.listen();
- preview.setDesiredSize((int) (10f * desiredAspectRatio), 10); // Wait...
- preview.mCropTask.await();
+ preview.mCropCallback = mock(CameraPreview.CropCallback.class);
+ Op op = new Op<>();
+ doEndOp(op, null).when(preview.mCropCallback).onCrop();
+ preview.setStreamSize((int) (10f * desiredAspectRatio), 10);
+
+ op.await(); // Wait...
assertEquals(desiredAspectRatio, getViewAspectRatioWithScale(), 0.01f);
}
@@ -149,8 +181,12 @@ public abstract class PreviewTest extends BaseTest {
private float getViewAspectRatioWithScale() {
Size size = preview.getSurfaceSize();
- int newWidth = (int) (((float) size.getWidth()) * preview.getView().getScaleX());
- int newHeight = (int) (((float) size.getHeight()) * preview.getView().getScaleY());
+ int newWidth = (int) (((float) size.getWidth()) * getCropScaleX());
+ int newHeight = (int) (((float) size.getHeight()) * getCropScaleY());
return AspectRatio.of(newWidth, newHeight).toFloat();
}
+
+ abstract protected float getCropScaleX();
+
+ abstract protected float getCropScaleY();
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/GlCameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/GlCameraPreviewTest.java
new file mode 100644
index 00000000..968ca5db
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/GlCameraPreviewTest.java
@@ -0,0 +1,43 @@
+package com.otaliastudios.cameraview.preview;
+
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import android.view.ViewGroup;
+
+import com.otaliastudios.cameraview.filter.Filter;
+import com.otaliastudios.cameraview.filter.Filters;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GlCameraPreviewTest extends CameraPreviewTest {
+
+ @Override
+ protected GlCameraPreview createPreview(Context context, ViewGroup parent) {
+ return new GlCameraPreview(context, parent);
+ }
+
+ @Override
+ protected float getCropScaleY() {
+ return 1F / preview.mCropScaleY;
+ }
+
+ @Override
+ protected float getCropScaleX() {
+ return 1F / preview.mCropScaleX;
+ }
+
+ @Test
+ public void testSetFilter() {
+ Filter filter = Filters.BLACK_AND_WHITE.newInstance();
+ preview.setFilter(filter);
+ assertEquals(filter, preview.getCurrentFilter());
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java
new file mode 100644
index 00000000..c4e5238c
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java
@@ -0,0 +1,63 @@
+package com.otaliastudios.cameraview.preview;
+
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.otaliastudios.cameraview.filter.Filter;
+import com.otaliastudios.cameraview.preview.CameraPreview;
+
+public class MockCameraPreview extends CameraPreview implements FilterCameraPreview {
+
+ public MockCameraPreview(Context context, ViewGroup parent) {
+ super(context, parent);
+ }
+
+ private View rootView;
+ private Filter filter;
+
+ @Override
+ public boolean supportsCropping() {
+ return true;
+ }
+
+ @NonNull
+ @Override
+ protected View onCreateView(@NonNull Context context, @NonNull ViewGroup parent) {
+ rootView = new View(context);
+ return rootView;
+ }
+
+ @NonNull
+ @Override
+ public Class getOutputClass() {
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public Void getOutput() {
+ return null;
+ }
+
+
+ @NonNull
+ @Override
+ public View getRootView() {
+ return rootView;
+ }
+
+ @Override
+ public void setFilter(@NonNull Filter filter) {
+ this.filter = filter;
+ }
+
+ @NonNull
+ @Override
+ public Filter getCurrentFilter() {
+ return filter;
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreviewTest.java
new file mode 100644
index 00000000..fcf9b282
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreviewTest.java
@@ -0,0 +1,30 @@
+package com.otaliastudios.cameraview.preview;
+
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import android.view.ViewGroup;
+
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SurfaceCameraPreviewTest extends CameraPreviewTest {
+
+ @Override
+ protected SurfaceCameraPreview createPreview(Context context, ViewGroup parent) {
+ return new SurfaceCameraPreview(context, parent);
+ }
+
+ @Override
+ protected float getCropScaleX() {
+ return 1F;
+ }
+
+ @Override
+ protected float getCropScaleY() {
+ return 1F;
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/TextureCameraPreviewTest.java
similarity index 50%
rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java
rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/TextureCameraPreviewTest.java
index 8a1f056e..0efbfeb5 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/TextureCameraPreviewTest.java
@@ -1,20 +1,21 @@
-package com.otaliastudios.cameraview;
+package com.otaliastudios.cameraview.preview;
import android.content.Context;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
import android.view.ViewGroup;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@SmallTest
-public class TextureCameraPreviewTest extends PreviewTest {
+public class TextureCameraPreviewTest extends CameraPreviewTest {
@Override
- protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) {
- return new TextureCameraPreview(context, parent, callback);
+ protected TextureCameraPreview createPreview(Context context, ViewGroup parent) {
+ return new TextureCameraPreview(context, parent);
}
@Override
@@ -22,7 +23,7 @@ public class TextureCameraPreviewTest extends PreviewTest {
if (isHardwareAccelerated()) {
super.ensureAvailable();
} else {
- preview.onSurfaceAvailable(
+ preview.dispatchOnSurfaceAvailable(
surfaceSize.getWidth(),
surfaceSize.getHeight());
}
@@ -33,11 +34,21 @@ public class TextureCameraPreviewTest extends PreviewTest {
super.ensureDestroyed();
if (!isHardwareAccelerated()) {
// Ensure it is called.
- preview.onSurfaceDestroyed();
+ preview.dispatchOnSurfaceDestroyed();
}
}
private boolean isHardwareAccelerated() {
return preview.getView().isHardwareAccelerated();
}
+
+ @Override
+ protected float getCropScaleX() {
+ return preview.getView().getScaleX();
+ }
+
+ @Override
+ protected float getCropScaleY() {
+ return preview.getView().getScaleY();
+ }
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/size/SizeSelectorParserTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/size/SizeSelectorParserTest.java
new file mode 100644
index 00000000..34047923
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/size/SizeSelectorParserTest.java
@@ -0,0 +1,211 @@
+package com.otaliastudios.cameraview.size;
+
+
+import android.content.res.TypedArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.StyleableRes;
+import androidx.arch.core.util.Function;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.R;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SizeSelectorParserTest extends BaseTest {
+
+ private MockTypedArray input;
+ private List sizes = Arrays.asList(
+ new Size(100, 200),
+ new Size(150, 300),
+ new Size(600, 900),
+ new Size(600, 600),
+ new Size(1600, 900),
+ new Size(30, 40),
+ new Size(40, 30),
+ new Size(2000, 4000)
+ );
+
+ @Before
+ public void setUp() {
+ input = new MockTypedArray();
+ }
+
+ @After
+ public void tearDown() {
+ input = null;
+ }
+
+ private void doAssert(@NonNull Function, Void> assertions) {
+ SizeSelectorParser parser = new SizeSelectorParser(input.array);
+ assertions.apply(parser.getPictureSizeSelector().select(sizes));
+ assertions.apply(parser.getVideoSizeSelector().select(sizes));
+ }
+
+ @Test
+ public void testWidth() {
+ input.setMinWidth(1500);
+ input.setMaxWidth(1700);
+ doAssert(new Function, Void>() {
+ @Override
+ public Void apply(List input) {
+ assertEquals(1, input.size());
+ assertEquals(new Size(1600, 900), input.get(0));
+ return null;
+ }
+ });
+ }
+
+ @Test
+ public void testHeight() {
+ input.setMinHeight(25);
+ input.setMaxHeight(35);
+ doAssert(new Function, Void>() {
+ @Override
+ public Void apply(List input) {
+ assertEquals(1, input.size());
+ assertEquals(new Size(40, 30), input.get(0));
+ return null;
+ }
+ });
+ }
+
+ @Test
+ public void testArea() {
+ input.setMinArea(30 * 30);
+ input.setMaxArea(40 * 40);
+ doAssert(new Function, Void>() {
+ @Override
+ public Void apply(List input) {
+ assertEquals(2, input.size());
+ assertTrue(input.contains(new Size(40, 30)));
+ assertTrue(input.contains(new Size(30, 40)));
+ return null;
+ }
+ });
+ }
+
+ @Test
+ public void testSmallest() {
+ input.setSmallest(true);
+ doAssert(new Function, Void>() {
+ @Override
+ public Void apply(List input) {
+ assertEquals(sizes.size(), input.size());
+ Size first = input.get(0);
+ assertEquals(30 * 40, first.getWidth() * first.getHeight());
+ return null;
+ }
+ });
+ }
+
+ @Test
+ public void testBiggest() {
+ input.setBiggest(true);
+ doAssert(new Function, Void>() {
+ @Override
+ public Void apply(List input) {
+ assertEquals(sizes.size(), input.size());
+ assertEquals(new Size(2000, 4000), input.get(0));
+ return null;
+ }
+ });
+ }
+
+ @Test
+ public void testAspectRatio() {
+ input.setAspectRatio("16:9");
+ doAssert(new Function, Void>() {
+ @Override
+ public Void apply(List input) {
+ assertEquals(1, input.size());
+ assertEquals(new Size(1600, 900), input.get(0));
+ return null;
+ }
+ });
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private class MockTypedArray {
+ private TypedArray array = mock(TypedArray.class);
+
+ private void setIntValue(@StyleableRes int index, int value) {
+ when(array.hasValue(index)).thenReturn(true);
+ when(array.getInteger(eq(index), anyInt())).thenReturn(value);
+ }
+
+ private void setBooleanValue(@StyleableRes int index, boolean value) {
+ when(array.hasValue(index)).thenReturn(true);
+ when(array.getBoolean(eq(index), anyBoolean())).thenReturn(value);
+ }
+
+ private void setStringValue(@StyleableRes int index, @NonNull String value) {
+ when(array.hasValue(index)).thenReturn(true);
+ when(array.getString(index)).thenReturn(value);
+ }
+
+ private void setMinWidth(int value) {
+ setIntValue(R.styleable.CameraView_cameraPictureSizeMinWidth, value);
+ setIntValue(R.styleable.CameraView_cameraVideoSizeMinWidth, value);
+ }
+
+ private void setMaxWidth(int value) {
+ setIntValue(R.styleable.CameraView_cameraPictureSizeMaxWidth, value);
+ setIntValue(R.styleable.CameraView_cameraVideoSizeMaxWidth, value);
+ }
+
+ private void setMinHeight(int value) {
+ setIntValue(R.styleable.CameraView_cameraPictureSizeMinHeight, value);
+ setIntValue(R.styleable.CameraView_cameraVideoSizeMinHeight, value);
+ }
+
+ private void setMaxHeight(int value) {
+ setIntValue(R.styleable.CameraView_cameraPictureSizeMaxHeight, value);
+ setIntValue(R.styleable.CameraView_cameraVideoSizeMaxHeight, value);
+ }
+
+ private void setMinArea(int value) {
+ setIntValue(R.styleable.CameraView_cameraPictureSizeMinArea, value);
+ setIntValue(R.styleable.CameraView_cameraVideoSizeMinArea, value);
+ }
+
+ private void setMaxArea(int value) {
+ setIntValue(R.styleable.CameraView_cameraPictureSizeMaxArea, value);
+ setIntValue(R.styleable.CameraView_cameraVideoSizeMaxArea, value);
+ }
+
+ private void setSmallest(boolean value) {
+ setBooleanValue(R.styleable.CameraView_cameraPictureSizeSmallest, value);
+ setBooleanValue(R.styleable.CameraView_cameraVideoSizeSmallest, value);
+ }
+
+ private void setBiggest(boolean value) {
+ setBooleanValue(R.styleable.CameraView_cameraPictureSizeBiggest, value);
+ setBooleanValue(R.styleable.CameraView_cameraVideoSizeBiggest, value);
+ }
+
+ private void setAspectRatio(@NonNull String value) {
+ setStringValue(R.styleable.CameraView_cameraPictureSizeAspectRatio, value);
+ setStringValue(R.styleable.CameraView_cameraVideoSizeAspectRatio, value);
+ }
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Emulator.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Emulator.java
new file mode 100644
index 00000000..053daa36
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Emulator.java
@@ -0,0 +1,12 @@
+package com.otaliastudios.cameraview.tools;
+
+import android.os.Build;
+
+public class Emulator {
+ public static boolean isEmulator() {
+ // From Android's RequiresDeviceFilter
+ return Build.HARDWARE.equals("goldfish")
+ || Build.HARDWARE.equals("ranchu")
+ || Build.HARDWARE.equals("gce_x86");
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Op.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Op.java
new file mode 100644
index 00000000..2d81c55f
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Op.java
@@ -0,0 +1,149 @@
+package com.otaliastudios.cameraview.tools;
+
+import androidx.annotation.NonNull;
+
+import com.google.android.gms.tasks.OnSuccessListener;
+import com.google.android.gms.tasks.Task;
+import com.otaliastudios.cameraview.controls.Control;
+
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.mockito.stubbing.Stubber;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A naive implementation of {@link java.util.concurrent.CountDownLatch}
+ * to help in testing.
+ */
+public class Op {
+
+ public class Controller {
+ private int mToBeIgnored;
+
+ private Controller() { }
+
+ /** Op owner method: notifies the action started. */
+ public void start() {
+ if (!isListening()) mToBeIgnored++;
+ }
+
+ /** Op owner method: notifies the action ended. */
+ public void end(T result) {
+ if (mToBeIgnored > 0) {
+ mToBeIgnored--;
+ return;
+ }
+
+ if (isListening()) { // Should be always true.
+ mResult = result;
+ mLatch.countDown();
+ }
+ }
+
+ public void from(@NonNull Task task) {
+ start();
+ task.addOnSuccessListener(new OnSuccessListener() {
+ @Override
+ public void onSuccess(T result) {
+ end(result);
+ }
+ });
+ }
+
+ @NonNull
+ public Stubber from(final int invocationArgument) {
+ return Mockito.doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) {
+ //noinspection unchecked
+ T o = (T) invocation.getArguments()[invocationArgument];
+ start();
+ end(o);
+ return null;
+ }
+ });
+ }
+ }
+
+ private CountDownLatch mLatch;
+ private Controller mController = new Controller();
+ private T mResult;
+
+ /**
+ * Listeners should:
+ * - call {@link #listen()} to notify they are interested in the next action
+ * - call {@link #await()} to know when the action is performed.
+ *
+ * Op owners should:
+ * - call {@link Controller#start()} when task started
+ * - call {@link Controller#end(Object)} when task ends
+ */
+ public Op() {
+ this(true);
+ }
+
+ public Op(boolean startListening) {
+ if (startListening) listen();
+ }
+
+ public Op(@NonNull Task task) {
+ listen();
+ controller().from(task);
+ }
+
+ private boolean isListening() {
+ return mLatch != null;
+ }
+
+ /**
+ * Listener method: notifies we are interested in the next action.
+ */
+ public void listen() {
+ if (isListening()) throw new RuntimeException("Should not happen.");
+ mResult = null;
+ mLatch = new CountDownLatch(1);
+ }
+
+ /**
+ * Listener method: waits for next task action to end.
+ * @param millis milliseconds
+ * @return the action result
+ */
+ public T await(long millis) {
+ return await(millis, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Listener method: waits 1 minute for next task action to end.
+ * @return the action result
+ */
+ public T await() {
+ return await(1, TimeUnit.MINUTES);
+ }
+
+ /**
+ * Listener method: waits for next task action to end.
+ * @param time time
+ * @param unit the time unit
+ * @return the action result
+ */
+ private T await(long time, @NonNull TimeUnit unit) {
+ try {
+ mLatch.await(time, unit);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ T result = mResult;
+ mResult = null;
+ mLatch = null;
+ return result;
+ }
+
+ @NonNull
+ public Controller controller() {
+ return mController;
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Retry.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Retry.java
new file mode 100644
index 00000000..88c3d452
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Retry.java
@@ -0,0 +1,9 @@
+package com.otaliastudios.cameraview.tools;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Retry {
+ boolean emulatorOnly() default false;
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/RetryRule.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/RetryRule.java
new file mode 100644
index 00000000..bad7dcd8
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/RetryRule.java
@@ -0,0 +1,52 @@
+package com.otaliastudios.cameraview.tools;
+
+import android.os.Build;
+
+import com.otaliastudios.cameraview.CameraLogger;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class RetryRule implements TestRule {
+
+ private final static String TAG = RetryRule.class.getSimpleName();
+ private final static CameraLogger LOG = CameraLogger.create(TAG);
+
+ private AtomicInteger retries;
+
+ public RetryRule(int retries) {
+ this.retries = new AtomicInteger(retries);
+ }
+
+ @Override
+ public Statement apply(final Statement base, final Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ Retry retry = description.getAnnotation(Retry.class);
+ if (retry == null || retry.emulatorOnly() && !Emulator.isEmulator()) {
+ base.evaluate();
+ } else {
+ Throwable caught = null;
+ while (retries.getAndDecrement() > 0) {
+ try {
+ base.evaluate();
+ return;
+ } catch (Throwable throwable) {
+ LOG.e("[RETRY] Test failed.", retries.get(),
+ "retries available...");
+ LOG.e("*******************************************************");
+ caught = throwable;
+ }
+ }
+ if (caught != null) {
+ throw caught;
+ }
+ }
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkExclude.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkExclude.java
new file mode 100644
index 00000000..8e4bdebb
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkExclude.java
@@ -0,0 +1,20 @@
+package com.otaliastudios.cameraview.tools;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Like {@link androidx.test.filters.SdkSuppress}, but negative.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface SdkExclude {
+ /** The minimum API level to drop (inclusive) */
+ int minSdkVersion() default 1;
+ /** The maximum API level to drop (inclusive) */
+ int maxSdkVersion() default Integer.MAX_VALUE;
+ /** Whether this filter only applies to emulators */
+ boolean emulatorOnly() default false;
+}
\ No newline at end of file
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkExcludeFilter.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkExcludeFilter.java
new file mode 100644
index 00000000..0cb621f3
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkExcludeFilter.java
@@ -0,0 +1,47 @@
+package com.otaliastudios.cameraview.tools;
+
+
+import android.os.Build;
+
+import androidx.annotation.Nullable;
+import androidx.test.internal.runner.filters.ParentFilter;
+
+import org.junit.runner.Description;
+
+/**
+ * Filter for {@link SdkExclude}, based on
+ * {@link androidx.test.internal.runner.TestRequestBuilder}'s SdkSuppressFilter.
+ */
+public class SdkExcludeFilter extends ParentFilter {
+
+ protected boolean evaluateTest(Description description) {
+ SdkExclude annotation = getAnnotationForTest(description);
+ if (annotation != null) {
+ if ((!annotation.emulatorOnly() || Emulator.isEmulator())
+ && Build.VERSION.SDK_INT >= annotation.minSdkVersion()
+ && Build.VERSION.SDK_INT <= annotation.maxSdkVersion()) {
+ return false; // exclude the test
+ }
+ return true; // run the test
+ }
+ return true; // no annotation, run the test
+ }
+
+ @Nullable
+ private SdkExclude getAnnotationForTest(Description description) {
+ final SdkExclude s = description.getAnnotation(SdkExclude.class);
+ if (s != null) {
+ return s;
+ }
+ final Class> testClass = description.getTestClass();
+ if (testClass != null) {
+ return testClass.getAnnotation(SdkExclude.class);
+ }
+ return null;
+ }
+
+ @Override
+ public String describe() {
+ return "Skip tests annotated with SdkExclude";
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkInclude.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkInclude.java
new file mode 100644
index 00000000..4083c56b
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkInclude.java
@@ -0,0 +1,20 @@
+package com.otaliastudios.cameraview.tools;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Like {@link androidx.test.filters.SdkSuppress}, but with emulatorOnly().
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface SdkInclude {
+ /** The minimum API level to run (inclusive) */
+ int minSdkVersion() default 1;
+ /** The maximum API level to run (inclusive) */
+ int maxSdkVersion() default Integer.MAX_VALUE;
+ /** Whether this filter only applies to emulators */
+ boolean emulatorOnly() default false;
+}
\ No newline at end of file
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkIncludeFilter.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkIncludeFilter.java
new file mode 100644
index 00000000..aa0937fb
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkIncludeFilter.java
@@ -0,0 +1,47 @@
+package com.otaliastudios.cameraview.tools;
+
+
+import android.os.Build;
+
+import androidx.annotation.Nullable;
+import androidx.test.internal.runner.filters.ParentFilter;
+
+import org.junit.runner.Description;
+
+/**
+ * Filter for {@link SdkInclude}, based on
+ * {@link androidx.test.internal.runner.TestRequestBuilder}'s SdkSuppressFilter.
+ */
+public class SdkIncludeFilter extends ParentFilter {
+
+ protected boolean evaluateTest(Description description) {
+ SdkInclude annotation = getAnnotationForTest(description);
+ if (annotation != null) {
+ if ((!annotation.emulatorOnly() || Emulator.isEmulator())
+ && Build.VERSION.SDK_INT >= annotation.minSdkVersion()
+ && Build.VERSION.SDK_INT <= annotation.maxSdkVersion()) {
+ return true; // run the test
+ }
+ return false; // drop the test
+ }
+ return true; // no annotation, run the test
+ }
+
+ @Nullable
+ private SdkInclude getAnnotationForTest(Description description) {
+ final SdkInclude s = description.getAnnotation(SdkInclude.class);
+ if (s != null) {
+ return s;
+ }
+ final Class> testClass = description.getTestClass();
+ if (testClass != null) {
+ return testClass.getAnnotation(SdkInclude.class);
+ }
+ return null;
+ }
+
+ @Override
+ public String describe() {
+ return "Skip tests annotated with SdkInclude";
+ }
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/video/VideoRecorderTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/video/VideoRecorderTest.java
new file mode 100644
index 00000000..edafd44c
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/video/VideoRecorderTest.java
@@ -0,0 +1,52 @@
+package com.otaliastudios.cameraview.video;
+
+
+import com.otaliastudios.cameraview.BaseTest;
+import com.otaliastudios.cameraview.VideoResult;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.lang.reflect.Constructor;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VideoRecorderTest extends BaseTest {
+
+ @Test
+ public void testRecorder() throws Exception {
+ VideoResult.Stub result = createStub();
+ VideoRecorder.VideoResultListener listener = Mockito.mock(VideoRecorder.VideoResultListener.class);
+ VideoRecorder recorder = new VideoRecorder(listener) {
+ @Override
+ protected void onStart() {
+ dispatchVideoRecordingStart();
+ }
+
+ @Override
+ protected void onStop(boolean isCameraShutdown) {
+ dispatchVideoRecordingEnd();
+ dispatchResult();
+ }
+ };
+ recorder.start(result);
+ Mockito.verify(listener,Mockito.times(1) )
+ .onVideoRecordingStart();
+ recorder.stop(false);
+ Mockito.verify(listener, Mockito.times(1))
+ .onVideoRecordingEnd();
+ Mockito.verify(listener, Mockito.times(1))
+ .onVideoResult(result, null);
+ }
+
+ private VideoResult.Stub createStub() throws Exception {
+ Constructor constructor = VideoResult.Stub.class.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ return constructor.newInstance();
+ }
+}
diff --git a/cameraview/src/androidTest/res/layout/not_overlay.xml b/cameraview/src/androidTest/res/layout/not_overlay.xml
new file mode 100644
index 00000000..158a0406
--- /dev/null
+++ b/cameraview/src/androidTest/res/layout/not_overlay.xml
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/cameraview/src/androidTest/res/layout/overlay.xml b/cameraview/src/androidTest/res/layout/overlay.xml
new file mode 100644
index 00000000..ff33576e
--- /dev/null
+++ b/cameraview/src/androidTest/res/layout/overlay.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/cameraview/src/main/AndroidManifest.xml b/cameraview/src/main/AndroidManifest.xml
index 5da7cf09..ce05aa56 100644
--- a/cameraview/src/main/AndroidManifest.xml
+++ b/cameraview/src/main/AndroidManifest.xml
@@ -1,4 +1,5 @@
@@ -20,6 +21,8 @@
android:name="android.hardware.microphone"
android:required="false"/>
+
+
\ No newline at end of file
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/BitmapCallback.java b/cameraview/src/main/java/com/otaliastudios/cameraview/BitmapCallback.java
new file mode 100644
index 00000000..e6d244bc
--- /dev/null
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/BitmapCallback.java
@@ -0,0 +1,21 @@
+package com.otaliastudios.cameraview;
+
+import android.graphics.Bitmap;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+/**
+ * Receives callbacks about a bitmap decoding operation.
+ */
+public interface BitmapCallback {
+
+ /**
+ * Notifies that the bitmap was successfully decoded.
+ * This is run on the UI thread.
+ * Returns a null object if a {@link OutOfMemoryError} was encountered.
+ *
+ * @param bitmap decoded bitmap, or null
+ */
+ @UiThread
+ void onBitmapReady(@Nullable Bitmap bitmap);
+}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
deleted file mode 100644
index b8af16b2..00000000
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
+++ /dev/null
@@ -1,924 +0,0 @@
-package com.otaliastudios.cameraview;
-
-import android.annotation.TargetApi;
-import android.graphics.ImageFormat;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.SurfaceTexture;
-import android.graphics.YuvImage;
-import android.hardware.Camera;
-import android.location.Location;
-import android.media.CamcorderProfile;
-import android.media.MediaRecorder;
-import android.os.Build;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-import android.util.Log;
-import android.view.SurfaceHolder;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-
-@SuppressWarnings("deprecation")
-class Camera1 extends CameraController implements Camera.PreviewCallback, Camera.ErrorCallback {
-
- private static final String TAG = Camera1.class.getSimpleName();
- private static final CameraLogger LOG = CameraLogger.create(TAG);
-
- private Camera mCamera;
- private boolean mIsBound = false;
-
- private final int mPostFocusResetDelay = 3000;
- private Runnable mPostFocusResetRunnable = new Runnable() {
- @Override
- public void run() {
- if (!isCameraAvailable()) return;
- mCamera.cancelAutoFocus();
- Camera.Parameters params = mCamera.getParameters();
- int maxAF = params.getMaxNumFocusAreas();
- int maxAE = params.getMaxNumMeteringAreas();
- if (maxAF > 0) params.setFocusAreas(null);
- if (maxAE > 0) params.setMeteringAreas(null);
- applyDefaultFocus(params); // Revert to internal focus.
- mCamera.setParameters(params);
- }
- };
-
- Camera1(CameraView.CameraCallbacks callback) {
- super(callback);
- mMapper = new Mapper.Mapper1();
- }
-
- private void schedule(@Nullable final Task task, final boolean ensureAvailable, final Runnable action) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- if (ensureAvailable && !isCameraAvailable()) {
- if (task != null) task.end(null);
- } else {
- action.run();
- if (task != null) task.end(null);
- }
- }
- });
- }
-
- // Preview surface is now available. If camera is open, set up.
- @Override
- public void onSurfaceAvailable() {
- LOG.i("onSurfaceAvailable:", "Size is", mPreview.getSurfaceSize());
- schedule(null, false, new Runnable() {
- @Override
- public void run() {
- LOG.i("onSurfaceAvailable:", "Inside handler. About to bind.");
- if (shouldBindToSurface()) bindToSurface();
- }
- });
- }
-
- // Preview surface did change its size. Compute a new preview size.
- // This requires stopping and restarting the preview.
- @Override
- public void onSurfaceChanged() {
- LOG.i("onSurfaceChanged, size is", mPreview.getSurfaceSize());
- schedule(null, true, new Runnable() {
- @Override
- public void run() {
- if (!mIsBound) return;
-
- // Compute a new camera preview size.
- Size newSize = computePreviewSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes()));
- if (newSize.equals(mPreviewSize)) return;
-
- // Apply.
- LOG.i("onSurfaceChanged:", "Computed a new preview size. Going on.");
- mPreviewSize = newSize;
- mCamera.stopPreview();
- applySizesAndStartPreview("onSurfaceChanged:");
- }
- });
- }
-
- private boolean shouldBindToSurface() {
- return isCameraAvailable() && mPreview != null && mPreview.isReady() && !mIsBound;
- }
-
- // The act of binding an "open" camera to a "ready" preview.
- // These can happen at different times but we want to end up here.
- @WorkerThread
- private void bindToSurface() {
- LOG.i("bindToSurface:", "Started");
- Object output = mPreview.getOutput();
- try {
- if (mPreview.getOutputClass() == SurfaceHolder.class) {
- mCamera.setPreviewDisplay((SurfaceHolder) output);
- } else {
- mCamera.setPreviewTexture((SurfaceTexture) output);
- }
- } catch (IOException e) {
- Log.e("bindToSurface:", "Failed to bind.", e);
- throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW);
- }
-
- mPictureSize = computePictureSize();
- mPreviewSize = computePreviewSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes()));
- applySizesAndStartPreview("bindToSurface:");
- mIsBound = true;
- }
-
- // To be called when the preview size is setup or changed.
- private void applySizesAndStartPreview(String log) {
- LOG.i(log, "Dispatching onCameraPreviewSizeChanged.");
- mCameraCallbacks.onCameraPreviewSizeChanged();
-
- boolean invertPreviewSizes = shouldFlipSizes();
- mPreview.setDesiredSize(
- invertPreviewSizes ? mPreviewSize.getHeight() : mPreviewSize.getWidth(),
- invertPreviewSizes ? mPreviewSize.getWidth() : mPreviewSize.getHeight()
- );
-
- Camera.Parameters params = mCamera.getParameters();
- mPreviewFormat = params.getPreviewFormat();
- params.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); // <- not allowed during preview
- params.setPictureSize(mPictureSize.getWidth(), mPictureSize.getHeight()); // <- allowed
- mCamera.setParameters(params);
-
- mCamera.setPreviewCallbackWithBuffer(null); // Release anything left
- mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves
- mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize);
-
- LOG.i(log, "Starting preview with startPreview().");
- try {
- mCamera.startPreview();
- } catch (Exception e) {
- LOG.e(log, "Failed to start preview.", e);
- throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW);
- }
- LOG.i(log, "Started preview.");
- }
-
- @WorkerThread
- @Override
- void onStart() {
- if (isCameraAvailable()) {
- LOG.w("onStart:", "Camera not available. Should not happen.");
- onStop(); // Should not happen.
- }
- if (collectCameraId()) {
- try {
- mCamera = Camera.open(mCameraId);
- } catch (Exception e) {
- LOG.e("onStart:", "Failed to connect. Maybe in use by another app?");
- throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT);
- }
- mCamera.setErrorCallback(this);
-
- // Set parameters that might have been set before the camera was opened.
- LOG.i("onStart:", "Applying default parameters.");
- Camera.Parameters params = mCamera.getParameters();
- mExtraProperties = new ExtraProperties(params);
- mCameraOptions = new CameraOptions(params, shouldFlipSizes());
- applyDefaultFocus(params);
- mergeFlash(params, Flash.DEFAULT);
- mergeLocation(params, null);
- mergeWhiteBalance(params, WhiteBalance.DEFAULT);
- mergeHdr(params, Hdr.DEFAULT);
- mergePlaySound(mPlaySounds);
- params.setRecordingHint(mSessionType == SessionType.VIDEO);
- mCamera.setParameters(params);
-
- // Try starting preview.
- mCamera.setDisplayOrientation(computeSensorToViewOffset()); // <- not allowed during preview
- if (shouldBindToSurface()) bindToSurface();
- LOG.i("onStart:", "Ended");
- }
- }
-
- @WorkerThread
- @Override
- void onStop() {
- LOG.i("onStop:", "About to clean up.");
- mHandler.get().removeCallbacks(mPostFocusResetRunnable);
- mFrameManager.release();
-
- if (mCamera != null) {
- LOG.i("onStop:", "Clean up.", "Ending video.");
- endVideoImmediately();
-
- try {
- LOG.i("onStop:", "Clean up.", "Stopping preview.");
- mCamera.setPreviewCallbackWithBuffer(null);
- mCamera.stopPreview();
- LOG.i("onStop:", "Clean up.", "Stopped preview.");
- } catch (Exception e) {
- LOG.w("onStop:", "Clean up.", "Exception while stopping preview.", e);
- }
-
- try {
- LOG.i("onStop:", "Clean up.", "Releasing camera.");
- mCamera.release();
- LOG.i("onStop:", "Clean up.", "Released camera.");
- } catch (Exception e) {
- LOG.w("onStop:", "Clean up.", "Exception while releasing camera.", e);
- }
- }
- mExtraProperties = null;
- mCameraOptions = null;
- mCamera = null;
- mPreviewSize = null;
- mPictureSize = null;
- mIsBound = false;
- mIsCapturingImage = false;
- mIsCapturingVideo = false;
- LOG.w("onStop:", "Clean up.", "Returning.");
-
- // We were saving a reference to the exception here and throwing to the user.
- // I don't think it's correct. We are closing and have already done our best
- // to clean up resources. No need to throw.
- // if (error != null) throw new CameraException(error);
- }
-
- private boolean collectCameraId() {
- int internalFacing = mMapper.map(mFacing);
- Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
- for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) {
- Camera.getCameraInfo(i, cameraInfo);
- if (cameraInfo.facing == internalFacing) {
- mSensorOffset = cameraInfo.orientation;
- mCameraId = i;
- return true;
- }
- }
- return false;
- }
-
- @Override
- public void onBufferAvailable(byte[] buffer) {
- // TODO: sync with handler?
- if (isCameraAvailable()) {
- mCamera.addCallbackBuffer(buffer);
- }
- }
-
- @Override
- public void onError(int error, Camera camera) {
- if (error == Camera.CAMERA_ERROR_SERVER_DIED) {
- // Looks like this is recoverable.
- LOG.w("Recoverable error inside the onError callback.", "CAMERA_ERROR_SERVER_DIED");
- stopImmediately();
- start();
- return;
- }
-
- LOG.e("Error inside the onError callback.", error);
- Exception runtime = new RuntimeException(CameraLogger.lastMessage);
- int reason;
- switch (error) {
- case Camera.CAMERA_ERROR_EVICTED: reason = CameraException.REASON_DISCONNECTED; break;
- case Camera.CAMERA_ERROR_UNKNOWN: reason = CameraException.REASON_UNKNOWN; break;
- default: reason = CameraException.REASON_UNKNOWN;
- }
- throw new CameraException(runtime, reason);
- }
-
- @Override
- void setSessionType(SessionType sessionType) {
- if (sessionType != mSessionType) {
- mSessionType = sessionType;
- schedule(null, true, new Runnable() {
- @Override
- public void run() {
- restart();
- }
- });
- }
- }
-
- @Override
- void setLocation(Location location) {
- final Location oldLocation = mLocation;
- mLocation = location;
- schedule(mLocationTask, true, new Runnable() {
- @Override
- public void run() {
- Camera.Parameters params = mCamera.getParameters();
- if (mergeLocation(params, oldLocation)) mCamera.setParameters(params);
- }
- });
- }
-
- private boolean mergeLocation(Camera.Parameters params, Location oldLocation) {
- if (mLocation != null) {
- params.setGpsLatitude(mLocation.getLatitude());
- params.setGpsLongitude(mLocation.getLongitude());
- params.setGpsAltitude(mLocation.getAltitude());
- params.setGpsTimestamp(mLocation.getTime());
- params.setGpsProcessingMethod(mLocation.getProvider());
-
- if (mIsCapturingVideo && mMediaRecorder != null) {
- mMediaRecorder.setLocation((float) mLocation.getLatitude(),
- (float) mLocation.getLongitude());
- }
- }
- return true;
- }
-
- @Override
- void setFacing(Facing facing) {
- if (facing != mFacing) {
- mFacing = facing;
- schedule(null, true, new Runnable() {
- @Override
- public void run() {
- if (collectCameraId()) {
- restart();
- }
- }
- });
- }
- }
-
- @Override
- void setWhiteBalance(WhiteBalance whiteBalance) {
- final WhiteBalance old = mWhiteBalance;
- mWhiteBalance = whiteBalance;
- schedule(mWhiteBalanceTask, true, new Runnable() {
- @Override
- public void run() {
- Camera.Parameters params = mCamera.getParameters();
- if (mergeWhiteBalance(params, old)) mCamera.setParameters(params);
- }
- });
- }
-
- private boolean mergeWhiteBalance(Camera.Parameters params, WhiteBalance oldWhiteBalance) {
- if (mCameraOptions.supports(mWhiteBalance)) {
- params.setWhiteBalance((String) mMapper.map(mWhiteBalance));
- return true;
- }
- mWhiteBalance = oldWhiteBalance;
- return false;
- }
-
- @Override
- void setHdr(Hdr hdr) {
- final Hdr old = mHdr;
- mHdr = hdr;
- schedule(mHdrTask, true, new Runnable() {
- @Override
- public void run() {
- Camera.Parameters params = mCamera.getParameters();
- if (mergeHdr(params, old)) mCamera.setParameters(params);
- }
- });
- }
-
- private boolean mergeHdr(Camera.Parameters params, Hdr oldHdr) {
- if (mCameraOptions.supports(mHdr)) {
- params.setSceneMode((String) mMapper.map(mHdr));
- return true;
- }
- mHdr = oldHdr;
- return false;
- }
-
- @TargetApi(17)
- private boolean mergePlaySound(boolean oldPlaySound) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- Camera.CameraInfo info = new Camera.CameraInfo();
- Camera.getCameraInfo(mCameraId, info);
- if (info.canDisableShutterSound) {
- mCamera.enableShutterSound(mPlaySounds);
- return true;
- }
- }
- if (mPlaySounds) {
- return true;
- }
- mPlaySounds = oldPlaySound;
- return false;
- }
-
-
- @Override
- void setAudio(Audio audio) {
- if (mAudio != audio) {
- if (mIsCapturingVideo) {
- LOG.w("Audio setting was changed while recording. " +
- "Changes will take place starting from next video");
- }
- mAudio = audio;
- }
- }
-
- @Override
- void setFlash(Flash flash) {
- final Flash old = mFlash;
- mFlash = flash;
- schedule(mFlashTask, true, new Runnable() {
- @Override
- public void run() {
- Camera.Parameters params = mCamera.getParameters();
- if (mergeFlash(params, old)) mCamera.setParameters(params);
- }
- });
- }
-
-
- private boolean mergeFlash(Camera.Parameters params, Flash oldFlash) {
- if (mCameraOptions.supports(mFlash)) {
- params.setFlashMode((String) mMapper.map(mFlash));
- return true;
- }
- mFlash = oldFlash;
- return false;
- }
-
-
- // Choose the best default focus, based on session type.
- private void applyDefaultFocus(Camera.Parameters params) {
- List modes = params.getSupportedFocusModes();
-
- if (mSessionType == SessionType.VIDEO &&
- modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
- params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
- return;
- }
-
- if (modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
- params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
- return;
- }
-
- if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) {
- params.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY);
- return;
- }
-
- if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) {
- params.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED);
- return;
- }
- }
-
-
- @Override
- void setVideoQuality(VideoQuality videoQuality) {
- final VideoQuality old = mVideoQuality;
- mVideoQuality = videoQuality;
- schedule(mVideoQualityTask, true, new Runnable() {
- @Override
- public void run() {
- if (mIsCapturingVideo) {
- // TODO: actually any call to getParameters() could fail while recording a video.
- // See. https://stackoverflow.com/questions/14941625/
- mVideoQuality = old;
- throw new IllegalStateException("Can't change video quality while recording a video.");
- }
-
- if (mSessionType == SessionType.VIDEO) {
- // Change capture size to a size that fits the video aspect ratio.
- Size oldSize = mPictureSize;
- mPictureSize = computePictureSize();
- if (!mPictureSize.equals(oldSize)) {
- // New video quality triggers a new aspect ratio.
- // Go on and see if preview size should change also.
- Camera.Parameters params = mCamera.getParameters();
- params.setPictureSize(mPictureSize.getWidth(), mPictureSize.getHeight());
- mCamera.setParameters(params);
- onSurfaceChanged();
- }
- LOG.i("setVideoQuality:", "captureSize:", mPictureSize);
- LOG.i("setVideoQuality:", "previewSize:", mPreviewSize);
- }
- }
- });
- }
-
- @Override
- void capturePicture() {
- LOG.v("capturePicture: scheduling");
- schedule(null, true, new Runnable() {
- @Override
- public void run() {
- LOG.v("capturePicture: performing.", mIsCapturingImage);
- if (mIsCapturingImage) return;
- if (mIsCapturingVideo && !mCameraOptions.isVideoSnapshotSupported()) return;
-
- mIsCapturingImage = true;
- final int sensorToOutput = computeSensorToOutputOffset();
- final int sensorToView = computeSensorToViewOffset();
- final boolean outputMatchesView = (sensorToOutput + sensorToView + 180) % 180 == 0;
- final boolean outputFlip = mFacing == Facing.FRONT;
- Camera.Parameters params = mCamera.getParameters();
- params.setRotation(sensorToOutput);
- mCamera.setParameters(params);
- mCamera.takePicture(
- new Camera.ShutterCallback() {
- @Override
- public void onShutter() {
- mCameraCallbacks.onShutter(false);
- }
- },
- null,
- null,
- new Camera.PictureCallback() {
- @Override
- public void onPictureTaken(byte[] data, final Camera camera) {
- mIsCapturingImage = false;
- mCameraCallbacks.processImage(data, outputMatchesView, outputFlip);
- camera.startPreview(); // This is needed, read somewhere in the docs.
- }
- }
- );
- }
- });
- }
-
-
- @Override
- void captureSnapshot() {
- LOG.v("captureSnapshot: scheduling");
- schedule(null, true, new Runnable() {
- @Override
- public void run() {
- LOG.v("captureSnapshot: performing.", mIsCapturingImage);
- if (mIsCapturingImage) return;
- // This won't work while capturing a video.
- // Switch to capturePicture.
- if (mIsCapturingVideo) {
- capturePicture();
- return;
- }
- mIsCapturingImage = true;
- mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
- @Override
- public void onPreviewFrame(final byte[] data, Camera camera) {
- mCameraCallbacks.onShutter(true);
-
- // Got to rotate the preview frame, since byte[] data here does not include
- // EXIF tags automatically set by camera. So either we add EXIF, or we rotate.
- // Adding EXIF to a byte array, unfortunately, is hard.
- final int sensorToOutput = computeSensorToOutputOffset();
- final int sensorToView = computeSensorToViewOffset();
- final boolean outputMatchesView = (sensorToOutput + sensorToView + 180) % 180 == 0;
- final boolean outputFlip = mFacing == Facing.FRONT;
- final boolean flip = sensorToOutput % 180 != 0;
- final int preWidth = mPreviewSize.getWidth();
- final int preHeight = mPreviewSize.getHeight();
- final int postWidth = flip ? preHeight : preWidth;
- final int postHeight = flip ? preWidth : preHeight;
- final int format = mPreviewFormat;
- WorkerHandler.run(new Runnable() {
- @Override
- public void run() {
-
- LOG.v("captureSnapshot: rotating.");
- byte[] rotatedData = RotationHelper.rotate(data, preWidth, preHeight, sensorToOutput);
- LOG.v("captureSnapshot: rotated.");
- YuvImage yuv = new YuvImage(rotatedData, format, postWidth, postHeight, null);
- mCameraCallbacks.processSnapshot(yuv, outputMatchesView, outputFlip);
- mIsCapturingImage = false;
- }
- });
-
- // It seems that the buffers are already cleared here, so we need to allocate again.
- mCamera.setPreviewCallbackWithBuffer(null); // Release anything left
- mCamera.setPreviewCallbackWithBuffer(Camera1.this); // Add ourselves
- mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize);
- }
- });
- }
- });
- }
-
- @Override
- public void onPreviewFrame(byte[] data, Camera camera) {
- Frame frame = mFrameManager.getFrame(data,
- System.currentTimeMillis(),
- computeSensorToOutputOffset(),
- mPreviewSize,
- mPreviewFormat);
- mCameraCallbacks.dispatchFrame(frame);
- }
-
- private boolean isCameraAvailable() {
- switch (mState) {
- // If we are stopped, don't.
- case STATE_STOPPED:
- return false;
- // If we are going to be closed, don't act on camera.
- // Even if mCamera != null, it might have been released.
- case STATE_STOPPING:
- return false;
- // If we are started, mCamera should never be null.
- case STATE_STARTED:
- return true;
- // If we are starting, theoretically we could act.
- // Just check that camera is available.
- case STATE_STARTING:
- return mCamera != null;
- }
- return false;
- }
-
- // -----------------
- // Video recording stuff.
-
-
- @Override
- void startVideo(@NonNull final File videoFile) {
- schedule(mStartVideoTask, true, new Runnable() {
- @Override
- public void run() {
- if (mIsCapturingVideo) return;
- if (mSessionType == SessionType.VIDEO) {
- mVideoFile = videoFile;
- mIsCapturingVideo = true;
- initMediaRecorder();
- try {
- mMediaRecorder.prepare();
- mMediaRecorder.start();
- } catch (Exception e) {
- LOG.e("Error while starting MediaRecorder. Swallowing.", e);
- mVideoFile = null;
- mCamera.lock();
- endVideoImmediately();
- }
- } else {
- throw new IllegalStateException("Can't record video while session type is picture");
- }
- }
- });
- }
-
- @Override
- void endVideo() {
- schedule(null, false, new Runnable() {
- @Override
- public void run() {
- endVideoImmediately();
- }
- });
- }
-
- @WorkerThread
- private void endVideoImmediately() {
- LOG.i("endVideoImmediately:", "is capturing:", mIsCapturingVideo);
- mIsCapturingVideo = false;
- if (mMediaRecorder != null) {
- try {
- mMediaRecorder.stop();
- } catch (Exception e) {
- // This can happen if endVideo() is called right after startVideo(). We don't care.
- LOG.w("endVideoImmediately:", "Error while closing media recorder. Swallowing", e);
- }
- mMediaRecorder.release();
- mMediaRecorder = null;
- }
- if (mVideoFile != null) {
- mCameraCallbacks.dispatchOnVideoTaken(mVideoFile);
- mVideoFile = null;
- }
- if (mCamera != null) {
- // This is needed to restore FrameProcessor. No re-allocation needed though.
- mCamera.setPreviewCallbackWithBuffer(this);
- }
- }
-
- @WorkerThread
- private void initMediaRecorder() {
- mMediaRecorder = new MediaRecorder();
- mCamera.unlock();
- mMediaRecorder.setCamera(mCamera);
-
- mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
- if (mAudio == Audio.ON) {
- // Must be called before setOutputFormat.
- mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
- }
- CamcorderProfile profile = getCamcorderProfile();
- mMediaRecorder.setOutputFormat(profile.fileFormat);
- mMediaRecorder.setVideoFrameRate(profile.videoFrameRate);
- mMediaRecorder.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight);
- if (mVideoCodec == VideoCodec.DEFAULT) {
- mMediaRecorder.setVideoEncoder(profile.videoCodec);
- } else {
- mMediaRecorder.setVideoEncoder(mMapper.map(mVideoCodec));
- }
- mMediaRecorder.setVideoEncodingBitRate(profile.videoBitRate);
- if (mAudio == Audio.ON) {
- mMediaRecorder.setAudioChannels(profile.audioChannels);
- mMediaRecorder.setAudioSamplingRate(profile.audioSampleRate);
- mMediaRecorder.setAudioEncoder(profile.audioCodec);
- mMediaRecorder.setAudioEncodingBitRate(profile.audioBitRate);
- }
-
- if (mLocation != null) {
- mMediaRecorder.setLocation(
- (float) mLocation.getLatitude(),
- (float) mLocation.getLongitude());
- }
-
- mMediaRecorder.setOutputFile(mVideoFile.getAbsolutePath());
- mMediaRecorder.setOrientationHint(computeSensorToOutputOffset());
-
- mMediaRecorder.setMaxFileSize(mVideoMaxSize);
- mMediaRecorder.setMaxDuration(mVideoMaxDuration);
-
- mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() {
- @Override
- public void onInfo(MediaRecorder mediaRecorder, int what, int extra) {
- switch (what) {
- case MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED:
- case MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED:
- endVideoImmediately();
- break;
- }
- }
- });
- // Not needed. mMediaRecorder.setPreviewDisplay(mPreview.getSurface());
- }
-
- // -----------------
- // Zoom and simpler stuff.
-
-
- @Override
- void setZoom(final float zoom, final PointF[] points, final boolean notify) {
- schedule(mZoomTask, true, new Runnable() {
- @Override
- public void run() {
- if (!mCameraOptions.isZoomSupported()) return;
-
- mZoomValue = zoom;
- Camera.Parameters params = mCamera.getParameters();
- float max = params.getMaxZoom();
- params.setZoom((int) (zoom * max));
- mCamera.setParameters(params);
-
- if (notify) {
- mCameraCallbacks.dispatchOnZoomChanged(zoom, points);
- }
- }
- });
- }
-
- @Override
- void setExposureCorrection(final float EVvalue, final float[] bounds,
- final PointF[] points, final boolean notify) {
- schedule(mExposureCorrectionTask, true, new Runnable() {
- @Override
- public void run() {
- if (!mCameraOptions.isExposureCorrectionSupported()) return;
-
- float value = EVvalue;
- float max = mCameraOptions.getExposureCorrectionMaxValue();
- float min = mCameraOptions.getExposureCorrectionMinValue();
- value = value < min ? min : value > max ? max : value; // cap
- mExposureCorrectionValue = value;
- Camera.Parameters params = mCamera.getParameters();
- int indexValue = (int) (value / params.getExposureCompensationStep());
- params.setExposureCompensation(indexValue);
- mCamera.setParameters(params);
-
- if (notify) {
- mCameraCallbacks.dispatchOnExposureCorrectionChanged(value, bounds, points);
- }
- }
- });
- }
-
- // -----------------
- // Tap to focus stuff.
-
-
- @Override
- void startAutoFocus(@Nullable final Gesture gesture, final PointF point) {
- // Must get width and height from the UI thread.
- int viewWidth = 0, viewHeight = 0;
- if (mPreview != null && mPreview.isReady()) {
- viewWidth = mPreview.getView().getWidth();
- viewHeight = mPreview.getView().getHeight();
- }
- final int viewWidthF = viewWidth;
- final int viewHeightF = viewHeight;
- // Schedule.
- schedule(null, true, new Runnable() {
- @Override
- public void run() {
- if (!mCameraOptions.isAutoFocusSupported()) return;
- final PointF p = new PointF(point.x, point.y); // copy.
- List meteringAreas2 = computeMeteringAreas(p.x, p.y,
- viewWidthF, viewHeightF, computeSensorToViewOffset());
- List meteringAreas1 = meteringAreas2.subList(0, 1);
-
- // At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent().
- Camera.Parameters params = mCamera.getParameters();
- int maxAF = params.getMaxNumFocusAreas();
- int maxAE = params.getMaxNumMeteringAreas();
- if (maxAF > 0) params.setFocusAreas(maxAF > 1 ? meteringAreas2 : meteringAreas1);
- if (maxAE > 0) params.setMeteringAreas(maxAE > 1 ? meteringAreas2 : meteringAreas1);
- params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
- mCamera.setParameters(params);
- mCameraCallbacks.dispatchOnFocusStart(gesture, p);
- // TODO this is not guaranteed to be called... Fix.
- try {
- mCamera.autoFocus(new Camera.AutoFocusCallback() {
- @Override
- public void onAutoFocus(boolean success, Camera camera) {
- // TODO lock auto exposure and white balance for a while
- mCameraCallbacks.dispatchOnFocusEnd(gesture, success, p);
- mHandler.get().removeCallbacks(mPostFocusResetRunnable);
- mHandler.get().postDelayed(mPostFocusResetRunnable, mPostFocusResetDelay);
- }
- });
- } catch (RuntimeException e) {
- // Handling random auto-focus exception on some devices
- // See https://github.com/natario1/CameraView/issues/181
- LOG.e("startAutoFocus:", "Error calling autoFocus", e);
- mCameraCallbacks.dispatchOnFocusEnd(gesture, false, p);
- }
- }
- });
- }
-
-
- @WorkerThread
- private static List computeMeteringAreas(double viewClickX, double viewClickY,
- int viewWidth, int viewHeight,
- int sensorToDisplay) {
- // Event came in view coordinates. We must rotate to sensor coordinates.
- // First, rescale to the -1000 ... 1000 range.
- int displayToSensor = -sensorToDisplay;
- viewClickX = -1000d + (viewClickX / (double) viewWidth) * 2000d;
- viewClickY = -1000d + (viewClickY / (double) viewHeight) * 2000d;
-
- // Apply rotation to this point.
- // https://academo.org/demos/rotation-about-point/
- double theta = ((double) displayToSensor) * Math.PI / 180;
- double sensorClickX = viewClickX * Math.cos(theta) - viewClickY * Math.sin(theta);
- double sensorClickY = viewClickX * Math.sin(theta) + viewClickY * Math.cos(theta);
- LOG.i("focus:", "viewClickX:", viewClickX, "viewClickY:", viewClickY);
- LOG.i("focus:", "sensorClickX:", sensorClickX, "sensorClickY:", sensorClickY);
-
- // Compute the rect bounds.
- Rect rect1 = computeMeteringArea(sensorClickX, sensorClickY, 150d);
- int weight1 = 1000; // 150 * 150 * 1000 = more than 10.000.000
- Rect rect2 = computeMeteringArea(sensorClickX, sensorClickY, 300d);
- int weight2 = 100; // 300 * 300 * 100 = 9.000.000
-
- List list = new ArrayList<>(2);
- list.add(new Camera.Area(rect1, weight1));
- list.add(new Camera.Area(rect2, weight2));
- return list;
- }
-
-
- private static Rect computeMeteringArea(double centerX, double centerY, double size) {
- double delta = size / 2d;
- int top = (int) Math.max(centerY - delta, -1000);
- int bottom = (int) Math.min(centerY + delta, 1000);
- int left = (int) Math.max(centerX - delta, -1000);
- int right = (int) Math.min(centerX + delta, 1000);
- LOG.i("focus:", "computeMeteringArea:", "top:", top, "left:", left, "bottom:", bottom, "right:", right);
- return new Rect(left, top, right, bottom);
- }
-
-
- // -----------------
- // Size stuff.
-
-
- @Nullable
- private List sizesFromList(List sizes) {
- if (sizes == null) return null;
- List result = new ArrayList<>(sizes.size());
- for (Camera.Size size : sizes) {
- Size add = new Size(size.width, size.height);
- if (!result.contains(add)) result.add(add);
- }
- LOG.i("size:", "sizesFromList:", result);
- return result;
- }
-
- @Override
- void setPlaySounds(boolean playSounds) {
- final boolean old = mPlaySounds;
- mPlaySounds = playSounds;
- schedule(mPlaySoundsTask, true, new Runnable() {
- @Override
- public void run() {
- mergePlaySound(old);
- }
- });
- }
-
- // -----------------
- // Additional helper info
-}
-
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java
deleted file mode 100644
index 396fe150..00000000
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package com.otaliastudios.cameraview;
-
-import android.annotation.TargetApi;
-import android.graphics.PointF;
-import android.location.Location;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.io.File;
-
-@TargetApi(21)
-class Camera2 extends CameraController {
-
- public Camera2(CameraView.CameraCallbacks callback) {
- super(callback);
- }
-
- @Override
- public void onSurfaceAvailable() {
-
- }
-
- @Override
- public void onSurfaceChanged() {
-
- }
-
- @Override
- void onStart() {
-
- }
-
- @Override
- void onStop() {
-
- }
-
- @Override
- void setSessionType(SessionType sessionType) {
-
- }
-
- @Override
- void setFacing(Facing facing) {
-
- }
-
- @Override
- void setZoom(float zoom, PointF[] points, boolean notify) {
-
- }
-
- @Override
- void setExposureCorrection(float EVvalue, float[] bounds, PointF[] points, boolean notify) {
-
- }
-
- @Override
- void setFlash(Flash flash) {
-
- }
-
- @Override
- void setWhiteBalance(WhiteBalance whiteBalance) {
-
- }
-
- @Override
- void setHdr(Hdr hdr) {
-
- }
-
- @Override
- void setAudio(Audio audio) {
-
- }
-
- @Override
- void setLocation(Location location) {
-
- }
-
- @Override
- void setVideoQuality(VideoQuality videoQuality) {
-
- }
-
- @Override
- void capturePicture() {
-
- }
-
- @Override
- void captureSnapshot() {
-
- }
-
- @Override
- void startVideo(@NonNull File file) {
-
- }
-
- @Override
- void endVideo() {
-
- }
-
- @Override
- void startAutoFocus(@Nullable Gesture gesture, PointF point) {
-
- }
-
- @Override
- public void onBufferAvailable(byte[] buffer) {
-
- }
-
- @Override
- void setPlaySounds(boolean playSounds) {
-
- }
-}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
deleted file mode 100644
index 3fb41e8a..00000000
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
+++ /dev/null
@@ -1,573 +0,0 @@
-package com.otaliastudios.cameraview;
-
-import android.graphics.PointF;
-import android.location.Location;
-
-
-import android.media.CamcorderProfile;
-import android.media.MediaRecorder;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-
-abstract class CameraController implements
- CameraPreview.SurfaceCallback,
- FrameManager.BufferCallback,
- Thread.UncaughtExceptionHandler {
-
- private static final String TAG = CameraController.class.getSimpleName();
- private static final CameraLogger LOG = CameraLogger.create(TAG);
-
- static final int STATE_STOPPING = -1; // Camera is about to be stopped.
- static final int STATE_STOPPED = 0; // Camera is stopped.
- static final int STATE_STARTING = 1; // Camera is about to start.
- static final int STATE_STARTED = 2; // Camera is available and we can set parameters.
-
- protected final CameraView.CameraCallbacks mCameraCallbacks;
- protected CameraPreview mPreview;
- protected WorkerHandler mHandler;
- /* for tests */ Handler mCrashHandler;
-
- protected Facing mFacing;
- protected Flash mFlash;
- protected WhiteBalance mWhiteBalance;
- protected VideoQuality mVideoQuality;
- protected VideoCodec mVideoCodec;
- protected SessionType mSessionType;
- protected Hdr mHdr;
- protected Location mLocation;
- protected Audio mAudio;
- protected float mZoomValue;
- protected float mExposureCorrectionValue;
- protected boolean mPlaySounds;
-
- protected int mCameraId;
- protected ExtraProperties mExtraProperties;
- protected CameraOptions mCameraOptions;
- protected Mapper mMapper;
- protected FrameManager mFrameManager;
- protected SizeSelector mPictureSizeSelector;
- protected MediaRecorder mMediaRecorder;
- protected File mVideoFile;
- protected long mVideoMaxSize;
- protected int mVideoMaxDuration;
- protected Size mPictureSize;
- protected Size mPreviewSize;
- protected int mPreviewFormat;
-
- protected int mSensorOffset;
- private int mDisplayOffset;
- private int mDeviceOrientation;
-
- protected boolean mIsCapturingImage = false;
- protected boolean mIsCapturingVideo = false;
-
- protected int mState = STATE_STOPPED;
-
- // Used for testing.
- Task mZoomTask = new Task<>();
- Task mExposureCorrectionTask = new Task<>();
- Task mFlashTask = new Task<>();
- Task mWhiteBalanceTask = new Task<>();
- Task mHdrTask = new Task<>();
- Task mLocationTask = new Task<>();
- Task mVideoQualityTask = new Task<>();
- Task mStartVideoTask = new Task<>();
- Task mPlaySoundsTask = new Task<>();
-
- CameraController(CameraView.CameraCallbacks callback) {
- mCameraCallbacks = callback;
- mCrashHandler = new Handler(Looper.getMainLooper());
- mHandler = WorkerHandler.get("CameraViewController");
- mHandler.getThread().setUncaughtExceptionHandler(this);
- mFrameManager = new FrameManager(2, this);
- }
-
- void setPreview(CameraPreview cameraPreview) {
- mPreview = cameraPreview;
- mPreview.setSurfaceCallback(this);
- }
-
- //region Error handling
-
- private static class NoOpExceptionHandler implements Thread.UncaughtExceptionHandler {
- @Override
- public void uncaughtException(Thread t, Throwable e) {
- // No-op.
- }
- }
-
- @Override
- public void uncaughtException(final Thread thread, final Throwable throwable) {
- // Something went wrong. Thread is terminated (about to?).
- // Move to other thread and release resources.
- if (!(throwable instanceof CameraException)) {
- // This is unexpected, either a bug or something the developer should know.
- // Release and crash the UI thread so we get bug reports.
- LOG.e("uncaughtException:", "Unexpected exception:", throwable);
- destroy();
- mCrashHandler.post(new Runnable() {
- @Override
- public void run() {
- RuntimeException exception;
- if (throwable instanceof RuntimeException) {
- exception = (RuntimeException) throwable;
- } else {
- exception = new RuntimeException(throwable);
- }
- throw exception;
- }
- });
- } else {
- // At the moment all CameraExceptions are unrecoverable, there was something
- // wrong when starting, stopping, or binding the camera to the preview.
- final CameraException error = (CameraException) throwable;
- LOG.e("uncaughtException:", "Interrupting thread with state:", ss(), "due to CameraException:", error);
- thread.interrupt();
- mHandler = WorkerHandler.get("CameraViewController");
- mHandler.getThread().setUncaughtExceptionHandler(this);
- LOG.i("uncaughtException:", "Calling stopImmediately and notifying.");
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- stopImmediately();
- mCameraCallbacks.dispatchError(error);
- }
- });
- }
- }
-
- final void destroy() {
- LOG.i("destroy:", "state:", ss());
- // Prevent CameraController leaks. Don't set to null, or exceptions
- // inside the standard stop() method might crash the main thread.
- mHandler.getThread().setUncaughtExceptionHandler(new NoOpExceptionHandler());
- // Stop if needed.
- stopImmediately();
- }
-
- //endregion
-
- //region Start&Stop
-
- private String ss() {
- switch (mState) {
- case STATE_STOPPING: return "STATE_STOPPING";
- case STATE_STOPPED: return "STATE_STOPPED";
- case STATE_STARTING: return "STATE_STARTING";
- case STATE_STARTED: return "STATE_STARTED";
- }
- return "null";
- }
-
- // Starts the preview asynchronously.
- final void start() {
- LOG.i("Start:", "posting runnable. State:", ss());
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- LOG.i("Start:", "executing. State:", ss());
- if (mState >= STATE_STARTING) return;
- mState = STATE_STARTING;
- LOG.i("Start:", "about to call onStart()", ss());
- onStart();
- LOG.i("Start:", "returned from onStart().", "Dispatching.", ss());
- mState = STATE_STARTED;
- mCameraCallbacks.dispatchOnCameraOpened(mCameraOptions);
- }
- });
- }
-
- // Stops the preview asynchronously.
- final void stop() {
- LOG.i("Stop:", "posting runnable. State:", ss());
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- LOG.i("Stop:", "executing. State:", ss());
- if (mState <= STATE_STOPPED) return;
- mState = STATE_STOPPING;
- LOG.i("Stop:", "about to call onStop()");
- onStop();
- LOG.i("Stop:", "returned from onStop().", "Dispatching.");
- mState = STATE_STOPPED;
- mCameraCallbacks.dispatchOnCameraClosed();
- }
- });
- }
-
- // Stops the preview synchronously, ensuring no exceptions are thrown.
- final void stopImmediately() {
- try {
- // Don't check, try stop again.
- LOG.i("stopImmediately:", "State was:", ss());
- if (mState == STATE_STOPPED) return;
- mState = STATE_STOPPING;
- onStop();
- mState = STATE_STOPPED;
- LOG.i("stopImmediately:", "Stopped. State is:", ss());
- } catch (Exception e) {
- // Do nothing.
- LOG.i("stopImmediately:", "Swallowing exception while stopping.", e);
- mState = STATE_STOPPED;
- }
- }
-
- // Forces a restart.
- protected final void restart() {
- LOG.i("Restart:", "posting runnable");
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- LOG.i("Restart:", "executing. Needs stopping:", mState > STATE_STOPPED, ss());
- // Don't stop if stopped.
- if (mState > STATE_STOPPED) {
- mState = STATE_STOPPING;
- onStop();
- mState = STATE_STOPPED;
- LOG.i("Restart:", "stopped. Dispatching.", ss());
- mCameraCallbacks.dispatchOnCameraClosed();
- }
-
- LOG.i("Restart: about to start. State:", ss());
- mState = STATE_STARTING;
- onStart();
- mState = STATE_STARTED;
- LOG.i("Restart: returned from start. Dispatching. State:", ss());
- mCameraCallbacks.dispatchOnCameraOpened(mCameraOptions);
- }
- });
- }
-
- // Starts the preview.
- // At the end of this method camera must be available, e.g. for setting parameters.
- @WorkerThread
- abstract void onStart();
-
- // Stops the preview.
- @WorkerThread
- abstract void onStop();
-
- // Returns current state.
- final int getState() {
- return mState;
- }
-
- //endregion
-
- //region Simple setters
-
- // This is called before start() and never again.
- final void setDisplayOffset(int displayOffset) {
- mDisplayOffset = displayOffset;
- }
-
- // This can be called multiple times.
- final void setDeviceOrientation(int deviceOrientation) {
- mDeviceOrientation = deviceOrientation;
- }
-
- final void setPictureSizeSelector(SizeSelector selector) {
- mPictureSizeSelector = selector;
- }
-
- final void setVideoMaxSize(long videoMaxSizeBytes) {
- mVideoMaxSize = videoMaxSizeBytes;
- }
-
- final void setVideoMaxDuration(int videoMaxDurationMillis) {
- mVideoMaxDuration = videoMaxDurationMillis;
- }
-
- final void setVideoCodec(VideoCodec codec) {
- mVideoCodec = codec;
- }
-
-
- //endregion
-
- //region Abstract setters and APIs
-
- // Should restart the session if active.
- abstract void setSessionType(SessionType sessionType);
-
- // Should restart the session if active.
- abstract void setFacing(Facing facing);
-
- // If closed, no-op. If opened, check supported and apply.
- abstract void setZoom(float zoom, PointF[] points, boolean notify);
-
- // If closed, no-op. If opened, check supported and apply.
- abstract void setExposureCorrection(float EVvalue, float[] bounds, PointF[] points, boolean notify);
-
- // If closed, keep. If opened, check supported and apply.
- abstract void setFlash(Flash flash);
-
- // If closed, keep. If opened, check supported and apply.
- abstract void setWhiteBalance(WhiteBalance whiteBalance);
-
- // If closed, keep. If opened, check supported and apply.
- abstract void setHdr(Hdr hdr);
-
- // If closed, keep. If opened, check supported and apply.
- abstract void setLocation(Location location);
-
- // Just set.
- abstract void setAudio(Audio audio);
-
- // Throw if capturing. If in video session, recompute capture size, and, if needed, preview size.
- abstract void setVideoQuality(VideoQuality videoQuality);
-
- abstract void capturePicture();
-
- abstract void captureSnapshot();
-
- abstract void startVideo(@NonNull File file);
-
- abstract void endVideo();
-
- abstract void startAutoFocus(@Nullable Gesture gesture, PointF point);
-
- abstract void setPlaySounds(boolean playSounds);
-
- //endregion
-
- //region final getters
-
- @Nullable
- final ExtraProperties getExtraProperties() {
- return mExtraProperties;
- }
-
- @Nullable
- final CameraOptions getCameraOptions() {
- return mCameraOptions;
- }
-
- final Facing getFacing() {
- return mFacing;
- }
-
- final Flash getFlash() {
- return mFlash;
- }
-
- final WhiteBalance getWhiteBalance() {
- return mWhiteBalance;
- }
-
- final VideoQuality getVideoQuality() {
- return mVideoQuality;
- }
-
- final VideoCodec getVideoCodec() {
- return mVideoCodec;
- }
-
- final long getVideoMaxSize() {
- return mVideoMaxSize;
- }
-
- final int getVideoMaxDuration() {
- return mVideoMaxDuration;
- }
-
- final SessionType getSessionType() {
- return mSessionType;
- }
-
- final Hdr getHdr() {
- return mHdr;
- }
-
- final Location getLocation() {
- return mLocation;
- }
-
- final Audio getAudio() {
- return mAudio;
- }
-
- final SizeSelector getPictureSizeSelector() {
- return mPictureSizeSelector;
- }
-
- final Size getPictureSize() {
- return mPictureSize;
- }
-
- final float getZoomValue() {
- return mZoomValue;
- }
-
- final float getExposureCorrectionValue() {
- return mExposureCorrectionValue;
- }
-
- final Size getPreviewSize() {
- return mPreviewSize;
- }
-
- final boolean isCapturingVideo() {
- return mIsCapturingVideo;
- }
-
- //endregion
-
- //region Orientation utils
-
- /**
- * The result of this should not change after start() is called: the sensor offset is the same,
- * and the display offset does not change.
- */
- final boolean shouldFlipSizes() {
- int offset = computeSensorToViewOffset();
- LOG.i("shouldFlipSizes:", "displayOffset=", mDisplayOffset, "sensorOffset=", mSensorOffset);
- LOG.i("shouldFlipSizes:", "sensorToDisplay=", offset);
- return offset % 180 != 0;
- }
-
- /**
- * Returns how much should the sensor image be rotated before being shown.
- * It is meant to be fed to Camera.setDisplayOrientation().
- * The result of this should not change after start() is called: the sensor offset is the same,
- * and the display offset does not change.
- */
- protected final int computeSensorToViewOffset() {
- if (mFacing == Facing.FRONT) {
- // Here we had ((mSensorOffset - mDisplayOffset) + 360 + 180) % 360
- // And it seemed to give the same results for various combinations, but not for all (e.g. 0 - 270).
- return (360 - ((mSensorOffset + mDisplayOffset) % 360)) % 360;
- } else {
- return (mSensorOffset - mDisplayOffset + 360) % 360;
- }
- }
-
- /**
- * Returns the orientation to be set as a exif tag.
- */
- protected final int computeSensorToOutputOffset() {
- if (mFacing == Facing.FRONT) {
- return (mSensorOffset - mDeviceOrientation + 360) % 360;
- } else {
- return (mSensorOffset + mDeviceOrientation) % 360;
- }
- }
-
- //endregion
-
- //region Size utils
-
- /**
- * This is called either on cameraView.start(), or when the underlying surface changes.
- * It is possible that in the first call the preview surface has not already computed its
- * dimensions.
- * But when it does, the {@link CameraPreview.SurfaceCallback} should be called,
- * and this should be refreshed.
- */
- protected final Size computePictureSize() {
- // The external selector is expecting stuff in the view world, not in the sensor world.
- // Use the list in the camera options, then flip the result if needed.
- boolean flip = shouldFlipSizes();
- SizeSelector selector;
-
- if (mSessionType == SessionType.PICTURE) {
- selector = SizeSelectors.or(mPictureSizeSelector, SizeSelectors.biggest());
- } else {
- // The Camcorder internally checks for cameraParameters.getSupportedVideoSizes() etc.
- // And we want the picture size to be the biggest picture consistent with the video aspect ratio.
- // -> Use the external picture selector, but enforce the ratio constraint.
- CamcorderProfile profile = getCamcorderProfile();
- AspectRatio targetRatio = AspectRatio.of(profile.videoFrameWidth, profile.videoFrameHeight);
- if (flip) targetRatio = targetRatio.inverse();
- LOG.i("size:", "computeCaptureSize:", "videoQuality:", mVideoQuality, "targetRatio:", targetRatio);
- SizeSelector matchRatio = SizeSelectors.aspectRatio(targetRatio, 0);
- selector = SizeSelectors.or(
- SizeSelectors.and(matchRatio, mPictureSizeSelector),
- SizeSelectors.and(matchRatio),
- mPictureSizeSelector
- );
- }
-
- List list = new ArrayList<>(mCameraOptions.getSupportedPictureSizes());
- Size result = selector.select(list).get(0);
- LOG.i("computePictureSize:", "result:", result, "flip:", flip);
- if (flip) result = result.flip();
- return result;
- }
-
- protected final Size computePreviewSize(List previewSizes) {
- // instead of flipping everything to the view world, we can just flip the
- // surface size to the sensor world
- boolean flip = shouldFlipSizes();
- AspectRatio targetRatio = AspectRatio.of(mPictureSize.getWidth(), mPictureSize.getHeight());
- Size targetMinSize = mPreview.getSurfaceSize();
- if (flip) targetMinSize = targetMinSize.flip();
- LOG.i("size:", "computePreviewSize:", "targetRatio:", targetRatio, "targetMinSize:", targetMinSize);
- SizeSelector matchRatio = SizeSelectors.aspectRatio(targetRatio, 0);
- SizeSelector matchSize = SizeSelectors.and(
- SizeSelectors.minHeight(targetMinSize.getHeight()),
- SizeSelectors.minWidth(targetMinSize.getWidth()));
- SizeSelector matchAll = SizeSelectors.or(
- SizeSelectors.and(matchRatio, matchSize),
- SizeSelectors.and(matchRatio, SizeSelectors.biggest()), // If couldn't match both, match ratio and biggest.
- SizeSelectors.biggest() // If couldn't match any, take the biggest.
- );
- Size result = matchAll.select(previewSizes).get(0);
- LOG.i("computePreviewSize:", "result:", result, "flip:", flip);
- return result;
- }
-
- @NonNull
- protected final CamcorderProfile getCamcorderProfile() {
- switch (mVideoQuality) {
- case HIGHEST:
- return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_HIGH);
-
- case MAX_2160P:
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
- CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_2160P)) {
- return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_2160P);
- }
- // Don't break.
-
- case MAX_1080P:
- if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_1080P)) {
- return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_1080P);
- }
- // Don't break.
-
- case MAX_720P:
- if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_720P)) {
- return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_720P);
- }
- // Don't break.
-
- case MAX_480P:
- if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_480P)) {
- return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_480P);
- }
- // Don't break.
-
- case MAX_QVGA:
- if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_QVGA)) {
- return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_QVGA);
- }
- // Don't break.
-
- case LOWEST:
- default:
- // Fallback to lowest.
- return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_LOW);
- }
- }
-
- //endregion
-}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java
index 62aa6cca..023ec2b4 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraException.java
@@ -1,6 +1,8 @@
package com.otaliastudios.cameraview;
+import com.otaliastudios.cameraview.controls.Facing;
+
/**
* Holds an error with the camera configuration.
*/
@@ -30,18 +32,60 @@ public class CameraException extends RuntimeException {
*/
public static final int REASON_DISCONNECTED = 3;
+ /**
+ * Could not take a picture or a picture snapshot,
+ * for some not specified reason.
+ */
+ public static final int REASON_PICTURE_FAILED = 4;
+
+ /**
+ * Could not take a video or a video snapshot,
+ * for some not specified reason.
+ */
+ public static final int REASON_VIDEO_FAILED = 5;
+
+ /**
+ * Indicates that we could not find a camera for the current {@link Facing}
+ * value.
+ * This can be solved by changing the facing value and starting again.
+ */
+ public static final int REASON_NO_CAMERA = 6;
+
private int reason = REASON_UNKNOWN;
- CameraException(Throwable cause) {
+ @SuppressWarnings("WeakerAccess")
+ public CameraException(Throwable cause) {
super(cause);
}
- CameraException(Throwable cause, int reason) {
+ public CameraException(Throwable cause, int reason) {
super(cause);
this.reason = reason;
}
+ public CameraException(int reason) {
+ super();
+ this.reason = reason;
+ }
+
public int getReason() {
return reason;
}
+
+ /**
+ * Whether this error is unrecoverable. If this function returns true,
+ * the Camera has been closed (or will be soon) and it is likely showing a black preview.
+ * This is the right moment to show an error dialog to the user.
+ *
+ * @return true if this error is unrecoverable
+ */
+ @SuppressWarnings("unused")
+ public boolean isUnrecoverable() {
+ switch (getReason()) {
+ case REASON_FAILED_TO_CONNECT: return true;
+ case REASON_FAILED_TO_START_PREVIEW: return true;
+ case REASON_DISCONNECTED: return true;
+ default: return false;
+ }
+ }
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
index 16927500..34161eea 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
@@ -1,11 +1,16 @@
package com.otaliastudios.cameraview;
import android.graphics.PointF;
-import android.support.annotation.NonNull;
-import android.support.annotation.UiThread;
-
-import java.io.File;
-
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+/**
+ * The base class for receiving updates from a {@link CameraView} instance.
+ * You can add and remove listeners using {@link CameraView#addCameraListener(CameraListener)}
+ * and {@link CameraView#removeCameraListener(CameraListener)}.
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class CameraListener {
@@ -16,68 +21,50 @@ public abstract class CameraListener {
* @param options camera supported options
*/
@UiThread
- public void onCameraOpened(CameraOptions options) {
-
- }
+ public void onCameraOpened(@NonNull CameraOptions options) { }
/**
* Notifies that the camera session was closed.
*/
@UiThread
- public void onCameraClosed() {
-
- }
+ public void onCameraClosed() { }
/**
* Notifies about an error during the camera setup or configuration.
- * At the moment, errors that are passed here are unrecoverable. When this is called,
- * the camera has been released and is presumably showing a black preview.
- *
- * This is the right moment to show an error dialog to the user.
- * You can try calling start() again, but that is not guaranteed to work - if it doesn't,
- * this callback will be invoked again.
*
- * In the future, more information will be passed through the {@link CameraException} instance.
+ * At this point you should inspect the {@link CameraException} reason using
+ * {@link CameraException#getReason()} and see what should be done, if anything.
+ * If the error is unrecoverable, this is the right moment to show an error dialog, for example.
*
* @param exception the error
*/
@UiThread
- public void onCameraError(@NonNull CameraException exception) {
-
- }
+ public void onCameraError(@NonNull CameraException exception) { }
/**
- * Notifies that a picture previously captured with {@link CameraView#capturePicture()}
- * or {@link CameraView#captureSnapshot()} is ready to be shown or saved.
+ * Notifies that a picture previously captured with {@link CameraView#takePicture()}
+ * or {@link CameraView#takePictureSnapshot()} is ready to be shown or saved to file.
*
- * If planning to get a bitmap, you can use {@link CameraUtils#decodeBitmap(byte[], CameraUtils.BitmapCallback)}
- * to decode the byte array taking care about orientation.
+ * If planning to show a bitmap, you can use
+ * {@link PictureResult#toBitmap(int, int, BitmapCallback)} to decode the byte array
+ * taking care about orientation and threading.
*
- * @param jpeg captured picture
+ * @param result captured picture
*/
@UiThread
- public void onPictureTaken(byte[] jpeg) {
- // TODO v2: use a PictureResult.
- }
+ public void onPictureTaken(@NonNull PictureResult result) { }
/**
- * Notifies that a video capture has just ended. The file parameter is the one that
- * was passed to {@link CameraView#startCapturingVideo(File)}, if any.
- * If not, the camera fallsback to:
- *
- * new File(getContext().getExternalFilesDir(null), "video.mp4");
- *
- *
- * @param video file hosting the mp4 video
+ * Notifies that a video capture has just ended.
+ *
+ * @param result the video result
*/
@UiThread
- public void onVideoTaken(File video) {
- // TODO v2: use a VideoResult.
- }
+ public void onVideoTaken(@NonNull VideoResult result) { }
/**
@@ -91,51 +78,46 @@ public abstract class CameraListener {
* @param orientation either 0, 90, 180 or 270
*/
@UiThread
- public void onOrientationChanged(int orientation) {
-
- }
+ public void onOrientationChanged(int orientation) { }
/**
- * Notifies that user interacted with the screen and started focus with a gesture,
- * and the autofocus is trying to focus around that area. This can be used to draw things on screen.
+ * Notifies that user interacted with the screen and started metering with a gesture,
+ * and touch metering routine is trying to focus around that area.
+ * This callback can be used to draw things on screen.
* Can also be triggered by {@link CameraView#startAutoFocus(float, float)}.
*
* @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight()
*/
@UiThread
- public void onFocusStart(PointF point) {
-
- }
+ public void onAutoFocusStart(@NonNull PointF point) { }
/**
- * Notifies that a gesture focus event just ended, and the camera converged
- * to a new focus (and possibly exposure and white balance).
+ * Notifies that a touch metering event just ended, and the camera converged
+ * to a new focus, exposure and possibly white balance.
* This might succeed or not.
* Can also be triggered by {@link CameraView#startAutoFocus(float, float)}.
*
- * @param successful whether camera succeeded
+ * @param successful whether metering succeeded
* @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight()
*/
@UiThread
- public void onFocusEnd(boolean successful, PointF point) {
-
- }
+ public void onAutoFocusEnd(boolean successful, @NonNull PointF point) { }
/**
- * Noitifies that a finger gesture just caused the camera zoom
+ * Notifies that a finger gesture just caused the camera zoom
* to be changed. This can be used to draw, for example, a seek bar.
*
* @param newValue the new zoom value
* @param bounds min and max bounds for newValue (fixed to 0 ... 1)
- * @param fingers finger positions that caused the event
+ * @param fingers finger positions that caused the event, null if not caused by touch
*/
@UiThread
- public void onZoomChanged(float newValue, float[] bounds, PointF[] fingers) {
-
- }
+ public void onZoomChanged(float newValue,
+ @NonNull float[] bounds,
+ @Nullable PointF[] fingers) { }
/**
@@ -144,11 +126,48 @@ public abstract class CameraListener {
*
* @param newValue the new correction value
* @param bounds min and max bounds for newValue, as returned by {@link CameraOptions}
- * @param fingers finger positions that caused the event
+ * @param fingers finger positions that caused the event, null if not caused by touch
*/
@UiThread
- public void onExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers) {
+ public void onExposureCorrectionChanged(float newValue,
+ @NonNull float[] bounds,
+ @Nullable PointF[] fingers) { }
+
+
+ /**
+ * Notifies that the actual video recording has started.
+ * This is the time when actual frames recording starts.
+ *
+ * This can be used to show some UI indicator for video recording or counting time.
+ *
+ * @see #onVideoRecordingEnd()
+ */
+ @UiThread
+ public void onVideoRecordingStart() {
+
+ }
+
+ /**
+ * Notifies that the actual video recording has ended.
+ * At this point recording has ended, though the file might still be processed.
+ * The {@link #onVideoTaken(VideoResult)} callback will be called soon.
+ *
+ * This can be used to remove UI indicators for video recording.
+ *
+ * @see #onVideoRecordingStart()
+ */
+ @UiThread
+ public void onVideoRecordingEnd() {
}
-}
\ No newline at end of file
+ /**
+ * Notifies that the picture capture has started. Can be used to update the UI for visual
+ * confirmation or sound effects.
+ */
+ @UiThread
+ public void onPictureShutter() {
+
+ }
+
+}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraLogger.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraLogger.java
new file mode 100644
index 00000000..e2aa80af
--- /dev/null
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraLogger.java
@@ -0,0 +1,203 @@
+package com.otaliastudios.cameraview;
+
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * Utility class that can log traces and info.
+ */
+@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
+public final class CameraLogger {
+
+ public final static int LEVEL_VERBOSE = 0;
+ public final static int LEVEL_INFO = 1;
+ public final static int LEVEL_WARNING = 2;
+ public final static int LEVEL_ERROR = 3;
+
+ /**
+ * Interface of integers representing log levels.
+ * @see #LEVEL_VERBOSE
+ * @see #LEVEL_INFO
+ * @see #LEVEL_WARNING
+ * @see #LEVEL_ERROR
+ */
+ @IntDef({LEVEL_VERBOSE, LEVEL_INFO, LEVEL_WARNING, LEVEL_ERROR})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LogLevel {}
+
+ /**
+ * A Logger can listen to internal log events
+ * and log them to different providers.
+ * The default logger will simply post to logcat.
+ */
+ public interface Logger {
+
+ /**
+ * Notifies that an internal log event was just triggered.
+ *
+ * @param level the log level
+ * @param tag the log tag
+ * @param message the log message
+ * @param throwable an optional throwable
+ */
+ void log(@LogLevel int level,
+ @NonNull String tag,
+ @NonNull String message,
+ @Nullable Throwable throwable);
+ }
+
+ @VisibleForTesting static String lastMessage;
+ @VisibleForTesting static String lastTag;
+
+ private static int sLevel;
+ private static Set sLoggers = new CopyOnWriteArraySet<>();
+
+ @VisibleForTesting static Logger sAndroidLogger = new Logger() {
+ @Override
+ public void log(int level,
+ @NonNull String tag,
+ @NonNull String message,
+ @Nullable Throwable throwable) {
+ switch (level) {
+ case LEVEL_VERBOSE: Log.v(tag, message, throwable); break;
+ case LEVEL_INFO: Log.i(tag, message, throwable); break;
+ case LEVEL_WARNING: Log.w(tag, message, throwable); break;
+ case LEVEL_ERROR: Log.e(tag, message, throwable); break;
+ }
+ }
+ };
+
+ static {
+ setLogLevel(LEVEL_ERROR);
+ sLoggers.add(sAndroidLogger);
+ }
+
+ /**
+ * Creates a CameraLogger that will stream logs into the
+ * internal logs and dispatch them to {@link Logger}s.
+ *
+ * @param tag the logger tag
+ * @return a new CameraLogger
+ */
+ public static CameraLogger create(@NonNull String tag) {
+ return new CameraLogger(tag);
+ }
+
+ /**
+ * Sets the log sLevel for logcat events.
+ *
+ * @see #LEVEL_VERBOSE
+ * @see #LEVEL_INFO
+ * @see #LEVEL_WARNING
+ * @see #LEVEL_ERROR
+ * @param logLevel the desired log sLevel
+ */
+ public static void setLogLevel(@LogLevel int logLevel) {
+ sLevel = logLevel;
+ }
+
+ /**
+ * Registers an external {@link Logger} for log events.
+ * Make sure to unregister using {@link #unregisterLogger(Logger)}.
+ *
+ * @param logger logger to add
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static void registerLogger(@NonNull Logger logger) {
+ sLoggers.add(logger);
+ }
+
+ /**
+ * Unregisters a previously registered {@link Logger} for log events.
+ * This is needed in order to avoid leaks.
+ *
+ * @param logger logger to remove
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static void unregisterLogger(@NonNull Logger logger) {
+ sLoggers.remove(logger);
+ }
+
+ @NonNull
+ private String mTag;
+
+ private CameraLogger(@NonNull String tag) {
+ mTag = tag;
+ }
+
+ private boolean should(int messageLevel) {
+ return sLevel <= messageLevel && sLoggers.size() > 0;
+ }
+
+ /**
+ * Log to the verbose channel.
+ * @param data log contents
+ * @return the log message, if logged
+ */
+ @Nullable
+ public String v(@NonNull Object... data) {
+ return log(LEVEL_VERBOSE, data);
+ }
+
+ /**
+ * Log to the info channel.
+ * @param data log contents
+ * @return the log message, if logged
+ */
+ @Nullable
+ public String i(@NonNull Object... data) {
+ return log(LEVEL_INFO, data);
+ }
+
+ /**
+ * Log to the warning channel.
+ * @param data log contents
+ * @return the log message, if logged
+ */
+ @Nullable
+ public String w(@NonNull Object... data) {
+ return log(LEVEL_WARNING, data);
+ }
+
+ /**
+ * Log to the error channel.
+ * @param data log contents
+ * @return the log message, if logged
+ */
+ @Nullable
+ public String e(@NonNull Object... data) {
+ return log(LEVEL_ERROR, data);
+ }
+
+ @Nullable
+ private String log(@LogLevel int level, @NonNull Object... data) {
+ if (!should(level)) return null;
+
+ StringBuilder message = new StringBuilder();
+ Throwable throwable = null;
+ for (Object object : data) {
+ if (object instanceof Throwable) {
+ throwable = (Throwable) object;
+ }
+ message.append(String.valueOf(object));
+ message.append(" ");
+ }
+ String string = message.toString().trim();
+ for (Logger logger : sLoggers) {
+ logger.log(level, mTag, string, throwable);
+ }
+ lastMessage = string;
+ lastTag = mTag;
+ return string;
+ }
+}
+
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
index e072eec0..6072ed87 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
@@ -1,106 +1,58 @@
package com.otaliastudios.cameraview;
-import android.annotation.TargetApi;
-import android.hardware.Camera;
-import android.hardware.camera2.CameraCharacteristics;
-import android.support.annotation.NonNull;
+import android.graphics.ImageFormat;
+
+import com.otaliastudios.cameraview.controls.Audio;
+import com.otaliastudios.cameraview.controls.AudioCodec;
+import com.otaliastudios.cameraview.controls.Control;
+import com.otaliastudios.cameraview.controls.Engine;
+import com.otaliastudios.cameraview.controls.Facing;
+import com.otaliastudios.cameraview.controls.Flash;
+import com.otaliastudios.cameraview.controls.PictureFormat;
+import com.otaliastudios.cameraview.controls.Preview;
+import com.otaliastudios.cameraview.gesture.GestureAction;
+import com.otaliastudios.cameraview.controls.Grid;
+import com.otaliastudios.cameraview.controls.Hdr;
+import com.otaliastudios.cameraview.controls.Mode;
+import com.otaliastudios.cameraview.controls.VideoCodec;
+import com.otaliastudios.cameraview.controls.WhiteBalance;
+import com.otaliastudios.cameraview.size.AspectRatio;
+import com.otaliastudios.cameraview.size.Size;
+
+import androidx.annotation.NonNull;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
-import java.util.List;
import java.util.Set;
/**
* Options telling you what is available and what is not.
*/
-public class CameraOptions {
-
- private Set supportedWhiteBalance = new HashSet<>(5);
- private Set supportedFacing = new HashSet<>(2);
- private Set supportedFlash = new HashSet<>(4);
- private Set supportedHdr = new HashSet<>(2);
- private Set supportedPictureSizes = new HashSet<>(15);
- private Set supportedPictureAspectRatio = new HashSet<>(4);
-
- private boolean zoomSupported;
- private boolean videoSnapshotSupported;
- private boolean exposureCorrectionSupported;
- private float exposureCorrectionMinValue;
- private float exposureCorrectionMaxValue;
- private boolean autoFocusSupported;
-
-
- // Camera1 constructor.
- @SuppressWarnings("deprecation")
- CameraOptions(Camera.Parameters params, boolean flipSizes) {
- List strings;
- Mapper mapper = new Mapper.Mapper1();
-
- // Facing
- Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
- for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) {
- Camera.getCameraInfo(i, cameraInfo);
- Facing value = mapper.unmapFacing(cameraInfo.facing);
- if (value != null) supportedFacing.add(value);
- }
-
- // WB
- strings = params.getSupportedWhiteBalance();
- if (strings != null) {
- for (String string : strings) {
- WhiteBalance value = mapper.unmapWhiteBalance(string);
- if (value != null) supportedWhiteBalance.add(value);
- }
- }
-
- // Flash
- strings = params.getSupportedFlashModes();
- if (strings != null) {
- for (String string : strings) {
- Flash value = mapper.unmapFlash(string);
- if (value != null) supportedFlash.add(value);
- }
- }
-
- // Hdr
- strings = params.getSupportedSceneModes();
- if (strings != null) {
- for (String string : strings) {
- Hdr value = mapper.unmapHdr(string);
- if (value != null) supportedHdr.add(value);
- }
- }
-
- zoomSupported = params.isZoomSupported();
- videoSnapshotSupported = params.isVideoSnapshotSupported();
- autoFocusSupported = params.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_AUTO);
-
- // Exposure correction
- float step = params.getExposureCompensationStep();
- exposureCorrectionMinValue = (float) params.getMinExposureCompensation() * step;
- exposureCorrectionMaxValue = (float) params.getMaxExposureCompensation() * step;
- exposureCorrectionSupported = params.getMinExposureCompensation() != 0
- || params.getMaxExposureCompensation() != 0;
-
- // Sizes
- List sizes = params.getSupportedPictureSizes();
- for (Camera.Size size : sizes) {
- int width = flipSizes ? size.height : size.width;
- int height = flipSizes ? size.width : size.height;
- supportedPictureSizes.add(new Size(width, height));
- supportedPictureAspectRatio.add(AspectRatio.of(width, height));
- }
- }
-
-
- // Camera2 constructor.
- @TargetApi(21)
- CameraOptions(CameraCharacteristics params) {}
-
+public abstract class CameraOptions {
+
+ protected Set supportedWhiteBalance = new HashSet<>(5);
+ protected Set supportedFacing = new HashSet<>(2);
+ protected Set supportedFlash = new HashSet<>(4);
+ protected Set supportedHdr = new HashSet<>(2);
+ protected Set supportedPictureSizes = new HashSet<>(15);
+ protected Set supportedVideoSizes = new HashSet<>(5);
+ protected Set supportedPictureAspectRatio = new HashSet<>(4);
+ protected Set supportedVideoAspectRatio = new HashSet<>(3);
+ protected Set supportedPictureFormats = new HashSet<>(2);
+ protected Set supportedFrameProcessingFormats = new HashSet<>(2);
+
+ protected boolean zoomSupported;
+ protected boolean exposureCorrectionSupported;
+ protected float exposureCorrectionMinValue;
+ protected float exposureCorrectionMaxValue;
+ protected boolean autoFocusSupported;
+ protected float previewFrameRateMinValue;
+ protected float previewFrameRateMaxValue;
+
+ protected CameraOptions() { }
/**
* Shorthand for getSupported*().contains(value).
@@ -108,11 +60,10 @@ public class CameraOptions {
* @param control value to check
* @return whether it's supported
*/
- public boolean supports(Control control) {
+ public final boolean supports(@NonNull Control control) {
return getSupportedControls(control.getClass()).contains(control);
}
-
/**
* Shorthand for other methods in this class,
* e.g. supports(GestureAction.ZOOM) == isZoomSupported().
@@ -120,12 +71,13 @@ public class CameraOptions {
* @param action value to be checked
* @return whether it's supported
*/
- public boolean supports(GestureAction action) {
+ public final boolean supports(@NonNull GestureAction action) {
switch (action) {
- case FOCUS:
- case FOCUS_WITH_MARKER:
+ case AUTO_FOCUS:
return isAutoFocusSupported();
- case CAPTURE:
+ case TAKE_PICTURE:
+ case FILTER_CONTROL_1:
+ case FILTER_CONTROL_2:
case NONE:
return true;
case ZOOM:
@@ -136,9 +88,10 @@ public class CameraOptions {
return false;
}
-
@SuppressWarnings("unchecked")
- public Collection getSupportedControls(@NonNull Class controlClass) {
+ @NonNull
+ public final