Compare commits

...

270 Commits

Author SHA1 Message Date
DeweyReed e26ac950be
Fix the setSize typo (#1198) 2 years ago
Dmitry Naymushin ae3f3da0e1
A couple of fixes and docs improvements (#1184) 2 years ago
Steve Xu 82ea49f6e2
#1088: add Pixel 4a into FpsRangeValidator (#1164) 3 years ago
Mattia Iavarone 0057981408
Release v2.7.2 (#1140) 3 years ago
Mattia Iavarone 399844321b
Fix orientation when activity handles rotations (#1117) 3 years ago
Mattia Iavarone c2e02923de Fix sonatype build 3 years ago
Mattia Iavarone 994b1dd434
Release 2.7.1 (#1111) 3 years ago
Jan Šmuk ceb0eca394
Add Pixel 4a into FpsRangeValidator (#1089) 4 years ago
Ryan Loebs bde88a946e
Add option to capture hardware accelerated overlays (#1066) 4 years ago
Ryan Loebs f608ec3918
Add flag to disable automatic gesture handling (#1068) 4 years ago
Mattia Iavarone a219073468
Release v2.7.0 (#1040) 4 years ago
Ezequiel Adrián d89491195c
Implement Shutter callback. Add GestureAction.TAKE_PICTURE_SNAPSHOT. (#1030) 4 years ago
Michael 5d10277d7b
add try catch to catch exceptions when takePicture fails (#1024) 4 years ago
Mattia Iavarone 0f6ab20c3e
Make filterFrameRateRanges protected (#1026) 4 years ago
Billy Sutomo a9fcd6506e
Update frame-processing.md (#941) 4 years ago
Philippe Banwarth 6aa2d3a489
Log errors when writing the image to a file (#960) 4 years ago
Mattia Iavarone 40ace54c19
Fix preview on Pixel devices, fix deadlock with surface preview (#1020) 4 years ago
Mattia Iavarone 2429cc114b
Add CameraEngine.getRepeatingRequestDefaultTemplate (#1004) 4 years ago
Mattia Iavarone 0001ab7a54
Rewrite orchestrator (#992) 4 years ago
Mattia Iavarone e3fcef286f
Release v2.6.4 (#954) 4 years ago
Mattia Iavarone 8207e67679
Some bug fixes (#953) 4 years ago
thijsonline daf7a0bf44
Fix for #927 OrientationHelper.enable() called twice (#928) 4 years ago
Steve 170d87ed39
Fix references on overlay documentation (#931) 4 years ago
Mattia Iavarone a7cf726636 Fix isMinifyEnabled 4 years ago
Mattia Iavarone 610f511a76
Release v2.6.3 (#912) 4 years ago
Mattia Iavarone a7c18324a5
Build tools update (#911) 4 years ago
Alexander 66c37373bd
Wrapped some crashes which happened on start camera and which connect with params. (#897) 4 years ago
Alexander 8455c23fb1
Wrapped some crashes which connect with empty camera params (#851) 4 years ago
jeffreyfjohnson b5921ec6bf
Restart Camera1 Frame Processing After Taking a Picture - Fixes Issue 824 (#877) 4 years ago
Ryan McClarnon 4364ec7c3a
Add capability to set audio codec to encode video files with. (#861) 5 years ago
Mattia Iavarone edbade24a9
Preview interface changes (#816) 5 years ago
Alexander 5530c011fa
Add ability to clear lifecycleObserver (#798) 5 years ago
Mattia Iavarone 778cdedc00
Release v2.6.2 (#794) 5 years ago
dependabot[bot] fa26b2a654
Bump nokogiri from 1.10.4 to 1.10.8 in /docs (#786) 5 years ago
Mattia Iavarone 124aee6908 Make LogAction log to info 5 years ago
Alexander 218b27319d
Add setter for flag enabled/disabled auto check permissions (#775) 5 years ago
Alexander 858495afde
Make CameraLogger ThreadSafe (#779) 5 years ago
Mattia Iavarone aa95d19902 Ads 5 years ago
Mattia Iavarone 968d181af4
GL cleanup (#766) 5 years ago
dependabot[bot] 7f23c309ab Bump rubyzip from 1.2.2 to 2.0.0 in /docs (#759) 5 years ago
hualong-shen 24b02caa28 Set accurate fps if possible (#754) 5 years ago
Mattia Iavarone e207e452bf Website mobile improvements 5 years ago
Mattia Iavarone 83bb45108a
Frame rotation improvements (#745) 5 years ago
Mattia Iavarone 1bb15dc595
Update bug_report.md 5 years ago
Mattia Iavarone 949ed22ea6
Release v2.6.1 (#744) 5 years ago
Mattia Iavarone 141902c6cb
Metering improvements (#741) 5 years ago
Sewar 1a88cd09f4 Add support to record video to FileDescriptor. (#732) 5 years ago
Mattia Iavarone e6ec1a15ac Fix page.html 5 years ago
Mattia Iavarone c0dab05063 Use Jekyll collections 5 years ago
Mattia Iavarone ef9a68ec6a Website improvements 5 years ago
Mattia Iavarone adf54cb029 Fix site header (3) 5 years ago
Mattia Iavarone 0f14fbd4e8 Fix site header (2) 5 years ago
Mattia Iavarone afd02f251b Fix site header 5 years ago
Mattia Iavarone 3c4d01c22f
Improve website (#734) 5 years ago
Mattia Iavarone 9c984f0c21
Update 2018-12-20-changelog.md 5 years ago
Mattia Iavarone 0eacd8c9b7
Release v2.6.0 (#725) 5 years ago
Mattia Iavarone df139994d7
Add startAutoFocus(Rect) (#724) 5 years ago
Mattia Iavarone 9b916f12fd Analytics 5 years ago
Mattia Iavarone 4afb82f1c2 Verification 5 years ago
Mattia Iavarone bdce730659 Website landing page (5) 5 years ago
Mattia Iavarone e2a4789d77 Website landing page (4) 5 years ago
Mattia Iavarone 0553fa9d20 Website landing page (3) 5 years ago
Mattia Iavarone 9b94fd289c Website landing page 5 years ago
Mattia Iavarone 14e777e7c7 Website landing page 5 years ago
Mattia Iavarone 821a21a1bd Fix changelog 5 years ago
Mattia Iavarone 4cf55714c5 Fix website links 5 years ago
Mattia Iavarone 840e05a3e8
Permission flag (#718) 5 years ago
Mattia Iavarone ce9ca64370
Frame processing threading APIs (#716) 5 years ago
Mattia Iavarone 097420a96b
Release v2.5.0 (#708) 5 years ago
Mattia Iavarone e1721bb77d
Frame Processing maxWidth, maxHeight and format (#704) 5 years ago
Mattia Iavarone 4a6b9be905
Testing and stability improvements (#696) 5 years ago
Mattia Iavarone 3db6fd3fc0
Rewrite engine threading (#697) 5 years ago
Mattia Iavarone fd17a8339e
Update README.md 5 years ago
Mattia Iavarone 859904ec6a
CI improvements (#695) 5 years ago
Mattia Iavarone bb3b23e69a
RAW support (#691) 5 years ago
Mattia Iavarone 79d00d5eba
Drop Travis and move to GitHub actions (#694) 5 years ago
Mattia Iavarone 129b43e657
Release v2.4.0 (#662) 5 years ago
Mattia Iavarone 218aa9d108
Preview FPS docs + Camera1 implementation (#661) 5 years ago
Vaibhav Bhandula fa88783d37 Support for higher frame rate (#653) 5 years ago
Mattia Iavarone 74c94e21ce
Add GitHub Sponsors link (#659) 5 years ago
Mattia Iavarone a63cd84726
Bug fixes (#651) 5 years ago
Mattia Iavarone 063c7a76fb
Update 2018-12-20-changelog.md 5 years ago
Mattia Iavarone 55e7a26278
Fix bugs (#630) 5 years ago
Suneet Agrawal 19f607a959 Move snapshot video start callback to onStart (#632) 5 years ago
Mattia Iavarone 824abbc55a
takeVideo improvements (#617) 5 years ago
Mattia Iavarone 5b92e81b83
Release v2.3.0 (#608) 5 years ago
Mattia Iavarone 5619dbdca5
Small improvements (#588) 5 years ago
Mattia Iavarone 4ddd2af731
Metering APIs (#580) 5 years ago
Mattia Iavarone a8fddc482f
Focus improvements (#574) 5 years ago
Mattia Iavarone 0731b64c2f
Release v2.2.0 (#573) 5 years ago
Mattia Iavarone c8d03ee116
Update 2018-12-20-frame-processing.md 5 years ago
Mattia Iavarone eddae18caa
Frame improvements (#572) 5 years ago
Mattia Iavarone e5fb4fa8fa
Update bug_report.md 5 years ago
Mattia Iavarone c36d2ce996
README changes (#565) 5 years ago
Mattia Iavarone d333348ce6
MultiFilter to merge filters (#559) 5 years ago
Mattia Iavarone 83307c527e
Fix layout editor preview (#564) 5 years ago
Mattia Iavarone f2ea77ce79
Encoders improvements (#545) 5 years ago
dependabot[bot] f48d1c17ba Bump nokogiri from 1.9.1 to 1.10.4 in /docs (#562) 5 years ago
Mattia Iavarone eeec7ac222
Video bugs (#551) 5 years ago
Mattia Iavarone 445b742455
Add SimpleFilter (#552) 5 years ago
Mattia Iavarone 0c04d6405c
Update README.md 5 years ago
Mattia Iavarone f7b0806d11
Update README.md 5 years ago
Mattia Iavarone 91eabbd4cd
Release v2.1.0 (#541) 5 years ago
Mattia Iavarone 95b1b2cdc3
Real-time filters gestures (#537) 5 years ago
Mattia Iavarone bf41489279
Improve realtime filters (#535) 5 years ago
Suneet Agrawal facd26f11d Feature Live Filters using Shaders (#527) 5 years ago
Mattia Iavarone ab48a33249 Fix javadocs 5 years ago
Mattia Iavarone 82ce5cd762
Release v2.0.0 (#531) 5 years ago
Mattia Iavarone c824c7db6c
Improve video snapshots (#530) 5 years ago
Mattia Iavarone 42de6e30a4
Fix issue 514 (#528) 5 years ago
Mattia Iavarone 3e0ae65dad
New colors and logo (#523) 5 years ago
Mattia Iavarone be037d6393
Small fixes (#524) 5 years ago
Mattia Iavarone bc3c72cdc8
Fix Camera2 picture recorder (#521) 5 years ago
Mattia Iavarone 03b5df7a10
Release v2.0.0-rc2 (#519) 5 years ago
Mattia Iavarone 535eebc865
Fix pom file (#517) 5 years ago
Mattia Iavarone f2350512f0
Fix #512 (#513) 5 years ago
Mattia Iavarone 86b18c5832
Release v2.0.0-rc1 (#508) 5 years ago
Mattia Iavarone 1318b7d10b
Improve coverage (#507) 5 years ago
Mattia Iavarone 6962744d4f
Improve video encoding (#506) 5 years ago
Mattia Iavarone ea952d1497
Feature/overlays (#502) 5 years ago
Suneet Agrawal 7d87d4af61 Actual Video recording callback (#498) 5 years ago
Mattia Iavarone 389da0467f
Fix sublist bug (#501) 5 years ago
Mattia Iavarone 8d3013ff53
Demo improvements (#499) 5 years ago
Mattia Iavarone dc31c40dd9
Use device orientation flag (#497) 5 years ago
Mattia Iavarone 71f006c6d9
Update 2018-12-20-more-features.md 5 years ago
Mattia Iavarone 299fd76472
Release version 2.0.0-beta06 (#495) 5 years ago
Mattia Iavarone 0f9c7f0e17
Make internals more open (#494) 5 years ago
Mattia Iavarone 64714017b8
Create FUNDING.yml 5 years ago
Mattia Iavarone e340764474
Tests + AGP updates (#492) 5 years ago
Mattia Iavarone e26042d2b6 Try fix filter 5 years ago
Mattia Iavarone 0a25d98312 Add docs 5 years ago
Mattia Iavarone 51e3e8992d Fix zoom test 5 years ago
Mattia Iavarone 0c26b14a99 Add more delay to some tests 5 years ago
Mattia Iavarone c64f3bf06e Add DoNotRunOnTravis annotation 5 years ago
Mattia Iavarone 301d5d5dc2 Add default to controls 5 years ago
Mattia Iavarone e38418ae6d Remove integration tests from CI 5 years ago
Mattia Iavarone 9761f1aeba Remove integration1 tests 5 years ago
Mattia Iavarone ef7d41c1b6 Apply zoom and EV to the capture builder 5 years ago
Mattia Iavarone 97e7038d6f Remove comments 5 years ago
Suneet Agrawal 365da96eed Camera2 Zoom and Exposure implemetation (#488) 5 years ago
Mattia Iavarone 6721f3400d Fix tests 5 years ago
Mattia Iavarone 14f03a43f6 Camera2 autofocus support 5 years ago
Mattia Iavarone bef1044819 Camera2 full support for hq pictures 5 years ago
Mattia Iavarone 87796ea2f7 Camera2 basic support for hq pictures 5 years ago
Mattia Iavarone e96c9f0846 Camera2 support for frame processing 5 years ago
Mattia Iavarone 06d805cb72 Prepare Camera2 for frame processing 5 years ago
Mattia Iavarone d9d213e67b Create setHasFrameProcessors API 5 years ago
Mattia Iavarone ccdc50ea42 Camera2 support for Flash.TORCH 5 years ago
Mattia Iavarone 06a99be2a6 Camera2 support for most parameters 5 years ago
Mattia Iavarone c353ae808e Camera2 support for videos 5 years ago
Mattia Iavarone 1dabf4db01 Camera2 support for picture and video snapshots 5 years ago
Mattia Iavarone e2d5aa1941 Camera2 working with all previews 5 years ago
Mattia Iavarone 4166031ce2 Basic Camera2 open-close integration 5 years ago
Mattia Iavarone 3c345826cc Create base Camera2 class 5 years ago
Mattia Iavarone e2e3054a77 Create Engine class 5 years ago
Mattia Iavarone e7727fb65c MarkerLayout tests 5 years ago
Mattia Iavarone ecd2cdba13
AutoFocusMarker (#484) 6 years ago
Mattia Iavarone cd5f0a12bf
Repackage / Expose (#482) 6 years ago
Mattia Iavarone 0c7726d5c5
Get CamcorderProfile from incoming video size (#477) 6 years ago
Mattia Iavarone 8b66d5b575
Quick fixes & bump version (#471) 6 years ago
Mattia Iavarone b9620b70e6
Fix processor OOM bug (#431) 6 years ago
Craig Neuwirt 3261b73967 Added setAutoFocusResetDelay api to control autofocus reset options (#435) 6 years ago
Mattia Iavarone fc4a75a24b
Make CameraPreview faster (#403) 6 years ago
Mattia Iavarone 36d073b197
Bump version to 2.0.0-beta04 (#402) 6 years ago
Mattia Iavarone 82b504acd5
Add sizes png 6 years ago
Mattia Iavarone 5e5af877e4
New snapshot max size APIs (#393) 6 years ago
Mattia Iavarone d462b83048
v2.0.0-beta03 (#392) 6 years ago
Mattia Iavarone 7411614433
Improve snapshot recording (#374) 6 years ago
Mattia Iavarone aec17d3e49
Update question.md 6 years ago
Mattia Iavarone 1f3a142a3d
Bump version (#361) 6 years ago
Mattia Iavarone 68a3133282
Update README.md 6 years ago
Mattia Iavarone 894ac46bc5
Flip snapshots, add PictureResult.getFacing and VideoResult.getFacing (#360) 6 years ago
Mattia Iavarone a8a4e09900
V2 bug fixes (#356) 6 years ago
Mattia Iavarone 7a5e0b33e4 Fix demo app 6 years ago
Mattia Iavarone 8e8818af78 Fix Javadocs 6 years ago
Mattia Iavarone 07f421e7e0 Fix CameraOptions tests 6 years ago
Mattia Iavarone 4f6ef724bb Remove wrong tests 6 years ago
Mattia Iavarone 9c8cde75f7 Add README notes about v2 6 years ago
Mattia Iavarone a2731f50b1 Fix tests and rebase process 6 years ago
Mattia Iavarone 8f00cb5dac Add toFile utility in CameraUtils and in PictureResult 6 years ago
Mattia Iavarone 2dba0fa308 Remove scale from EglViewport, was used in oval shader 6 years ago
Mattia Iavarone e9df75b7ae Video files now must not be null 6 years ago
Mattia Iavarone d7a593e070 CameraListeners now must be non null 6 years ago
Mattia Iavarone 4cfd2e0584 Fix README links 6 years ago
Mattia Iavarone 115df70b5c Fix coverage reports 6 years ago
Mattia Iavarone 714d228168 Fix tests 6 years ago
Mattia Iavarone cc72056684 Add donation page 6 years ago
Mattia Iavarone 79896f8ce5 Add seo tag plugin 6 years ago
Mattia Iavarone f53f273f23 Add Disqus 6 years ago
Mattia Iavarone 90886012ec Missing docs about exposure, zoom and location 6 years ago
Mattia Iavarone 10790dfe2e Improve take video docs 6 years ago
Mattia Iavarone ae2eada2a2 Migrate to AndroidX 6 years ago
Mattia Iavarone bc85178acf Add setPreviewSize method 6 years ago
Mattia Iavarone 26f313c74e Documentation in GitHub pages 6 years ago
Mattia Iavarone c26eb87a87 Add beta and release candidate in travis 6 years ago
Mattia Iavarone 3eb0c2fb2e Fix video snapshots rotation 6 years ago
Mattia Iavarone e0c5b3efde Remove oval experiment 6 years ago
Mattia Iavarone d1ed8033e6 Add nullable and nonnull annotations everywhere 6 years ago
Mattia Iavarone f10ca69517 Migration document layout 6 years ago
Mattia Iavarone 55047f1e9c Update gradle 6 years ago
Mattia Iavarone 5038c9e81a Rename methods 6 years ago
Mattia Iavarone 1a22bdc2d5 Refined oval support 6 years ago
Mattia Iavarone fd69768ac0 Improvements for supporting a round preview and output 6 years ago
Mattia Iavarone 97c839af87 Better video error handling 6 years ago
Mattia Iavarone 1b5a20b034 Fix demo app null pointer, add null annotations 6 years ago
Mattia Iavarone bb0e08c731 Fix OutOfMemoryError in BitmapCallback 6 years ago
Mattia Iavarone b1f584aede Changes in the demo app 6 years ago
Mattia Iavarone 622693e7c9 Remove outdated docs 6 years ago
Mattia Iavarone 437838056c Add gridColor option 6 years ago
Mattia Iavarone ddf7c960c3 Remove cameraFacing definition in demo app 6 years ago
Mattia Iavarone 69349b0187 Facing now defaults to FRONT if we have no BACK cameras. Throw a new CameraException if no camera found 6 years ago
Mattia Iavarone a1e7718f0e Add support for videoBitRate and audioBitRate 6 years ago
Mattia Iavarone 8fbc1675ec Migration comments 6 years ago
Mattia Iavarone 4f95c3e7d5 Add max size and duration constraints to videos 6 years ago
Mattia Iavarone 20691effb3 Small changes to audio recording. Still bug 6 years ago
Mattia Iavarone a87b73a35d Audio recording 6 years ago
Mattia Iavarone 1e2fbceb64 Comments 6 years ago
Mattia Iavarone 98c4c684e2 Add comments 6 years ago
Mattia Iavarone d08c56c821 Delete old media encoder 6 years ago
Mattia Iavarone 1f0341256a Add thread annotations 6 years ago
Mattia Iavarone c63fe6e89d Fix muxer issue 6 years ago
Mattia Iavarone cfc46afaf2 Create new MediaEncoder interfaces to support audio 6 years ago
Mattia Iavarone 439e49d513 Fast snapshot with new GL preview. Take snapshots while taking video 6 years ago
Mattia Iavarone 24a965c572 Fix output size for video snapshots might be flipped 6 years ago
Mattia Iavarone 6d170f33a6 Add legacy implementation stup 6 years ago
Mattia Iavarone 419ff34736 Create PictureRecorder and two implementations 6 years ago
Mattia Iavarone 481062a08b Change demo app appearance 6 years ago
Mattia Iavarone 34b9958c3e Add Preview control and setPreview 6 years ago
Mattia Iavarone f6759f6b9e Fix rotation issue 6 years ago
Mattia Iavarone f7cf379966 Rename video recorders 6 years ago
Mattia Iavarone 2999e9aa65 Respect VideoCodec from options 6 years ago
Mattia Iavarone 8662d9ba2e Fix rotation 6 years ago
Mattia Iavarone 926b03cf35 Fix transformation issues in preview and final video result 6 years ago
Mattia Iavarone af11e24653 Remove useless code 6 years ago
Mattia Iavarone 2760b64953 Throw if api level or preview do not support the video snapshot feature 6 years ago
Mattia Iavarone d291d88814 Working implementation of MediaCodecVideoRecorder 6 years ago
Mattia Iavarone caff00455f Fix camerapreview tests 6 years ago
Mattia Iavarone 217d197fe8 Improve MediaRecorderVideoRecorder 6 years ago
Mattia Iavarone 31a5d20fcd Revert demo app changes 6 years ago
Mattia Iavarone 50f7e384dc Rename some methods, have controllers implement onSurfaceDestroyed, revisit controller lifecycle 6 years ago
Mattia Iavarone e207041a31 Add cropping functionality to GLCameraPreview 6 years ago
Mattia Iavarone ab576286f3 Temp commit 6 years ago
Mattia Iavarone 4f6271d670 Add GLCameraPreview based on GLSurfaceView, preview appears to be working with some bugs 6 years ago
Mattia Iavarone c1e3cced4b Refactor Previews 6 years ago
Mattia Iavarone 90de3917c9 Add VideoRecorder interface and MediaRecorderVideoRecorder for standard videos 6 years ago
Mattia Iavarone 5e7cdf1efa Add setVideoSize, video size selectors and available video sizes in CameraOptions 6 years ago
Mattia Iavarone 1a2c42f0a1 Move to PictureResult, rewrote takePicture and takePictureSnapshot 6 years ago
Mattia Iavarone 29c32090eb Refactor onVideoTaken, introducing VideoResult 6 years ago
Mattia Iavarone bd7a4d3cc9 Add isTakingPicture(), use in demo app 6 years ago
Mattia Iavarone dbdade2525 Fix VideoRecorder bug 6 years ago
Mattia Iavarone 92b0b05c68 Add VideoRecorder interface and MediaRecorderVideoRecorder for standard videos 6 years ago
Mattia Iavarone 7b4a8ea8e8 Better README 6 years ago
Mattia Iavarone 5a633ab7f4 Update docs for VideoSize 6 years ago
Mattia Iavarone d409bb2449 Add setVideoSize, video size selectors and available video sizes in CameraOptions 6 years ago
Mattia Iavarone acf1b4cdde Removed VideoQuality. 6 years ago
Mattia Iavarone a37caf0760 Rename SessionType to Mode. takePicture fails when Mode == VIDEO 6 years ago
Mattia Iavarone 969c5a8c74 Fix preview size computation, demo app details 6 years ago
Mattia Iavarone 8c41a12e66 Update docs 6 years ago
Mattia Iavarone 24d08c7aa1 Nits 6 years ago
Mattia Iavarone 2cd8ad1fa6 getPictureSize and getPreviewSize now return sizes in the output reference 6 years ago
Mattia Iavarone 3a937a9787 Fix tests, refactoring 6 years ago
Mattia Iavarone 2500b81b78 Move to PictureResult, rewrote takePicture and takePictureSnapshot 6 years ago
Mattia Iavarone a144b375af Improve README 6 years ago
Mattia Iavarone 68ff331a80 Refactor onVideoTaken, introducing VideoResult 6 years ago
Mattia Iavarone eeca06f1da Remove ExtraProprties 6 years ago
Mattia Iavarone 49fa18901e take APIs refactoring, removing deprecated 6 years ago
Mattia Iavarone b341144642 Remove cropOutput docs 6 years ago
Mattia Iavarone 4a926b30bb Remove cropOutput 6 years ago
Mattia Iavarone fb7c5cfa09 Remove JpegQuality 6 years ago
Mattia Iavarone ad48d0be76 Refactor Mapper class 6 years ago
  1. 0
      .github/CODE_OF_CONDUCT.md
  2. 1
      .github/CONTRIBUTING.md
  3. 12
      .github/FUNDING.yml
  4. 21
      .github/ISSUE_TEMPLATE/bug_report.md
  5. 5
      .github/ISSUE_TEMPLATE/question.md
  6. 12
      .github/pull_request_template.md
  7. 119
      .github/workflows/build.yml
  8. 23
      .github/workflows/deploy.yml
  9. 14
      .github/workflows/emulator_script.sh
  10. 25
      .github/workflows/snapshot.yml
  11. 23
      .run/runAndroidTests.run.xml
  12. 23
      .run/runUnitTests.run.xml
  13. 86
      .travis.yml
  14. 156
      CHANGELOG.md
  15. 2
      LICENSE
  16. 884
      README.md
  17. BIN
      art/icon.png
  18. BIN
      art/old_screen1.png
  19. BIN
      art/old_screen2.png
  20. BIN
      art/screen1.jpg
  21. BIN
      art/screen2.jpg
  22. BIN
      art/screen3.jpg
  23. 35
      build.gradle
  24. 30
      build.gradle.kts
  25. 217
      cameraview/build.gradle
  26. 137
      cameraview/build.gradle.kts
  27. 36
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseEglTest.java
  28. 140
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java
  29. 36
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraLoggerTest.java
  30. 129
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java
  31. 277
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java
  32. 755
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  33. 76
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CropHelperTest.java
  34. 32
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/ExtraProperties1Test.java
  35. 65
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/GestureLayoutTest.java
  36. 644
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
  37. 60
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/Mapper1Test.java
  38. 36
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MapperTest.java
  39. 132
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java
  40. 48
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraPreview.java
  41. 99
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java
  42. 55
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/PictureResultTest.java
  43. 66
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/PinchGestureLayoutTest.java
  44. 92
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/ScrollGestureLayoutTest.java
  45. 19
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceCameraPreviewTest.java
  46. 77
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/TapGestureLayoutTest.java
  47. 8
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/TestActivity.java
  48. 146
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java
  49. 39
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/WorkerHandlerTest.java
  50. 51
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera1IntegrationTest.java
  51. 86
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera2IntegrationTest.java
  52. 1227
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
  53. 215
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java
  54. 68
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/mappers/Camera1MapperTest.java
  55. 101
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/mappers/Camera2MapperTest.java
  56. 196
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/options/Camera1OptionsTest.java
  57. 172
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java
  58. 52
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FilterParserTest.java
  59. 30
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FiltersTest.java
  60. 212
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/MultiFilterTest.java
  61. 35
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/NoFilterTest.java
  62. 43
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/SimpleFilterTest.java
  63. 112
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/ByteBufferFrameManagerTest.java
  64. 73
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/FrameManagerTest.java
  65. 90
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/GestureFinderTest.java
  66. 71
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/PinchGestureFinderTest.java
  67. 98
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinderTest.java
  68. 85
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/TapGestureFinderTest.java
  69. 61
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CamcorderProfilesTest.java
  70. 53
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CropHelperTest.java
  71. 219
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/DeviceEncodersTest.java
  72. 53
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/GridLinesLayoutTest.java
  73. 109
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/OrientationHelperTest.java
  74. 53
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/RotationHelperTest.java
  75. 242
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/WorkerHandlerTest.java
  76. 104
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarkerTest.java
  77. 128
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerLayoutTest.java
  78. 79
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerParserTest.java
  79. 137
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/metering/MeteringRegionsTest.java
  80. 78
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java
  81. 177
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayLayoutTest.java
  82. 43
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/picture/PictureRecorderTest.java
  83. 96
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java
  84. 43
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/GlCameraPreviewTest.java
  85. 63
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java
  86. 30
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreviewTest.java
  87. 27
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/TextureCameraPreviewTest.java
  88. 211
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/size/SizeSelectorParserTest.java
  89. 12
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Emulator.java
  90. 149
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Op.java
  91. 9
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Retry.java
  92. 52
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/RetryRule.java
  93. 20
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkExclude.java
  94. 47
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkExcludeFilter.java
  95. 20
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkInclude.java
  96. 47
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkIncludeFilter.java
  97. 52
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/video/VideoRecorderTest.java
  98. 4
      cameraview/src/androidTest/res/layout/not_overlay.xml
  99. 8
      cameraview/src/androidTest/res/layout/overlay.xml
  100. 3
      cameraview/src/main/AndroidManifest.xml
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1 @@
Contributing guidelines are [hosted here](https://natario1.github.io/CameraView/extra/contributing).

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

@ -8,26 +8,39 @@ assignees: ''
--- ---
### Describe the bug ### 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* - CameraView version: *version number*
- Camera engine used: *camera1/camera2/both*
- Reproducible in official demo app: *yes/no* - Reproducible in official demo app: *yes/no*
- Device / Android version: *Pixel, API 28* - Device / Android version: *Pixel, API 28*
- I have read the [FAQ page](https://natario1.github.io/CameraView/about/faq): *yes/no*
### To Reproduce ### To Reproduce
Steps to reproduce the behavior, possibly in the demo app: Steps to reproduce the behavior, possibly in the demo app:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '...'
3. See error 3. See error
### Expected behavior ### Expected behavior
A clear and concise description of what you expected to happen. 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
<CameraView
android:layout_width="match_parent"
android:layout_height="match_parent"
...>
</CameraView>
```
### Screenshots ### Screenshots
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
### Logs ### 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. Use `CameraLogger.registerLogger()` to export to file or crash reporting service.
### APK ### APK

@ -8,5 +8,8 @@ assignees: ''
--- ---
### How do I? ### 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. Questions not strictly related to CameraView should be asked elsewhere.
### Version used
CameraView exact version.

@ -1,10 +1,14 @@
### Before you go ### Before you go
Unless this is a simple fix (typos, bugs with obvious solution), please open an issue first. Unless this is a simple fix (typos, bugs with obvious solution), please open an issue first so that
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. we can discuss the best approach to address the problem. Without a reference issue and discussion,
unfortunately, this PR will likely be ignored.
- Fixes ... (*issue number*) 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*) - Tests: ... (*yes/no*)
- Docs updated: ... (*yes/no*) - Docs updated: ... (*yes/no*)
### Solution ### Solution
If applicable, briefly describe how the issue was addressed. If applicable, describe briefly how the issue was addressed.

@ -0,0 +1,119 @@
# 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:
- main
pull_request:
jobs:
ANDROID_BASE_CHECKS:
name: Base Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
java-version: 11
distribution: temurin
cache: gradle
- 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@v2
with:
java-version: 11
distribution: temurin
cache: gradle
- 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@v2
with:
java-version: 11
distribution: temurin
cache: gradle
- name: Execute emulator tests
timeout-minutes: 30
uses: reactivecircus/android-emulator-runner@v2.21.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
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@v2
with:
java-version: 11
distribution: temurin
cache: gradle
- 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

@ -0,0 +1,23 @@
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
name: Deploy
on:
release:
types: [published]
jobs:
MAVEN_UPLOAD:
name: Maven Upload
runs-on: ubuntu-latest
env:
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
java-version: 11
distribution: temurin
cache: gradle
- name: Perform maven upload
run: ./gradlew publishToSonatype

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

@ -0,0 +1,25 @@
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
# Renaming ? Change the README badge.
name: Snapshot
on:
push:
branches:
- main
jobs:
SNAPSHOT:
name: Publish Snapshot
runs-on: ubuntu-latest
env:
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
java-version: 11
distribution: temurin
cache: gradle
- name: Publish sonatype snapshot
run: ./gradlew publishToSonatypeSnapshot

@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="runAndroidTests" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/cameraview" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runAndroidTests" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="runUnitTests" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/cameraview" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runUnitTests" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

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

@ -1,156 +0,0 @@
## v1.6.1
This is the last release before v2.
- Fixed: crash when using TextureView in API 28, thanks to [@Keyrillanskiy][Keyrillanskiy] ([#297][297])
- Fixed: restore Frame Processor callbacks after taking videos, thanks to [@stefanJi][stefanJi] ([#344][344])
- Enhancement: when horizontal, camera now uses the last available orientation, thanks to [@aartikov][aartikov] ([#290][290])
- Changed: we now swallow exceptions during autoFocus that were happening unpredictably on some devices, thanks to [@mahdi-ninja][mahdi-ninja] ([#332][332])
https://github.com/natario1/CameraView/compare/v1.6.0...v1.6.1
## v1.6.0
- Lifecycle support. Use `setLifecycleOwner` instead of calling start, stop and destroy ([#265][265])
- Enhancement: provide synchronous version of CameraUtils.decodeBitmap thanks to [@athornz][athornz] ([#224][224])
- Enhancement: prevent possible context leak thanks to [@MatFl][MatFl] ([#245][245])
- Bug: fix crash when using default VideoCodec thanks to [@Namazed][Namazed] ([#264][264])
- Enhancement: CameraException.getReason() gives some insight about the error ([#265][265])
- Enhancement: Common crashes are now being posted to the error callback instead of crashing the app ([#265][265])
https://github.com/natario1/CameraView/compare/v1.5.1...v1.6.0
### v1.5.1
- Bug: byte array length for Frames was incorrect thanks to [@ssakhavi][ssakhavi] ([#205][205])
- Bug: gestures were crashing in some conditions ([#222][222])
- Bug: import correctly the ExifInterface library ([#222][222])
- Updated dependencies thanks to [@caleb-allen][caleb-allen] ([#190][190])
https://github.com/natario1/CameraView/compare/v1.5.0...v1.5.1
## v1.5.0
- New: set encoder for video recordings with `cameraVideoCodec` ([#174][174])
- New: set max duration for videos with `cameraVideoMaxDuration` ([#172][172])
- Enhancement: reduced lag with continuous gestures (ev, zoom) ([#170][170])
- Bug: tap to focus was crashing on some devices ([#167][167])
- Bug: capturePicture was breaking if followed by another event soon after ([#173][173])
https://github.com/natario1/CameraView/compare/v1.4.2...v1.5.0
### v1.4.2
- Add prefix to XML resources so they don't collide, thanks to [@RocketRider][RocketRider] ([#162][162])
- Add `videoMaxSize` API and XML attribute, to set max size video in bytes, thanks to [@chaitanyaraghav][chaitanyaraghav] ([#104][104])
- Improved the preview size selection, thanks to [@YeungKC][YeungKC] ([#133][133])
- Improved the playSounds attribute, was playing incorrectly, thanks to [@xp-vit][xp-vit] ([#143][143])
https://github.com/natario1/CameraView/compare/v1.4.1...v1.4.2
### v1.4.1
- Fixed a bug that would flip the front camera preview on some devices ([#112][112])
- Two new `CameraOptions` APIs: `o.getSupportedPictureSizes()` and `o.getSupportedPictureAspectRatios()` ([#101][101])
- Most controls (video quality, hdr, grid, session type, audio, white balance, flash, facing) now inherit
from a base `Control` class ([#105][105]). This let us add new APIs:
- `CameraView.set(Control)`: sets the control to the given value, e.g. `set(Flash.AUTO)`
- `CameraOptions.supports(Control)`: returns true if the control is supported
- `CameraOptions.getSupportedControls(Class<? extends Control>)`: returns list of supported controls of a given kind
https://github.com/natario1/CameraView/compare/v1.4.0...v1.4.1
## v1.4.0
- CameraView is now completely thread-safe. All actions are asynchronous. ([#97][97])
This has some breaking drawbacks. Specifically, the `get` methods (e.g., `getWhiteBalance`) might
not return the correct value while it is being changed. So don't trust them right after you have changed the value.
Instead, always check the `CameraOptions` to see if the value you want is supported.
- Added error handling ([#97][97]) in `CameraListener.onCameraError(CameraException)`.
At the moment, all exceptions there are unrecoverable. When the method is called, the camera is showing
a black preview. This is a good moment to show an error dialog to the user.
You can also try to `start()` again but that is not guaranteed to work.
- Long requested ability to set the picture output size ([#99][99]). Can be done through
`CameraView.setPictureSize()` or through new XML attributes starting with `cameraPictureSize`.
Please refer to docs about it.
- Deprecated `toggleFacing`. It was unreliable and will be removed.
- Deprecated `getCaptureSize`. Use `getPictureSize` instead.
- Fixed bugs.
https://github.com/natario1/CameraView/compare/v1.3.2...v1.4.0
### v1.3.2
- Fixed a memory leak thanks to [@andrewmunn][andrewmunn] ([#92][92])
- Reduced memory usage when using cropOutput thanks to [@RobertoMorelos][RobertoMorelos] ([#93][93])
- Improved efficiency for Frame processors, recycle buffers and Frames ([#94][94])
https://github.com/natario1/CameraView/compare/v1.3.1...v1.3.2
### v1.3.1
- Fixed a bug that would make setFacing and other APIs freeze the camera ([#86][86])
- Fixed ConcurrentModificationExceptions during CameraListener callbacks ([#88][88])
https://github.com/natario1/CameraView/compare/v1.3.0...v1.3.1
## v1.3.0
- Ability to inject frame processors to do your own visual tasks (barcodes, facial recognition etc.) ([#82][82])
- Ability to inject external loggers (e.g. Crashlytics) to listen for internal logging events ([#80][80])
- Improved CameraUtils.decodeBitmap, you can now pass maxWidth and maxHeight to avoid OOM ([#83][83])
- Updated dependencies thanks to [@v-gar][v-gar] ([#73][73])
https://github.com/natario1/CameraView/compare/v1.2.3...v1.3.0
[aartikov]: https://github.com/aartikov
[athornz]: https://github.com/athornz
[v-gar]: https://github.com/v-gar
[andrewmunn]: https://github.com/andrewmunn
[chaitanyaraghav]: https://github.com/chaitanyaraghav
[YeungKC]: https://github.com/YeungKC
[RobertoMorelos]: https://github.com/RobertoMorelos
[RocketRider]: https://github.com/RocketRider
[xp-vit]: https://github.com/xp-vit
[caleb-allen]: https://github.com/caleb-allen
[ssakhavi]: https://github.com/ssakhavi
[MatFl]: https://github.com/MatFl
[Namazed]: https://github.com/Namazed
[Keyrillanskiy]: https://github.com/Keyrillanskiy
[mahdi-ninja]: https://github.com/mahdi-ninja
[stefanJi]: https://github.com/stefanJi
[73]: https://github.com/natario1/CameraView/pull/73
[80]: https://github.com/natario1/CameraView/pull/80
[82]: https://github.com/natario1/CameraView/pull/82
[83]: https://github.com/natario1/CameraView/pull/83
[86]: https://github.com/natario1/CameraView/pull/86
[88]: https://github.com/natario1/CameraView/pull/88
[92]: https://github.com/natario1/CameraView/pull/92
[93]: https://github.com/natario1/CameraView/pull/93
[94]: https://github.com/natario1/CameraView/pull/94
[97]: https://github.com/natario1/CameraView/pull/97
[99]: https://github.com/natario1/CameraView/pull/99
[101]: https://github.com/natario1/CameraView/pull/101
[104]: https://github.com/natario1/CameraView/pull/104
[105]: https://github.com/natario1/CameraView/pull/105
[112]: https://github.com/natario1/CameraView/pull/112
[133]: https://github.com/natario1/CameraView/pull/133
[143]: https://github.com/natario1/CameraView/pull/143
[162]: https://github.com/natario1/CameraView/pull/162
[167]: https://github.com/natario1/CameraView/pull/167
[170]: https://github.com/natario1/CameraView/pull/170
[172]: https://github.com/natario1/CameraView/pull/172
[173]: https://github.com/natario1/CameraView/pull/173
[174]: https://github.com/natario1/CameraView/pull/174
[190]: https://github.com/natario1/CameraView/pull/190
[205]: https://github.com/natario1/CameraView/pull/205
[222]: https://github.com/natario1/CameraView/pull/222
[224]: https://github.com/natario1/CameraView/pull/224
[245]: https://github.com/natario1/CameraView/pull/245
[264]: https://github.com/natario1/CameraView/pull/264
[265]: https://github.com/natario1/CameraView/pull/265
[290]: https://github.com/natario1/CameraView/pull/290
[297]: https://github.com/natario1/CameraView/pull/297
[332]: https://github.com/natario1/CameraView/pull/332
[334]: https://github.com/natario1/CameraView/pull/334

@ -1,7 +1,7 @@
Copyrights for portions of project CameraView are held by WonderKiln, Inc as Copyrights for portions of project CameraView are held by WonderKiln, Inc as
part of project CameraKit-Android (https://github.com/wonderkiln/CameraKit-Android). part of project CameraKit-Android (https://github.com/wonderkiln/CameraKit-Android).
Their original license is available below, or at Their original license is available below, or at
https://github.com/wonderkiln/CameraKit-Android/blob/master/LICENSE . https://github.com/wonderkiln/CameraKit-Android/blob/main/LICENSE .
All other copyrights for project CameraView are held by Otalia Studios, 2017. All other copyrights for project CameraView are held by Otalia Studios, 2017.
------------------------------------------------------------------------------ ------------------------------------------------------------------------------

@ -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) [![Code Coverage](https://codecov.io/gh/natario1/CameraView/branch/main/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)
&#10240; <!-- Hack to add whitespace -->
<p align="center"> <p align="center">
<img src="art/icon.png" vspace="10" width="250" height="250"> <img src="docs/static/banner.png" width="100%">
</p> </p>
*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 <a href="mailto:mat.iavarone@gmail.com">get in touch</a>.*
# CameraView # CameraView
CameraView is a well documented, high-level library that makes capturing pictures and videos easy, 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. 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 ```groovy
compile 'com.otaliastudios:cameraview:1.6.1' api 'com.otaliastudios:cameraview:2.7.2'
``` ```
Make sure your project repositories include the `jcenter()`: - Fast & reliable
- Gestures support [[docs]](https://natario1.github.io/CameraView/docs/gestures)
```groovy - Real-time filters [[docs]](https://natario1.github.io/CameraView/docs/filters)
allprojects { - Camera1 or Camera2 powered engine [[docs]](https://natario1.github.io/CameraView/docs/previews)
repositories { - Frame processing support [[docs]](https://natario1.github.io/CameraView/docs/frame-processing)
jcenter() - 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)
<p> - RAW pictures support [[docs]](https://natario1.github.io/CameraView/docs/controls)
<img src="art/screen1.jpg" width="250" vspace="20" hspace="5"> - Lightweight
<img src="art/screen2.jpg" width="250" vspace="20" hspace="5">
<img src="art/screen3.jpg" width="250" vspace="20" hspace="5">
</p>
*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`
- Works down to API level 15 - Works down to API level 15
- Well tested
# Docs &#10240; <!-- Hack to add whitespace -->
- [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
<com.otaliastudios.cameraview.CameraView
android:id="@+id/camera"
android:keepScreenOn="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
```
`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) {}
/** <p align="center">
* Notifies that a picture previously captured with capturePicture() <img src="docs/static/screen1.png" width="250" hspace="5"><img src="docs/static/screen2.png" width="250" hspace="5"><img src="docs/static/screen3.png" width="250" hspace="5">
* or captureSnapshot() is ready to be shown or saved. </p>
*
* 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: &#10240; <!-- Hack to add whitespace -->
- Not every mapping is valid. For example, you can't control zoom with long taps, or start focusing by pinching. ## Support
- Some actions might not be supported by the sensor. Check out `CameraOptions` to know what's legit and what's not.
|Gesture (XML)|Description|Can be mapped to| 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
|`PINCH` (`cameraGesturePinch`)|Pinch gesture, typically assigned to the zoom control.|`zoom` `exposureCorrection` `none`| have your company logo here, get private support hours or simply help me push this forward.
|`TAP` (`cameraGestureTap`)|Single tap gesture, typically assigned to the focus control.|`focus` `focusWithMarker` `capture` `none`| If you prefer, you can also [donate](https://natario1.github.io/CameraView/extra/donate)
|`LONG_TAP` (`cameraGestureLongTap`)|Long tap gesture.|`focus` `focusWithMarker` `capture` `none`| to our OpenCollective page.
|`SCROLL_HORIZONTAL` (`cameraGestureScrollHorizontal`)|Horizontal movement gesture.|`zoom` `exposureCorrection` `none`|
|`SCROLL_VERTICAL` (`cameraGestureScrollVertical`)|Vertical movement gesture.|`zoom` `exposureCorrection` `none`|
CameraView is trusted and supported by [ShareChat](https://sharechat.com/), a social media app with over 100 million downloads.
## Sizing Behavior <p align="center">
<img src="docs/static/sharechat.png" width="100%">
</p>
### Preview Size Feel free to <a href="mailto:mat.iavarone@gmail.com">contact me</a> 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. Thanks to all our project backers... [[become a backer]](https://opencollective.com/cameraview#backer)
Measuring is controlled simply by `layout_width` and `layout_height` attributes, with this meaning:
- `WRAP_CONTENT` : try to stretch this dimension to respect the preview aspect ratio. <a href="https://opencollective.com/cameraview#backers" target="_blank"><img src="https://opencollective.com/cameraview/backers.svg?width=890"></a>
- `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, ...and to all our project sponsors! [[become a sponsor]](https://opencollective.com/cameraview#sponsor)
the preview will never be distorted.
#### Center Inside <a href="https://opencollective.com/cameraview/sponsor/0/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/1/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/2/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/3/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/4/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/5/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/6/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/7/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/8/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/9/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/9/avatar.svg"></a>
You can emulate a **center inside** behavior (like the `ImageView` scaletype) by setting ## Setup
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.
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 ```xml
<com.otaliastudios.cameraview.CameraView <com.otaliastudios.cameraview.CameraView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" />
```
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
<com.otaliastudios.cameraview.CameraView
android:layout_width="match_parent"
android:layout_height="match_parent" />
```
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<Size> select(List<Size> 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
<com.otaliastudios.cameraview.CameraView
android:id="@+id/camera"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:keepScreenOn="true" app:cameraPictureSizeMinWidth="@integer/picture_min_width"
app:cameraFacing="back" app:cameraPictureSizeMinHeight="@integer/picture_min_height"
app:cameraFlash="off" app:cameraPictureSizeMaxWidth="@integer/picture_max_width"
app:cameraGrid="off" app:cameraPictureSizeMaxHeight="@integer/picture_max_height"
app:cameraSessionType="picture" app:cameraPictureSizeMinArea="@integer/picture_min_area"
app:cameraCropOutput="false" app:cameraPictureSizeMaxArea="@integer/picture_max_area"
app:cameraJpegQuality="100" app:cameraPictureSizeSmallest="false|true"
app:cameraVideoQuality="max480p" app:cameraPictureSizeBiggest="false|true"
app:cameraVideoCodec="deviceDefault" app:cameraPictureSizeAspectRatio="@string/video_ratio"
app:cameraWhiteBalance="auto" app:cameraVideoSizeMinWidth="@integer/video_min_width"
app:cameraHdr="off" app:cameraVideoSizeMinHeight="@integer/video_min_height"
app:cameraAudio="on" app:cameraVideoSizeMaxWidth="@integer/video_max_width"
app:cameraPlaySounds="true" app:cameraVideoSizeMaxHeight="@integer/video_max_height"
app:cameraVideoMaxSize="0" app:cameraVideoSizeMinArea="@integer/video_min_area"
app:cameraVideoMaxDuration="0"/> app:cameraVideoSizeMaxArea="@integer/video_max_area"
``` app:cameraVideoSizeSmallest="false|true"
app:cameraVideoSizeBiggest="false|true"
|XML Attribute|Method|Values|Default Value| app:cameraVideoSizeAspectRatio="@string/video_ratio"
|-------------|------|------|-------------| app:cameraSnapshotMaxWidth="@integer/snapshot_max_width"
|[`cameraSessionType`](#camerasessiontype)|`setSessionType()`|`picture` `video`|`picture`| app:cameraSnapshotMaxHeight="@integer/snapshot_max_height"
|[`cameraFacing`](#camerafacing)|`setFacing()`|`back` `front`|`back`| app:cameraFrameProcessingMaxWidth="@integer/processing_max_width"
|[`cameraFlash`](#cameraflash)|`setFlash()`|`off` `on` `auto` `torch`|`off`| app:cameraFrameProcessingMaxHeight="@integer/processing_max_height"
|[`cameraGrid`](#cameragrid)|`setGrid()`|`off` `draw3x3` `draw4x4` `drawPhi`|`off`| app:cameraFrameProcessingFormat="@integer/processing_format"
|[`cameraCropOutput`](#cameracropoutput)|`setCropOutput()`|`true` `false`|`false`| app:cameraFrameProcessingPoolSize="@integer/processing_pool_size"
|[`cameraJpegQuality`](#camerajpegquality)|`setJpegQuality()`|`0 < n <= 100`|`100`| app:cameraFrameProcessingExecutors="@integer/processing_executors"
|[`cameraVideoQuality`](#cameravideoquality)|`setVideoQuality()`|`lowest` `highest` `maxQvga` `max480p` `max720p` `max1080p` `max2160p`|`max480p`| app:cameraVideoBitRate="@integer/video_bit_rate"
|[`cameraVideoCodec`](#cameravideocodec)|`setVideoCodec()`|`deviceDefault` `h263` `h264`|`deviceDefault`| app:cameraAudioBitRate="@integer/audio_bit_rate"
|[`cameraWhiteBalance`](#camerawhitebalance)|`setWhiteBalance()`|`auto` `incandescent` `fluorescent` `daylight` `cloudy`|`auto`| app:cameraGestureTap="none|autoFocus|takePicture"
|[`cameraHdr`](#camerahdr)|`setHdr()`|`off` `on`|`off`| app:cameraGestureLongTap="none|autoFocus|takePicture"
|[`cameraAudio`](#cameraaudio)|`setAudio()`|`off` `on`|`on`| app:cameraGesturePinch="none|zoom|exposureCorrection|filterControl1|filterControl2"
|[`cameraPlaySounds`](#cameraplaysounds)|`setPlaySounds()`|`true` `false`|`true`| app:cameraGestureScrollHorizontal="none|zoom|exposureCorrection|filterControl1|filterControl2"
|[`cameraVideoMaxSize`](#cameravideomaxsize)|`setVideoMaxSize()`|number|`0`| app:cameraGestureScrollVertical="none|zoom|exposureCorrection|filterControl1|filterControl2"
|[`cameraVideoMaxDuration`](#cameravideomaxduration)|`setVideoMaxDuration()`|number|`0`| app:cameraEngine="camera1|camera2"
app:cameraPreview="glSurface|surface|texture"
#### cameraSessionType app:cameraPreviewFrameRate="@integer/preview_frame_rate"
app:cameraPreviewFrameRateExact="false|true"
What to capture - either picture or video. This has a couple of consequences: app:cameraFacing="back|front"
app:cameraHdr="on|off"
- Sizing: picture and preview size are chosen among the available picture or video sizes, app:cameraFlash="on|auto|torch|off"
depending on the flag. The picture size is chosen according to the given [size selector](#picture-size). app:cameraWhiteBalance="auto|cloudy|daylight|fluorescent|incandescent"
When `video`, in addition, we try to match the `videoQuality` aspect ratio. app:cameraMode="picture|video"
- Picture capturing: due to sizing behavior, capturing pictures in `video` mode might lead to app:cameraAudio="on|off|mono|stereo"
inconsistent results. In this case it is encouraged to use `captureSnapshot` instead, which will app:cameraGrid="draw3x3|draw4x4|drawPhi|off"
capture preview frames. This is fast and thus works well with slower camera sensors. app:cameraGridColor="@color/grid_color"
- Picture capturing: while recording a video, image capturing might work, but it is not guaranteed app:cameraPlaySounds="true|false"
(it's device dependent) app:cameraVideoMaxSize="@integer/video_max_size"
- Permission behavior: when requesting a `video` session, the record audio permission will be requested. app:cameraVideoMaxDuration="@integer/video_max_duration"
If this is needed, the audio permission should be added to your manifest or the app will crash. app:cameraVideoCodec="deviceDefault|h264|h263"
app:cameraAutoFocusResetDelay="@integer/autofocus_delay"
```java app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker"
cameraView.setSessionType(SessionType.PICTURE); app:cameraUseDeviceOrientation="true|false"
cameraView.setSessionType(SessionType.VIDEO); app:cameraFilter="@string/real_time_filter"
``` app:cameraPictureMetering="true|false"
app:cameraPictureSnapshotMetering="false|true"
#### cameraFacing app:cameraPictureFormat="jpeg|dng"
app:cameraRequestPermissions="true|false"
Which camera to use, either back facing or front facing. app:cameraExperimental="false|true">
```java <!-- Watermark! -->
cameraView.setFacing(Facing.BACK); <ImageView
cameraView.setFacing(Facing.FRONT); android:layout_width="wrap_content"
``` android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
#### cameraFlash android:src="@drawable/watermark"
app:layout_drawOnPreview="true|false"
Flash mode, either off, on, auto or *torch*. app:layout_drawOnPictureSnapshot="true|false"
app:layout_drawOnVideoSnapshot="true|false"/>
```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
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
```
- If you want your app to be installed only on devices that have a camera, you should add:
```xml
<uses-feature
android:name="android.hardware.camera"
android:required="true"/>
```
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 </com.otaliastudios.cameraview.CameraView>
CameraLogger.registerLogger(new Logger() {
@Override
public void log(@LogLevel int level, String tag, String message, @Nullable Throwable throwable) {
// For example...
Crashlytics.log(message);
}
});
``` ```
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, <a href="mailto:mat.iavarone@gmail.com">send an email.</a>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

@ -1,35 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.0-rc03'
// https://inthecheesefactory.com/blog/how-to-upload-library-to-jcenter-maven-central-as-dependency/en
// https://www.theguardian.com/technology/developer-blog/2016/dec/06/how-to-publish-an-android-library-a-mysterious-conversation
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0'
}
}
allprojects {
repositories {
jcenter()
google()
}
}
ext {
compileSdkVersion = 28
supportLibVersion = '28.0.0-rc02'
lifecycleVersion = '1.1.1'
minSdkVersion = 15
targetSdkVersion = 28
}
task clean(type: Delete) {
delete rootProject.buildDir
}

@ -0,0 +1,30 @@
buildscript {
extra["minSdkVersion"] = 15
extra["compileSdkVersion"] = 31
extra["targetSdkVersion"] = 31
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.0.3")
classpath("io.deepmedia.tools:publisher:0.6.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
tasks.register("clean", Delete::class) {
delete(buildDir)
}

@ -1,217 +0,0 @@
apply plugin: 'com.android.library'
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
// Required by bintray
version = '1.6.1'
group = 'com.otaliastudios'
//region android dependencies
android {
compileSdkVersion rootProject.ext.compileSdkVersion
// buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName project.version
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
debug {
testCoverageEnabled true
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main.java.srcDirs += 'src/main/options'
main.java.srcDirs += 'src/main/views'
main.java.srcDirs += 'src/main/utils'
}
}
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:1.10.19'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
api "com.android.support:exifinterface:$supportLibVersion"
api "android.arch.lifecycle:common:$lifecycleVersion"
implementation "com.android.support:support-annotations:$supportLibVersion"
}
//endregion
//region bintray
install {
repositories.mavenInstaller {
pom.project {
name 'CameraView'
description 'A well documented, high-level Android interface that makes capturing pictures ' +
'and videos easy, addressing most of the common issues and needs.'
url 'https://github.com/natario1/CameraView'
packaging 'aar'
groupId project.group
artifactId 'cameraview'
version project.version
licenses {
license {
name 'The Apache Software License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
scm {
connection 'https://github.com/natario1/CameraView.git'
developerConnection 'https://github.com/natario1/CameraView.git'
url 'https://github.com/natario1/CameraView'
}
developers {
developer {
id = 'natario'
name 'Natario'
}
}
}
}
}
def bintrayUser
def bintrayKey
def travis = System.getenv("TRAVIS")
if (travis) {
bintrayUser = System.getenv("BINTRAY_USER")
bintrayKey = System.getenv("BINTRAY_KEY")
} else {
Properties props = new Properties()
props.load(project.rootProject.file('local.properties').newDataInputStream())
bintrayUser = props.getProperty('bintray.user')
bintrayKey = props.get('bintray.key')
}
bintray {
// https://github.com/bintray/gradle-bintray-plugin
user = bintrayUser
key = bintrayKey
configurations = ['archives']
pkg {
repo = 'android'
name = 'CameraView'
licenses = ['Apache-2.0']
vcsUrl = 'https://github.com/natario1/CameraView.git'
publish = true
override = true
version {
name = project.version
desc = 'CameraView v. '+project.version
released = new Date()
vcsTag = 'v'+project.version
}
}
}
//endregion
//region javadoc and sources
// From official sample https://github.com/bintray/bintray-examples/blob/master/gradle-aar-example/build.gradle
task sourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.sourceFiles
}
task javadoc(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
classpath += project.files("${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
project.android.libraryVariants.all { variant ->
if (variant.name == 'release') {
classpath += files(variant.javaCompile.classpath)
}
}
exclude '**/BuildConfig.java'
exclude '**/R.java'
exclude '**/internal/**'
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives javadocJar
archives sourcesJar
}
//endregion
//region code coverage
// 1. running androidTests with connectedCheck will generate an .ec file
// in build/outputs/code-coverage/connected, plus the XML result in
// in build/reports/coverage/debug/report.xml .
// 2. running unit tests with testDebugUnitTest will just generate the .exec file.
// The JacocoReport task from the jacoco plugin can create the XML report out of it.
// to have a unified report, we just pass both the .exec and the .ec file
// to the jacoco task, so we get a unified XML report with total coverage.
// Reference: https://medium.com/@rafael_toledo/setting-up-an-unified-coverage-report-in-android-with-jacoco-robolectric-and-espresso-ffe239aaf3fa
apply plugin: 'jacoco'
def reportsDirectory = "$buildDir/reports/"
jacoco {
toolVersion = "0.8.1"
reportsDir = file(reportsDirectory)
}
task mergedCoverageReport(type: JacocoReport) {
dependsOn "testDebugUnitTest"
dependsOn "connectedCheck"
def testData = "jacoco/testDebugUnitTest.exec"
def androidTestData = "outputs/code-coverage/connected/*coverage.ec"
executionData = fileTree(dir: "$buildDir", includes: [testData, androidTestData])
// Sources.
sourceDirectories = android.sourceSets.main.java.sourceFiles
// Add BuildConfig and R.
additionalSourceDirs = files([
"$buildDir/generated/source/buildConfig/debug",
"$buildDir/generated/source/r/debug"
])
// Classes.
def debugDir = "$buildDir/intermediates/classes/debug"
def filter = ['**/R.class', '**/R$*.class', '**/*$ViewInjector*.*',
'**/BuildConfig.*', '**/Manifest*.*']
classDirectories = fileTree(dir: debugDir, excludes: filter);
reports.xml.enabled = true
reports.html.enabled = true
reports.xml.destination = "$reportsDirectory/mergedCoverageReport/report.xml"
}
//endregion
// export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home
// To deploy ./gradlew bintrayUpload

@ -0,0 +1,137 @@
import io.deepmedia.tools.publisher.common.License
import io.deepmedia.tools.publisher.common.Release
import io.deepmedia.tools.publisher.common.GithubScm
plugins {
id("com.android.library")
id("kotlin-android")
id("io.deepmedia.tools.publisher")
id("jacoco")
}
android {
compileSdk = property("compileSdkVersion") as Int
defaultConfig {
minSdk = property("minSdkVersion") as Int
targetSdk = property("targetSdkVersion") as Int
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["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.1")
testImplementation("org.mockito:mockito-inline:2.28.2")
androidTestImplementation("androidx.test:runner:1.4.0")
androidTestImplementation("androidx.test:rules:1.4.0")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("org.mockito:mockito-android:2.28.2")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
api("androidx.exifinterface:exifinterface:1.3.3")
api("androidx.lifecycle:lifecycle-common:2.3.1")
api("com.google.android.gms:play-services-tasks:17.2.1")
implementation("androidx.annotation:annotation:1.2.0")
implementation("com.otaliastudios.opengl:egloo:0.6.1")
}
// 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.scm = GithubScm("natario1", "CameraView")
project.addLicense(License.APACHE_2_0)
project.addDeveloper("natario1", "mat.iavarone@gmail.com")
release.sources = Release.SOURCES_AUTO
release.docs = Release.DOCS_AUTO
release.version = "2.7.2"
directory()
sonatype {
auth.user = "SONATYPE_USER"
auth.password = "SONATYPE_PASSWORD"
signing.key = "SIGNING_KEY"
signing.password = "SIGNING_PASSWORD"
}
sonatype("snapshot") {
repository = io.deepmedia.tools.publisher.sonatype.Sonatype.OSSRH_SNAPSHOT_1
release.version = "latest-SNAPSHOT"
auth.user = "SONATYPE_USER"
auth.password = "SONATYPE_PASSWORD"
signing.key = "SIGNING_KEY"
signing.password = "SIGNING_PASSWORD"
}
}
// 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/outputs/unit_test_code_coverage/debugUnitTest/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.required.set(true)
reports.xml.required.set(true)
reports.html.outputLocation.set(file("$coverageOutputDir/html"))
reports.xml.outputLocation.set(file("$coverageOutputDir/xml/report.xml"))
}

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

@ -1,63 +1,50 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview;
import android.annotation.SuppressLint;
import android.app.KeyguardManager; import android.app.KeyguardManager;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.PowerManager; import android.os.PowerManager;
import android.support.test.InstrumentationRegistry;
import android.support.test.annotation.UiThreadTest; import androidx.annotation.NonNull;
import android.support.test.espresso.core.internal.deps.guava.collect.ObjectArrays; import androidx.test.platform.app.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule; import androidx.test.rule.GrantPermissionRule;
import android.view.View;
import com.otaliastudios.cameraview.tools.Op;
import org.junit.After; import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Rule; import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.mockito.stubbing.Stubber; import org.mockito.stubbing.Stubber;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import static android.content.Context.KEYGUARD_SERVICE; import static android.content.Context.KEYGUARD_SERVICE;
import static android.content.Context.POWER_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.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class BaseTest { public class BaseTest {
public static CameraLogger LOG = CameraLogger.create("Test");
private static KeyguardManager.KeyguardLock keyguardLock; private static KeyguardManager.KeyguardLock keyguardLock;
private static PowerManager.WakeLock wakeLock; 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 // https://github.com/linkedin/test-butler/blob/bc2bb4df13d0a554d2e2b0ea710795017717e710/test-butler-app/src/main/java/com/linkedin/android/testbutler/ButlerService.java#L121
@BeforeClass @BeforeClass
@SuppressWarnings("MissingPermission") public static void beforeClass_wakeUp() {
public static void wakeUp() {
CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE); CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE);
// Acquire a keyguard lock to prevent the lock screen from randomly appearing and breaking tests // 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 = keyguardManager.newKeyguardLock("CameraViewLock");
keyguardLock.disableKeyguard(); keyguardLock.disableKeyguard();
// Acquire a wake lock to prevent the cpu from going to sleep and breaking tests // 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 wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK
| PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ACQUIRE_CAUSES_WAKEUP
| PowerManager.ON_AFTER_RELEASE, "CameraViewLock"); | PowerManager.ON_AFTER_RELEASE, "CameraViewLock");
@ -65,109 +52,68 @@ public class BaseTest {
} }
@AfterClass @AfterClass
@SuppressWarnings("MissingPermission") public static void afterClass_releaseWakeUp() {
public static void releaseWakeUp() { CameraLogger.setLogLevel(CameraLogger.LEVEL_ERROR);
wakeLock.release(); wakeLock.release();
keyguardLock.reenableKeyguard(); keyguardLock.reenableKeyguard();
} }
public static void ui(Runnable runnable) { /**
InstrumentationRegistry.getInstrumentation().runOnMainSync(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.
public static void uiAsync(Runnable runnable) { */
new Handler(Looper.getMainLooper()).post(runnable); @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(); return InstrumentationRegistry.getInstrumentation().getContext();
} }
public static void uiRequestLayout(final View view) { protected static void uiSync(Runnable runnable) {
ui(new Runnable() { InstrumentationRegistry.getInstrumentation().runOnMainSync(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);
} }
public static byte[] mockJpeg(int width, int height) { @SuppressWarnings("unused")
Bitmap source = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); protected static void uiAsync(Runnable runnable) {
ByteArrayOutputStream os = new ByteArrayOutputStream(); new Handler(Looper.getMainLooper()).post(runnable);
source.compress(Bitmap.CompressFormat.JPEG, 100, os);
return os.toByteArray();
} }
public static YuvImage mockYuv(int width, int height) { @SuppressWarnings("unused")
YuvImage y = mock(YuvImage.class); protected static void waitUiIdle() {
when(y.getWidth()).thenReturn(width); InstrumentationRegistry.getInstrumentation().waitForIdleSync();
when(y.getHeight()).thenReturn(height);
when(y.compressToJpeg(any(Rect.class), anyInt(), any(OutputStream.class))).thenAnswer(new Answer<Boolean>() {
@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;
} }
public static Stubber doCountDown(final CountDownLatch latch) { @NonNull
protected static Stubber doCountDown(@NonNull final CountDownLatch latch) {
return doAnswer(new Answer<Object>() { return doAnswer(new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) {
latch.countDown(); latch.countDown();
return null; return null;
} }
}); });
} }
public static <T> Stubber doEndTask(final Task<T> task, final T response) { @NonNull
protected static <T> Stubber doEndOp(final Op<T> op, final T response) {
return doAnswer(new Answer<Object>() { return doAnswer(new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) {
task.end(response); op.controller().end(response);
return null; return null;
} }
}); });
} }
public static Stubber doEndTask(final Task task, final int withReturnArgument) { @NonNull
return doAnswer(new Answer<Object>() { protected static <T> Stubber doEndOp(final Op<T> op, final int withReturnArgument) {
@Override return op.controller().from(withReturnArgument);
public Object answer(InvocationOnMock invocation) throws Throwable {
Object o = invocation.getArguments()[withReturnArgument];
//noinspection unchecked
task.end(o);
return null;
}
});
} }
} }

@ -1,15 +1,15 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview;
import android.support.test.filters.SmallTest; import com.otaliastudios.cameraview.tools.Op;
import android.support.test.runner.AndroidJUnit4;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -24,11 +24,13 @@ public class CameraLoggerTest extends BaseTest {
@Before @Before
public void setUp() { public void setUp() {
CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE); CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE);
CameraLogger.unregisterLogger(CameraLogger.sAndroidLogger); // Avoid writing into Logs during these tests
logger = CameraLogger.create(loggerTag); logger = CameraLogger.create(loggerTag);
} }
@After @After
public void tearDown() { public void tearDown() {
CameraLogger.registerLogger(CameraLogger.sAndroidLogger);
logger = null; logger = null;
} }
@ -107,27 +109,21 @@ public class CameraLoggerTest extends BaseTest {
CameraLogger.Logger mock = mock(CameraLogger.Logger.class); CameraLogger.Logger mock = mock(CameraLogger.Logger.class);
CameraLogger.registerLogger(mock); CameraLogger.registerLogger(mock);
final Task<Throwable> task = new Task<>(); final Op<Throwable> op = new Op<>(false);
doAnswer(new Answer() { doEndOp(op, 3)
@Override .when(mock)
public Object answer(InvocationOnMock invocation) throws Throwable { .log(anyInt(), anyString(), anyString(), any(Throwable.class));
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(); op.listen();
logger.e("Got no error."); logger.e("Got no error.");
assertNull(task.await(100)); assertNull(op.await(100));
task.listen(); op.listen();
logger.e("Got error:", new RuntimeException("")); 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"); logger.e("Got", new RuntimeException(""), "while starting");
assertNotNull(task.await(100)); assertNotNull(op.await(100));
} }
} }

@ -1,21 +1,26 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview;
import android.annotation.TargetApi;
import android.app.Instrumentation;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Color; import android.graphics.Color;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest; import com.otaliastudios.cameraview.tools.Op;
import android.support.test.internal.runner.InstrumentationConnection;
import android.support.test.runner.AndroidJUnit4; 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.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream; 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.mockito.Mockito.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -29,35 +34,75 @@ public class CameraUtilsTest extends BaseTest {
Context context = mock(Context.class); Context context = mock(Context.class);
PackageManager pm = mock(PackageManager.class); PackageManager pm = mock(PackageManager.class);
when(context.getPackageManager()).thenReturn(pm); 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)); assertTrue(CameraUtils.hasCameras(context));
when(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)).thenReturn(false);
when(pm.hasSystemFeature(anyString())).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)); assertFalse(CameraUtils.hasCameras(context));
} }
// Encodes bitmap and decodes again using our utility. @NonNull
private Task<Bitmap> encodeDecodeTask(Bitmap source) { private Op<String> writeAndReadString(@NonNull String data) {
return encodeDecodeTask(source, 0, 0); final File file = new File(getContext().getFilesDir(), "string.txt");
final byte[] bytes = data.getBytes(Charset.forName("UTF-8"));
final Op<String> 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<String> op = writeAndReadString("testString");
String result = op.await(2000);
assertEquals("testString", result);
}
// Encodes bitmap and decodes again using our utility. // Encodes bitmap and decodes again using our utility.
private Task<Bitmap> encodeDecodeTask(Bitmap source, final int maxWidth, final int maxHeight) { private Op<Bitmap> encodeDecodeTask(@NonNull Bitmap source, final int maxWidth, final int maxHeight, boolean async) {
final ByteArrayOutputStream os = new ByteArrayOutputStream(); final ByteArrayOutputStream os = new ByteArrayOutputStream();
// Using lossy JPG we can't have strict comparison of values after compression. // Using lossy JPG we can't have strict comparison of values after compression.
source.compress(Bitmap.CompressFormat.PNG, 100, os); source.compress(Bitmap.CompressFormat.PNG, 100, os);
final byte[] data = os.toByteArray(); final byte[] data = os.toByteArray();
final Task<Bitmap> decode = new Task<>(true); final Op<Bitmap> decode = new Op<>();
final CameraUtils.BitmapCallback callback = new CameraUtils.BitmapCallback() { if (async) {
final BitmapCallback callback = new BitmapCallback() {
@Override @Override
public void onBitmapReady(Bitmap bitmap) { public void onBitmapReady(Bitmap bitmap) {
decode.end(bitmap); decode.controller().end(bitmap);
} }
}; };
// Run on ui because it involves handlers. // Run on ui because it involves handlers.
ui(new Runnable() { uiSync(new Runnable() {
@Override @Override
public void run() { public void run() {
if (maxWidth > 0 && maxHeight > 0) { if (maxWidth > 0 && maxHeight > 0) {
@ -67,6 +112,15 @@ public class CameraUtilsTest extends BaseTest {
} }
} }
}); });
} 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; return decode;
} }
@ -76,7 +130,7 @@ public class CameraUtilsTest extends BaseTest {
Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
source.setPixel(0, 0, color); source.setPixel(0, 0, color);
Task<Bitmap> decode = encodeDecodeTask(source); Op<Bitmap> decode = encodeDecodeTask(source, 0, 0, true);
Bitmap other = decode.await(800); Bitmap other = decode.await(800);
assertNotNull(other); assertNotNull(other);
assertEquals(100, w); assertEquals(100, w);
@ -85,8 +139,23 @@ public class CameraUtilsTest extends BaseTest {
assertEquals(0, other.getPixel(0, h-1)); assertEquals(0, other.getPixel(0, h-1));
assertEquals(0, other.getPixel(w-1, 0)); assertEquals(0, other.getPixel(w-1, 0));
assertEquals(0, other.getPixel(w-1, h-1)); assertEquals(0, other.getPixel(w-1, h-1));
}
// TODO: improve when we add EXIF writing to byte arrays @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);
Op<Bitmap> 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() { public void testDecodeDownscaledBitmap() {
int width = 1000, height = 2000; int width = 1000, height = 2000;
Bitmap source = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Bitmap source = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Task<Bitmap> task; Op<Bitmap> op;
Bitmap other; Bitmap other;
task = encodeDecodeTask(source, 100, 100); op = encodeDecodeTask(source, 100, 100, true);
other = task.await(800); other = op.await(800);
assertNotNull(other); assertNotNull(other);
assertTrue(other.getWidth() <= 100); assertTrue(other.getWidth() <= 100);
assertTrue(other.getHeight() <= 100); assertTrue(other.getHeight() <= 100);
task = encodeDecodeTask(source, Integer.MAX_VALUE, Integer.MAX_VALUE); op = encodeDecodeTask(source, Integer.MAX_VALUE, Integer.MAX_VALUE, true);
other = task.await(800); other = op.await(800);
assertNotNull(other); assertNotNull(other);
assertTrue(other.getWidth() == width); assertEquals(other.getWidth(), width);
assertTrue(other.getHeight() == height); assertEquals(other.getHeight(), height);
task = encodeDecodeTask(source, 6000, 6000); op = encodeDecodeTask(source, 6000, 6000, true);
other = task.await(800); other = op.await(800);
assertNotNull(other); assertNotNull(other);
assertTrue(other.getWidth() == width); assertEquals(other.getWidth(), width);
assertTrue(other.getHeight() == height); assertEquals(other.getHeight(), height);
} }
} }

@ -2,75 +2,95 @@ package com.otaliastudios.cameraview;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PointF; 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 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.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; 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 junit.framework.Assert.assertNotNull;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyFloat;
import static org.mockito.Matchers.anyInt; 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.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
/**
* Tests {@link CameraView#mCameraCallbacks} dispatch functions.
*/
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@MediumTest @MediumTest
public class CameraViewCallbacksTest extends BaseTest { public class CameraViewCallbacksTest extends BaseTest {
private final static long DELAY = 500;
private CameraView camera; private CameraView camera;
private CameraListener listener; private CameraListener listener;
private FrameProcessor processor; private FrameProcessor processor;
private MockCameraController mockController; private MockCameraEngine mockController;
private MockCameraPreview mockPreview; private MockCameraPreview mockPreview;
private Task<Boolean> task; private Op<Boolean> op;
@Before @Before
public void setUp() { public void setUp() {
ui(new Runnable() { uiSync(new Runnable() {
@Override @Override
public void run() { public void run() {
Context context = context(); Context context = getContext();
listener = mock(CameraListener.class); listener = mock(CameraListener.class);
processor = mock(FrameProcessor.class); processor = mock(FrameProcessor.class);
camera = new CameraView(context) { camera = new CameraView(context) {
@NonNull
@Override @Override
protected CameraController instantiateCameraController(CameraCallbacks callbacks) { protected CameraEngine instantiateCameraEngine(@NonNull Engine engine, @NonNull CameraEngine.Callback callback) {
mockController = new MockCameraController(callbacks); mockController = new MockCameraEngine(callback);
return mockController; return mockController;
} }
@NonNull
@Override @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); mockPreview = new MockCameraPreview(context, container);
return mockPreview; return mockPreview;
} }
@Override @Override
protected boolean checkPermissions(SessionType sessionType, Audio audio) { protected boolean checkPermissions(@NonNull Audio audio) {
return true; return true;
} }
}; };
camera.instantiatePreview(); camera.doInstantiatePreview();
camera.addCameraListener(listener); camera.addCameraListener(listener);
camera.addFrameProcessor(processor); camera.addFrameProcessor(processor);
task = new Task<>(true); op = new Op<>();
} }
}); });
} }
@ -83,211 +103,176 @@ public class CameraViewCallbacksTest extends BaseTest {
listener = null; 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 @Test
public void testDontDispatchIfRemoved() { public void testDontDispatchIfRemoved() {
camera.removeCameraListener(listener); camera.removeCameraListener(listener);
completeTask().when(listener).onCameraOpened(null); CameraOptions options = mock(CameraOptions.class);
camera.mCameraCallbacks.dispatchOnCameraOpened(null); doEndOp(op, true).when(listener).onCameraOpened(options);
camera.mCameraCallbacks.dispatchOnCameraOpened(options);
assertNull(task.await(200)); assertNull(op.await(DELAY));
verify(listener, never()).onCameraOpened(null); verify(listener, never()).onCameraOpened(options);
} }
@Test @Test
public void testDontDispatchIfCleared() { public void testDontDispatchIfCleared() {
camera.clearCameraListeners(); camera.clearCameraListeners();
completeTask().when(listener).onCameraOpened(null); CameraOptions options = mock(CameraOptions.class);
camera.mCameraCallbacks.dispatchOnCameraOpened(null); doEndOp(op, true).when(listener).onCameraOpened(options);
camera.mCameraCallbacks.dispatchOnCameraOpened(options);
assertNull(task.await(200)); assertNull(op.await(DELAY));
verify(listener, never()).onCameraOpened(null); verify(listener, never()).onCameraOpened(options);
} }
@Test @Test
public void testDispatchOnCameraOpened() { public void testDispatchOnCameraOpened() {
completeTask().when(listener).onCameraOpened(null); CameraOptions options = mock(CameraOptions.class);
camera.mCameraCallbacks.dispatchOnCameraOpened(null); doEndOp(op, true).when(listener).onCameraOpened(options);
camera.mCameraCallbacks.dispatchOnCameraOpened(options);
assertNotNull(task.await(200)); assertNotNull(op.await(DELAY));
verify(listener, times(1)).onCameraOpened(null); verify(listener, times(1)).onCameraOpened(options);
} }
@Test @Test
public void testDispatchOnCameraClosed() { public void testDispatchOnCameraClosed() {
completeTask().when(listener).onCameraClosed(); doEndOp(op, true).when(listener).onCameraClosed();
camera.mCameraCallbacks.dispatchOnCameraClosed(); camera.mCameraCallbacks.dispatchOnCameraClosed();
assertNotNull(task.await(200)); assertNotNull(op.await(DELAY));
verify(listener, times(1)).onCameraClosed(); 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 @Test
public void testDispatchOnVideoTaken() { public void testDispatchOnVideoTaken() {
completeTask().when(listener).onVideoTaken(null); VideoResult.Stub stub = new VideoResult.Stub();
camera.mCameraCallbacks.dispatchOnVideoTaken(null); doEndOp(op, true).when(listener).onVideoTaken(any(VideoResult.class));
camera.mCameraCallbacks.dispatchOnVideoTaken(stub);
assertNotNull(task.await(200)); assertNotNull(op.await(DELAY));
verify(listener, times(1)).onVideoTaken(null); 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 @Test
public void testDispatchOnZoomChanged() { 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); camera.mCameraCallbacks.dispatchOnZoomChanged(0f, null);
assertNotNull(task.await(200)); assertNotNull(op.await(DELAY));
verify(listener, times(1)).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class)); verify(listener, times(1)).onZoomChanged(eq(0f), eq(new float[]{0, 1}), nullable(PointF[].class));
} }
@Test @Test
public void testDispatchOnExposureCorrectionChanged() { public void testDispatchOnExposureCorrectionChanged() {
completeTask().when(listener).onExposureCorrectionChanged(0f, null, null); float[] bounds = new float[]{};
camera.mCameraCallbacks.dispatchOnExposureCorrectionChanged(0f, null, null); doEndOp(op, true).when(listener).onExposureCorrectionChanged(0f, bounds, null);
camera.mCameraCallbacks.dispatchOnExposureCorrectionChanged(0f, bounds, null);
assertNotNull(task.await(200)); assertNotNull(op.await(DELAY));
verify(listener, times(1)).onExposureCorrectionChanged(0f, null, null); verify(listener, times(1)).onExposureCorrectionChanged(0f, bounds, null);
} }
@Test @Test
public void testDispatchOnFocusStart() { public void testDispatchOnFocusStart() {
// Enable tap gesture. // Enable tap gesture.
// Can't mock package protected. camera.mTapGestureLayout = mock(TapGestureLayout.class); // Can't mock package protected. camera.mTapGestureFinder = mock(TapGestureLayout.class);
camera.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER); 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(); PointF point = new PointF();
completeTask().when(listener).onFocusStart(point); doEndOp(op, true).when(listener).onAutoFocusStart(point);
camera.mCameraCallbacks.dispatchOnFocusStart(Gesture.TAP, point); camera.mCameraCallbacks.dispatchOnFocusStart(Gesture.TAP, point);
assertNotNull(task.await(200)); assertNotNull(op.await(DELAY));
verify(listener, times(1)).onFocusStart(point); verify(listener, times(1)).onAutoFocusStart(point);
// Can't mock package protected. verify(camera.mTapGestureLayout, times(1)).onFocusStart(point); verify(marker, times(1)).onAutoFocusStart(AutoFocusTrigger.GESTURE, point);
verify(markerLayout, times(1)).onEvent(eq(MarkerLayout.TYPE_AUTOFOCUS), any(PointF[].class));
} }
@Test @Test
public void testDispatchOnFocusEnd() { public void testDispatchOnFocusEnd() {
// Enable tap gesture. // Enable tap gesture.
// Can't mock package protected. camera.mTapGestureLayout = mock(TapGestureLayout.class); // Can't mock package protected. camera.mTapGestureFinder = mock(TapGestureLayout.class);
camera.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER); camera.mapGesture(Gesture.TAP, GestureAction.AUTO_FOCUS);
AutoFocusMarker marker = mock(AutoFocusMarker.class);
camera.setAutoFocusMarker(marker);
PointF point = new PointF(); PointF point = new PointF();
boolean success = true; boolean success = true;
completeTask().when(listener).onFocusEnd(success, point); doEndOp(op, true).when(listener).onAutoFocusEnd(success, point);
camera.mCameraCallbacks.dispatchOnFocusEnd(Gesture.TAP, success, point); camera.mCameraCallbacks.dispatchOnFocusEnd(Gesture.TAP, success, point);
assertNotNull(task.await(200)); assertNotNull(op.await(DELAY));
verify(listener, times(1)).onFocusEnd(success, point); verify(listener, times(1)).onAutoFocusEnd(success, point);
// Can't mock package protected. verify(camera.mTapGestureLayout, times(1)).onFocusEnd(success); verify(marker, times(1)).onAutoFocusEnd(AutoFocusTrigger.GESTURE, success, point);
// Can't mock package protected. verify(camera.mTapGestureFinder, times(1)).onAutoFocusEnd(success);
} }
@Test @Test
public void testOrientationCallbacks() { public void testOrientationCallbacks() {
completeTask().when(listener).onOrientationChanged(anyInt()); doEndOp(op, true).when(listener).onOrientationChanged(anyInt());
camera.mCameraCallbacks.onDeviceOrientationChanged(90); camera.mCameraCallbacks.onDeviceOrientationChanged(90);
assertNotNull(task.await(200)); assertNotNull(op.await(DELAY));
verify(listener, times(1)).onOrientationChanged(anyInt()); 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 @Test
public void testCameraError() { public void testCameraError() {
CameraException error = new CameraException(new RuntimeException("Error")); CameraException error = new CameraException(new RuntimeException("Error"));
completeTask().when(listener).onCameraError(error); doEndOp(op, true).when(listener).onCameraError(error);
camera.mCameraCallbacks.dispatchError(error); camera.mCameraCallbacks.dispatchError(error);
assertNotNull(task.await(200)); assertNotNull(op.await(DELAY));
verify(listener, times(1)).onCameraError(error); 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<byte[]> 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 @Test
public void testProcessFrame() { public void testProcessFrame() {
Frame mock = mock(Frame.class); Frame mock = mock(Frame.class);
completeTask().when(processor).process(mock); doEndOp(op, true).when(processor).process(mock);
camera.mCameraCallbacks.dispatchFrame(mock); camera.mCameraCallbacks.dispatchFrame(mock);
assertNotNull(task.await(200)); assertNotNull(op.await(DELAY));
verify(processor, times(1)).process(mock); verify(processor, times(1)).process(mock);
} }
} }

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

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

@ -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<T extends GestureLayout> extends BaseTest {
protected abstract T create(Context context);
@Rule
public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
protected T layout;
protected Task<Gesture> 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.<View>is(layout))
.inRoot(RootMatchers.withDecorView(
Matchers.is(rule.getActivity().getWindow().getDecorView())));
}
}

@ -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<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
private CameraView camera;
private Camera1 controller;
private CameraListener listener;
private Task<Throwable> 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<CameraOptions> 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<Boolean> 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<Boolean> 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<byte[]> 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<PointF> 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
}

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

@ -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> T map(Flash flash) { return null; }
<T> T map(Facing facing) { return null; }
<T> T map(WhiteBalance whiteBalance) { return null; }
<T> T map(Hdr hdr) { return null; }
<T> Flash unmapFlash(T cameraConstant) { return null; }
<T> Facing unmapFacing(T cameraConstant) { return null; }
<T> WhiteBalance unmapWhiteBalance(T cameraConstant) { return null; }
<T> 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);
}
}

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

@ -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<View, Void> {
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<Void> getOutputClass() {
return null;
}
@Override
Void getOutput() {
return 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);
}
}

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

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

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

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

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

@ -2,18 +2,18 @@ package com.otaliastudios.cameraview;
import android.app.Activity; import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Context;
import android.graphics.Point; import android.graphics.Point;
import android.os.Bundle; import android.os.Bundle;
import android.os.PersistableBundle;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import com.otaliastudios.cameraview.size.Size;
import static android.view.ViewGroup.LayoutParams.*; import static android.view.ViewGroup.LayoutParams.*;
public class TestActivity extends Activity { public class TestActivity extends Activity {

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

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

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

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

@ -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<CameraOptions> onStartEngine() {
return Tasks.forResult(mCameraOptions);
}
@NonNull
@Override
protected Task<Void> onStopEngine() {
return Tasks.forResult(null);
}
@NonNull
@Override
protected Task<Void> onStartBind() {
return Tasks.forResult(null);
}
@NonNull
@Override
protected Task<Void> onStopBind() {
return Tasks.forResult(null);
}
@NonNull
@Override
protected Task<Void> onStartPreview() {
return Tasks.forResult(null);
}
@NonNull
@Override
protected Task<Void> 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<Void> change = getOrchestrator().scheduleStateChange(getState(),
state,
false,
new Callable<Task<Void>>() {
@Override
public Task<Void> 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<Size> getPreviewStreamAvailableSizes() {
return new ArrayList<>();
}
@NonNull
@Override
protected List<Size> 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;
}
}

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

@ -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<Pair<Integer, Integer>> 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<Flash> 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));
}
}

@ -1,9 +1,28 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview.engine.options;
import android.graphics.ImageFormat;
import android.hardware.Camera; 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.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -23,22 +42,26 @@ import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class CameraOptions1Test extends BaseTest { public class Camera1OptionsTest extends BaseTest {
@Test @Test
public void testEmpty() { 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.getSupportedPictureAspectRatios().isEmpty());
assertTrue(o.getSupportedPictureSizes().isEmpty()); assertTrue(o.getSupportedPictureSizes().isEmpty());
assertTrue(o.getSupportedWhiteBalance().isEmpty()); assertTrue(o.getSupportedWhiteBalance().isEmpty());
assertTrue(o.getSupportedFlash().isEmpty()); assertEquals(1, o.getSupportedFlash().size()); // Flash.OFF is always there
assertTrue(o.getSupportedHdr().isEmpty()); assertEquals(1, o.getSupportedHdr().size()); // Hdr.OFF is always there
assertFalse(o.isAutoFocusSupported()); assertFalse(o.isAutoFocusSupported());
assertFalse(o.isExposureCorrectionSupported()); assertFalse(o.isExposureCorrectionSupported());
assertFalse(o.isVideoSnapshotSupported());
assertFalse(o.isZoomSupported()); assertFalse(o.isZoomSupported());
assertEquals(o.getExposureCorrectionMaxValue(), 0f, 0); assertEquals(o.getExposureCorrectionMaxValue(), 0f, 0);
assertEquals(o.getExposureCorrectionMinValue(), 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) { private Camera.Size mockCameraSize(int width, int height) {
@ -58,8 +81,8 @@ public class CameraOptions1Test extends BaseTest {
); );
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedPictureSizes()).thenReturn(sizes); when(params.getSupportedPictureSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Set<Size> supportedSizes = o.getSupportedPictureSizes(); Collection<Size> supportedSizes = o.getSupportedPictureSizes();
assertEquals(supportedSizes.size(), sizes.size()); assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
Size internalSize = new Size(size.width, size.height); Size internalSize = new Size(size.width, size.height);
@ -77,8 +100,8 @@ public class CameraOptions1Test extends BaseTest {
); );
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedPictureSizes()).thenReturn(sizes); when(params.getSupportedPictureSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, true); CameraOptions o = new Camera1Options(params, 0, true);
Set<Size> supportedSizes = o.getSupportedPictureSizes(); Collection<Size> supportedSizes = o.getSupportedPictureSizes();
assertEquals(supportedSizes.size(), sizes.size()); assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
Size internalSize = new Size(size.width, size.height).flip(); 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); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedPictureSizes()).thenReturn(sizes); when(params.getSupportedPictureSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Set<AspectRatio> supportedRatios = o.getSupportedPictureAspectRatios(); Collection<AspectRatio> 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<Camera.Size> 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<Size> 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<Camera.Size> 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<Size> 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<Camera.Size> 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<Size> 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<Camera.Size> sizes = Arrays.asList(
mockCameraSize(10, 20),
mockCameraSize(5, 5),
mockCameraSize(16, 9),
mockCameraSize(20, 40)
);
Set<AspectRatio> 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<AspectRatio> supportedRatios = o.getSupportedVideoAspectRatios();
assertEquals(supportedRatios.size(), expected.size()); assertEquals(supportedRatios.size(), expected.size());
for (AspectRatio ratio : expected) { for (AspectRatio ratio : expected) {
assertTrue(supportedRatios.contains(ratio)); assertTrue(supportedRatios.contains(ratio));
@ -118,12 +227,13 @@ public class CameraOptions1Test extends BaseTest {
when(params.getMaxExposureCompensation()).thenReturn(0); when(params.getMaxExposureCompensation()).thenReturn(0);
when(params.getMinExposureCompensation()).thenReturn(0); when(params.getMinExposureCompensation()).thenReturn(0);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
assertFalse(o.supports(GestureAction.FOCUS)); assertFalse(o.supports(GestureAction.AUTO_FOCUS));
assertFalse(o.supports(GestureAction.FOCUS_WITH_MARKER)); assertTrue(o.supports(GestureAction.TAKE_PICTURE));
assertTrue(o.supports(GestureAction.CAPTURE));
assertTrue(o.supports(GestureAction.NONE)); assertTrue(o.supports(GestureAction.NONE));
assertTrue(o.supports(GestureAction.ZOOM)); 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)); assertFalse(o.supports(GestureAction.EXPOSURE_CORRECTION));
} }
@ -131,15 +241,17 @@ public class CameraOptions1Test extends BaseTest {
public void testAlwaysSupportedControls() { public void testAlwaysSupportedControls() {
// Grid, VideoQuality, SessionType and Audio are always supported. // Grid, VideoQuality, SessionType and Audio are always supported.
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Collection<Grid> grids = o.getSupportedControls(Grid.class); Collection<Grid> grids = o.getSupportedControls(Grid.class);
Collection<VideoQuality> video = o.getSupportedControls(VideoQuality.class); Collection<VideoCodec> video = o.getSupportedControls(VideoCodec.class);
Collection<SessionType> sessions = o.getSupportedControls(SessionType.class); Collection<AudioCodec> audioCodecs = o.getSupportedControls(AudioCodec.class);
Collection<Mode> sessions = o.getSupportedControls(Mode.class);
Collection<Audio> audio = o.getSupportedControls(Audio.class); Collection<Audio> audio = o.getSupportedControls(Audio.class);
assertEquals(grids.size(), Grid.values().length); assertEquals(grids.size(), Grid.values().length);
assertEquals(video.size(), VideoQuality.values().length); assertEquals(video.size(), VideoCodec.values().length);
assertEquals(sessions.size(), SessionType.values().length); assertEquals(audioCodecs.size(), AudioCodec.values().length);
assertEquals(sessions.size(), Mode.values().length);
assertEquals(audio.size(), Audio.values().length); assertEquals(audio.size(), Audio.values().length);
} }
@ -152,12 +264,12 @@ public class CameraOptions1Test extends BaseTest {
supported.add(cameraInfo.facing); supported.add(cameraInfo.facing);
} }
CameraOptions o = new CameraOptions(mock(Camera.Parameters.class), false); CameraOptions o = new Camera1Options(mock(Camera.Parameters.class), 0, false);
Mapper m = new Mapper.Mapper1(); Camera1Mapper m = Camera1Mapper.get();
Collection<Facing> s = o.getSupportedControls(Facing.class); Collection<Facing> s = o.getSupportedControls(Facing.class);
assertEquals(s.size(), supported.size()); assertEquals(s.size(), supported.size());
for (Facing facing : s) { for (Facing facing : s) {
assertTrue(supported.contains(m.<Integer>map(facing))); assertTrue(supported.contains(m.mapFacing(facing)));
assertTrue(o.supports(facing)); assertTrue(o.supports(facing));
} }
} }
@ -171,7 +283,7 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters.WHITE_BALANCE_SHADE // Not supported Camera.Parameters.WHITE_BALANCE_SHADE // Not supported
)); ));
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Collection<WhiteBalance> w = o.getSupportedControls(WhiteBalance.class); Collection<WhiteBalance> w = o.getSupportedControls(WhiteBalance.class);
assertEquals(w.size(), 2); assertEquals(w.size(), 2);
assertTrue(w.contains(WhiteBalance.AUTO)); assertTrue(w.contains(WhiteBalance.AUTO));
@ -184,16 +296,20 @@ public class CameraOptions1Test extends BaseTest {
public void testFlash() { public void testFlash() {
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedFlashModes()).thenReturn(Arrays.asList( when(params.getSupportedFlashModes()).thenReturn(Arrays.asList(
Camera.Parameters.FLASH_MODE_OFF, // Supported
Camera.Parameters.FLASH_MODE_AUTO, // Supported Camera.Parameters.FLASH_MODE_AUTO, // Supported
Camera.Parameters.FLASH_MODE_TORCH, // Supported Camera.Parameters.FLASH_MODE_TORCH, // Supported
Camera.Parameters.FLASH_MODE_RED_EYE // Not supported Camera.Parameters.FLASH_MODE_RED_EYE // Not supported
)); ));
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Collection<Flash> f = o.getSupportedControls(Flash.class); Collection<Flash> f = o.getSupportedControls(Flash.class);
assertEquals(f.size(), 2); assertEquals(f.size(), 3);
assertTrue(f.contains(Flash.OFF));
assertTrue(f.contains(Flash.AUTO)); assertTrue(f.contains(Flash.AUTO));
assertTrue(f.contains(Flash.TORCH)); assertTrue(f.contains(Flash.TORCH));
assertTrue(o.supports(Flash.OFF));
assertTrue(o.supports(Flash.AUTO)); assertTrue(o.supports(Flash.AUTO));
assertTrue(o.supports(Flash.TORCH)); assertTrue(o.supports(Flash.TORCH));
} }
@ -207,11 +323,12 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters.SCENE_MODE_FIREWORKS // Not supported Camera.Parameters.SCENE_MODE_FIREWORKS // Not supported
)); ));
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Collection<Hdr> h = o.getSupportedControls(Hdr.class); Collection<Hdr> h = o.getSupportedControls(Hdr.class);
assertEquals(h.size(), 2); assertEquals(h.size(), 2);
assertTrue(h.contains(Hdr.OFF)); assertTrue(h.contains(Hdr.OFF));
assertTrue(h.contains(Hdr.ON)); assertTrue(h.contains(Hdr.ON));
assertTrue(o.supports(Hdr.OFF)); assertTrue(o.supports(Hdr.OFF));
assertTrue(o.supports(Hdr.ON)); assertTrue(o.supports(Hdr.ON));
} }
@ -221,9 +338,9 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.isVideoSnapshotSupported()).thenReturn(true); when(params.isVideoSnapshotSupported()).thenReturn(true);
when(params.isZoomSupported()).thenReturn(true); when(params.isZoomSupported()).thenReturn(true);
//noinspection ArraysAsListWithZeroOrOneArgument
when(params.getSupportedFocusModes()).thenReturn(Arrays.asList(Camera.Parameters.FOCUS_MODE_AUTO)); when(params.getSupportedFocusModes()).thenReturn(Arrays.asList(Camera.Parameters.FOCUS_MODE_AUTO));
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
assertTrue(o.isVideoSnapshotSupported());
assertTrue(o.isZoomSupported()); assertTrue(o.isZoomSupported());
assertTrue(o.isAutoFocusSupported()); assertTrue(o.isAutoFocusSupported());
} }
@ -234,10 +351,23 @@ public class CameraOptions1Test extends BaseTest {
when(params.getMaxExposureCompensation()).thenReturn(10); when(params.getMaxExposureCompensation()).thenReturn(10);
when(params.getMinExposureCompensation()).thenReturn(-10); when(params.getMinExposureCompensation()).thenReturn(-10);
when(params.getExposureCompensationStep()).thenReturn(0.5f); when(params.getExposureCompensationStep()).thenReturn(0.5f);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
assertTrue(o.isExposureCorrectionSupported()); assertTrue(o.isExposureCorrectionSupported());
assertEquals(o.getExposureCorrectionMinValue(), -10f * 0.5f, 0f); assertEquals(o.getExposureCorrectionMinValue(), -10f * 0.5f, 0f);
assertEquals(o.getExposureCorrectionMaxValue(), 10f * 0.5f, 0f); assertEquals(o.getExposureCorrectionMaxValue(), 10f * 0.5f, 0f);
} }
@Test
public void testPreviewFrameRate() {
Camera.Parameters params = mock(Camera.Parameters.class);
List<int[]> result = Arrays.asList(
new int[]{20000, 30000},
new int[]{30000, 60000},
new int[]{60000, 120000}
);
when(params.getSupportedPreviewFpsRange()).thenReturn(result);
CameraOptions o = new Camera1Options(params, 0, false);
assertEquals(20F, o.getPreviewFrameRateMinValue(), 0.001F);
assertEquals(120F, o.getPreviewFrameRateMaxValue(), 0.001F);
}
} }

@ -0,0 +1,172 @@
package com.otaliastudios.cameraview.filter;
import android.opengl.GLES20;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseEglTest;
import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.opengl.program.GlTextureProgram;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class BaseFilterTest extends BaseEglTest {
public static class TestFilter extends BaseFilter implements TwoParameterFilter {
private float param1;
private float param2;
@NonNull
@Override
public String getFragmentShader() {
return createDefaultFragmentShader();
}
@Override
public void setParameter1(float value) {
param1 = value;
}
@Override
public void setParameter2(float value) {
param2 = value;
}
@Override
public float getParameter1() {
return param1;
}
@Override
public float getParameter2() {
return param2;
}
}
private TestFilter filter;
@Test
public void testCreateDefaultFragmentShader() {
filter = new TestFilter();
filter.fragmentTextureCoordinateName = "XXX";
String defaultFragmentShader = filter.createDefaultFragmentShader();
assertNotNull(defaultFragmentShader);
assertTrue(defaultFragmentShader.contains(filter.fragmentTextureCoordinateName));
}
@Test
public void testCreateDefaultVertexShader() {
filter = new TestFilter();
filter.vertexModelViewProjectionMatrixName = "AAA";
filter.vertexPositionName = "BBB";
filter.vertexTextureCoordinateName = "CCC";
filter.vertexTransformMatrixName = "DDD";
filter.fragmentTextureCoordinateName = "EEE";
String defaultVertexShader = filter.createDefaultVertexShader();
assertNotNull(defaultVertexShader);
assertTrue(defaultVertexShader.contains(filter.vertexModelViewProjectionMatrixName));
assertTrue(defaultVertexShader.contains(filter.vertexPositionName));
assertTrue(defaultVertexShader.contains(filter.vertexTextureCoordinateName));
assertTrue(defaultVertexShader.contains(filter.vertexTransformMatrixName));
assertTrue(defaultVertexShader.contains(filter.fragmentTextureCoordinateName));
}
@Test
public void testOnProgramCreate() {
filter = new TestFilter();
int handle = GlTextureProgram.create(filter.getVertexShader(), filter.getFragmentShader());
filter.onCreate(handle);
assertNotNull(filter.program);
filter.onDestroy();
assertNull(filter.program);
GLES20.glDeleteProgram(handle);
}
@Test
public void testDraw_whenInvalid() {
filter = spy(new TestFilter());
float[] matrix = new float[16];
filter.draw(0L, matrix);
verify(filter, never()).onPreDraw(0L, matrix);
verify(filter, never()).onDraw(0L);
verify(filter, never()).onPostDraw(0L);
}
@Test
public void testDraw() {
// Use a drawer which cares about GL setup.
filter = spy(new TestFilter());
GlTextureDrawer drawer = new GlTextureDrawer();
drawer.setFilter(filter);
float[] matrix = drawer.getTextureTransform();
drawer.draw(0L);
verify(filter, times(1)).onPreDraw(0L, matrix);
verify(filter, times(1)).onDraw(0L);
verify(filter, times(1)).onPostDraw(0L);
drawer.release();
}
@Test(expected = RuntimeException.class)
public void testOnCopy_invalid() {
// Anonymous inner classes do not have a public constructor.
Filter filter = new BaseFilter() {
@NonNull
@Override
public String getFragmentShader() {
return "whatever";
}
};
filter.copy();
}
@Test
public void testOnCopy() {
filter = new TestFilter();
BaseFilter other = filter.copy();
assertTrue(other instanceof TestFilter);
}
@Test
public void testCopy_withSize() {
filter = new TestFilter();
filter.setSize(WIDTH, HEIGHT);
BaseFilter other = filter.copy();
assertEquals(WIDTH, other.size.getWidth());
assertEquals(HEIGHT, other.size.getHeight());
}
@Test
public void testCopy_withParameter1() {
filter = new TestFilter();
filter.setParameter1(0.5F);
TestFilter other = (TestFilter) filter.copy();
assertEquals(filter.getParameter1(), other.getParameter1(), 0.001F);
}
@Test
public void testCopy_withParameter2() {
filter = new TestFilter();
filter.setParameter2(0.5F);
TestFilter other = (TestFilter) filter.copy();
assertEquals(filter.getParameter2(), other.getParameter2(), 0.001F);
}
}

@ -0,0 +1,52 @@
package com.otaliastudios.cameraview.filter;
import android.content.res.TypedArray;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.R;
import org.junit.Test;
import org.junit.runner.RunWith;
import static junit.framework.TestCase.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class FilterParserTest extends BaseTest {
@Test
public void testFallback() {
TypedArray array = mock(TypedArray.class);
when(array.hasValue(R.styleable.CameraView_cameraFilter)).thenReturn(false);
when(array.getString(R.styleable.CameraView_cameraFilter)).thenReturn(null);
FilterParser parser = new FilterParser(array);
assertNotNull(parser.getFilter());
assertTrue(parser.getFilter() instanceof NoFilter);
}
@Test
public void testConstructor() {
TypedArray array = mock(TypedArray.class);
when(array.hasValue(R.styleable.CameraView_cameraFilter)).thenReturn(true);
when(array.getString(R.styleable.CameraView_cameraFilter)).thenReturn(MyFilter.class.getName());
FilterParser parser = new FilterParser(array);
assertNotNull(parser.getFilter());
assertTrue(parser.getFilter() instanceof MyFilter);
}
public static class MyFilter extends BaseFilter {
@NonNull
@Override
public String getFragmentShader() {
return createDefaultFragmentShader();
}
}
}

@ -0,0 +1,30 @@
package com.otaliastudios.cameraview.filter;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class FiltersTest extends BaseTest {
@Test
public void testNewInstance() {
// At least tests that all our default filters have a no-args constructor.
Filters[] filtersArray = Filters.values();
for (Filters filters : filtersArray) {
Filter filter = filters.newInstance();
assertNotNull(filter);
}
}
}

@ -0,0 +1,212 @@
package com.otaliastudios.cameraview.filter;
import android.opengl.GLES20;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseEglTest;
import com.otaliastudios.cameraview.filters.AutoFixFilter;
import com.otaliastudios.cameraview.filters.BrightnessFilter;
import com.otaliastudios.cameraview.filters.DuotoneFilter;
import com.otaliastudios.cameraview.filters.VignetteFilter;
import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.opengl.program.GlProgram;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class MultiFilterTest extends BaseEglTest {
@Test
public void testConstructor1() {
MultiFilter multiFilter = new MultiFilter(
new DuotoneFilter(),
new AutoFixFilter()
);
assertEquals(2, multiFilter.filters.size());
}
@Test
public void testConstructor2() {
List<Filter> filters = new ArrayList<>();
filters.add(new DuotoneFilter());
filters.add(new AutoFixFilter());
MultiFilter multiFilter = new MultiFilter(filters);
assertEquals(2, multiFilter.filters.size());
}
@Test
public void testAddFilter() {
MultiFilter multiFilter = new MultiFilter();
assertEquals(0, multiFilter.filters.size());
multiFilter.addFilter(new DuotoneFilter());
assertEquals(1, multiFilter.filters.size());
multiFilter.addFilter(new AutoFixFilter());
assertEquals(2, multiFilter.filters.size());
}
@Test
public void testAddFilter_multi() {
MultiFilter multiFilter = new MultiFilter(new DuotoneFilter());
assertEquals(1, multiFilter.filters.size());
MultiFilter other = new MultiFilter(
new AutoFixFilter(),
new BrightnessFilter(),
new VignetteFilter());
multiFilter.addFilter(other);
assertEquals(4, multiFilter.filters.size());
}
@Test
public void testSetSize() {
DuotoneFilter filter = new DuotoneFilter();
MultiFilter multiFilter = new MultiFilter(filter);
MultiFilter.State state = multiFilter.states.get(filter);
assertNotNull(state);
assertNull(state.size);
multiFilter.setSize(WIDTH, HEIGHT);
assertNotNull(state.size);
}
@Test
public void testCopy() {
DuotoneFilter filter = spy(new DuotoneFilter());
MultiFilter multiFilter = new MultiFilter(filter);
MultiFilter multiFilterCopy = (MultiFilter) multiFilter.copy();
assertEquals(1, multiFilterCopy.filters.size());
verify(filter, times(1)).onCopy();
}
@Test
public void testParameter1() {
DuotoneFilter filter = new DuotoneFilter();
MultiFilter multiFilter = new MultiFilter(filter);
float desired = 0.21582F; // whatever
multiFilter.setParameter1(desired);
assertEquals(desired, multiFilter.getParameter1(), 0.001F);
assertEquals(desired, filter.getParameter1(), 0.001F);
}
@Test
public void testParameter2() {
DuotoneFilter filter = new DuotoneFilter();
MultiFilter multiFilter = new MultiFilter(filter);
float desired = 0.21582F; // whatever
multiFilter.setParameter2(desired);
assertEquals(desired, multiFilter.getParameter2(), 0.001F);
assertEquals(desired, filter.getParameter2(), 0.001F);
}
@Test
public void testOnCreate_isLazy() {
DuotoneFilter filter = spy(new DuotoneFilter());
MultiFilter multiFilter = new MultiFilter(filter);
int program = GlProgram.create(multiFilter.getVertexShader(),
multiFilter.getFragmentShader());
multiFilter.onCreate(program);
verify(filter, never()).onCreate(anyInt());
multiFilter.onDestroy();
GLES20.glDeleteProgram(program);
verify(filter, never()).onDestroy();
}
@Test
public void testDraw_simple() {
DuotoneFilter filter = spy(new DuotoneFilter());
MultiFilter multiFilter = new MultiFilter(filter);
multiFilter.setSize(WIDTH, HEIGHT);
GlTextureDrawer drawer = new GlTextureDrawer();
drawer.setFilter(multiFilter);
float[] matrix = drawer.getTextureTransform();
drawer.draw(0L);
drawer.release();
// The child should have experienced the whole lifecycle.
verify(filter, atLeastOnce()).getVertexShader();
verify(filter, atLeastOnce()).getFragmentShader();
verify(filter, atLeastOnce()).setSize(anyInt(), anyInt());
verify(filter, times(1)).onCreate(anyInt());
verify(filter, times(1)).draw(0L, matrix);
verify(filter, times(1)).onDestroy();
}
@Test
public void testDraw_multi() {
// Want to test that when filter1 is drawn, the current framebuffer is a
// non-default one. When filter2 is drawn, the current framebuffer is 0.
final DuotoneFilter filter1 = spy(new DuotoneFilter());
final DuotoneFilter filter2 = spy(new DuotoneFilter());
final MultiFilter multiFilter = new MultiFilter(filter1, filter2);
multiFilter.setSize(WIDTH, HEIGHT);
GlTextureDrawer drawer = new GlTextureDrawer();
drawer.setFilter(multiFilter);
float[] matrix = drawer.getTextureTransform();
final int[] result = new int[1];
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) {
MultiFilter.State state = multiFilter.states.get(filter1);
assertNotNull(state);
assertTrue(state.isProgramCreated);
assertTrue(state.isFramebufferCreated);
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0);
// assertTrue(result[0] != 0);
return null;
}
}).when(filter1).draw(0L, matrix);
// Note: second filter is drawn with the identity matrix!
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) {
// The last filter has no FBO / texture.
MultiFilter.State state = multiFilter.states.get(filter2);
assertNotNull(state);
assertTrue(state.isProgramCreated);
assertFalse(state.isFramebufferCreated);
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0);
assertEquals(0, result[0]);
return null;
}
}).when(filter2).draw(eq(0L), any(float[].class));
drawer.draw(0L);
drawer.release();
// Verify that both are drawn.
verify(filter1, times(1)).draw(0L, matrix);
verify(filter2, times(1)).draw(eq(0L), any(float[].class));
}
}

@ -0,0 +1,35 @@
package com.otaliastudios.cameraview.filter;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class NoFilterTest extends BaseTest {
public static class DummyFilter extends BaseFilter {
@NonNull
@Override
public String getFragmentShader() {
return "whatever";
}
}
@Test
public void testGetFragmentShader() {
NoFilter filter = new NoFilter();
String defaultFragmentShader = new DummyFilter().createDefaultFragmentShader();
assertEquals(defaultFragmentShader, filter.getFragmentShader());
}
}

@ -0,0 +1,43 @@
package com.otaliastudios.cameraview.filter;
import android.content.res.TypedArray;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.R;
import org.junit.Test;
import org.junit.runner.RunWith;
import static junit.framework.TestCase.assertNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class SimpleFilterTest extends BaseTest {
@Test
public void testGetFragmentShader() {
String shader = "shader";
SimpleFilter filter = new SimpleFilter(shader);
assertEquals(shader, filter.getFragmentShader());
}
@Test
public void testCopy() {
String shader = "shader";
SimpleFilter filter = new SimpleFilter(shader);
BaseFilter copy = filter.copy();
assertTrue(copy instanceof SimpleFilter);
SimpleFilter simpleCopy = (SimpleFilter) copy;
assertEquals(shader, simpleCopy.getFragmentShader());
}
}

@ -0,0 +1,112 @@
package com.otaliastudios.cameraview.frame;
import android.graphics.ImageFormat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.size.Size;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ByteBufferFrameManagerTest extends BaseTest {
private final Angles angles = new Angles();
private ByteBufferFrameManager.BufferCallback callback;
@Before
public void setUp() {
callback = mock(ByteBufferFrameManager.BufferCallback.class);
}
@After
public void tearDown() {
callback = null;
}
@Test
public void testAllocate() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
verify(callback, times(1)).onBufferAvailable(any(byte[].class));
reset(callback);
manager = new ByteBufferFrameManager(5, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
verify(callback, times(5)).onBufferAvailable(any(byte[].class));
}
@Test
public void testOnFrameReleased_alreadyFull() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
int length = manager.getFrameBytes();
Frame frame1 = manager.getFrame(new byte[length], 0);
assertNotNull(frame1);
// Since frame1 is already taken and poolSize = 1, getFrame() would return null.
// To create a new frame, freeze the first one.
Frame frame2 = frame1.freeze();
// Now release the first frame so it goes back into the pool.
manager.onFrameReleased(frame1, (byte[]) frame1.getData());
reset(callback);
// Release the second. The pool is already full, so onBufferAvailable should not be called
// since this Frame instance will NOT be reused.
manager.onFrameReleased(frame2, (byte[]) frame2.getData());
verify(callback, never()).onBufferAvailable((byte[]) frame2.getData());
}
@Test
public void testOnFrameReleased_sameLength() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
int length = manager.getFrameBytes();
// A camera preview frame comes. Request a frame.
byte[] picture = new byte[length];
Frame frame = manager.getFrame(picture, 0);
assertNotNull(frame);
// Release the frame and ensure that onBufferAvailable is called.
reset(callback);
manager.onFrameReleased(frame, (byte[]) frame.getData());
verify(callback, times(1)).onBufferAvailable(picture);
}
@Test
public void testOnFrameReleased_differentLength() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
int length = manager.getFrameBytes();
// A camera preview frame comes. Request a frame.
byte[] picture = new byte[length];
Frame frame = manager.getFrame(picture, 0);
assertNotNull(frame);
// Don't release the frame. Change the allocation size.
manager.setUp(ImageFormat.NV16, new Size(15, 15), angles);
// Now release the old frame and ensure that onBufferAvailable is NOT called,
// because the released data has wrong length.
manager.onFrameReleased(frame, (byte[]) frame.getData());
reset(callback);
verify(callback, never()).onBufferAvailable(picture);
}
}

@ -0,0 +1,73 @@
package com.otaliastudios.cameraview.frame;
import android.graphics.ImageFormat;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.size.Size;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class FrameManagerTest extends BaseTest {
private final Angles angles = new Angles();
@Test
public void testFrameRecycling() {
// A 1-pool manager will always recycle the same frame.
FrameManager<String> manager = new FrameManager<String>(1, String.class) {
@Override
protected void onFrameDataReleased(@NonNull String data, boolean recycled) { }
@NonNull
@Override
protected String onCloneFrameData(@NonNull String data) {
return data;
}
};
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
Frame first = manager.getFrame("foo", 0);
assertNotNull(first);
first.release();
Frame second = manager.getFrame("bar", 0);
assertNotNull(second);
second.release();
assertEquals(first, second);
}
@Test
public void testGetFrame() {
FrameManager<String> manager = new FrameManager<String>(1, String.class) {
@Override
protected void onFrameDataReleased(@NonNull String data, boolean recycled) { }
@NonNull
@Override
protected String onCloneFrameData(@NonNull String data) {
return data;
}
};
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
Frame first = manager.getFrame("foo", 0);
assertNotNull(first);
Frame second = manager.getFrame("bar", 0);
assertNull(second);
}
}

@ -0,0 +1,90 @@
package com.otaliastudios.cameraview.gesture;
import android.annotation.TargetApi;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.rule.ActivityTestRule;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.TestActivity;
import com.otaliastudios.cameraview.tools.Op;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Rule;
import static androidx.test.espresso.Espresso.onView;
@TargetApi(17)
public abstract class GestureFinderTest<T extends GestureFinder> extends BaseTest {
protected abstract T createFinder(@NonNull GestureFinder.Controller controller);
@Rule
public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
@SuppressWarnings("WeakerAccess")
protected T finder;
@SuppressWarnings("WeakerAccess")
protected Op<Gesture> touchOp;
@SuppressWarnings("WeakerAccess")
protected ViewGroup layout;
@Before
public void setUp() {
uiSync(new Runnable() {
@Override
public void run() {
TestActivity a = rule.getActivity();
layout = new FrameLayout(a);
finder = createFinder(new Controller());
finder.setActive(true);
a.inflate(layout);
touchOp = new Op<>(false);
layout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
boolean found = finder.onTouchEvent(motionEvent);
if (found) touchOp.controller().end(finder.getGesture());
return true;
}
});
}
});
}
@SuppressWarnings("WeakerAccess")
protected final ViewInteraction onLayout() {
return onView(Matchers.<View>is(layout))
.inRoot(RootMatchers.withDecorView(
Matchers.is(rule.getActivity().getWindow().getDecorView())));
}
private class Controller implements GestureFinder.Controller {
@NonNull
@Override
public Context getContext() {
return layout.getContext();
}
@Override
public int getWidth() {
return layout.getWidth();
}
@Override
public int getHeight() {
return layout.getHeight();
}
}
}

@ -0,0 +1,71 @@
package com.otaliastudios.cameraview.gesture;
import androidx.annotation.NonNull;
import androidx.test.espresso.ViewAction;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* On API 26 these tests fail during Espresso's inRoot() - the window never gains focus.
* This might be due to a system popup or something similar.
*/
@SdkExclude(minSdkVersion = 26, maxSdkVersion = 26)
@RunWith(AndroidJUnit4.class)
@SmallTest
public class PinchGestureFinderTest extends GestureFinderTest<PinchGestureFinder> {
@Override
protected PinchGestureFinder createFinder(@NonNull GestureFinder.Controller controller) {
return new PinchGestureFinder(controller);
}
@Test
public void testDefaults() {
assertEquals(finder.getGesture(), Gesture.PINCH);
assertEquals(finder.getPoints().length, 2);
assertEquals(finder.getPoints()[0].x, 0, 0);
assertEquals(finder.getPoints()[0].y, 0, 0);
assertEquals(finder.getPoints()[1].x, 0, 0);
assertEquals(finder.getPoints()[1].y, 0, 0);
}
// TODO: test pinch open
// TODO: test pinch close
// TODO: test pinch disabled
// Possible approach: mimic pinch gesture and let espresso test.
// Too lazy to do this now, but it's possible.
// https://stackoverflow.com/questions/11523423/how-to-generate-zoom-pinch-gesture-for-testing-for-android
public abstract class PinchViewAction implements ViewAction {
}
private void testPinch(ViewAction action, boolean increasing) {
touchOp.listen();
touchOp.controller().start();
onLayout().perform(action);
Gesture found = touchOp.await(10000);
assertNotNull(found);
// How will this move our parameter?
float curr = 0.5f, min = 0f, max = 1f;
float newValue = finder.computeValue(curr, min, max);
if (increasing) {
assertTrue(newValue > curr);
assertTrue(newValue <= max);
} else {
assertTrue(newValue < curr);
assertTrue(newValue >= min);
}
}
}

@ -0,0 +1,98 @@
package com.otaliastudios.cameraview.gesture;
import androidx.annotation.NonNull;
import androidx.test.espresso.ViewAction;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.action.ViewActions.swipeDown;
import static androidx.test.espresso.action.ViewActions.swipeLeft;
import static androidx.test.espresso.action.ViewActions.swipeRight;
import static androidx.test.espresso.action.ViewActions.swipeUp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* On API 26 these tests fail during Espresso's inRoot() - the window never gains focus.
* This might be due to a system popup or something similar.
*/
@SdkExclude(minSdkVersion = 26, maxSdkVersion = 26)
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ScrollGestureFinderTest extends GestureFinderTest<ScrollGestureFinder> {
private final static long WAIT = 2000; // 500 was too short
@Override
protected ScrollGestureFinder createFinder(@NonNull GestureFinder.Controller controller) {
return new ScrollGestureFinder(controller);
}
@Test
public void testDefaults() {
assertNull(finder.mType);
assertEquals(finder.getPoints().length, 2);
assertEquals(finder.getPoints()[0].x, 0, 0);
assertEquals(finder.getPoints()[0].y, 0, 0);
assertEquals(finder.getPoints()[1].x, 0, 0);
assertEquals(finder.getPoints()[1].y, 0, 0);
}
@Test
public void testScrollDisabled() {
finder.setActive(false);
touchOp.listen();
touchOp.controller().start();
onLayout().perform(swipeUp());
Gesture found = touchOp.await(WAIT);
assertNull(found);
}
private void testScroll(ViewAction scroll, Gesture expected, boolean increasing) {
touchOp.listen();
touchOp.controller().start();
onLayout().perform(scroll);
Gesture found = touchOp.await(WAIT);
assertEquals(found, expected);
// How will this move our parameter?
float curr = 0.5f, min = 0f, max = 1f;
float newValue = finder.computeValue(curr, min, max);
if (increasing) {
assertTrue(newValue >= curr);
assertTrue(newValue <= max);
} else {
assertTrue(newValue <= curr);
assertTrue(newValue >= min);
}
}
@Test
public void testScrollLeft() {
testScroll(swipeLeft(), Gesture.SCROLL_HORIZONTAL, false);
}
@Test
public void testScrollRight() {
testScroll(swipeRight(), Gesture.SCROLL_HORIZONTAL, true);
}
@Test
public void testScrollUp() {
testScroll(swipeUp(), Gesture.SCROLL_VERTICAL, true);
}
@Test
public void testScrollDown() {
testScroll(swipeDown(), Gesture.SCROLL_VERTICAL, false);
}
}

@ -0,0 +1,85 @@
package com.otaliastudios.cameraview.gesture;
import androidx.annotation.NonNull;
import androidx.test.espresso.action.GeneralClickAction;
import androidx.test.espresso.action.GeneralLocation;
import androidx.test.espresso.action.Press;
import androidx.test.espresso.action.Tap;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import android.view.InputDevice;
import android.view.MotionEvent;
import com.otaliastudios.cameraview.tools.SdkExclude;
import com.otaliastudios.cameraview.size.Size;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.action.ViewActions.*;
import static org.junit.Assert.*;
/**
* On API 26 these tests fail during Espresso's inRoot() - the window never gains focus.
* This might be due to a system popup or something similar.
*/
@SdkExclude(minSdkVersion = 26, maxSdkVersion = 26)
@RunWith(AndroidJUnit4.class)
@SmallTest
public class TapGestureFinderTest extends GestureFinderTest<TapGestureFinder> {
@Override
protected TapGestureFinder createFinder(@NonNull GestureFinder.Controller controller) {
return new TapGestureFinder(controller);
}
@Test
public void testDefaults() {
assertNull(finder.mType);
assertEquals(finder.getPoints().length, 1);
assertEquals(finder.getPoints()[0].x, 0, 0);
assertEquals(finder.getPoints()[0].y, 0, 0);
}
@Test
public void testTap() {
touchOp.listen();
touchOp.controller().start();
GeneralClickAction a = new GeneralClickAction(
Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER,
InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY);
onLayout().perform(a);
Gesture found = touchOp.await(500);
assertEquals(found, Gesture.TAP);
Size size = rule.getActivity().getContentSize();
assertEquals(finder.getPoints()[0].x, (size.getWidth() / 2f), 1f);
assertEquals(finder.getPoints()[0].y, (size.getHeight() / 2f), 1f);
}
@Test
public void testTapWhileDisabled() {
finder.setActive(false);
touchOp.listen();
touchOp.controller().start();
onLayout().perform(click());
Gesture found = touchOp.await(500);
assertNull(found);
}
@Test
public void testLongTap() {
touchOp.listen();
touchOp.controller().start();
GeneralClickAction a = new GeneralClickAction(
Tap.LONG, GeneralLocation.CENTER, Press.FINGER,
InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY);
onLayout().perform(a);
Gesture found = touchOp.await(500);
assertEquals(found, Gesture.LONG_TAP);
Size size = rule.getActivity().getContentSize();
assertEquals(finder.getPoints()[0].x, (size.getWidth() / 2f), 1f);
assertEquals(finder.getPoints()[0].y, (size.getHeight() / 2f), 1f);
}
}

@ -0,0 +1,61 @@
package com.otaliastudios.cameraview.internal;
import android.media.CamcorderProfile;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.CameraUtils;
import com.otaliastudios.cameraview.internal.CamcorderProfiles;
import com.otaliastudios.cameraview.tools.SdkExclude;
import com.otaliastudios.cameraview.size.Size;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CamcorderProfilesTest extends BaseTest {
private String getCameraId() {
if (CameraUtils.hasCameras(getContext())) {
return "0";
}
return null;
}
@Test
public void testInvalidCameraReturnsLowest() {
CamcorderProfile invalid = CamcorderProfiles.get("invalid", new Size(100, 100));
CamcorderProfile lowest = CamcorderProfile.get(CamcorderProfile.QUALITY_LOW);
assertEquals(lowest.videoFrameWidth, invalid.videoFrameWidth);
assertEquals(lowest.videoFrameHeight, invalid.videoFrameHeight);
}
/**
* For some reason this fails on emulator 26.
*/
@SdkExclude(minSdkVersion = 26, maxSdkVersion = 26)
@Test
public void testGet() {
String cameraId = getCameraId();
if (cameraId == null) return;
int cameraIdInt = Integer.parseInt(cameraId);
// Not much we can test. Let's just ask for lowest and highest.
CamcorderProfile low = CamcorderProfiles.get(cameraId, new Size(1, 1));
CamcorderProfile high = CamcorderProfiles.get(cameraId, new Size(Integer.MAX_VALUE, Integer.MAX_VALUE));
// Compare with lowest
CamcorderProfile lowest = CamcorderProfile.get(cameraIdInt, CamcorderProfile.QUALITY_LOW);
CamcorderProfile highest = CamcorderProfile.get(cameraIdInt, CamcorderProfile.QUALITY_HIGH);
assertEquals(lowest.videoFrameWidth, low.videoFrameWidth);
assertEquals(lowest.videoFrameHeight, low.videoFrameHeight);
assertEquals(highest.videoFrameWidth, high.videoFrameWidth);
assertEquals(highest.videoFrameHeight, high.videoFrameHeight);
}
}

@ -0,0 +1,53 @@
package com.otaliastudios.cameraview.internal;
import android.graphics.Rect;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CropHelperTest extends BaseTest {
@Test
public void testCrop() {
testCrop(new Size(1600, 1600), AspectRatio.of(16, 16));
testCrop(new Size(1600, 1600), AspectRatio.of(16, 9));
testCrop(new Size(1600, 1600), AspectRatio.of(9, 16));
}
private void testCrop(final Size inSize, final AspectRatio outRatio) {
AspectRatio inRatio = AspectRatio.of(inSize.getWidth(), inSize.getHeight());
Rect out = CropHelper.computeCrop(inSize, outRatio);
Size outSize = new Size(out.width(), out.height());
assertTrue(outRatio.matches(outSize));
if (outRatio.matches(inSize)) {
// They are equal.
assertEquals(outSize.getWidth(), inSize.getWidth());
assertEquals(outSize.getHeight(), inSize.getHeight());
} else if (outRatio.toFloat() > inRatio.toFloat()) {
// Width must match.
assertEquals(outSize.getWidth(), inSize.getWidth());
assertNotEquals(outSize.getHeight(), inSize.getHeight());
} else {
// Height must match.
assertEquals(outSize.getHeight(), inSize.getHeight());
assertNotEquals(outSize.getWidth(), inSize.getWidth());
}
}
}

@ -0,0 +1,219 @@
package com.otaliastudios.cameraview.internal;
import android.media.MediaCodecInfo;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class DeviceEncodersTest extends BaseTest {
// This is guaranteed to work, see
// https://developer.android.com/guide/topics/media/media-formats
private final static Size GUARANTEED_SIZE = new Size(176, 144);
private boolean enabled;
@Before
public void setUp() {
enabled = DeviceEncoders.ENABLED;
}
@After
public void tearDown() {
DeviceEncoders.ENABLED = enabled;
}
@NonNull
private DeviceEncoders create() {
return new DeviceEncoders(DeviceEncoders.MODE_RESPECT_ORDER,
"video/avc",
"audio/mp4a-latm",
0,
0);
}
@Test
public void testGetDeviceEncoders() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
List<MediaCodecInfo> infos = deviceEncoders.getDeviceEncoders();
for (MediaCodecInfo info : infos) {
assertTrue(info.isEncoder());
}
}
}
@Test
public void testIsHardwareEncoder() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
assertFalse(deviceEncoders.isHardwareEncoder("OMX.google.encoder"));
assertTrue(deviceEncoders.isHardwareEncoder("OMX.other.encoder"));
}
}
@Test
public void testFindDeviceEncoder() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
List<MediaCodecInfo> allEncoders = deviceEncoders.getDeviceEncoders();
MediaCodecInfo encoder = deviceEncoders.findDeviceEncoder(allEncoders,
"video/avc", DeviceEncoders.MODE_RESPECT_ORDER, 0);
assertNotNull(encoder);
List<String> encoderTypes = Arrays.asList(encoder.getSupportedTypes());
assertTrue(encoderTypes.contains("video/avc"));
}
}
@Test
public void testGetVideoEncoder() {
if (DeviceEncoders.ENABLED) {
DeviceEncoders deviceEncoders = create();
assertNotNull(deviceEncoders.getVideoEncoder());
}
DeviceEncoders.ENABLED = false;
DeviceEncoders deviceEncoders = create();
assertNull(deviceEncoders.getVideoEncoder());
}
@Test
public void testGetAudioEncoder() {
if (DeviceEncoders.ENABLED) {
DeviceEncoders deviceEncoders = create();
assertNotNull(deviceEncoders.getAudioEncoder());
}
DeviceEncoders.ENABLED = false;
DeviceEncoders deviceEncoders = create();
assertNull(deviceEncoders.getAudioEncoder());
}
@Test
public void testGetSupportedVideoSize_disabled() {
DeviceEncoders.ENABLED = false;
DeviceEncoders deviceEncoders = create();
Size input = new Size(GUARANTEED_SIZE.getWidth(), GUARANTEED_SIZE.getHeight());
Size output = deviceEncoders.getSupportedVideoSize(input);
assertSame(input, output);
}
@Test
public void testGetSupportedVideoSize_scalesDown() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
Size input = new Size(
GUARANTEED_SIZE.getWidth() * 1000,
GUARANTEED_SIZE.getHeight() * 1000);
try {
Size output = deviceEncoders.getSupportedVideoSize(input);
assertTrue(AspectRatio.of(input).matches(output, 0.01F));
} catch (RuntimeException e) {
// The scaled down size happens to be not supported.
// I see no way of testing this easily if we're not sure of supported ranges.
// This depends highly on the alignment since scaling down, while keeping AR,
// can change the alignment and require width / height changes.
}
}
}
@Test
public void testGetSupportedVideoSize_aligns() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
Size input = new Size(GUARANTEED_SIZE.getWidth() + 1,
GUARANTEED_SIZE.getHeight() + 1);
Size output = deviceEncoders.getSupportedVideoSize(input);
assertTrue(output.getWidth() <= input.getWidth());
assertTrue(output.getHeight() <= input.getHeight());
}
}
@Test
public void testGetSupportedVideoBitRate_disabled() {
DeviceEncoders.ENABLED = false;
DeviceEncoders deviceEncoders = create();
int input = 1000;
int output = deviceEncoders.getSupportedVideoBitRate(input);
assertEquals(input, output);
}
@Test
public void testGetSupportedVideoBitRate_enabled() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
// Ensure it's clamped: we can pass a negative value and check it's >= 0.
int input = -1000;
int output = deviceEncoders.getSupportedVideoBitRate(input);
assertNotEquals(input, output);
assertTrue(output >= 0);
}
}
@Test
public void testGetSupportedAudioBitRate_disabled() {
DeviceEncoders.ENABLED = false;
DeviceEncoders deviceEncoders = create();
int input = 1000;
int output = deviceEncoders.getSupportedAudioBitRate(input);
assertEquals(input, output);
}
@Test
public void testGetSupportedAudioBitRate_enabled() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
// Ensure it's clamped: we can pass a negative value and check it's >= 0.
int input = -1000;
int output = deviceEncoders.getSupportedAudioBitRate(input);
assertNotEquals(input, output);
assertTrue(output >= 0);
}
}
@Test
public void testGetSupportedFrameRate_disabled() {
DeviceEncoders.ENABLED = false;
DeviceEncoders deviceEncoders = create();
int input = 1000;
int output = deviceEncoders.getSupportedVideoFrameRate(GUARANTEED_SIZE, input);
assertEquals(input, output);
}
@Test
public void testGetSupportedFrameRate_enabled() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
// Ensure it's clamped: we can pass a negative value and check it's >= 0.
int input = -10;
Size inputSize = deviceEncoders.getSupportedVideoSize(GUARANTEED_SIZE);
int output = deviceEncoders.getSupportedVideoFrameRate(inputSize, input);
assertNotEquals(input, output);
assertTrue(output >= 0);
}
}
}

@ -1,18 +1,25 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview.internal;
import android.graphics.Canvas; import com.otaliastudios.cameraview.BaseTest;
import android.support.test.filters.MediumTest; import com.otaliastudios.cameraview.TestActivity;
import android.support.test.rule.ActivityTestRule; import com.otaliastudios.cameraview.controls.Grid;
import android.support.test.runner.AndroidJUnit4; import com.otaliastudios.cameraview.tools.Op;
import com.otaliastudios.cameraview.tools.Retry;
import com.otaliastudios.cameraview.tools.RetryRule;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ActivityTestRule;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@MediumTest @MediumTest
@ -21,54 +28,68 @@ public class GridLinesLayoutTest extends BaseTest {
@Rule @Rule
public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class); public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
@Rule
public RetryRule retryRule = new RetryRule(3);
private GridLinesLayout layout; private GridLinesLayout layout;
@NonNull
private Op<Integer> getDrawOp() {
final Op<Integer> op = new Op<>();
layout.callback = mock(GridLinesLayout.DrawCallback.class);
doEndOp(op, 0).when(layout.callback).onDraw(anyInt());
return op;
}
@Before @Before
public void setUp() { public void setUp() {
ui(new Runnable() { uiSync(new Runnable() {
@Override @Override
public void run() { public void run() {
TestActivity a = rule.getActivity(); TestActivity a = rule.getActivity();
layout = new GridLinesLayout(a); layout = new GridLinesLayout(a);
layout.setGridMode(Grid.OFF); layout.setGridMode(Grid.OFF);
layout.drawTask.listen(); Op<Integer> op = getDrawOp();
a.getContentView().addView(layout); a.getContentView().addView(layout);
op.await(1000);
} }
}); });
// Wait for first draw.
layout.drawTask.await(1000);
} }
private int setGridAndWait(Grid value) { private int setGridAndWait(Grid value) {
layout.drawTask.listen();
layout.setGridMode(value); layout.setGridMode(value);
Integer result = layout.drawTask.await(1000); Op<Integer> op = getDrawOp();
Integer result = op.await(1000);
assertNotNull(result); assertNotNull(result);
return result; return result;
} }
@Retry
@Test @Test
public void testOff() { public void testOff() {
int linesDrawn = setGridAndWait(Grid.OFF); int linesDrawn = setGridAndWait(Grid.OFF);
assertEquals(linesDrawn, 0); assertEquals(0, linesDrawn);
} }
@Retry
@Test @Test
public void test3x3() { public void test3x3() {
int linesDrawn = setGridAndWait(Grid.DRAW_3X3); int linesDrawn = setGridAndWait(Grid.DRAW_3X3);
assertEquals(linesDrawn, 2); assertEquals(2, linesDrawn);
} }
@Retry
@Test @Test
public void testPhi() { public void testPhi() {
int linesDrawn = setGridAndWait(Grid.DRAW_PHI); int linesDrawn = setGridAndWait(Grid.DRAW_PHI);
assertEquals(linesDrawn, 2); assertEquals(2, linesDrawn);
} }
@Retry
@Test @Test
public void test4x4() { public void test4x4() {
int linesDrawn = setGridAndWait(Grid.DRAW_4X4); int linesDrawn = setGridAndWait(Grid.DRAW_4X4);
assertEquals(linesDrawn, 3); assertEquals(3, linesDrawn);
} }
} }

@ -0,0 +1,109 @@
package com.otaliastudios.cameraview.internal;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import android.view.OrientationEventListener;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.OrientationHelper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class OrientationHelperTest extends BaseTest {
private OrientationHelper helper;
private OrientationHelper.Callback callback;
@Before
public void setUp() {
uiSync(new Runnable() {
@Override
public void run() {
callback = mock(OrientationHelper.Callback.class);
helper = new OrientationHelper(getContext(), callback);
}
});
}
@After
public void tearDown() {
callback = null;
helper = null;
}
@Test
public void testEnable() {
// On some API levels, enable() needs to be run on the UI thread.
uiSync(new Runnable() {
@Override
public void run() {
assertEquals(helper.getLastDisplayOffset(), -1);
assertEquals(helper.getLastDeviceOrientation(), -1);
helper.enable();
assertNotEquals(helper.getLastDisplayOffset(), -1); // Don't know about device orientation.
// Ensure nothing bad if called twice.
helper.enable();
assertNotEquals(helper.getLastDisplayOffset(), -1);
helper.disable();
assertEquals(helper.getLastDisplayOffset(), -1);
assertEquals(helper.getLastDeviceOrientation(), -1);
}
});
}
@Test
public void testRotation() {
// On some API levels, enable() needs to be run on the UI thread.
uiSync(new Runnable() {
@Override
public void run() {
// Sometimes (on some APIs) the helper will trigger an update to 0
// right after enabling. But that's fine for us, times(1) will be OK either way.
helper.enable();
helper.mDeviceOrientationListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
assertEquals(helper.getLastDeviceOrientation(), 0);
helper.mDeviceOrientationListener.onOrientationChanged(10);
assertEquals(helper.getLastDeviceOrientation(), 0);
helper.mDeviceOrientationListener.onOrientationChanged(-10);
assertEquals(helper.getLastDeviceOrientation(), 0);
helper.mDeviceOrientationListener.onOrientationChanged(44);
assertEquals(helper.getLastDeviceOrientation(), 0);
helper.mDeviceOrientationListener.onOrientationChanged(360);
assertEquals(helper.getLastDeviceOrientation(), 0);
// Callback called just once.
verify(callback, times(1)).onDeviceOrientationChanged(0);
helper.mDeviceOrientationListener.onOrientationChanged(90);
helper.mDeviceOrientationListener.onOrientationChanged(91);
assertEquals(helper.getLastDeviceOrientation(), 90);
verify(callback, times(1)).onDeviceOrientationChanged(90);
helper.mDeviceOrientationListener.onOrientationChanged(180);
assertEquals(helper.getLastDeviceOrientation(), 180);
verify(callback, times(1)).onDeviceOrientationChanged(180);
helper.mDeviceOrientationListener.onOrientationChanged(270);
assertEquals(helper.getLastDeviceOrientation(), 270);
verify(callback, times(1)).onDeviceOrientationChanged(270);
// It is still 270 after ORIENTATION_UNKNOWN
helper.mDeviceOrientationListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
assertEquals(helper.getLastDeviceOrientation(), 270);
verify(callback, times(1)).onDeviceOrientationChanged(270);
}
});
}
}

@ -0,0 +1,53 @@
package com.otaliastudios.cameraview.internal;
import android.graphics.ImageFormat;
import android.graphics.YuvImage;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.RotationHelper;
import com.otaliastudios.cameraview.size.Size;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class RotationHelperTest extends BaseTest {
@Test(expected = IllegalArgumentException.class)
public void testInvalidRotation1() {
RotationHelper.rotate(new byte[10], new Size(1, 1), -1);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidRotation2() {
RotationHelper.rotate(new byte[10], new Size(1, 1), -90);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidRotation3() {
RotationHelper.rotate(new byte[10], new Size(1, 1), 360);
}
@Test
public void testRotate() {
// Just test that nothing happens.
Size inputSize = new Size(160, 90);
int inputSizeBits = inputSize.getWidth() * inputSize.getHeight() * ImageFormat.getBitsPerPixel(ImageFormat.NV21);
int inputSizeBytes = (int) Math.ceil(inputSizeBits / 8.0d);
byte[] input = new byte[inputSizeBytes];
byte[] output = RotationHelper.rotate(input, inputSize, 90);
assertEquals(input.length, output.length);
Size outputSize = inputSize.flip();
YuvImage image = new YuvImage(output, ImageFormat.NV21, outputSize.getWidth(), outputSize.getHeight(), null);
assertNotNull(image);
}
}

@ -0,0 +1,242 @@
package com.otaliastudios.cameraview.internal;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.WorkerHandler;
import com.otaliastudios.cameraview.tools.Op;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import static org.junit.Assert.*;
import static org.junit.Assert.assertNotNull;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class WorkerHandlerTest extends BaseTest {
@Test
public void testGetFromCache() {
WorkerHandler first = WorkerHandler.get("first");
WorkerHandler second = WorkerHandler.get("first");
assertSame(first, second);
}
@Test
public void testGetAnother() {
WorkerHandler first = WorkerHandler.get("first");
WorkerHandler second = WorkerHandler.get("second");
assertNotSame(first, second);
}
@NonNull
private Runnable getRunnableForOp(final @NonNull Op<Boolean> op) {
return new Runnable() {
@Override
public void run() {
op.controller().end(true);
}
};
}
@NonNull
private Callable<Boolean> getCallableForOp(final @NonNull Op<Boolean> op) {
return new Callable<Boolean>() {
@Override
public Boolean call() {
op.controller().end(true);
return true;
}
};
}
@NonNull
private Callable<Void> getThrowCallable() {
return new Callable<Void>() {
@Override
public Void call() {
throw new RuntimeException("Fake error");
}
};
}
private void waitOp(@NonNull Op<Boolean> op) {
Boolean result = op.await(500);
assertNotNull(result);
assertTrue(result);
}
@Test
public void testFallbackExecute() {
final Op<Boolean> op = new Op<>();
WorkerHandler.execute(getRunnableForOp(op));
waitOp(op);
}
@Test
public void testPostRunnable() {
WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>();
handler.post(getRunnableForOp(op));
waitOp(op);
}
@Test
public void testPostCallable() {
WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>();
handler.post(getCallableForOp(op));
waitOp(op);
}
@Test
public void testPostCallable_throws() {
WorkerHandler handler = WorkerHandler.get("handler");
Task<Void> task = handler.post(getThrowCallable());
try { Tasks.await(task); } catch (ExecutionException | InterruptedException ignore) {}
assertTrue(task.isComplete());
assertFalse(task.isSuccessful());
}
@Test
public void testRunRunnable_background() {
WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>();
handler.run(getRunnableForOp(op));
waitOp(op);
}
@Test
public void testRunRunnable_sameThread() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op1 = new Op<>();
final Op<Boolean> op2 = new Op<>();
handler.post(new Runnable() {
@Override
public void run() {
handler.run(getRunnableForOp(op2));
assertTrue(op2.await(0)); // Do not wait.
op1.controller().end(true);
}
});
waitOp(op1);
}
@Test
public void testRunCallable_background() {
WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>();
handler.run(getCallableForOp(op));
waitOp(op);
}
@Test
public void testRunCallable_sameThread() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op1 = new Op<>();
final Op<Boolean> op2 = new Op<>();
handler.post(new Runnable() {
@Override
public void run() {
handler.run(getCallableForOp(op2));
assertTrue(op2.await(0)); // Do not wait.
op1.controller().end(true);
}
});
waitOp(op1);
}
@Test
public void testRunCallable_sameThread_throws() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>();
handler.post(new Runnable() {
@Override
public void run() {
Task<Void> task = handler.run(getThrowCallable());
assertTrue(task.isComplete()); // Already complete
assertFalse(task.isSuccessful());
op.controller().end(true);
}
});
waitOp(op);
}
@Test
public void testPostDelayed_tooEarly() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>();
handler.post(1000, getRunnableForOp(op));
assertNull(op.await(500));
}
@Test
public void testPostDelayed() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>();
handler.post(1000, getRunnableForOp(op));
assertNotNull(op.await(2000));
}
@Test
public void testRemove() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>();
Runnable runnable = getRunnableForOp(op);
handler.post(1000, runnable);
handler.remove(runnable);
assertNull(op.await(2000));
}
@Test
public void testGetters() {
final WorkerHandler handler = WorkerHandler.get("handler");
assertNotNull(handler.getExecutor());
assertNotNull(handler.getHandler());
assertNotNull(handler.getLooper());
assertNotNull(handler.getThread());
}
@Test
public void testExecutor() {
final WorkerHandler handler = WorkerHandler.get("handler");
Executor executor = handler.getExecutor();
final Op<Boolean> op = new Op<>();
executor.execute(getRunnableForOp(op));
waitOp(op);
}
@Test
public void testDestroy() {
final WorkerHandler handler = WorkerHandler.get("handler");
assertTrue(handler.getThread().isAlive());
handler.destroy();
WorkerHandler newHandler = WorkerHandler.get("handler");
assertNotSame(handler, newHandler);
assertTrue(newHandler.getThread().isAlive());
// Ensure old thread dies at some point.
try { handler.getThread().join(500); } catch (InterruptedException ignore) {}
assertFalse(handler.getThread().isAlive());
}
@Test
public void testDestroyAll() {
final WorkerHandler handler1 = WorkerHandler.get("handler1");
final WorkerHandler handler2 = WorkerHandler.get("handler2");
WorkerHandler.destroyAll();
WorkerHandler newHandler1 = WorkerHandler.get("handler1");
WorkerHandler newHandler2 = WorkerHandler.get("handler2");
assertNotSame(handler1, newHandler1);
assertNotSame(handler2, newHandler2);
}
}

@ -0,0 +1,104 @@
package com.otaliastudios.cameraview.markers;
import android.graphics.PointF;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.test.annotation.UiThreadTest;
import com.otaliastudios.cameraview.BaseTest;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mockito;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
public class DefaultAutoFocusMarkerTest extends BaseTest {
private DefaultAutoFocusMarker marker;
@Before
public void setUp() {
marker = new DefaultAutoFocusMarker();
}
@After
public void tearDown() {
marker = null;
}
@Test
public void testOnAttach() {
assertNull(marker.mContainer);
assertNull(marker.mFill);
ViewGroup container = new FrameLayout(getContext());
View result = marker.onAttach(getContext(), container);
assertNotNull(result);
assertNotNull(marker.mContainer);
assertNotNull(marker.mFill);
}
@UiThreadTest
@Test
public void testOnAutoFocusStart() {
View mockContainer = spy(new View(getContext()));
View mockFill = spy(new View(getContext()));
marker.mContainer = mockContainer;
marker.mFill = mockFill;
marker.onAutoFocusStart(AutoFocusTrigger.GESTURE, new PointF());
verify(mockContainer, atLeastOnce()).clearAnimation();
verify(mockFill, atLeastOnce()).clearAnimation();
verify(mockContainer, atLeastOnce()).animate();
verify(mockFill, atLeastOnce()).animate();
}
@UiThreadTest
@Test
public void testOnAutoFocusStart_fromMethod() {
View mockContainer = spy(new View(getContext()));
View mockFill = spy(new View(getContext()));
marker.mContainer = mockContainer;
marker.mFill = mockFill;
marker.onAutoFocusStart(AutoFocusTrigger.METHOD, new PointF());
verify(mockContainer, never()).clearAnimation();
verify(mockFill, never()).clearAnimation();
verify(mockContainer, never()).animate();
verify(mockFill, never()).animate();
}
@UiThreadTest
@Test
public void testOnAutoFocusEnd() {
View mockContainer = spy(new View(getContext()));
View mockFill = spy(new View(getContext()));
marker.mContainer = mockContainer;
marker.mFill = mockFill;
marker.onAutoFocusEnd(AutoFocusTrigger.GESTURE, true, new PointF());
verify(mockContainer, atLeastOnce()).animate();
verify(mockFill, atLeastOnce()).animate();
}
@UiThreadTest
@Test
public void testOnAutoFocusEnd_fromMethod() {
View mockContainer = spy(new View(getContext()));
View mockFill = spy(new View(getContext()));
marker.mContainer = mockContainer;
marker.mFill = mockFill;
marker.onAutoFocusEnd(AutoFocusTrigger.METHOD, true, new PointF());
verify(mockContainer, never()).animate();
verify(mockFill, never()).animate();
}
}

@ -0,0 +1,128 @@
package com.otaliastudios.cameraview.markers;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.PointF;
import android.view.View;
import android.view.ViewGroup;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.TestActivity;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mockito;
import androidx.annotation.NonNull;
import androidx.test.rule.ActivityTestRule;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Not clear why, but for some reason on API 28+ the UiThreadTests here crash for an internal NPE
* in FrameLayout.onMeasure.
*/
@SdkExclude(minSdkVersion = 28)
@TargetApi(17)
public class MarkerLayoutTest extends BaseTest {
@Rule
public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
private MarkerLayout markerLayout;
private AutoFocusMarker autoFocusMarker;
@Before
public void setUp() {
uiSync(new Runnable() {
@Override
public void run() {
TestActivity a = rule.getActivity();
markerLayout = spy(new MarkerLayout(a));
a.inflate(markerLayout);
autoFocusMarker = spy(new DefaultAutoFocusMarker());
}
});
}
@Test
public void testOnMarker_callsOnAttach() {
uiSync(new Runnable() {
@Override
public void run() {
markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker);
Mockito.verify(autoFocusMarker, times(1)).onAttach(
Mockito.any(Context.class),
Mockito.eq(markerLayout));
}
});
}
@Test
public void testOnMarker_addsView() {
uiSync(new Runnable() {
@Override
public void run() {
Assert.assertEquals(markerLayout.getChildCount(), 0);
markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker);
Assert.assertEquals(markerLayout.getChildCount(), 1);
}
});
}
@Test
public void testOnMarker_removesView() {
uiSync(new Runnable() {
@Override
public void run() {
markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker);
Assert.assertEquals(markerLayout.getChildCount(), 1);
markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker);
Assert.assertEquals(markerLayout.getChildCount(), 1);
markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, null);
Assert.assertEquals(markerLayout.getChildCount(), 0);
Mockito.verify(autoFocusMarker, times(2)).onAttach(
Mockito.any(Context.class),
Mockito.eq(markerLayout));
}
});
}
@Test
public void testOnEvent() {
uiSync(new Runnable() {
@Override
public void run() {
final View mockView = spy(new View(getContext()));
// These fail, however it's not really needed.
// when(mockView.getWidth()).thenReturn(50);
// when(mockView.getHeight()).thenReturn(50);
AutoFocusMarker mockMarker = new AutoFocusMarker() {
public void onAutoFocusStart(@NonNull AutoFocusTrigger trigger, @NonNull PointF point) { }
public void onAutoFocusEnd(@NonNull AutoFocusTrigger trigger, boolean successful, @NonNull PointF point) { }
@Override
public View onAttach(@NonNull Context context, @NonNull ViewGroup container) {
return mockView;
}
};
markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, mockMarker);
reset(mockView);
markerLayout.onEvent(MarkerLayout.TYPE_AUTOFOCUS, new PointF[]{new PointF(0, 0)});
verify(mockView, times(1)).clearAnimation();
verify(mockView, times(1)).setTranslationX(anyFloat());
verify(mockView, times(1)).setTranslationY(anyFloat());
}
});
}
}

@ -0,0 +1,79 @@
package com.otaliastudios.cameraview.markers;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PointF;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleableRes;
import androidx.arch.core.util.Function;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.R;
import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.size.SizeSelectorParser;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.List;
import static junit.framework.TestCase.assertNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class MarkerParserTest extends BaseTest {
@Test
public void testNullConstructor() {
TypedArray array = mock(TypedArray.class);
when(array.hasValue(R.styleable.CameraView_cameraAutoFocusMarker)).thenReturn(false);
when(array.getString(R.styleable.CameraView_cameraAutoFocusMarker)).thenReturn(null);
MarkerParser parser = new MarkerParser(array);
assertNull(parser.getAutoFocusMarker());
}
@Test
public void testConstructor() {
TypedArray array = mock(TypedArray.class);
when(array.hasValue(R.styleable.CameraView_cameraAutoFocusMarker)).thenReturn(true);
when(array.getString(R.styleable.CameraView_cameraAutoFocusMarker)).thenReturn(Marker.class.getName());
MarkerParser parser = new MarkerParser(array);
assertNotNull(parser.getAutoFocusMarker());
assertTrue(parser.getAutoFocusMarker() instanceof Marker);
}
public static class Marker implements AutoFocusMarker {
public Marker() { }
@Nullable
@Override
public View onAttach(@NonNull Context context, @NonNull ViewGroup container) {
return null;
}
@Override
public void onAutoFocusStart(@NonNull AutoFocusTrigger trigger, @NonNull PointF point) { }
@Override
public void onAutoFocusEnd(@NonNull AutoFocusTrigger trigger, boolean successful, @NonNull PointF point) { }
}
}

@ -0,0 +1,137 @@
package com.otaliastudios.cameraview.metering;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.R;
import com.otaliastudios.cameraview.markers.AutoFocusMarker;
import com.otaliastudios.cameraview.markers.AutoFocusTrigger;
import com.otaliastudios.cameraview.markers.MarkerParser;
import com.otaliastudios.cameraview.size.Size;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.List;
import static junit.framework.TestCase.assertNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class MeteringRegionsTest extends BaseTest {
private final Size bounds = new Size(1000, 1000);
private void checkRegion(@NonNull MeteringRegion region, @NonNull PointF center, int weight) {
assertEquals(center.x, region.mRegion.centerX(), 0.01F);
assertEquals(center.y, region.mRegion.centerY(), 0.01F);
assertEquals(weight, region.mWeight);
}
@Test
public void testFromPoint() {
PointF center = new PointF(500, 500);
MeteringRegions regions = MeteringRegions.fromPoint(bounds, center);
assertEquals(2, regions.mRegions.size());
MeteringRegion first = regions.mRegions.get(0);
MeteringRegion second = regions.mRegions.get(1);
checkRegion(first, center, MeteringRegion.MAX_WEIGHT);
checkRegion(second, center,
Math.round(MeteringRegions.BLUR_FACTOR_WEIGHT * MeteringRegion.MAX_WEIGHT));
}
@Test
public void testFromArea() {
RectF area = new RectF(400, 400, 600, 600);
MeteringRegions regions = MeteringRegions.fromArea(bounds, area);
assertEquals(1, regions.mRegions.size());
MeteringRegion region = regions.mRegions.get(0);
checkRegion(region, new PointF(area.centerX(), area.centerY()), MeteringRegion.MAX_WEIGHT);
}
@Test
public void testFromArea_withBlur() {
RectF area = new RectF(400, 400, 600, 600);
MeteringRegions regions = MeteringRegions.fromArea(bounds, area,
MeteringRegion.MAX_WEIGHT, true);
assertEquals(2, regions.mRegions.size());
MeteringRegion first = regions.mRegions.get(0);
MeteringRegion second = regions.mRegions.get(1);
PointF center = new PointF(area.centerX(), area.centerY());
checkRegion(first, center, MeteringRegion.MAX_WEIGHT);
checkRegion(second, center,
Math.round(MeteringRegions.BLUR_FACTOR_WEIGHT * MeteringRegion.MAX_WEIGHT));
}
@Test
public void testTransform() {
MeteringTransform transform = mock(MeteringTransform.class);
when(transform.transformMeteringPoint(any(PointF.class))).then(new Answer<PointF>() {
@Override
public PointF answer(InvocationOnMock invocation) {
PointF in = invocation.getArgument(0);
// This will swap x and y coordinates
//noinspection SuspiciousNameCombination
return new PointF(in.y, in.x);
}
});
RectF area = new RectF(0, 0, 100, 500); // tall area
RectF expected = new RectF(0, 0, 500, 100); // wide area
MeteringRegions regions = MeteringRegions.fromArea(bounds, area);
MeteringRegions transformed = regions.transform(transform);
verify(transform, times(4)).transformMeteringPoint(any(PointF.class));
assertEquals(1, transformed.mRegions.size());
assertEquals(expected, transformed.mRegions.get(0).mRegion);
}
@Test
public void testGet() {
MeteringTransform<Integer> transform = new MeteringTransform<Integer>() {
@NonNull
@Override
public PointF transformMeteringPoint(@NonNull PointF point) {
return point;
}
@NonNull
@Override
public Integer transformMeteringRegion(@NonNull RectF region, int weight) {
return weight;
}
};
MeteringRegions regions = MeteringRegions.fromArea(bounds,
new RectF(400, 400, 600, 600),
900,
true);
assertEquals(2, regions.mRegions.size());
List<Integer> result = regions.get(1, transform);
assertEquals(1, result.size());
assertEquals(900, (int) result.get(0));
}
}

@ -0,0 +1,78 @@
package com.otaliastudios.cameraview.overlay;
import android.graphics.Canvas;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseEglTest;
import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.cameraview.size.Size;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class OverlayDrawerTest extends BaseEglTest {
@Test
public void testDraw() {
Overlay overlay = mock(Overlay.class);
OverlayDrawer drawer = new OverlayDrawer(overlay, new Size(WIDTH, HEIGHT));
drawer.draw(Overlay.Target.PICTURE_SNAPSHOT);
verify(overlay, times(1)).drawOn(
eq(Overlay.Target.PICTURE_SNAPSHOT),
any(Canvas.class));
}
@Test
public void testGetTransform() {
// We'll check that the transform is not all zeros, which is highly unlikely
// (the default transform should be the identity matrix)
OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT));
drawer.draw(Overlay.Target.PICTURE_SNAPSHOT);
assertThat(drawer.getTransform(), new BaseMatcher<float[]>() {
public void describeTo(Description description) { }
public boolean matches(Object item) {
float[] array = (float[]) item;
for (float value : array) {
if (value != 0.0F) return true;
}
return false;
}
});
}
@Test
public void testRender() {
OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT));
drawer.mTextureDrawer = spy(drawer.mTextureDrawer);
drawer.draw(Overlay.Target.PICTURE_SNAPSHOT);
drawer.render(0L);
verify(drawer.mTextureDrawer, times(1)).draw(0L);
}
@Test
public void testRelease() {
OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT));
GlTextureDrawer textureDrawer = spy(drawer.mTextureDrawer);
drawer.mTextureDrawer = textureDrawer;
drawer.release();
verify(textureDrawer, times(1)).release();
}
}

@ -0,0 +1,177 @@
package com.otaliastudios.cameraview.overlay;
import android.content.res.XmlResourceParser;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Xml;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class OverlayLayoutTest extends BaseTest {
private OverlayLayout overlayLayout;
@Before
public void setUp() {
overlayLayout = spy(new OverlayLayout(getContext()));
}
@After
public void tearDown() {
overlayLayout = null;
}
@Test
public void testIsOverlay_LayoutParams() {
ViewGroup.LayoutParams params;
params = new ViewGroup.LayoutParams(10, 10);
assertFalse(overlayLayout.isOverlay(params));
params = new OverlayLayout.LayoutParams(10, 10);
assertTrue(overlayLayout.isOverlay(params));
}
@Test
public void testIsOverlay_attributeSet() throws Exception {
int layout1 = com.otaliastudios.cameraview.test.R.layout.overlay;
int layout2 = com.otaliastudios.cameraview.test.R.layout.not_overlay;
AttributeSet set1 = getAttributeSet(layout1);
assertTrue(overlayLayout.isOverlay(set1));
AttributeSet set2 = getAttributeSet(layout2);
assertFalse(overlayLayout.isOverlay(set2));
}
@NonNull
private AttributeSet getAttributeSet(int layout) throws Exception {
// Get the attribute set in the correct state: use a parser and move to START_TAG
XmlResourceParser parser = getContext().getResources().getLayout(layout);
//noinspection StatementWithEmptyBody
while (parser.next() != XmlResourceParser.START_TAG) {}
return Xml.asAttributeSet(parser);
}
@Test
public void testLayoutParams_drawsOn() {
OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
assertFalse(params.drawsOn(Overlay.Target.PREVIEW));
assertFalse(params.drawsOn(Overlay.Target.PICTURE_SNAPSHOT));
assertFalse(params.drawsOn(Overlay.Target.VIDEO_SNAPSHOT));
params.drawOnPreview = true;
assertTrue(params.drawsOn(Overlay.Target.PREVIEW));
params.drawOnPictureSnapshot = true;
assertTrue(params.drawsOn(Overlay.Target.PICTURE_SNAPSHOT));
params.drawOnVideoSnapshot = true;
assertTrue(params.drawsOn(Overlay.Target.VIDEO_SNAPSHOT));
}
@Test
public void testLayoutParams_toString() {
OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
String string = params.toString();
assertTrue(string.contains("drawOnPreview"));
assertTrue(string.contains("drawOnPictureSnapshot"));
assertTrue(string.contains("drawOnVideoSnapshot"));
}
@Test
public void testDrawChild() {
Canvas canvas = new Canvas();
OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
View child = new View(getContext());
child.setLayoutParams(params);
when(overlayLayout.doDrawChild(canvas, child, 0)).thenReturn(true);
overlayLayout.currentTarget = Overlay.Target.PREVIEW;
assertFalse(overlayLayout.drawChild(canvas, child, 0));
params.drawOnPreview = true;
assertTrue(overlayLayout.drawChild(canvas, child, 0));
overlayLayout.currentTarget = Overlay.Target.PICTURE_SNAPSHOT;
assertFalse(overlayLayout.drawChild(canvas, child, 0));
params.drawOnPictureSnapshot = true;
assertTrue(overlayLayout.drawChild(canvas, child, 0));
overlayLayout.currentTarget = Overlay.Target.VIDEO_SNAPSHOT;
assertFalse(overlayLayout.drawChild(canvas, child, 0));
params.drawOnVideoSnapshot = true;
assertTrue(overlayLayout.drawChild(canvas, child, 0));
}
@SdkExclude(minSdkVersion = 31) // spying views does not work properly on 31, should investigate
@UiThreadTest
@Test
public void testDraw() {
Canvas canvas = new Canvas();
when(overlayLayout.drawsOn(Overlay.Target.PREVIEW)).thenReturn(false);
overlayLayout.draw(canvas);
verify(overlayLayout, never()).drawOn(Overlay.Target.PREVIEW, canvas);
when(overlayLayout.drawsOn(Overlay.Target.PREVIEW)).thenReturn(true);
overlayLayout.draw(canvas);
verify(overlayLayout, times(1)).drawOn(Overlay.Target.PREVIEW, canvas);
}
@SdkExclude(minSdkVersion = 31) // spying views does not work properly on 31, should investigate
@UiThreadTest
@Test
public void testDrawOn() {
Canvas canvas = spy(new Canvas());
View child = new View(getContext());
OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
params.drawOnPreview = true;
params.drawOnPictureSnapshot = true;
params.drawOnVideoSnapshot = true;
overlayLayout.addView(child, params);
overlayLayout.drawOn(Overlay.Target.PREVIEW, canvas);
verify(canvas, never()).scale(anyFloat(), anyFloat());
verify(overlayLayout, times(1)).doDrawChild(eq(canvas), eq(child), anyLong());
reset(canvas);
reset(overlayLayout);
overlayLayout.drawOn(Overlay.Target.PICTURE_SNAPSHOT, canvas);
verify(canvas, times(1)).scale(anyFloat(), anyFloat());
verify(overlayLayout, times(1)).doDrawChild(eq(canvas), eq(child), anyLong());
reset(canvas);
reset(overlayLayout);
overlayLayout.drawOn(Overlay.Target.VIDEO_SNAPSHOT, canvas);
verify(canvas, times(1)).scale(anyFloat(), anyFloat());
verify(overlayLayout, times(1)).doDrawChild(eq(canvas), eq(child), anyLong());
reset(canvas);
reset(overlayLayout);
}
}

@ -0,0 +1,43 @@
package com.otaliastudios.cameraview.picture;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.PictureResult;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.lang.reflect.Constructor;
import static org.junit.Assert.assertNull;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class PictureRecorderTest extends BaseTest {
@Test
public void testRecorder() throws Exception {
PictureResult.Stub result = createStub();
PictureRecorder.PictureResultListener listener = Mockito.mock(PictureRecorder.PictureResultListener.class);
PictureRecorder recorder = new PictureRecorder(result, listener) {
public void take() {
dispatchResult();
}
};
recorder.take();
Mockito.verify(listener, Mockito.times(1)).onPictureResult(result, null);
assertNull(recorder.mListener);
assertNull(recorder.mResult);
}
private PictureResult.Stub createStub() throws Exception {
Constructor<PictureResult.Stub> constructor = PictureResult.Stub.class.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
}
}

@ -1,10 +1,16 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview.preview;
import android.content.Context; import android.content.Context;
import android.support.test.rule.ActivityTestRule; import androidx.test.rule.ActivityTestRule;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.TestActivity;
import com.otaliastudios.cameraview.tools.Op;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
@ -17,55 +23,71 @@ import static org.junit.Assert.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
public abstract class PreviewTest extends BaseTest { public abstract class CameraPreviewTest<T extends CameraPreview> extends BaseTest {
private final static long DELAY = 4000;
protected abstract CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback); protected abstract T createPreview(Context context, ViewGroup parent);
@Rule @Rule
public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class); public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
protected CameraPreview preview; protected T preview;
@SuppressWarnings("WeakerAccess")
protected Size surfaceSize; protected Size surfaceSize;
private CameraPreview.SurfaceCallback callback; private CameraPreview.SurfaceCallback callback;
private Task<Boolean> availability;
private Op<Boolean> available;
private Op<Boolean> destroyed;
@Before @Before
public void setUp() { public void setUp() {
availability = new Task<>(true); available = new Op<>();
destroyed = new Op<>();
ui(new Runnable() { uiSync(new Runnable() {
@Override @Override
public void run() { public void run() {
TestActivity a = rule.getActivity(); TestActivity a = rule.getActivity();
surfaceSize = a.getContentSize(); surfaceSize = a.getContentSize();
callback = mock(CameraPreview.SurfaceCallback.class); callback = mock(CameraPreview.SurfaceCallback.class);
doAnswer(new Answer() { doAnswer(new Answer() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) {
if (availability != null) availability.end(true); if (available != null) available.controller().end(true);
return null; return null;
} }
}).when(callback).onSurfaceAvailable(); }).when(callback).onSurfaceAvailable();
preview = createPreview(a, a.getContentView(), callback);
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) {
if (destroyed != null) destroyed.controller().end(true);
return null;
}
}).when(callback).onSurfaceDestroyed();
preview = createPreview(a, a.getContentView());
preview.setSurfaceCallback(callback);
} }
}); });
} }
// Wait for surface to be available. // Wait for surface to be available.
protected void ensureAvailable() { protected void ensureAvailable() {
assertNotNull(availability.await(2000)); assertNotNull(available.await(DELAY));
} }
// Trigger a destroy. // Trigger a destroy.
protected void ensureDestroyed() { protected void ensureDestroyed() {
ui(new Runnable() { uiSync(new Runnable() {
@Override @Override
public void run() { public void run() {
rule.getActivity().getContentView().removeView(preview.getView()); rule.getActivity().getContentView().removeView(preview.getRootView());
} }
}); });
idle(); assertNotNull(destroyed.await(DELAY));
} }
@After @After
@ -73,22 +95,24 @@ public abstract class PreviewTest extends BaseTest {
preview = null; preview = null;
callback = null; callback = null;
surfaceSize = null; surfaceSize = null;
availability = null; available = null;
destroyed = null;
} }
@Test @Test
public void testDefaults() { public void testDefaults() {
ensureAvailable(); ensureAvailable();
assertTrue(preview.isReady()); assertTrue(preview.hasSurface());
assertNotNull(preview.getView()); assertNotNull(preview.getView());
assertNotNull(preview.getRootView());
assertNotNull(preview.getOutputClass()); assertNotNull(preview.getOutputClass());
} }
@Test @Test
public void testDesiredSize() { public void testDesiredSize() {
preview.setDesiredSize(160, 90); preview.setStreamSize(160, 90);
assertEquals(160, preview.getDesiredSize().getWidth()); assertEquals(160, preview.getStreamSize().getWidth());
assertEquals(90, preview.getDesiredSize().getHeight()); assertEquals(90, preview.getStreamSize().getHeight());
} }
@Test @Test
@ -103,12 +127,14 @@ public abstract class PreviewTest extends BaseTest {
public void testSurfaceDestroyed() { public void testSurfaceDestroyed() {
ensureAvailable(); ensureAvailable();
ensureDestroyed(); ensureDestroyed();
// This might be called twice in Texture because it overrides ensureDestroyed method
verify(callback, atLeastOnce()).onSurfaceDestroyed();
assertEquals(0, preview.getSurfaceSize().getWidth()); assertEquals(0, preview.getSurfaceSize().getWidth());
assertEquals(0, preview.getSurfaceSize().getHeight()); assertEquals(0, preview.getSurfaceSize().getHeight());
} }
@Test @Test
public void testCropCenter() throws Exception { public void testCropCenter() {
ensureAvailable(); ensureAvailable();
// This is given by the activity, it's the fixed size. // This is given by the activity, it's the fixed size.
@ -127,17 +153,23 @@ public abstract class PreviewTest extends BaseTest {
// Since desired is 'desired', let's fake a new view size that is consistent with it. // Since desired is 'desired', let's fake a new view size that is consistent with it.
// Ensure crop is not happening anymore. // Ensure crop is not happening anymore.
preview.mCropTask.listen(); preview.mCropCallback = mock(CameraPreview.CropCallback.class);
preview.onSurfaceSizeChanged((int) (50f * desired), 50); // Wait... Op<Void> op = new Op<>();
preview.mCropTask.await(); doEndOp(op, null).when(preview.mCropCallback).onCrop();
preview.dispatchOnSurfaceSizeChanged((int) (50f * desired), 50);
op.await(); // Wait...
assertEquals(desired, getViewAspectRatioWithScale(), 0.01f); assertEquals(desired, getViewAspectRatioWithScale(), 0.01f);
assertFalse(preview.isCropping()); assertFalse(preview.isCropping());
} }
private void setDesiredAspectRatio(float desiredAspectRatio) { private void setDesiredAspectRatio(float desiredAspectRatio) {
preview.mCropTask.listen(); preview.mCropCallback = mock(CameraPreview.CropCallback.class);
preview.setDesiredSize((int) (10f * desiredAspectRatio), 10); // Wait... Op<Void> op = new Op<>();
preview.mCropTask.await(); doEndOp(op, null).when(preview.mCropCallback).onCrop();
preview.setStreamSize((int) (10f * desiredAspectRatio), 10);
op.await(); // Wait...
assertEquals(desiredAspectRatio, getViewAspectRatioWithScale(), 0.01f); assertEquals(desiredAspectRatio, getViewAspectRatioWithScale(), 0.01f);
} }
@ -149,8 +181,12 @@ public abstract class PreviewTest extends BaseTest {
private float getViewAspectRatioWithScale() { private float getViewAspectRatioWithScale() {
Size size = preview.getSurfaceSize(); Size size = preview.getSurfaceSize();
int newWidth = (int) (((float) size.getWidth()) * preview.getView().getScaleX()); int newWidth = (int) (((float) size.getWidth()) * getCropScaleX());
int newHeight = (int) (((float) size.getHeight()) * preview.getView().getScaleY()); int newHeight = (int) (((float) size.getHeight()) * getCropScaleY());
return AspectRatio.of(newWidth, newHeight).toFloat(); return AspectRatio.of(newWidth, newHeight).toFloat();
} }
abstract protected float getCropScaleX();
abstract protected float getCropScaleY();
} }

@ -0,0 +1,43 @@
package com.otaliastudios.cameraview.preview;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import android.view.ViewGroup;
import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.Filters;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class GlCameraPreviewTest extends CameraPreviewTest<GlCameraPreview> {
@Override
protected GlCameraPreview createPreview(Context context, ViewGroup parent) {
return new GlCameraPreview(context, parent);
}
@Override
protected float getCropScaleY() {
return 1F / preview.mCropScaleY;
}
@Override
protected float getCropScaleX() {
return 1F / preview.mCropScaleX;
}
@Test
public void testSetFilter() {
Filter filter = Filters.BLACK_AND_WHITE.newInstance();
preview.setFilter(filter);
assertEquals(filter, preview.getCurrentFilter());
}
}

@ -0,0 +1,63 @@
package com.otaliastudios.cameraview.preview;
import android.content.Context;
import androidx.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.preview.CameraPreview;
public class MockCameraPreview extends CameraPreview<View, Void> implements FilterCameraPreview {
public MockCameraPreview(Context context, ViewGroup parent) {
super(context, parent);
}
private View rootView;
private Filter filter;
@Override
public boolean supportsCropping() {
return true;
}
@NonNull
@Override
protected View onCreateView(@NonNull Context context, @NonNull ViewGroup parent) {
rootView = new View(context);
return rootView;
}
@NonNull
@Override
public Class<Void> getOutputClass() {
return null;
}
@NonNull
@Override
public Void getOutput() {
return null;
}
@NonNull
@Override
public View getRootView() {
return rootView;
}
@Override
public void setFilter(@NonNull Filter filter) {
this.filter = filter;
}
@NonNull
@Override
public Filter getCurrentFilter() {
return filter;
}
}

@ -0,0 +1,30 @@
package com.otaliastudios.cameraview.preview;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import android.view.ViewGroup;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class SurfaceCameraPreviewTest extends CameraPreviewTest<SurfaceCameraPreview> {
@Override
protected SurfaceCameraPreview createPreview(Context context, ViewGroup parent) {
return new SurfaceCameraPreview(context, parent);
}
@Override
protected float getCropScaleX() {
return 1F;
}
@Override
protected float getCropScaleY() {
return 1F;
}
}

@ -1,20 +1,21 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview.preview;
import android.content.Context; import android.content.Context;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class TextureCameraPreviewTest extends PreviewTest { public class TextureCameraPreviewTest extends CameraPreviewTest<TextureCameraPreview> {
@Override @Override
protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) { protected TextureCameraPreview createPreview(Context context, ViewGroup parent) {
return new TextureCameraPreview(context, parent, callback); return new TextureCameraPreview(context, parent);
} }
@Override @Override
@ -22,7 +23,7 @@ public class TextureCameraPreviewTest extends PreviewTest {
if (isHardwareAccelerated()) { if (isHardwareAccelerated()) {
super.ensureAvailable(); super.ensureAvailable();
} else { } else {
preview.onSurfaceAvailable( preview.dispatchOnSurfaceAvailable(
surfaceSize.getWidth(), surfaceSize.getWidth(),
surfaceSize.getHeight()); surfaceSize.getHeight());
} }
@ -33,11 +34,21 @@ public class TextureCameraPreviewTest extends PreviewTest {
super.ensureDestroyed(); super.ensureDestroyed();
if (!isHardwareAccelerated()) { if (!isHardwareAccelerated()) {
// Ensure it is called. // Ensure it is called.
preview.onSurfaceDestroyed(); preview.dispatchOnSurfaceDestroyed();
} }
} }
private boolean isHardwareAccelerated() { private boolean isHardwareAccelerated() {
return preview.getView().isHardwareAccelerated(); return preview.getView().isHardwareAccelerated();
} }
@Override
protected float getCropScaleX() {
return preview.getView().getScaleX();
}
@Override
protected float getCropScaleY() {
return preview.getView().getScaleY();
}
} }

@ -0,0 +1,211 @@
package com.otaliastudios.cameraview.size;
import android.content.res.TypedArray;
import androidx.annotation.NonNull;
import androidx.annotation.StyleableRes;
import androidx.arch.core.util.Function;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.R;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class SizeSelectorParserTest extends BaseTest {
private MockTypedArray input;
private List<Size> sizes = Arrays.asList(
new Size(100, 200),
new Size(150, 300),
new Size(600, 900),
new Size(600, 600),
new Size(1600, 900),
new Size(30, 40),
new Size(40, 30),
new Size(2000, 4000)
);
@Before
public void setUp() {
input = new MockTypedArray();
}
@After
public void tearDown() {
input = null;
}
private void doAssert(@NonNull Function<List<Size>, Void> assertions) {
SizeSelectorParser parser = new SizeSelectorParser(input.array);
assertions.apply(parser.getPictureSizeSelector().select(sizes));
assertions.apply(parser.getVideoSizeSelector().select(sizes));
}
@Test
public void testWidth() {
input.setMinWidth(1500);
input.setMaxWidth(1700);
doAssert(new Function<List<Size>, Void>() {
@Override
public Void apply(List<Size> input) {
assertEquals(1, input.size());
assertEquals(new Size(1600, 900), input.get(0));
return null;
}
});
}
@Test
public void testHeight() {
input.setMinHeight(25);
input.setMaxHeight(35);
doAssert(new Function<List<Size>, Void>() {
@Override
public Void apply(List<Size> input) {
assertEquals(1, input.size());
assertEquals(new Size(40, 30), input.get(0));
return null;
}
});
}
@Test
public void testArea() {
input.setMinArea(30 * 30);
input.setMaxArea(40 * 40);
doAssert(new Function<List<Size>, Void>() {
@Override
public Void apply(List<Size> input) {
assertEquals(2, input.size());
assertTrue(input.contains(new Size(40, 30)));
assertTrue(input.contains(new Size(30, 40)));
return null;
}
});
}
@Test
public void testSmallest() {
input.setSmallest(true);
doAssert(new Function<List<Size>, Void>() {
@Override
public Void apply(List<Size> input) {
assertEquals(sizes.size(), input.size());
Size first = input.get(0);
assertEquals(30 * 40, first.getWidth() * first.getHeight());
return null;
}
});
}
@Test
public void testBiggest() {
input.setBiggest(true);
doAssert(new Function<List<Size>, Void>() {
@Override
public Void apply(List<Size> input) {
assertEquals(sizes.size(), input.size());
assertEquals(new Size(2000, 4000), input.get(0));
return null;
}
});
}
@Test
public void testAspectRatio() {
input.setAspectRatio("16:9");
doAssert(new Function<List<Size>, Void>() {
@Override
public Void apply(List<Size> input) {
assertEquals(1, input.size());
assertEquals(new Size(1600, 900), input.get(0));
return null;
}
});
}
@SuppressWarnings("SameParameterValue")
private class MockTypedArray {
private TypedArray array = mock(TypedArray.class);
private void setIntValue(@StyleableRes int index, int value) {
when(array.hasValue(index)).thenReturn(true);
when(array.getInteger(eq(index), anyInt())).thenReturn(value);
}
private void setBooleanValue(@StyleableRes int index, boolean value) {
when(array.hasValue(index)).thenReturn(true);
when(array.getBoolean(eq(index), anyBoolean())).thenReturn(value);
}
private void setStringValue(@StyleableRes int index, @NonNull String value) {
when(array.hasValue(index)).thenReturn(true);
when(array.getString(index)).thenReturn(value);
}
private void setMinWidth(int value) {
setIntValue(R.styleable.CameraView_cameraPictureSizeMinWidth, value);
setIntValue(R.styleable.CameraView_cameraVideoSizeMinWidth, value);
}
private void setMaxWidth(int value) {
setIntValue(R.styleable.CameraView_cameraPictureSizeMaxWidth, value);
setIntValue(R.styleable.CameraView_cameraVideoSizeMaxWidth, value);
}
private void setMinHeight(int value) {
setIntValue(R.styleable.CameraView_cameraPictureSizeMinHeight, value);
setIntValue(R.styleable.CameraView_cameraVideoSizeMinHeight, value);
}
private void setMaxHeight(int value) {
setIntValue(R.styleable.CameraView_cameraPictureSizeMaxHeight, value);
setIntValue(R.styleable.CameraView_cameraVideoSizeMaxHeight, value);
}
private void setMinArea(int value) {
setIntValue(R.styleable.CameraView_cameraPictureSizeMinArea, value);
setIntValue(R.styleable.CameraView_cameraVideoSizeMinArea, value);
}
private void setMaxArea(int value) {
setIntValue(R.styleable.CameraView_cameraPictureSizeMaxArea, value);
setIntValue(R.styleable.CameraView_cameraVideoSizeMaxArea, value);
}
private void setSmallest(boolean value) {
setBooleanValue(R.styleable.CameraView_cameraPictureSizeSmallest, value);
setBooleanValue(R.styleable.CameraView_cameraVideoSizeSmallest, value);
}
private void setBiggest(boolean value) {
setBooleanValue(R.styleable.CameraView_cameraPictureSizeBiggest, value);
setBooleanValue(R.styleable.CameraView_cameraVideoSizeBiggest, value);
}
private void setAspectRatio(@NonNull String value) {
setStringValue(R.styleable.CameraView_cameraPictureSizeAspectRatio, value);
setStringValue(R.styleable.CameraView_cameraVideoSizeAspectRatio, value);
}
}
}

@ -0,0 +1,12 @@
package com.otaliastudios.cameraview.tools;
import android.os.Build;
public class Emulator {
public static boolean isEmulator() {
// From Android's RequiresDeviceFilter
return Build.HARDWARE.equals("goldfish")
|| Build.HARDWARE.equals("ranchu")
|| Build.HARDWARE.equals("gce_x86");
}
}

@ -0,0 +1,149 @@
package com.otaliastudios.cameraview.tools;
import androidx.annotation.NonNull;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.otaliastudios.cameraview.controls.Control;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.mockito.stubbing.Stubber;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* A naive implementation of {@link java.util.concurrent.CountDownLatch}
* to help in testing.
*/
public class Op<T> {
public class Controller {
private int mToBeIgnored;
private Controller() { }
/** Op owner method: notifies the action started. */
public void start() {
if (!isListening()) mToBeIgnored++;
}
/** Op owner method: notifies the action ended. */
public void end(T result) {
if (mToBeIgnored > 0) {
mToBeIgnored--;
return;
}
if (isListening()) { // Should be always true.
mResult = result;
mLatch.countDown();
}
}
public void from(@NonNull Task<T> task) {
start();
task.addOnSuccessListener(new OnSuccessListener<T>() {
@Override
public void onSuccess(T result) {
end(result);
}
});
}
@NonNull
public Stubber from(final int invocationArgument) {
return Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) {
//noinspection unchecked
T o = (T) invocation.getArguments()[invocationArgument];
start();
end(o);
return null;
}
});
}
}
private CountDownLatch mLatch;
private Controller mController = new Controller();
private T mResult;
/**
* Listeners should:
* - call {@link #listen()} to notify they are interested in the next action
* - call {@link #await()} to know when the action is performed.
*
* Op owners should:
* - call {@link Controller#start()} when task started
* - call {@link Controller#end(Object)} when task ends
*/
public Op() {
this(true);
}
public Op(boolean startListening) {
if (startListening) listen();
}
public Op(@NonNull Task<T> task) {
listen();
controller().from(task);
}
private boolean isListening() {
return mLatch != null;
}
/**
* Listener method: notifies we are interested in the next action.
*/
public void listen() {
if (isListening()) throw new RuntimeException("Should not happen.");
mResult = null;
mLatch = new CountDownLatch(1);
}
/**
* Listener method: waits for next task action to end.
* @param millis milliseconds
* @return the action result
*/
public T await(long millis) {
return await(millis, TimeUnit.MILLISECONDS);
}
/**
* Listener method: waits 1 minute for next task action to end.
* @return the action result
*/
public T await() {
return await(1, TimeUnit.MINUTES);
}
/**
* Listener method: waits for next task action to end.
* @param time time
* @param unit the time unit
* @return the action result
*/
private T await(long time, @NonNull TimeUnit unit) {
try {
mLatch.await(time, unit);
} catch (Exception e) {
e.printStackTrace();
}
T result = mResult;
mResult = null;
mLatch = null;
return result;
}
@NonNull
public Controller controller() {
return mController;
}
}

@ -0,0 +1,9 @@
package com.otaliastudios.cameraview.tools;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
boolean emulatorOnly() default false;
}

@ -0,0 +1,52 @@
package com.otaliastudios.cameraview.tools;
import android.os.Build;
import com.otaliastudios.cameraview.CameraLogger;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.util.concurrent.atomic.AtomicInteger;
public class RetryRule implements TestRule {
private final static String TAG = RetryRule.class.getSimpleName();
private final static CameraLogger LOG = CameraLogger.create(TAG);
private AtomicInteger retries;
public RetryRule(int retries) {
this.retries = new AtomicInteger(retries);
}
@Override
public Statement apply(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
Retry retry = description.getAnnotation(Retry.class);
if (retry == null || retry.emulatorOnly() && !Emulator.isEmulator()) {
base.evaluate();
} else {
Throwable caught = null;
while (retries.getAndDecrement() > 0) {
try {
base.evaluate();
return;
} catch (Throwable throwable) {
LOG.e("[RETRY] Test failed.", retries.get(),
"retries available...");
LOG.e("*******************************************************");
caught = throwable;
}
}
if (caught != null) {
throw caught;
}
}
}
};
}
}

@ -0,0 +1,20 @@
package com.otaliastudios.cameraview.tools;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Like {@link androidx.test.filters.SdkSuppress}, but negative.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface SdkExclude {
/** The minimum API level to drop (inclusive) */
int minSdkVersion() default 1;
/** The maximum API level to drop (inclusive) */
int maxSdkVersion() default Integer.MAX_VALUE;
/** Whether this filter only applies to emulators */
boolean emulatorOnly() default false;
}

@ -0,0 +1,47 @@
package com.otaliastudios.cameraview.tools;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.test.internal.runner.filters.ParentFilter;
import org.junit.runner.Description;
/**
* Filter for {@link SdkExclude}, based on
* {@link androidx.test.internal.runner.TestRequestBuilder}'s SdkSuppressFilter.
*/
public class SdkExcludeFilter extends ParentFilter {
protected boolean evaluateTest(Description description) {
SdkExclude annotation = getAnnotationForTest(description);
if (annotation != null) {
if ((!annotation.emulatorOnly() || Emulator.isEmulator())
&& Build.VERSION.SDK_INT >= annotation.minSdkVersion()
&& Build.VERSION.SDK_INT <= annotation.maxSdkVersion()) {
return false; // exclude the test
}
return true; // run the test
}
return true; // no annotation, run the test
}
@Nullable
private SdkExclude getAnnotationForTest(Description description) {
final SdkExclude s = description.getAnnotation(SdkExclude.class);
if (s != null) {
return s;
}
final Class<?> testClass = description.getTestClass();
if (testClass != null) {
return testClass.getAnnotation(SdkExclude.class);
}
return null;
}
@Override
public String describe() {
return "Skip tests annotated with SdkExclude";
}
}

@ -0,0 +1,20 @@
package com.otaliastudios.cameraview.tools;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Like {@link androidx.test.filters.SdkSuppress}, but with emulatorOnly().
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface SdkInclude {
/** The minimum API level to run (inclusive) */
int minSdkVersion() default 1;
/** The maximum API level to run (inclusive) */
int maxSdkVersion() default Integer.MAX_VALUE;
/** Whether this filter only applies to emulators */
boolean emulatorOnly() default false;
}

@ -0,0 +1,47 @@
package com.otaliastudios.cameraview.tools;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.test.internal.runner.filters.ParentFilter;
import org.junit.runner.Description;
/**
* Filter for {@link SdkInclude}, based on
* {@link androidx.test.internal.runner.TestRequestBuilder}'s SdkSuppressFilter.
*/
public class SdkIncludeFilter extends ParentFilter {
protected boolean evaluateTest(Description description) {
SdkInclude annotation = getAnnotationForTest(description);
if (annotation != null) {
if ((!annotation.emulatorOnly() || Emulator.isEmulator())
&& Build.VERSION.SDK_INT >= annotation.minSdkVersion()
&& Build.VERSION.SDK_INT <= annotation.maxSdkVersion()) {
return true; // run the test
}
return false; // drop the test
}
return true; // no annotation, run the test
}
@Nullable
private SdkInclude getAnnotationForTest(Description description) {
final SdkInclude s = description.getAnnotation(SdkInclude.class);
if (s != null) {
return s;
}
final Class<?> testClass = description.getTestClass();
if (testClass != null) {
return testClass.getAnnotation(SdkInclude.class);
}
return null;
}
@Override
public String describe() {
return "Skip tests annotated with SdkInclude";
}
}

@ -0,0 +1,52 @@
package com.otaliastudios.cameraview.video;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.VideoResult;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.lang.reflect.Constructor;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class VideoRecorderTest extends BaseTest {
@Test
public void testRecorder() throws Exception {
VideoResult.Stub result = createStub();
VideoRecorder.VideoResultListener listener = Mockito.mock(VideoRecorder.VideoResultListener.class);
VideoRecorder recorder = new VideoRecorder(listener) {
@Override
protected void onStart() {
dispatchVideoRecordingStart();
}
@Override
protected void onStop(boolean isCameraShutdown) {
dispatchVideoRecordingEnd();
dispatchResult();
}
};
recorder.start(result);
Mockito.verify(listener,Mockito.times(1) )
.onVideoRecordingStart();
recorder.stop(false);
Mockito.verify(listener, Mockito.times(1))
.onVideoRecordingEnd();
Mockito.verify(listener, Mockito.times(1))
.onVideoResult(result, null);
}
private VideoResult.Stub createStub() throws Exception {
Constructor<VideoResult.Stub> constructor = VideoResult.Stub.class.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
}
}

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:layout_drawOnPreview="true"
app:layout_drawOnPictureSnapshot="true"
app:layout_drawOnVideoSnapshot="true"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.otaliastudios.cameraview"> package="com.otaliastudios.cameraview">
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
@ -20,6 +21,8 @@
android:name="android.hardware.microphone" android:name="android.hardware.microphone"
android:required="false"/> android:required="false"/>
<uses-sdk tools:overrideLibrary="com.otaliastudios.opengl" />
<application/> <application/>
</manifest> </manifest>

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

Loading…
Cancel
Save