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)`: 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 @@ -[![Build Status](https://travis-ci.org/natario1/CameraView.svg?branch=master)](https://travis-ci.org/natario1/CameraView) +[![Build Status](https://github.com/natario1/CameraView/workflows/Build/badge.svg?event=push)](https://github.com/natario1/CameraView/actions) [![Code Coverage](https://codecov.io/gh/natario1/CameraView/branch/master/graph/badge.svg)](https://codecov.io/gh/natario1/CameraView) +[![Release](https://img.shields.io/github/release/natario1/CameraView.svg)](https://github.com/natario1/CameraView/releases) +[![Issues](https://img.shields.io/github/issues-raw/natario1/CameraView.svg)](https://github.com/natario1/CameraView/issues) +[![Funding](https://img.shields.io/opencollective/all/CameraView.svg?colorB=r)](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 " /> + + - 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