├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── dependabot.yml └── workflows │ ├── foss-release.yml │ ├── gplay-release.yml │ ├── image-minimizer.yml │ ├── no-response.yml │ ├── pr-labeler.yml │ ├── pr.yml │ ├── release-commenter.yml │ └── testing-build.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── detekt-baseline.xml ├── lint-baseline.xml ├── proguard-rules.pro └── src │ ├── debug │ └── res │ │ └── values │ │ └── strings.xml │ ├── foss │ └── res │ │ └── values │ │ └── bools.xml │ ├── gplay │ └── res │ │ └── values │ │ └── bools.xml │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── kotlin │ └── org │ │ └── fossify │ │ └── camera │ │ ├── activities │ │ ├── MainActivity.kt │ │ ├── SettingsActivity.kt │ │ ├── SimpleActivity.kt │ │ └── SplashActivity.kt │ │ ├── extensions │ │ ├── CameraSelector.kt │ │ ├── Context.kt │ │ ├── Int.kt │ │ ├── MaterialButton.kt │ │ ├── Quality.kt │ │ └── View.kt │ │ ├── helpers │ │ ├── BitmapUtils.kt │ │ ├── CameraErrorHandler.kt │ │ ├── Config.kt │ │ ├── Constants.kt │ │ ├── GestureDetectorListener.java │ │ ├── ImageQualityManager.kt │ │ ├── ImageSaver.kt │ │ ├── ImageUtil.kt │ │ ├── MediaActionSound.kt │ │ ├── MediaOutputHelper.kt │ │ ├── MediaSizeStore.kt │ │ ├── MediaSoundHelper.kt │ │ ├── PhotoProcessor.kt │ │ ├── PinchToZoomOnScaleGestureListener.kt │ │ ├── SimpleLocationManager.kt │ │ ├── TabSelectedListener.kt │ │ ├── VideoQualityManager.kt │ │ └── ZoomCalculator.kt │ │ ├── implementations │ │ ├── CameraXInitializer.kt │ │ ├── CameraXPreview.kt │ │ └── CameraXPreviewListener.kt │ │ ├── interfaces │ │ └── MyPreview.kt │ │ ├── models │ │ ├── CameraSelectorImageQualities.kt │ │ ├── CameraSelectorVideoQualities.kt │ │ ├── CaptureMode.kt │ │ ├── MediaOutput.kt │ │ ├── MySize.kt │ │ ├── ResolutionOption.kt │ │ ├── TimerMode.kt │ │ └── VideoQuality.kt │ │ ├── receivers │ │ └── HardwareShutterReceiver.kt │ │ └── views │ │ ├── FocusCircleView.kt │ │ └── ShadowDrawable.kt │ └── res │ ├── color │ ├── camera_option_color.xml │ └── tab_color.xml │ ├── drawable │ ├── ic_camera_front_vector.xml │ ├── ic_camera_rear_vector.xml │ ├── ic_flash_auto_vector.xml │ ├── ic_flash_off_vector.xml │ ├── ic_flash_on_vector.xml │ ├── ic_flashlight_vector.xml │ ├── ic_launcher_foreground.xml │ ├── ic_launcher_monochrome.xml │ ├── ic_photo_16x9_vector.xml │ ├── ic_photo_1x1_vector.xml │ ├── ic_photo_4x3_vector.xml │ ├── ic_photo_full_vector.xml │ ├── ic_settings_vector.xml │ ├── ic_shutter_animated.xml │ ├── ic_shutter_timer_cancel.xml │ ├── ic_shutter_vector.xml │ ├── ic_timer_10_vector.xml │ ├── ic_timer_3_vector.xml │ ├── ic_timer_5_vector.xml │ ├── ic_timer_off_vector.xml │ ├── ic_video_fhd_vector.xml │ ├── ic_video_hd_vector.xml │ ├── ic_video_rec_animated.xml │ ├── ic_video_rec_vector.xml │ ├── ic_video_sd_vector.xml │ ├── ic_video_uhd_vector.xml │ ├── shutter_pressed_to_unpressed.xml │ ├── shutter_unpressed_to_pressed.xml │ ├── tab_indicator.xml │ ├── video_rec_idle_to_record.xml │ ├── video_rec_pressed_to_unpressed.xml │ ├── video_rec_record_to_idle.xml │ └── video_rec_unpressed_to_pressed.xml │ ├── layout │ ├── activity_main.xml │ ├── activity_settings.xml │ ├── layout_button.xml │ ├── layout_flash.xml │ ├── layout_timer.xml │ ├── layout_top.xml │ └── timer_text.xml │ ├── menu │ └── menu.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ ├── ic_launcher_amber.xml │ ├── ic_launcher_blue.xml │ ├── ic_launcher_blue_grey.xml │ ├── ic_launcher_brown.xml │ ├── ic_launcher_cyan.xml │ ├── ic_launcher_deep_orange.xml │ ├── ic_launcher_deep_purple.xml │ ├── ic_launcher_grey_black.xml │ ├── ic_launcher_indigo.xml │ ├── ic_launcher_light_blue.xml │ ├── ic_launcher_light_green.xml │ ├── ic_launcher_lime.xml │ ├── ic_launcher_orange.xml │ ├── ic_launcher_pink.xml │ ├── ic_launcher_purple.xml │ ├── ic_launcher_red.xml │ ├── ic_launcher_teal.xml │ └── ic_launcher_yellow.xml │ ├── raw │ ├── beep.mp3 │ └── beep_2_secs.mp3 │ ├── values-ar │ └── strings.xml │ ├── values-az │ └── strings.xml │ ├── values-b+es+419 │ └── strings.xml │ ├── values-be │ └── strings.xml │ ├── values-bg │ └── strings.xml │ ├── values-bn-rBD │ └── strings.xml │ ├── values-bn │ └── strings.xml │ ├── values-br │ └── strings.xml │ ├── values-bs │ └── strings.xml │ ├── values-ca │ └── strings.xml │ ├── values-ckb │ └── strings.xml │ ├── values-cr │ └── strings.xml │ ├── values-cs │ └── strings.xml │ ├── values-cy │ └── strings.xml │ ├── values-da │ └── strings.xml │ ├── values-de │ └── strings.xml │ ├── values-el │ └── strings.xml │ ├── values-en-rGB │ └── strings.xml │ ├── values-en-rIN │ └── strings.xml │ ├── values-eo │ └── strings.xml │ ├── values-es-rUS │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-et │ └── strings.xml │ ├── values-eu │ └── strings.xml │ ├── values-fa │ └── strings.xml │ ├── values-fi │ └── strings.xml │ ├── values-fil │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-ga │ └── strings.xml │ ├── values-gl │ └── strings.xml │ ├── values-hi │ └── strings.xml │ ├── values-hr │ └── strings.xml │ ├── values-hu │ └── strings.xml │ ├── values-ia │ └── strings.xml │ ├── values-in │ └── strings.xml │ ├── values-is │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-iw │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-kn │ └── strings.xml │ ├── values-ko │ └── strings.xml │ ├── values-kr │ └── strings.xml │ ├── values-lt │ └── strings.xml │ ├── values-ltg │ └── strings.xml │ ├── values-lv │ └── strings.xml │ ├── values-mk │ └── strings.xml │ ├── values-ml │ └── strings.xml │ ├── values-ms │ └── strings.xml │ ├── values-my │ └── strings.xml │ ├── values-nb-rNO │ └── strings.xml │ ├── values-ne │ └── strings.xml │ ├── values-nl │ └── strings.xml │ ├── values-nn │ └── strings.xml │ ├── values-or │ └── strings.xml │ ├── values-pa-rPK │ └── strings.xml │ ├── values-pa │ └── strings.xml │ ├── values-pl │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-pt-rPT │ └── strings.xml │ ├── values-pt │ └── strings.xml │ ├── values-ro │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-sat │ └── strings.xml │ ├── values-si │ └── strings.xml │ ├── values-sk │ └── strings.xml │ ├── values-sl │ └── strings.xml │ ├── values-sr │ └── strings.xml │ ├── values-sv │ └── strings.xml │ ├── values-sw600dp │ └── dimens.xml │ ├── values-ta │ └── strings.xml │ ├── values-te │ └── strings.xml │ ├── values-th │ └── strings.xml │ ├── values-tr │ └── strings.xml │ ├── values-uk │ └── strings.xml │ ├── values-vi │ └── strings.xml │ ├── values-zgh │ └── strings.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rHK │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ ├── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── donottranslate.xml │ ├── ids.xml │ ├── integers.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── provider_paths.xml ├── build.gradle.kts ├── fastlane ├── Appfile ├── Fastfile ├── README.md └── metadata │ └── android │ ├── ca │ ├── full_description.txt │ └── short_description.txt │ ├── cs-CZ │ ├── full_description.txt │ └── short_description.txt │ ├── de-DE │ ├── full_description.txt │ └── short_description.txt │ ├── en-US │ ├── changelogs │ │ ├── 1.txt │ │ ├── 2.txt │ │ └── 3.txt │ ├── full_description.txt │ ├── images │ │ ├── featureGraphic.png │ │ ├── icon.png │ │ ├── phoneScreenshots │ │ │ ├── 1_en-US.png │ │ │ ├── 2_en-US.png │ │ │ ├── 3_en-US.png │ │ │ ├── 4_en-US.png │ │ │ ├── 5_en-US.png │ │ │ └── 6_en-US.png │ │ └── tenInchScreenshots │ │ │ ├── 1_en-US.png │ │ │ ├── 2_en-US.png │ │ │ ├── 3_en-US.png │ │ │ ├── 4_en-US.png │ │ │ ├── 5_en-US.png │ │ │ └── 6_en-US.png │ ├── short_description.txt │ └── title.txt │ ├── eo │ └── short_description.txt │ ├── es-ES │ ├── full_description.txt │ └── short_description.txt │ ├── et │ ├── full_description.txt │ └── short_description.txt │ ├── fr-FR │ ├── full_description.txt │ └── short_description.txt │ ├── gl-ES │ ├── full_description.txt │ └── short_description.txt │ ├── it-IT │ ├── full_description.txt │ └── short_description.txt │ ├── iw-IL │ ├── full_description.txt │ └── short_description.txt │ ├── pl-PL │ ├── full_description.txt │ └── short_description.txt │ ├── pt-BR │ ├── full_description.txt │ └── short_description.txt │ ├── ru-RU │ ├── full_description.txt │ └── short_description.txt │ ├── sv-SE │ └── short_description.txt │ ├── tr-TR │ ├── full_description.txt │ └── short_description.txt │ ├── uk │ ├── full_description.txt │ └── short_description.txt │ ├── zh-CN │ ├── full_description.txt │ └── short_description.txt │ └── zh-TW │ └── short_description.txt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── graphics ├── foreground.svg ├── icon.svg └── icon.webp ├── keystore.properties_sample └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig 2 | # http://EditorConfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # LF end-of-line, insert an empty new line and UTF-8 8 | [*] 9 | end_of_line = lf 10 | insert_final_newline = true 11 | charset = utf-8 12 | indent_style = space 13 | indent_size = 4 14 | continuation_indent_size = 4 15 | max_line_length = 160 16 | 17 | [*.xml] 18 | continuation_indent_size = 4 19 | 20 | [*.kt] 21 | ij_kotlin_name_count_to_use_star_import = 5 22 | ij_kotlin_name_count_to_use_star_import_for_members = 5 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions 4 | url: https://github.com/FossifyOrg/Camera/discussions 5 | about: Please ask and answer questions here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | labels: [ "feature request", "needs triage" ] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## Hi there 👋 9 | Thanks for helping us improve Fossify Camera by suggesting a feature! 10 | Please fill in as much information as possible about your feature request to avoid unnecessary and time-consuming back-and-forth communication. 11 | 12 | - type: checkboxes 13 | id: checklist 14 | attributes: 15 | label: "Checklist" 16 | options: 17 | - label: "I made sure that there are **no existing issues** - [open](https://github.com/FossifyOrg/Camera/issues) or [closed](https://github.com/FossifyOrg/Camera/issues?q=is%3Aissue+is%3Aclosed) - to which I could contribute my information." 18 | required: true 19 | - label: "I made sure that there are **no existing discussions** - [open](https://github.com/FossifyOrg/Camera/discussions) or [closed](https://github.com/FossifyOrg/Camera/discussions?discussions_q=is%3Aclosed) - to which I could contribute my information." 20 | required: true 21 | - label: "I have read the FAQs inside the app (Menu -> About -> FAQs) and my problem isn't listed." 22 | required: true 23 | - label: "**I have taken the time to fill in all the required details. I understand that the request will be dismissed otherwise.**" 24 | required: true 25 | - label: "This issue contains only one feature request." 26 | required: true 27 | - label: "I have read and understood the [contribution guidelines](https://github.com/FossifyOrg/Camera/blob/master/CONTRIBUTING.md)." 28 | required: true 29 | 30 | - type: textarea 31 | id: feature-description 32 | attributes: 33 | label: Feature description 34 | description: | 35 | Explain how you want the app's look or behavior to change to suit your needs. 36 | 37 | ⚠️ Please **DO NOT** add links to SimpleMobileTools issues as they can be deleted at any time. Instead, copy-paste any useful information manually. 38 | validations: 39 | required: true 40 | 41 | - type: textarea 42 | id: why-is-the-feature-requested 43 | attributes: 44 | label: Why do you want this feature? 45 | description: | 46 | Describe any problem or limitation you come across while using the app which would be solved by this feature. 47 | validations: 48 | required: true 49 | 50 | - type: textarea 51 | id: additional-information 52 | attributes: 53 | label: Additional information 54 | description: Any other information you'd like to include, for instance sketches, mockups, pictures of rabbits, etc. 55 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | registries: 4 | maven-google: 5 | type: maven-repository 6 | url: "https://dl.google.com/dl/android/maven2/" 7 | 8 | maven-central: 9 | type: maven-repository 10 | url: "https://repo.maven.apache.org/maven2" 11 | 12 | jitpack: 13 | type: maven-repository 14 | url: "https://jitpack.io" 15 | 16 | updates: 17 | - package-ecosystem: "bundler" 18 | directory: "/" 19 | schedule: 20 | interval: "weekly" 21 | commit-message: 22 | prefix: "chore" 23 | prefix-development: "chore" 24 | include: "scope" 25 | assignees: 26 | - "naveensingh" 27 | 28 | - package-ecosystem: "gradle" 29 | directory: "/" 30 | registries: 31 | - maven-central 32 | - maven-google 33 | - jitpack 34 | schedule: 35 | interval: "weekly" 36 | commit-message: 37 | prefix: "chore" 38 | prefix-development: "chore" 39 | include: "scope" 40 | assignees: 41 | - "naveensingh" 42 | 43 | - package-ecosystem: "github-actions" 44 | directory: "/" 45 | schedule: 46 | interval: "weekly" 47 | commit-message: 48 | prefix: "chore" 49 | prefix-development: "chore" 50 | include: "scope" 51 | assignees: 52 | - "naveensingh" 53 | -------------------------------------------------------------------------------- /.github/workflows/foss-release.yml: -------------------------------------------------------------------------------- 1 | name: Github Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*.*.*" 7 | 8 | jobs: 9 | call-release-workflow: 10 | uses: FossifyOrg/.github/.github/workflows/release.yml@main 11 | with: 12 | tag: ${{ github.ref_name }} 13 | flavor: "foss" 14 | package_name: "org.fossify.camera" 15 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/gplay-release.yml: -------------------------------------------------------------------------------- 1 | name: Google Play Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | track: 7 | description: "Google Play Store release track" 8 | required: false 9 | type: choice 10 | default: "beta" 11 | options: 12 | - internal 13 | - alpha 14 | - beta 15 | - production 16 | rollout: 17 | description: "Rollout fraction (0.0-1.0)" 18 | required: false 19 | type: string 20 | default: "0.05" 21 | validate_only: 22 | description: "Fastlane dry-run?" 23 | required: false 24 | type: boolean 25 | default: false 26 | 27 | jobs: 28 | call-release-workflow: 29 | uses: FossifyOrg/.github/.github/workflows/release.yml@main 30 | with: 31 | flavor: "gplay" 32 | package_name: "org.fossify.camera" 33 | track: ${{ github.event.inputs.track }} 34 | rollout: ${{ github.event.inputs.rollout }} 35 | validate_only: ${{ github.event.inputs.validate_only == 'true' }} 36 | secrets: inherit 37 | -------------------------------------------------------------------------------- /.github/workflows/image-minimizer.yml: -------------------------------------------------------------------------------- 1 | name: Image Minimizer 2 | 3 | on: 4 | issue_comment: 5 | types: [created, edited] 6 | issues: 7 | types: [opened, edited] 8 | pull_request_target: 9 | types: [opened, edited] 10 | 11 | jobs: 12 | call-image-minimizer-workflow: 13 | uses: FossifyOrg/.github/.github/workflows/image-minimizer.yml@main 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | name: No Response 2 | 3 | on: 4 | schedule: 5 | - cron: "0 12 * * *" # Runs daily at noon 6 | workflow_dispatch: 7 | 8 | jobs: 9 | call-no-response-workflow: 10 | uses: FossifyOrg/.github/.github/workflows/no-response.yml@main 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.github/workflows/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | name: PR Labeler 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened] 6 | 7 | jobs: 8 | call-pr-labeler-workflow: 9 | uses: FossifyOrg/.github/.github/workflows/pr-labeler.yml@main 10 | secrets: inherit 11 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | call-pr-workflow: 9 | uses: FossifyOrg/.github/.github/workflows/pr.yml@main 10 | -------------------------------------------------------------------------------- /.github/workflows/release-commenter.yml: -------------------------------------------------------------------------------- 1 | name: Release Commenter 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | call-release-commenter: 9 | uses: FossifyOrg/.github/.github/workflows/release-commenter.yml@main 10 | secrets: inherit 11 | -------------------------------------------------------------------------------- /.github/workflows/testing-build.yml: -------------------------------------------------------------------------------- 1 | name: Testing build (on PR) 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | types: [ labeled, opened, synchronize, reopened ] 7 | 8 | jobs: 9 | call-testing-build-workflow: 10 | uses: FossifyOrg/.github/.github/workflows/testing-build.yml@main 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.aab 3 | .gradle 4 | /local.properties 5 | /.idea/ 6 | .DS_Store 7 | /build 8 | /captures 9 | keystore.jks 10 | keystore.properties 11 | fastlane/fastlane.json 12 | fastlane/report.xml 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Fixed 11 | 12 | - Fixed loud shutter sound on some devices ([#97]) 13 | 14 | ## [1.0.2] - 2025-05-28 15 | 16 | ### Changed 17 | 18 | - Updated translations 19 | - Marked app as beta 20 | 21 | ### Fixed 22 | 23 | - All camera buttons now rotate properly on device rotation 24 | - Buttons are now properly updated switching between photo and video modes 25 | 26 | ## [1.0.1] - 2024-10-19 27 | 28 | ### Fixed 29 | 30 | - Fix app name in metadata 31 | 32 | ## [1.0.0] - 2024-10-19 33 | 34 | ### Added 35 | 36 | - Initial release 37 | 38 | [Unreleased]: https://github.com/FossifyOrg/Camera/compare/1.0.2...HEAD 39 | [1.0.2]: https://github.com/FossifyOrg/Camera/compare/1.0.1...1.0.2 40 | [1.0.1]: https://github.com/FossifyOrg/Camera/compare/1.0.0...1.0.1 41 | [1.0.0]: https://github.com/FossifyOrg/Camera/releases/tag/1.0.0 42 | 43 | [#97]: https://github.com/FossifyOrg/Camera/issues/97 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Reporting 2 | Before you report something, read the reporting rules [here](https://github.com/FossifyOrg/General-Discussion#how-do-i-suggest-an-improvement-ask-a-question-or-report-an-issue) please. 3 | 4 | ### Contributing as a developer 5 | Some instructions about code style and everything that has to be done to increase the chance of your code getting accepted can be found at the [General Discussion](https://github.com/FossifyOrg/General-Discussion#contribution-rules-for-developers) section. 6 | 7 | ### Contributing as a non developer 8 | In case you just want to for example improve a translation, you can find the way of doing it [here](https://github.com/FossifyOrg/General-Discussion#how-can-i-suggest-an-edit-to-a-file). 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | gem "fastlane-plugin-fossify", "~> 1.0" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fossify Camera 2 | 3 | Logo 4 | 5 | Get it on Google Play Get it on F-Droid Get it on IzzyOnDroid 6 | 7 | Fossify Camera is your go-to app for capturing life’s moments with precision and privacy. Whether 8 | you’re snapping photos or recording videos, this fully customizable, privacy-respecting camera app 9 | is designed to meet your needs. 10 | 11 | **📸 YOUR PRIVACY, OUR PRIORITY:** 12 | With the Fossify Camera app, your data remains private. Enjoy a camera that works without internet 13 | access or intrusive permissions, ensuring your photos and videos stay secure. 14 | 15 | **🚀 SEAMLESS PERFORMANCE:** 16 | Fossify Camera provides a fluid and responsive interface. Switch between photo and video modes, 17 | adjust zoom, and toggle between front and rear cameras instantly. Capture moments with zero lag and 18 | experience smooth performance at all times. 19 | 20 | **🖼️ COMPLETE CUSTOMIZATION:** 21 | Personalize every aspect of your camera experience. Adjust the output quality, customize the save 22 | path, and set the resolution to suit your needs. You can even customize colors and themes to match 23 | your style. 24 | 25 | **⚡ DYNAMIC CONTROLS:** 26 | Toggle settings with ease—control flash, aspect ratio, and zoom directly from the camera view. The 27 | app is designed for quick access, letting you capture moments efficiently, with intuitive controls. 28 | 29 | **🖼️ MATERIAL DESIGN:** 30 | Enjoy a sleek, user-friendly interface with material design and a dynamic theme that adapts to your 31 | preferences. Whether you're using the app during the day or at night, Fossify Camera provides a 32 | smooth and intuitive experience. 33 | 34 | **🌐 OPEN-SOURCE ASSURANCE:** 35 | Fossify Camera is built on an open-source foundation. With our commitment to transparency, you can 36 | review the code on GitHub and be part of a community that values privacy and trust. 37 | 38 | Fossify Camera offers everything you need to capture moments effortlessly while respecting your 39 | privacy. 40 | 41 | ➡️ Explore more Fossify apps: https://www.fossify.org
42 | ➡️ Open-Source Code: https://www.github.com/FossifyOrg
43 | ➡️ Join the community on Reddit: https://www.reddit.com/r/Fossify
44 | ➡️ Connect on Telegram: https://t.me/Fossify 45 | 46 |
47 | App image 48 | App image 49 | App image 50 |
51 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /foss 3 | /gplay 4 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FossifyOrg/Camera/cf2aae62b57d69d807b1f7c77a6c7bd6851474ee/app/proguard-rules.pro -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Camera_debug 4 | 5 | -------------------------------------------------------------------------------- /app/src/foss/res/values/bools.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | true 6 | 7 | -------------------------------------------------------------------------------- /app/src/gplay/res/values/bools.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | false 5 | true 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FossifyOrg/Camera/cf2aae62b57d69d807b1f7c77a6c7bd6851474ee/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/activities/SimpleActivity.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.activities 2 | 3 | import org.fossify.camera.R 4 | import org.fossify.commons.activities.BaseSimpleActivity 5 | 6 | open class SimpleActivity : BaseSimpleActivity() { 7 | override fun getAppIconIDs() = arrayListOf( 8 | R.mipmap.ic_launcher_red, 9 | R.mipmap.ic_launcher_pink, 10 | R.mipmap.ic_launcher_purple, 11 | R.mipmap.ic_launcher_deep_purple, 12 | R.mipmap.ic_launcher_indigo, 13 | R.mipmap.ic_launcher_blue, 14 | R.mipmap.ic_launcher_light_blue, 15 | R.mipmap.ic_launcher_cyan, 16 | R.mipmap.ic_launcher_teal, 17 | R.mipmap.ic_launcher, 18 | R.mipmap.ic_launcher_light_green, 19 | R.mipmap.ic_launcher_lime, 20 | R.mipmap.ic_launcher_yellow, 21 | R.mipmap.ic_launcher_amber, 22 | R.mipmap.ic_launcher_orange, 23 | R.mipmap.ic_launcher_deep_orange, 24 | R.mipmap.ic_launcher_brown, 25 | R.mipmap.ic_launcher_blue_grey, 26 | R.mipmap.ic_launcher_grey_black 27 | ) 28 | 29 | override fun getAppLauncherName() = getString(R.string.app_launcher_name) 30 | 31 | override fun getRepositoryName() = "Camera" 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/activities/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.activities 2 | 3 | import android.content.Intent 4 | import org.fossify.commons.activities.BaseSplashActivity 5 | 6 | class SplashActivity : BaseSplashActivity() { 7 | override fun initActivity() { 8 | startActivity(Intent(this, MainActivity::class.java)) 9 | finish() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/extensions/CameraSelector.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.extensions 2 | 3 | import androidx.camera.core.CameraSelector 4 | 5 | fun CameraSelector.toLensFacing(): Int { 6 | return if (this == CameraSelector.DEFAULT_FRONT_CAMERA) { 7 | CameraSelector.LENS_FACING_FRONT 8 | } else { 9 | CameraSelector.LENS_FACING_BACK 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/extensions/Context.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.extensions 2 | 3 | import android.content.Context 4 | import org.fossify.camera.helpers.Config 5 | import org.fossify.commons.extensions.hasPermission 6 | import org.fossify.commons.helpers.PERMISSION_ACCESS_COARSE_LOCATION 7 | import org.fossify.commons.helpers.PERMISSION_ACCESS_FINE_LOCATION 8 | import java.io.File 9 | import java.text.SimpleDateFormat 10 | import java.util.Date 11 | import java.util.Locale 12 | 13 | val Context.config: Config get() = Config.newInstance(applicationContext) 14 | 15 | fun Context.getOutputMediaFilePath(isPhoto: Boolean): String { 16 | val mediaStorageDir = File(config.savePhotosFolder) 17 | 18 | if (!mediaStorageDir.exists()) { 19 | if (!mediaStorageDir.mkdirs()) { 20 | return "" 21 | } 22 | } 23 | 24 | val mediaName = getRandomMediaName(isPhoto) 25 | return if (isPhoto) { 26 | "${mediaStorageDir.path}/$mediaName.jpg" 27 | } else { 28 | "${mediaStorageDir.path}/$mediaName.mp4" 29 | } 30 | } 31 | 32 | fun Context.getOutputMediaFileName(isPhoto: Boolean): String { 33 | val mediaName = getRandomMediaName(isPhoto) 34 | return if (isPhoto) { 35 | "$mediaName.jpg" 36 | } else { 37 | "$mediaName.mp4" 38 | } 39 | } 40 | 41 | fun getRandomMediaName(isPhoto: Boolean): String { 42 | val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) 43 | return if (isPhoto) { 44 | "IMG_$timestamp" 45 | } else { 46 | "VID_$timestamp" 47 | } 48 | } 49 | 50 | fun Context.checkLocationPermission(): Boolean { 51 | return hasPermission(PERMISSION_ACCESS_FINE_LOCATION) || hasPermission( 52 | PERMISSION_ACCESS_COARSE_LOCATION 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/extensions/Int.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.extensions 2 | 3 | import androidx.camera.core.CameraSelector 4 | import androidx.camera.core.ImageCapture 5 | import org.fossify.camera.R 6 | import org.fossify.camera.helpers.FLASH_ALWAYS_ON 7 | import org.fossify.camera.helpers.FLASH_AUTO 8 | import org.fossify.camera.helpers.FLASH_OFF 9 | import org.fossify.camera.helpers.FLASH_ON 10 | 11 | fun Int.toCameraXFlashMode(): Int { 12 | return when (this) { 13 | FLASH_ON -> ImageCapture.FLASH_MODE_ON 14 | FLASH_OFF -> ImageCapture.FLASH_MODE_OFF 15 | FLASH_AUTO -> ImageCapture.FLASH_MODE_AUTO 16 | FLASH_ALWAYS_ON -> ImageCapture.FLASH_MODE_OFF 17 | else -> throw IllegalArgumentException("Unknown mode: $this") 18 | } 19 | } 20 | 21 | fun Int.toAppFlashMode(): Int { 22 | return when (this) { 23 | ImageCapture.FLASH_MODE_ON -> FLASH_ON 24 | ImageCapture.FLASH_MODE_OFF -> FLASH_OFF 25 | ImageCapture.FLASH_MODE_AUTO -> FLASH_AUTO 26 | else -> throw IllegalArgumentException("Unknown mode: $this") 27 | } 28 | } 29 | 30 | fun Int.toFlashModeId(): Int { 31 | return when (this) { 32 | FLASH_ON -> R.id.flash_on 33 | FLASH_OFF -> R.id.flash_off 34 | FLASH_AUTO -> R.id.flash_auto 35 | FLASH_ALWAYS_ON -> R.id.flash_always_on 36 | else -> throw IllegalArgumentException("Unknown mode: $this") 37 | } 38 | } 39 | 40 | fun Int.toCameraSelector(): CameraSelector { 41 | return if (this == CameraSelector.LENS_FACING_FRONT) { 42 | CameraSelector.DEFAULT_FRONT_CAMERA 43 | } else { 44 | CameraSelector.DEFAULT_BACK_CAMERA 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/extensions/MaterialButton.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.extensions 2 | 3 | import androidx.annotation.DrawableRes 4 | import com.google.android.material.button.MaterialButton 5 | import org.fossify.camera.R 6 | import org.fossify.camera.views.ShadowDrawable 7 | 8 | fun MaterialButton.setShadowIcon(@DrawableRes drawableResId: Int) { 9 | icon = ShadowDrawable(context, drawableResId, R.style.TopIconShadow) 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/extensions/Quality.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.extensions 2 | 3 | import androidx.camera.video.Quality 4 | import org.fossify.camera.models.VideoQuality 5 | 6 | fun Quality.toVideoQuality(): VideoQuality { 7 | return when (this) { 8 | Quality.UHD -> VideoQuality.UHD 9 | Quality.FHD -> VideoQuality.FHD 10 | Quality.HD -> VideoQuality.HD 11 | Quality.SD -> VideoQuality.SD 12 | else -> throw IllegalArgumentException("Unsupported quality: $this") 13 | } 14 | } 15 | 16 | fun VideoQuality.toCameraXQuality(): Quality { 17 | return when (this) { 18 | VideoQuality.UHD -> Quality.UHD 19 | VideoQuality.FHD -> Quality.FHD 20 | VideoQuality.HD -> Quality.HD 21 | VideoQuality.SD -> Quality.SD 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/extensions/View.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.extensions 2 | 3 | import android.view.View 4 | import org.fossify.commons.helpers.SHORT_ANIMATION_DURATION 5 | 6 | fun View.fadeIn() { 7 | animate().alpha(1f).setDuration(SHORT_ANIMATION_DURATION).withStartAction { isClickable = true } 8 | .start() 9 | } 10 | 11 | fun View.fadeOut() { 12 | animate().alpha(0f).setDuration(SHORT_ANIMATION_DURATION).withEndAction { isClickable = false } 13 | .start() 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/helpers/BitmapUtils.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.helpers 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import kotlin.math.ceil 6 | import kotlin.math.floor 7 | import kotlin.math.sqrt 8 | 9 | //inspired by https://android.googlesource.com/platform/packages/apps/Camera2/+/refs/heads/master/src/com/android/camera/util/CameraUtil.java#244 10 | object BitmapUtils { 11 | private const val INLINE_BITMAP_MAX_PIXEL_NUM = 50 * 1024 12 | 13 | fun makeBitmap( 14 | jpegData: ByteArray, 15 | maxNumOfPixels: Int = INLINE_BITMAP_MAX_PIXEL_NUM 16 | ): Bitmap? { 17 | return try { 18 | val options = BitmapFactory.Options() 19 | options.inJustDecodeBounds = true 20 | 21 | BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size, options) 22 | 23 | if (options.mCancel || options.outWidth == -1 || options.outHeight == -1) { 24 | return null 25 | } 26 | options.inSampleSize = computeSampleSize(options, -1, maxNumOfPixels) 27 | options.inJustDecodeBounds = false 28 | options.inDither = false 29 | options.inPreferredConfig = Bitmap.Config.ARGB_8888 30 | BitmapFactory.decodeByteArray( 31 | jpegData, 0, jpegData.size, 32 | options 33 | ) 34 | } catch (ex: OutOfMemoryError) { 35 | null 36 | } 37 | } 38 | 39 | private fun computeSampleSize( 40 | options: BitmapFactory.Options, 41 | minSideLength: Int, 42 | maxNumOfPixels: Int 43 | ): Int { 44 | val initialSize = computeInitialSampleSize( 45 | options, minSideLength, 46 | maxNumOfPixels 47 | ) 48 | var roundedSize: Int 49 | if (initialSize <= 8) { 50 | roundedSize = 1 51 | while (roundedSize < initialSize) { 52 | roundedSize = roundedSize shl 1 53 | } 54 | } else { 55 | roundedSize = (initialSize + 7) / 8 * 8 56 | } 57 | return roundedSize 58 | } 59 | 60 | private fun computeInitialSampleSize( 61 | options: BitmapFactory.Options, 62 | minSideLength: Int, 63 | maxNumOfPixels: Int 64 | ): Int { 65 | val w = options.outWidth.toDouble() 66 | val h = options.outHeight.toDouble() 67 | val lowerBound = if (maxNumOfPixels < 0) 1 else ceil(sqrt(w * h / maxNumOfPixels)).toInt() 68 | val upperBound = 69 | if (minSideLength < 0) 128 else floor(w / minSideLength).coerceAtMost(floor(h / minSideLength)) 70 | .toInt() 71 | if (upperBound < lowerBound) { 72 | // return the larger one when there is no overlapping zone. 73 | return lowerBound 74 | } 75 | return if (maxNumOfPixels < 0 && minSideLength < 0) { 76 | 1 77 | } else if (minSideLength < 0) { 78 | lowerBound 79 | } else { 80 | upperBound 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/helpers/CameraErrorHandler.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.helpers 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import androidx.camera.core.CameraState 6 | import androidx.camera.core.ImageCapture 7 | import androidx.camera.video.VideoRecordEvent 8 | import org.fossify.camera.R 9 | import org.fossify.commons.extensions.toast 10 | 11 | class CameraErrorHandler( 12 | private val context: Context, 13 | ) { 14 | 15 | fun handleCameraError(error: CameraState.StateError?) { 16 | when (error?.code) { 17 | CameraState.ERROR_MAX_CAMERAS_IN_USE, 18 | CameraState.ERROR_CAMERA_IN_USE -> context.toast( 19 | id = R.string.camera_in_use_error, 20 | length = Toast.LENGTH_LONG 21 | ) 22 | 23 | CameraState.ERROR_CAMERA_FATAL_ERROR -> context.toast(R.string.camera_unavailable) 24 | CameraState.ERROR_STREAM_CONFIG -> context.toast(R.string.camera_configure_error) 25 | CameraState.ERROR_CAMERA_DISABLED -> context.toast(R.string.camera_disabled_by_admin_error) 26 | CameraState.ERROR_DO_NOT_DISTURB_MODE_ENABLED -> context.toast( 27 | id = R.string.camera_dnd_error, 28 | length = Toast.LENGTH_LONG 29 | ) 30 | 31 | CameraState.ERROR_OTHER_RECOVERABLE_ERROR -> {} 32 | } 33 | } 34 | 35 | fun handleImageCaptureError(imageCaptureError: Int) { 36 | when (imageCaptureError) { 37 | ImageCapture.ERROR_FILE_IO -> context.toast(R.string.photo_not_saved) 38 | else -> context.toast(R.string.photo_capture_failed) 39 | } 40 | } 41 | 42 | fun handleVideoRecordingError(error: Int) { 43 | when (error) { 44 | VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE -> context.toast(R.string.video_capture_insufficient_storage_error) 45 | VideoRecordEvent.Finalize.ERROR_NONE -> {} 46 | else -> context.toast(R.string.video_recording_failed) 47 | } 48 | } 49 | 50 | fun showSaveToInternalStorage() { 51 | context.toast(R.string.save_error_internal_storage) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/helpers/Constants.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.helpers 2 | 3 | const val ORIENT_PORTRAIT = 0 4 | const val ORIENT_LANDSCAPE_LEFT = 1 5 | const val ORIENT_LANDSCAPE_RIGHT = 2 6 | 7 | // shared preferences 8 | const val SAVE_PHOTOS = "save_photos" 9 | const val SOUND = "sound" 10 | const val VOLUME_BUTTONS_AS_SHUTTER = "volume_buttons_as_shutter" 11 | const val FLIP_PHOTOS = "flip_photos" 12 | const val LAST_USED_CAMERA = "last_used_camera_3" 13 | const val LAST_USED_CAMERA_LENS = "last_used_camera_lens" 14 | const val FLASHLIGHT_STATE = "flashlight_state" 15 | const val INIT_PHOTO_MODE = "init_photo_mode" 16 | const val BACK_PHOTO_RESOLUTION_INDEX = "back_photo_resolution_index_3" 17 | const val BACK_VIDEO_RESOLUTION_INDEX = "back_video_resolution_index_3" 18 | const val FRONT_PHOTO_RESOLUTION_INDEX = "front_photo_resolution_index_3" 19 | const val FRONT_VIDEO_RESOLUTION_INDEX = "front_video_resolution_index_3" 20 | const val SAVE_PHOTO_METADATA = "save_photo_metadata" 21 | const val SAVE_PHOTO_VIDEO_LOCATION = "save_photo_video_location" 22 | const val PHOTO_QUALITY = "photo_quality" 23 | const val CAPTURE_MODE = "capture_mode" 24 | const val TIMER_MODE = "timer_mode" 25 | 26 | const val FLASH_OFF = 0 27 | const val FLASH_ON = 1 28 | const val FLASH_AUTO = 2 29 | const val FLASH_ALWAYS_ON = 3 30 | 31 | fun compensateDeviceRotation(orientation: Int) = when (orientation) { 32 | ORIENT_LANDSCAPE_LEFT -> 270 33 | ORIENT_LANDSCAPE_RIGHT -> 90 34 | else -> 0 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/helpers/GestureDetectorListener.java: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.helpers; 2 | 3 | import android.view.GestureDetector; 4 | import android.view.MotionEvent; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | public class GestureDetectorListener extends GestureDetector.SimpleOnGestureListener { 9 | @Override 10 | public boolean onFling(@Nullable MotionEvent e1, @Nullable MotionEvent e2, float velocityX, float velocityY) { 11 | return super.onFling(e1, e2, velocityX, velocityY); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/helpers/MediaSizeStore.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.helpers 2 | 3 | class MediaSizeStore(private val config: Config) { 4 | 5 | fun storeSize(isPhotoCapture: Boolean, isFrontCamera: Boolean, currentIndex: Int) { 6 | if (isPhotoCapture) { 7 | if (isFrontCamera) { 8 | config.frontPhotoResIndex = currentIndex 9 | } else { 10 | config.backPhotoResIndex = currentIndex 11 | } 12 | } else { 13 | if (isFrontCamera) { 14 | config.frontVideoResIndex = currentIndex 15 | } else { 16 | config.backVideoResIndex = currentIndex 17 | } 18 | } 19 | } 20 | 21 | fun getCurrentSizeIndex(isPhotoCapture: Boolean, isFrontCamera: Boolean): Int { 22 | return if (isPhotoCapture) { 23 | if (isFrontCamera) { 24 | config.frontPhotoResIndex 25 | } else { 26 | config.backPhotoResIndex 27 | } 28 | } else { 29 | if (isFrontCamera) { 30 | config.frontVideoResIndex 31 | } else { 32 | config.backVideoResIndex 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/helpers/MediaSoundHelper.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.helpers 2 | 3 | import android.content.Context 4 | 5 | class MediaSoundHelper(context: Context) { 6 | private val mediaActionSound = MediaActionSound(context) 7 | 8 | fun loadSounds() { 9 | mediaActionSound.load(MediaActionSound.START_VIDEO_RECORDING) 10 | mediaActionSound.load(MediaActionSound.STOP_VIDEO_RECORDING) 11 | mediaActionSound.load(MediaActionSound.SHUTTER_CLICK) 12 | mediaActionSound.load(MediaActionSound.TIMER_COUNTDOWN) 13 | mediaActionSound.load(MediaActionSound.TIMER_COUNTDOWN_2_SECONDS) 14 | } 15 | 16 | fun playShutterSound() { 17 | mediaActionSound.play(MediaActionSound.SHUTTER_CLICK) 18 | } 19 | 20 | fun playStartVideoRecordingSound(onPlayComplete: () -> Unit) { 21 | mediaActionSound.play(MediaActionSound.START_VIDEO_RECORDING, onPlayComplete) 22 | } 23 | 24 | fun playStopVideoRecordingSound() { 25 | mediaActionSound.play(MediaActionSound.STOP_VIDEO_RECORDING) 26 | } 27 | 28 | fun playTimerCountdownSound() { 29 | mediaActionSound.play(MediaActionSound.TIMER_COUNTDOWN) 30 | } 31 | 32 | fun playTimerCountdown2SecondsSound() { 33 | mediaActionSound.play(MediaActionSound.TIMER_COUNTDOWN_2_SECONDS) 34 | } 35 | 36 | fun stopTimerCountdown2SecondsSound() { 37 | mediaActionSound.stop(MediaActionSound.TIMER_COUNTDOWN_2_SECONDS) 38 | } 39 | 40 | fun release() { 41 | mediaActionSound.release() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/helpers/PinchToZoomOnScaleGestureListener.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.helpers 2 | 3 | import android.view.ScaleGestureDetector 4 | import androidx.camera.core.CameraControl 5 | import androidx.camera.core.CameraInfo 6 | 7 | class PinchToZoomOnScaleGestureListener( 8 | private val cameraInfo: CameraInfo, 9 | private val cameraControl: CameraControl, 10 | ) : ScaleGestureDetector.SimpleOnScaleGestureListener() { 11 | private val zoomCalculator = ZoomCalculator() 12 | 13 | override fun onScale(detector: ScaleGestureDetector): Boolean { 14 | val zoomState = cameraInfo.zoomState.value ?: return false 15 | val zoomRatio = zoomCalculator.calculateZoomRatio(zoomState, detector.scaleFactor) 16 | cameraControl.setZoomRatio(zoomRatio) 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/helpers/SimpleLocationManager.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.helpers 2 | 3 | import android.Manifest 4 | import android.location.Location 5 | import android.location.LocationListener 6 | import android.location.LocationManager 7 | import androidx.annotation.RequiresPermission 8 | import org.fossify.camera.extensions.checkLocationPermission 9 | import org.fossify.commons.activities.BaseSimpleActivity 10 | 11 | class SimpleLocationManager(private val activity: BaseSimpleActivity) { 12 | 13 | companion object { 14 | private const val LOCATION_UPDATE_MIN_TIME_INTERVAL_MS = 5000L 15 | private const val LOCATION_UPDATE_MIN_DISTANCE_M = 10F 16 | } 17 | 18 | private val locationManager = activity.getSystemService(LocationManager::class.java)!! 19 | private val locationListener = LocationListener { location -> 20 | this@SimpleLocationManager.location = location 21 | } 22 | 23 | private var location: Location? = null 24 | 25 | fun getLocation(): Location? { 26 | if (location == null) { 27 | location = getLastKnownLocation() 28 | } 29 | 30 | return location 31 | } 32 | 33 | private fun getLastKnownLocation(): Location? { 34 | return if (activity.checkLocationPermission()) { 35 | var accurateLocation: Location? = null 36 | for (provider in locationManager.allProviders) { 37 | val location = locationManager.getLastKnownLocation(provider) ?: continue 38 | if (accurateLocation == null || location.accuracy < accurateLocation.accuracy) { 39 | accurateLocation = location 40 | } 41 | } 42 | accurateLocation 43 | } else { 44 | null 45 | } 46 | } 47 | 48 | @RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]) 49 | fun requestLocationUpdates() { 50 | locationManager.allProviders.forEach { provider -> 51 | locationManager.requestLocationUpdates( 52 | provider, 53 | LOCATION_UPDATE_MIN_TIME_INTERVAL_MS, 54 | LOCATION_UPDATE_MIN_DISTANCE_M, 55 | locationListener 56 | ) 57 | } 58 | } 59 | 60 | fun dropLocationUpdates() { 61 | locationManager.removeUpdates(locationListener) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/helpers/TabSelectedListener.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.helpers 2 | 3 | import com.google.android.material.tabs.TabLayout 4 | 5 | interface TabSelectedListener : TabLayout.OnTabSelectedListener { 6 | override fun onTabReselected(tab: TabLayout.Tab?) {} 7 | 8 | override fun onTabUnselected(tab: TabLayout.Tab?) {} 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/helpers/VideoQualityManager.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.helpers 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import androidx.camera.core.CameraSelector 5 | import androidx.camera.lifecycle.ProcessCameraProvider 6 | import androidx.camera.video.Quality 7 | import androidx.camera.video.QualitySelector 8 | import org.fossify.camera.extensions.config 9 | import org.fossify.camera.extensions.toVideoQuality 10 | import org.fossify.camera.models.CameraSelectorVideoQualities 11 | import org.fossify.camera.models.VideoQuality 12 | import org.fossify.commons.extensions.showErrorToast 13 | 14 | class VideoQualityManager( 15 | private val activity: AppCompatActivity, 16 | ) { 17 | companion object { 18 | private val QUALITIES = listOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD) 19 | private val CAMERA_SELECTORS = 20 | arrayOf(CameraSelector.DEFAULT_BACK_CAMERA, CameraSelector.DEFAULT_FRONT_CAMERA) 21 | } 22 | 23 | private val videoQualities = mutableListOf() 24 | private val mediaSizeStore = MediaSizeStore(activity.config) 25 | 26 | fun initSupportedQualities(cameraProvider: ProcessCameraProvider) { 27 | if (videoQualities.isEmpty()) { 28 | for (camSelector in CAMERA_SELECTORS) { 29 | cameraProvider.unbindAll() 30 | val camera = cameraProvider.bindToLifecycle(activity, camSelector) 31 | try { 32 | if (cameraProvider.hasCamera(camSelector)) { 33 | QualitySelector.getSupportedQualities(camera.cameraInfo) 34 | .filter(QUALITIES::contains) 35 | .also { allQualities -> 36 | val qualities = allQualities.map { it.toVideoQuality() } 37 | videoQualities.add( 38 | CameraSelectorVideoQualities( 39 | camSelector, 40 | qualities 41 | ) 42 | ) 43 | } 44 | } 45 | } catch (e: Exception) { 46 | activity.showErrorToast(e) 47 | } 48 | } 49 | } 50 | } 51 | 52 | fun getUserSelectedQuality(cameraSelector: CameraSelector): VideoQuality { 53 | val isFrontCamera = cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA 54 | val selectionIndex = mediaSizeStore.getCurrentSizeIndex( 55 | isPhotoCapture = false, 56 | isFrontCamera = isFrontCamera 57 | ).coerceAtLeast(0) 58 | return getSupportedQualities(cameraSelector).getOrElse(selectionIndex) { VideoQuality.HD } 59 | } 60 | 61 | fun getSupportedQualities(cameraSelector: CameraSelector): List { 62 | return videoQualities.filter { it.camSelector == cameraSelector } 63 | .flatMap { it.qualities } 64 | .sortedByDescending { it.pixels } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/helpers/ZoomCalculator.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.helpers 2 | 3 | import androidx.camera.core.ZoomState 4 | 5 | class ZoomCalculator { 6 | 7 | fun calculateZoomRatio(zoomState: ZoomState, pinchToZoomScale: Float): Float { 8 | val clampedRatio = zoomState.zoomRatio * speedUpZoomBy2X(pinchToZoomScale) 9 | // Clamp the ratio with the zoom range. 10 | return clampedRatio.coerceAtLeast(zoomState.minZoomRatio) 11 | .coerceAtMost(zoomState.maxZoomRatio) 12 | } 13 | 14 | private fun speedUpZoomBy2X(scaleFactor: Float): Float { 15 | return if (scaleFactor > 1f) { 16 | 1.0f + (scaleFactor - 1.0f) * 2 17 | } else { 18 | 1.0f - (1.0f - scaleFactor) * 2 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/implementations/CameraXInitializer.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.implementations 2 | 3 | import android.net.Uri 4 | import androidx.camera.view.PreviewView 5 | import org.fossify.camera.helpers.CameraErrorHandler 6 | import org.fossify.camera.helpers.MediaOutputHelper 7 | import org.fossify.camera.helpers.MediaSoundHelper 8 | import org.fossify.commons.activities.BaseSimpleActivity 9 | 10 | class CameraXInitializer(private val activity: BaseSimpleActivity) { 11 | 12 | fun createCameraXPreview( 13 | previewView: PreviewView, 14 | listener: CameraXPreviewListener, 15 | mediaSoundHelper: MediaSoundHelper, 16 | outputUri: Uri?, 17 | isThirdPartyIntent: Boolean, 18 | initInPhotoMode: Boolean, 19 | ): CameraXPreview { 20 | val cameraErrorHandler = newCameraErrorHandler() 21 | val mediaOutputHelper = 22 | newMediaOutputHelper(cameraErrorHandler, outputUri, isThirdPartyIntent) 23 | return CameraXPreview( 24 | activity, 25 | previewView, 26 | mediaSoundHelper, 27 | mediaOutputHelper, 28 | cameraErrorHandler, 29 | listener, 30 | isThirdPartyIntent = isThirdPartyIntent, 31 | initInPhotoMode = initInPhotoMode, 32 | ) 33 | } 34 | 35 | private fun newMediaOutputHelper( 36 | cameraErrorHandler: CameraErrorHandler, 37 | outputUri: Uri?, 38 | isThirdPartyIntent: Boolean, 39 | ): MediaOutputHelper { 40 | return MediaOutputHelper( 41 | activity, 42 | cameraErrorHandler, 43 | outputUri, 44 | isThirdPartyIntent, 45 | ) 46 | } 47 | 48 | private fun newCameraErrorHandler(): CameraErrorHandler { 49 | return CameraErrorHandler(activity) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/implementations/CameraXPreviewListener.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.implementations 2 | 3 | import android.graphics.Bitmap 4 | import android.net.Uri 5 | import org.fossify.camera.models.ResolutionOption 6 | 7 | interface CameraXPreviewListener { 8 | fun onInitPhotoMode() 9 | fun onInitVideoMode() 10 | fun setCameraAvailable(available: Boolean) {} 11 | fun setHasFrontAndBackCamera(hasFrontAndBack: Boolean) 12 | fun setFlashAvailable(available: Boolean) 13 | fun onChangeCamera(frontCamera: Boolean) 14 | fun shutterAnimation() 15 | fun onMediaSaved(uri: Uri) 16 | fun onImageCaptured(bitmap: Bitmap) 17 | fun onChangeFlashMode(flashMode: Int) 18 | fun onPhotoCaptureStart() 19 | fun onPhotoCaptureEnd() 20 | fun onVideoRecordingStarted() 21 | fun onVideoRecordingStopped() 22 | fun onVideoDurationChanged(durationNanos: Long) 23 | fun onFocusCamera(xPos: Float, yPos: Float) 24 | fun onTouchPreview() 25 | fun displaySelectedResolution(resolutionOption: ResolutionOption) 26 | fun showImageSizes( 27 | selectedResolution: ResolutionOption, 28 | resolutions: List, 29 | isPhotoCapture: Boolean, 30 | isFrontCamera: Boolean, 31 | onSelect: (index: Int, changed: Boolean) -> Unit, 32 | ) 33 | 34 | fun showFlashOptions(photoCapture: Boolean) 35 | fun adjustPreviewView(requiresCentering: Boolean) 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/interfaces/MyPreview.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.interfaces 2 | 3 | interface MyPreview { 4 | 5 | fun isInPhotoMode(): Boolean 6 | 7 | fun setFlashlightState(state: Int) 8 | 9 | fun toggleFrontBackCamera() 10 | 11 | fun handleFlashlightClick() 12 | 13 | fun tryTakePicture() 14 | 15 | fun toggleRecording() 16 | 17 | fun initPhotoMode() 18 | 19 | fun initVideoMode() 20 | 21 | fun showChangeResolution() 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/models/CameraSelectorImageQualities.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.models 2 | 3 | import androidx.camera.core.CameraSelector 4 | 5 | data class CameraSelectorImageQualities( 6 | val camSelector: CameraSelector, 7 | val qualities: List, 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/models/CameraSelectorVideoQualities.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.models 2 | 3 | import androidx.camera.core.CameraSelector 4 | 5 | data class CameraSelectorVideoQualities( 6 | val camSelector: CameraSelector, 7 | val qualities: List, 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/models/CaptureMode.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.models 2 | 3 | import androidx.annotation.StringRes 4 | import org.fossify.camera.R 5 | 6 | enum class CaptureMode(@StringRes val stringResId: Int) { 7 | MINIMIZE_LATENCY(R.string.minimize_latency), 8 | MAXIMIZE_QUALITY(R.string.maximize_quality) 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/models/MediaOutput.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.models 2 | 3 | import android.content.ContentValues 4 | import android.net.Uri 5 | import android.os.ParcelFileDescriptor 6 | import java.io.File 7 | import java.io.OutputStream 8 | 9 | sealed class MediaOutput( 10 | open val uri: Uri?, 11 | ) { 12 | sealed interface ImageCaptureOutput 13 | sealed interface VideoCaptureOutput 14 | 15 | data class MediaStoreOutput( 16 | val contentValues: ContentValues, 17 | val contentUri: Uri, 18 | ) : MediaOutput(null), ImageCaptureOutput, VideoCaptureOutput 19 | 20 | data class OutputStreamMediaOutput( 21 | val outputStream: OutputStream, 22 | override val uri: Uri, 23 | ) : MediaOutput(uri), ImageCaptureOutput 24 | 25 | data class FileDescriptorMediaOutput( 26 | val fileDescriptor: ParcelFileDescriptor, 27 | override val uri: Uri, 28 | ) : MediaOutput(uri), VideoCaptureOutput 29 | 30 | data class FileMediaOutput( 31 | val file: File, 32 | override val uri: Uri, 33 | ) : MediaOutput(uri), VideoCaptureOutput, ImageCaptureOutput 34 | 35 | object BitmapOutput : MediaOutput(null), ImageCaptureOutput 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/models/MySize.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.models 2 | 3 | import android.content.Context 4 | import androidx.annotation.DrawableRes 5 | import androidx.annotation.IdRes 6 | import org.fossify.camera.R 7 | 8 | data class MySize(val width: Int, val height: Int, val isFullScreen: Boolean = false) { 9 | companion object { 10 | private const val ONE_MEGA_PIXEL = 1000000 11 | private const val ZERO_MEGA_PIXEL = "0.0" 12 | } 13 | 14 | private val ratio = width / height.toFloat() 15 | 16 | val pixels: Int = width * height 17 | 18 | val megaPixels: String = String.format("%.1f", (width * height.toFloat()) / ONE_MEGA_PIXEL) 19 | 20 | fun requiresCentering(): Boolean { 21 | return !isFullScreen && (isFourToThree() || isThreeToTwo() || isSquare()) 22 | } 23 | 24 | fun isSixteenToNine() = ratio == 16 / 9f 25 | 26 | private fun isFiveToThree() = ratio == 5 / 3f 27 | 28 | private fun isFourToThree() = ratio == 4 / 3f 29 | 30 | private fun isTwoToOne() = ratio == 2f 31 | 32 | private fun isThreeToFour() = ratio == 3 / 4f 33 | 34 | private fun isThreeToTwo() = ratio == 3 / 2f 35 | 36 | private fun isSixToFive() = ratio == 6 / 5f 37 | 38 | private fun isNineteenToNine() = ratio == 19 / 9f 39 | 40 | private fun isNineteenToEight() = ratio == 19 / 8f 41 | 42 | private fun isOneNineToOne() = ratio == 1.9f 43 | 44 | private fun isSquare() = width == height 45 | 46 | fun isSupported(isFullScreenSize16x9: Boolean): Boolean { 47 | return if (isFullScreenSize16x9) { 48 | isFourToThree() || isSquare() 49 | } else { 50 | isFourToThree() || isSixteenToNine() || isSquare() 51 | } && megaPixels != ZERO_MEGA_PIXEL 52 | } 53 | 54 | fun getAspectRatio(context: Context) = when { 55 | isSixteenToNine() -> "16:9" 56 | isFiveToThree() -> "5:3" 57 | isFourToThree() -> "4:3" 58 | isThreeToFour() -> "3:4" 59 | isThreeToTwo() -> "3:2" 60 | isSixToFive() -> "6:5" 61 | isOneNineToOne() -> "1.9:1" 62 | isNineteenToNine() -> "19:9" 63 | isNineteenToEight() -> "19:8" 64 | isSquare() -> "1:1" 65 | isTwoToOne() -> "2:1" 66 | else -> context.resources.getString(org.fossify.commons.R.string.other) 67 | } 68 | 69 | @DrawableRes 70 | fun getImageResId(): Int = when { 71 | isFullScreen -> R.drawable.ic_photo_full_vector 72 | isSixteenToNine() -> R.drawable.ic_photo_16x9_vector 73 | isFourToThree() -> R.drawable.ic_photo_4x3_vector 74 | isSquare() -> R.drawable.ic_photo_1x1_vector 75 | else -> throw UnsupportedOperationException("This size $this is not supported") 76 | } 77 | 78 | @IdRes 79 | fun getButtonId(): Int = when { 80 | isFullScreen -> R.id.photo_full 81 | isSixteenToNine() -> R.id.photo_16x9 82 | isFourToThree() -> R.id.photo_4x3 83 | isSquare() -> R.id.photo_1x1 84 | else -> throw UnsupportedOperationException("This size $this is not supported") 85 | } 86 | 87 | fun toResolutionOption(): ResolutionOption { 88 | return ResolutionOption(buttonViewId = getButtonId(), imageDrawableResId = getImageResId()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/models/ResolutionOption.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.models 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.IdRes 5 | 6 | data class ResolutionOption( 7 | @IdRes val buttonViewId: Int, 8 | @DrawableRes val imageDrawableResId: Int, 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/models/TimerMode.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.models 2 | 3 | import org.fossify.camera.R 4 | 5 | enum class TimerMode(val millisInFuture: Long) { 6 | OFF(0), 7 | TIMER_3(3000), 8 | TIMER_5(5000), 9 | TIMER_10(10000); 10 | 11 | fun getTimerModeResId(): Int { 12 | return when (this) { 13 | OFF -> R.id.timer_off 14 | TIMER_3 -> R.id.timer_3s 15 | TIMER_5 -> R.id.timer_5s 16 | TIMER_10 -> R.id.timer_10_s 17 | } 18 | } 19 | 20 | fun getTimerModeDrawableRes(): Int { 21 | return when (this) { 22 | OFF -> R.drawable.ic_timer_off_vector 23 | TIMER_3 -> R.drawable.ic_timer_3_vector 24 | TIMER_5 -> R.drawable.ic_timer_5_vector 25 | TIMER_10 -> R.drawable.ic_timer_10_vector 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/models/VideoQuality.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.models 2 | 3 | import android.content.Context 4 | import androidx.annotation.DrawableRes 5 | import androidx.annotation.IdRes 6 | import org.fossify.camera.R 7 | 8 | private const val ONE_MEGA_PIXEL = 1000000 9 | 10 | enum class VideoQuality(val width: Int, val height: Int) { 11 | UHD(3840, 2160), 12 | FHD(1920, 1080), 13 | HD(1280, 720), 14 | SD(720, 480); 15 | 16 | val pixels: Int = width * height 17 | 18 | val megaPixels: String = 19 | String.format("%.1f", (width * height.toFloat()) / ONE_MEGA_PIXEL) 20 | 21 | val ratio = width / height.toFloat() 22 | 23 | private fun isSixteenToNine() = ratio == 16 / 9f 24 | 25 | private fun isFiveToThree() = ratio == 5 / 3f 26 | 27 | private fun isFourToThree() = ratio == 4 / 3f 28 | 29 | private fun isTwoToOne() = ratio == 2f 30 | 31 | private fun isThreeToFour() = ratio == 3 / 4f 32 | 33 | private fun isThreeToTwo() = ratio == 3 / 2f 34 | 35 | private fun isSixToFive() = ratio == 6 / 5f 36 | 37 | private fun isNineteenToNine() = ratio == 19 / 9f 38 | 39 | private fun isNineteenToEight() = ratio == 19 / 8f 40 | 41 | private fun isOneNineToOne() = ratio == 1.9f 42 | 43 | private fun isSquare() = width == height 44 | 45 | fun getAspectRatio(context: Context) = when { 46 | isSixteenToNine() -> "16:9" 47 | isFiveToThree() -> "5:3" 48 | isFourToThree() -> "4:3" 49 | isThreeToFour() -> "3:4" 50 | isThreeToTwo() -> "3:2" 51 | isSixToFive() -> "6:5" 52 | isOneNineToOne() -> "1.9:1" 53 | isNineteenToNine() -> "19:9" 54 | isNineteenToEight() -> "19:8" 55 | isSquare() -> "1:1" 56 | isTwoToOne() -> "2:1" 57 | else -> context.resources.getString(org.fossify.commons.R.string.other) 58 | } 59 | 60 | @DrawableRes 61 | fun getImageResId(): Int = when (this) { 62 | UHD -> R.drawable.ic_video_uhd_vector 63 | FHD -> R.drawable.ic_video_fhd_vector 64 | HD -> R.drawable.ic_video_hd_vector 65 | SD -> R.drawable.ic_video_sd_vector 66 | } 67 | 68 | @IdRes 69 | fun getButtonId(): Int = when (this) { 70 | UHD -> R.id.video_uhd 71 | FHD -> R.id.video_fhd 72 | HD -> R.id.video_hd 73 | SD -> R.id.video_sd 74 | } 75 | 76 | fun toResolutionOption(): ResolutionOption { 77 | return ResolutionOption(buttonViewId = getButtonId(), imageDrawableResId = getImageResId()) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/receivers/HardwareShutterReceiver.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.receivers 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import org.fossify.camera.activities.MainActivity 7 | 8 | class HardwareShutterReceiver : BroadcastReceiver() { 9 | 10 | override fun onReceive(context: Context, intent: Intent) { 11 | Intent(context.applicationContext, MainActivity::class.java).apply { 12 | addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) 13 | context.startActivity(this) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/kotlin/org/fossify/camera/views/FocusCircleView.kt: -------------------------------------------------------------------------------- 1 | package org.fossify.camera.views 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.os.Handler 7 | import android.view.ViewGroup 8 | import org.fossify.commons.extensions.getProperPrimaryColor 9 | 10 | class FocusCircleView(context: Context) : ViewGroup(context) { 11 | private val CIRCLE_RADIUS = 50f 12 | private val CIRCLE_DURATION = 500L 13 | 14 | private var mDrawCircle = false 15 | private var mHandler: Handler 16 | private var mPaint: Paint 17 | private var mLastCenterX = 0f 18 | private var mLastCenterY = 0f 19 | 20 | init { 21 | setWillNotDraw(false) 22 | mHandler = Handler() 23 | mPaint = Paint().apply { 24 | style = Paint.Style.STROKE 25 | color = context.getProperPrimaryColor() 26 | strokeWidth = 2f 27 | } 28 | } 29 | 30 | fun setStrokeColor(color: Int) { 31 | mPaint.color = color 32 | } 33 | 34 | fun drawFocusCircle(x: Float, y: Float) { 35 | mLastCenterX = x 36 | mLastCenterY = y 37 | toggleCircle(true) 38 | 39 | mHandler.removeCallbacksAndMessages(null) 40 | mHandler.postDelayed({ 41 | toggleCircle(false) 42 | }, CIRCLE_DURATION) 43 | } 44 | 45 | private fun toggleCircle(show: Boolean) { 46 | mDrawCircle = show 47 | invalidate() 48 | } 49 | 50 | override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {} 51 | 52 | override fun onDraw(canvas: Canvas) { 53 | super.onDraw(canvas) 54 | if (mDrawCircle) { 55 | canvas.drawCircle(mLastCenterX, mLastCenterY, CIRCLE_RADIUS, mPaint) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/res/color/camera_option_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/color/tab_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_camera_front_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_camera_rear_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_flash_auto_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_flash_off_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_flash_on_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_flashlight_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_photo_1x1_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_photo_4x3_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_photo_full_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_shutter_animated.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 18 | 19 | 22 | 23 | 27 | 28 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_shutter_timer_cancel.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_shutter_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_timer_10_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_timer_3_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_timer_5_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_timer_off_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_video_fhd_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_video_hd_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_video_rec_animated.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 12 | 16 | 17 | 20 | 21 | 25 | 26 | 30 | 31 | 35 | 36 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_video_rec_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_video_sd_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_video_uhd_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shutter_pressed_to_unpressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 16 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shutter_unpressed_to_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 16 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tab_indicator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_rec_idle_to_record.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 16 | 17 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_rec_pressed_to_unpressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 16 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_rec_record_to_idle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 16 | 17 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_rec_unpressed_to_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 16 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_button.xml: -------------------------------------------------------------------------------- 1 | 2 |