summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorInna Palant <ipalant@google.com>2024-01-22 21:46:11 +0000
committerInna Palant <ipalant@google.com>2024-01-22 21:46:11 +0000
commit1280ae1bb0718dbd855c28e9065fe71d448dd057 (patch)
tree50ff5afdf8d00fefa49e04d8723f46f77d2d85fc
parentfd636b5f9e1a1755bc14dd6e6cc7b6d7f7f58700 (diff)
parent368572b731459ece3fb9d70eb11198ea92bc0870 (diff)
downloadjetpack-camera-app-1280ae1bb0718dbd855c28e9065fe71d448dd057.tar.gz
Merge remote-tracking branch 'origin/upstream'
Import b/309514655
-rw-r--r--.editorconfig3
-rw-r--r--.github/workflows/PushWorkflow.yaml108
-rw-r--r--.gitignore17
-rw-r--r--.idea/.gitignore3
-rw-r--r--.idea/.name1
-rw-r--r--.idea/androidTestResultsUserPreferences.xml35
-rw-r--r--.idea/codeStyles/Project.xml123
-rw-r--r--.idea/codeStyles/codeStyleConfig.xml5
-rw-r--r--.idea/compiler.xml6
-rw-r--r--.idea/copyright/AOSP.xml6
-rw-r--r--.idea/copyright/profiles_settings.xml5
-rw-r--r--.idea/gradle.xml31
-rw-r--r--.idea/inspectionProfiles/Project_Default.xml41
-rw-r--r--.idea/kotlinc.xml6
-rw-r--r--.idea/migrations.xml10
-rw-r--r--.idea/misc.xml58
-rw-r--r--.idea/vcs.xml40
-rw-r--r--Android.bp17
-rw-r--r--LICENSE202
-rw-r--r--METADATA20
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--OWNERS7
-rw-r--r--README.md45
-rw-r--r--app/.gitignore1
-rw-r--r--app/Android.bp35
-rw-r--r--app/build.gradle.kts126
-rw-r--r--app/proguard-rules.pro21
-rw-r--r--app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt83
-rw-r--r--app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt89
-rw-r--r--app/src/androidTest/java/com/google/jetpackcamera/UiTestUtil.kt36
-rw-r--r--app/src/main/AndroidManifest.xml54
-rw-r--r--app/src/main/java/com/google/jetpackcamera/JetpackCameraApplication.kt25
-rw-r--r--app/src/main/java/com/google/jetpackcamera/MainActivity.kt124
-rw-r--r--app/src/main/java/com/google/jetpackcamera/MainActivityViewModel.kt47
-rw-r--r--app/src/main/java/com/google/jetpackcamera/ui/JcaApp.kt73
-rw-r--r--app/src/main/java/com/google/jetpackcamera/ui/PermissionsUi.kt238
-rw-r--r--app/src/main/java/com/google/jetpackcamera/ui/Routes.kt21
-rw-r--r--app/src/main/java/com/google/jetpackcamera/ui/theme/Color.kt26
-rw-r--r--app/src/main/java/com/google/jetpackcamera/ui/theme/Theme.kt78
-rw-r--r--app/src/main/java/com/google/jetpackcamera/ui/theme/Type.kt51
-rw-r--r--app/src/main/res/drawable-v24/ic_launcher_foreground.xml46
-rw-r--r--app/src/main/res/drawable/ic_launcher_background.xml185
-rw-r--r--app/src/main/res/drawable/photo_camera.xml25
-rw-r--r--app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml21
-rw-r--r--app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml21
-rw-r--r--app/src/main/res/mipmap-hdpi/ic_launcher.webpbin0 -> 1404 bytes
-rw-r--r--app/src/main/res/mipmap-hdpi/ic_launcher_round.webpbin0 -> 2898 bytes
-rw-r--r--app/src/main/res/mipmap-mdpi/ic_launcher.webpbin0 -> 982 bytes
-rw-r--r--app/src/main/res/mipmap-mdpi/ic_launcher_round.webpbin0 -> 1772 bytes
-rw-r--r--app/src/main/res/mipmap-xhdpi/ic_launcher.webpbin0 -> 1900 bytes
-rw-r--r--app/src/main/res/mipmap-xhdpi/ic_launcher_round.webpbin0 -> 3918 bytes
-rw-r--r--app/src/main/res/mipmap-xxhdpi/ic_launcher.webpbin0 -> 2884 bytes
-rw-r--r--app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webpbin0 -> 5914 bytes
-rw-r--r--app/src/main/res/mipmap-xxxhdpi/ic_launcher.webpbin0 -> 3844 bytes
-rw-r--r--app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webpbin0 -> 7778 bytes
-rw-r--r--app/src/main/res/values/strings.xml25
-rw-r--r--app/src/main/res/values/themes.xml20
-rw-r--r--app/src/main/res/xml/backup_rules.xml22
-rw-r--r--app/src/main/res/xml/data_extraction_rules.xml30
-rw-r--r--build.gradle.kts38
-rw-r--r--camera-viewfinder-compose/.gitignore1
-rw-r--r--camera-viewfinder-compose/Android.bp23
-rw-r--r--camera-viewfinder-compose/build.gradle.kts87
-rw-r--r--camera-viewfinder-compose/consumer-rules.pro0
-rw-r--r--camera-viewfinder-compose/proguard-rules.pro21
-rw-r--r--camera-viewfinder-compose/src/main/AndroidManifest.xml19
-rw-r--r--camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/CameraPreview.kt133
-rw-r--r--camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/CombinedSurface.kt100
-rw-r--r--camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/Surface.kt83
-rw-r--r--camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/SurfaceTransformationUtil.kt211
-rw-r--r--camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/Texture.kt202
-rw-r--r--core/common/.gitignore1
-rw-r--r--core/common/Android.bp19
-rw-r--r--core/common/build.gradle.kts69
-rw-r--r--core/common/consumer-rules.pro0
-rw-r--r--core/common/proguard-rules.pro21
-rw-r--r--core/common/src/main/AndroidManifest.xml19
-rw-r--r--core/common/src/main/java/com/google/jetpackcamera/core/common/CommonModule.kt32
-rw-r--r--data/settings/.gitignore1
-rw-r--r--data/settings/Android.bp43
-rw-r--r--data/settings/build.gradle.kts103
-rw-r--r--data/settings/consumer-rules.pro0
-rw-r--r--data/settings/proguard-rules.pro21
-rw-r--r--data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt58
-rw-r--r--data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt151
-rw-r--r--data/settings/src/main/AndroidManifest.xml19
-rw-r--r--data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt51
-rw-r--r--data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt45
-rw-r--r--data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt141
-rw-r--r--data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt34
-rw-r--r--data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepository.kt46
-rw-r--r--data/settings/src/main/java/com/google/jetpackcamera/settings/model/AspectRatio.kt42
-rw-r--r--data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt31
-rw-r--r--data/settings/src/main/java/com/google/jetpackcamera/settings/model/CaptureMode.kt21
-rw-r--r--data/settings/src/main/java/com/google/jetpackcamera/settings/model/DarkMode.kt22
-rw-r--r--data/settings/src/main/java/com/google/jetpackcamera/settings/model/FlashMode.kt22
-rw-r--r--data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeDataStoreModule.kt38
-rw-r--r--data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeJcaSettingsSerializer.kt51
-rw-r--r--data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeSettingsRepository.kt68
-rw-r--r--data/settings/src/main/proto/com/google/jetpackcamera/settings/aspect_ratio.proto27
-rw-r--r--data/settings/src/main/proto/com/google/jetpackcamera/settings/capture_mode.proto26
-rw-r--r--data/settings/src/main/proto/com/google/jetpackcamera/settings/dark_mode.proto26
-rw-r--r--data/settings/src/main/proto/com/google/jetpackcamera/settings/flash_mode.proto26
-rw-r--r--data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto36
-rw-r--r--data/settings/src/test/java/com/google/jetpackcamera/settings/ExampleUnitTest.kt31
-rw-r--r--docs/CONTRIBUTING.md33
-rw-r--r--docs/code-of-conduct.md93
-rw-r--r--domain/camera/.gitignore1
-rw-r--r--domain/camera/Android.bp25
-rw-r--r--domain/camera/build.gradle.kts85
-rw-r--r--domain/camera/consumer-rules.pro0
-rw-r--r--domain/camera/proguard-rules.pro21
-rw-r--r--domain/camera/src/main/AndroidManifest.xml20
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraModule.kt31
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraUseCase.kt112
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt323
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CoroutineCameraProvider.kt75
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/EmptySurfaceProcessor.kt143
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/SingleSurfaceForcingEffect.kt44
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/test/FakeCameraUseCase.kt114
-rw-r--r--feature/preview/.gitignore1
-rw-r--r--feature/preview/Android.bp35
-rw-r--r--feature/preview/build.gradle.kts113
-rw-r--r--feature/preview/consumer-rules.pro0
-rw-r--r--feature/preview/proguard-rules.pro21
-rw-r--r--feature/preview/src/main/AndroidManifest.xml19
-rw-r--r--feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/MultipleEventsCutter.kt37
-rw-r--r--feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt240
-rw-r--r--feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt61
-rw-r--r--feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt262
-rw-r--r--feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt248
-rw-r--r--feature/preview/src/main/res/values/strings.xml23
-rw-r--r--feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt117
-rw-r--r--feature/quicksettings/.gitignore1
-rw-r--r--feature/quicksettings/Android.bp28
-rw-r--r--feature/quicksettings/build.gradle.kts89
-rw-r--r--feature/quicksettings/proguard-rules.pro21
-rw-r--r--feature/quicksettings/src/main/AndroidManifest.xml19
-rw-r--r--feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/QuickSettingsEnums.kt99
-rw-r--r--feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/QuickSettingsScreen.kt190
-rw-r--r--feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/ui/QuickSettingsComponents.kt302
-rw-r--r--feature/quicksettings/src/main/res/drawable/baseline_aspect_ratio_72.xml21
-rw-r--r--feature/quicksettings/src/main/res/drawable/baseline_cameraswitch_72.xml23
-rw-r--r--feature/quicksettings/src/main/res/drawable/baseline_expand_more_72.xml21
-rw-r--r--feature/quicksettings/src/main/res/drawable/baseline_flash_auto_72.xml21
-rw-r--r--feature/quicksettings/src/main/res/drawable/baseline_flash_off_72.xml21
-rw-r--r--feature/quicksettings/src/main/res/drawable/baseline_flash_on_72.xml21
-rw-r--r--feature/quicksettings/src/main/res/drawable/baseline_timer_10_72.xml21
-rw-r--r--feature/quicksettings/src/main/res/drawable/baseline_timer_3_72.xml21
-rw-r--r--feature/quicksettings/src/main/res/drawable/baseline_timer_off_72.xml23
-rw-r--r--feature/quicksettings/src/main/res/values/dimens.xml22
-rw-r--r--feature/quicksettings/src/main/res/values/strings.xml49
-rw-r--r--feature/settings/.gitignore1
-rw-r--r--feature/settings/Android.bp31
-rw-r--r--feature/settings/build.gradle.kts104
-rw-r--r--feature/settings/consumer-rules.pro0
-rw-r--r--feature/settings/proguard-rules.pro21
-rw-r--r--feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt116
-rw-r--r--feature/settings/src/main/AndroidManifest.xml19
-rw-r--r--feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsScreen.kt87
-rw-r--r--feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt26
-rw-r--r--feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsViewModel.kt129
-rw-r--r--feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt376
-rw-r--r--feature/settings/src/main/res/values/strings.xml73
-rw-r--r--gradle.properties25
-rw-r--r--gradle/init.gradle.kts58
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 59203 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xgradlew185
-rw-r--r--gradlew.bat89
-rw-r--r--hooks/pre-commit23
-rw-r--r--settings.gradle.kts42
-rw-r--r--spotless/copyright.kt15
-rw-r--r--spotless/copyright.kts15
-rw-r--r--spotless/copyright.xml16
175 files changed, 9338 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..d93f847
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,3 @@
+[*.{kt,kts}]
+ktlint_code_style = android_studio
+ktlint_standard_function-naming = disabled \ No newline at end of file
diff --git a/.github/workflows/PushWorkflow.yaml b/.github/workflows/PushWorkflow.yaml
new file mode 100644
index 0000000..06c8921
--- /dev/null
+++ b/.github/workflows/PushWorkflow.yaml
@@ -0,0 +1,108 @@
+name: Presubmit
+
+on: [push]
+
+concurrency:
+ group: build-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ JDK_VERSION: 17
+ DISTRIBUTION: 'zulu'
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ timeout-minutes: 120
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/wrapper-validation-action@v1
+
+ - name: Set up JDK
+ uses: actions/setup-java@v3
+ with:
+ distribution: ${{ env.DISTRIBUTION }}
+ java-version: ${{ env.JDK_VERSION }}
+ cache: gradle
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Build all build type and flavor permutations
+ run: ./gradlew assemble --parallel --build-cache
+
+ - name: Upload build outputs (APKs)
+ uses: actions/upload-artifact@v3
+ with:
+ name: build-outputs
+ path: app/build/outputs
+
+ - name: Upload build reports
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@v3
+ with:
+ name: build-reports
+ path: "*/build/reports"
+
+ test:
+ name: Unit Tests
+ runs-on: ubuntu-latest
+ timeout-minutes: 120
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/wrapper-validation-action@v1
+
+ - name: Set up JDK
+ uses: actions/setup-java@v3
+ with:
+ distribution: ${{ env.DISTRIBUTION }}
+ java-version: ${{ env.JDK_VERSION }}
+ cache: gradle
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2
+ continue-on-error: true
+
+ - name: Run local tests
+ run: ./gradlew test --parallel --build-cache
+
+ - name: Upload test reports on failure
+ if: failure()
+ uses: actions/upload-artifact@v3
+ with:
+ name: test-reports
+ path: "*/build/reports/tests"
+
+ spotless:
+ name: Spotless Check
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/wrapper-validation-action@v1
+
+ - name: Set up JDK
+ uses: actions/setup-java@v3.9.0
+ with:
+ distribution: ${{ env.DISTRIBUTION }}
+ java-version: ${{ env.JDK_VERSION }}
+ cache: gradle
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Spotless Check
+ run: ./gradlew spotlessCheck --init-script gradle/init.gradle.kts --parallel --build-cache
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8b3881d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+**/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+.idea/deploymentTargetDropDown.xml
+.idea/gradle.xml
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..e3b6108
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+Jetpack Camera \ No newline at end of file
diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml
new file mode 100644
index 0000000..6abdd98
--- /dev/null
+++ b/.idea/androidTestResultsUserPreferences.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="AndroidTestResultsUserPreferences">
+ <option name="androidTestResultsTableState">
+ <map>
+ <entry key="401594821">
+ <value>
+ <AndroidTestResultsTableState>
+ <option name="preferredColumnWidths">
+ <map>
+ <entry key="Duration" value="90" />
+ <entry key="Pixel_6_Pro_API_30" value="120" />
+ <entry key="Tests" value="360" />
+ </map>
+ </option>
+ </AndroidTestResultsTableState>
+ </value>
+ </entry>
+ <entry key="2043991187">
+ <value>
+ <AndroidTestResultsTableState>
+ <option name="preferredColumnWidths">
+ <map>
+ <entry key="Duration" value="90" />
+ <entry key="Pixel_6_Pro_API_30" value="120" />
+ <entry key="Tests" value="360" />
+ </map>
+ </option>
+ </AndroidTestResultsTableState>
+ </value>
+ </entry>
+ </map>
+ </option>
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..7643783
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,123 @@
+<component name="ProjectCodeStyleConfiguration">
+ <code_scheme name="Project" version="173">
+ <JetCodeStyleSettings>
+ <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+ </JetCodeStyleSettings>
+ <codeStyleSettings language="XML">
+ <option name="FORCE_REARRANGE_MODE" value="1" />
+ <indentOptions>
+ <option name="CONTINUATION_INDENT_SIZE" value="4" />
+ </indentOptions>
+ <arrangement>
+ <rules>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>xmlns:android</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>xmlns:.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:id</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:name</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>name</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>style</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>ANDROID_ATTRIBUTE_ORDER</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>.*</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ </rules>
+ </arrangement>
+ </codeStyleSettings>
+ <codeStyleSettings language="kotlin">
+ <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+ </codeStyleSettings>
+ </code_scheme>
+</component> \ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+ <state>
+ <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+ </state>
+</component> \ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b589d56
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="CompilerConfiguration">
+ <bytecodeTargetLevel target="17" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/copyright/AOSP.xml b/.idea/copyright/AOSP.xml
new file mode 100644
index 0000000..d266867
--- /dev/null
+++ b/.idea/copyright/AOSP.xml
@@ -0,0 +1,6 @@
+<component name="CopyrightManager">
+ <copyright>
+ <option name="notice" value="Copyright (C) &amp;#36;today.year The Android Open Source Project&#10;&#10;Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10;you may not use this file except in compliance with the License.&#10;You may obtain a copy of the License at&#10;&#10; http://www.apache.org/licenses/LICENSE-2.0&#10;&#10;Unless required by applicable law or agreed to in writing, software&#10;distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10;See the License for the specific language governing permissions and&#10;limitations under the License. " />
+ <option name="myName" value="AOSP" />
+ </copyright>
+</component> \ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..3cca366
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,5 @@
+<component name="CopyrightManager">
+ <settings default="AOSP">
+ <LanguageOptions name="__TEMPLATE__" />
+ </settings>
+</component> \ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..ba0df2e
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="GradleMigrationSettings" migrationVersion="1" />
+ <component name="GradleSettings">
+ <option name="linkedExternalProjectsSettings">
+ <GradleProjectSettings>
+ <option name="testRunner" value="GRADLE" />
+ <option name="externalProjectPath" value="$PROJECT_DIR$" />
+ <option name="gradleJvm" value="JDK" />
+ <option name="modules">
+ <set>
+ <option value="$PROJECT_DIR$" />
+ <option value="$PROJECT_DIR$/app" />
+ <option value="$PROJECT_DIR$/camera-viewfinder-compose" />
+ <option value="$PROJECT_DIR$/core" />
+ <option value="$PROJECT_DIR$/core/common" />
+ <option value="$PROJECT_DIR$/data" />
+ <option value="$PROJECT_DIR$/data/settings" />
+ <option value="$PROJECT_DIR$/domain" />
+ <option value="$PROJECT_DIR$/domain/camera" />
+ <option value="$PROJECT_DIR$/feature" />
+ <option value="$PROJECT_DIR$/feature/preview" />
+ <option value="$PROJECT_DIR$/feature/quicksettings" />
+ <option value="$PROJECT_DIR$/feature/settings" />
+ </set>
+ </option>
+ <option name="resolveExternalAnnotations" value="false" />
+ </GradleProjectSettings>
+ </option>
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..44ca2d9
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,41 @@
+<component name="InspectionProjectProfileManager">
+ <profile version="1.0">
+ <option name="myName" value="Project Default" />
+ <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ </profile>
+</component> \ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..2b8a50f
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="KotlinJpsPluginSettings">
+ <option name="version" value="1.8.0" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectMigrations">
+ <option name="MigrateToGradleLocalJavaHome">
+ <set>
+ <option value="$PROJECT_DIR$" />
+ </set>
+ </option>
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..e67ad2b
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,58 @@
+<project version="4">
+ <component name="ExternalStorageConfigurationManager" enabled="true" />
+ <component name="NullableNotNullManager">
+ <option name="myDefaultNullable" value="androidx.annotation.Nullable" />
+ <option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
+ <option name="myNullables">
+ <value>
+ <list size="17">
+ <item index="0" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
+ <item index="1" class="java.lang.String" itemvalue="org.jspecify.nullness.Nullable" />
+ <item index="2" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
+ <item index="3" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
+ <item index="4" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
+ <item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
+ <item index="6" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
+ <item index="7" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
+ <item index="8" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
+ <item index="9" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
+ <item index="10" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
+ <item index="11" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
+ <item index="12" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
+ <item index="13" class="java.lang.String" itemvalue="android.annotation.Nullable" />
+ <item index="14" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
+ <item index="15" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
+ <item index="16" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
+ </list>
+ </value>
+ </option>
+ <option name="myNotNulls">
+ <value>
+ <list size="16">
+ <item index="0" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
+ <item index="1" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
+ <item index="2" class="java.lang.String" itemvalue="org.jspecify.nullness.NonNull" />
+ <item index="3" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
+ <item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
+ <item index="5" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
+ <item index="6" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
+ <item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
+ <item index="8" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
+ <item index="9" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
+ <item index="10" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
+ <item index="11" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
+ <item index="12" class="java.lang.String" itemvalue="android.annotation.NonNull" />
+ <item index="13" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
+ <item index="14" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
+ <item index="15" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
+ </list>
+ </value>
+ </option>
+ </component>
+ <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
+ <output url="file://$PROJECT_DIR$/build/classes" />
+ </component>
+ <component name="ProjectType">
+ <option name="id" value="Android" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..df496fd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="IssueNavigationConfiguration">
+ <option name="links">
+ <list>
+ <IssueNavigationLink>
+ <option name="issueRegexp" value="\bb/(\d+)(#\w+)?\b" />
+ <option name="linkRegexp" value="https://buganizer.corp.google.com/issues/$1$2" />
+ </IssueNavigationLink>
+ <IssueNavigationLink>
+ <option name="issueRegexp" value="\b(?:BUG=|FIXED=)(\d+)\b" />
+ <option name="linkRegexp" value="https://buganizer.corp.google.com/issues/$1" />
+ </IssueNavigationLink>
+ <IssueNavigationLink>
+ <option name="issueRegexp" value="\b(?:cl/|cr/|OCL=|DIFFBASE=|ROLLBACK_OF=)(\d+)\b" />
+ <option name="linkRegexp" value="https://critique.corp.google.com/$1" />
+ </IssueNavigationLink>
+ <IssueNavigationLink>
+ <option name="issueRegexp" value="\bomg/(\d+)\b" />
+ <option name="linkRegexp" value="https://omg.corp.google.com/$1" />
+ </IssueNavigationLink>
+ <IssueNavigationLink>
+ <option name="issueRegexp" value="\b(?:go/|goto/)([^,.&lt;&gt;()&quot;\s]+(?:[.,][^,.&lt;&gt;()&quot;\s]+)*)" />
+ <option name="linkRegexp" value="https://goto.google.com/$1" />
+ </IssueNavigationLink>
+ <IssueNavigationLink>
+ <option name="issueRegexp" value="\bcs/([^\s]+[\w$])" />
+ <option name="linkRegexp" value="https://cs.corp.google.com/search/?q=$1" />
+ </IssueNavigationLink>
+ <IssueNavigationLink>
+ <option name="issueRegexp" value="(LINT\.IfChange)|(LINT\.ThenChange)" />
+ <option name="linkRegexp" value="https://goto.google.com/ifthisthenthatlint" />
+ </IssueNavigationLink>
+ </list>
+ </option>
+ </component>
+ <component name="VcsDirectoryMappings">
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
+ </component>
+</project> \ No newline at end of file
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..efeb54e
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,17 @@
+package {
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+subdirs = [
+ "app",
+ "core/common",
+ "camera-viewfinder-compose",
+ "data/settings",
+ "domain/camera",
+ "feature",
+
+ ]
+
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..11465c3
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,20 @@
+name: "jetpack-camera-app"
+description:
+ "Jetpack Camera App is (will be) a fully functional camera app, focused on "
+ "features used by app developers, and built entirely with CameraX, Kotlin "
+ "and Jetpack Compose. It follows Android design and development best "
+ "practices and it's intended to be a useful reference for developers."
+
+third_party {
+ url {
+ type: HOMEPAGE
+ value: "https://github.com/google/jetpack-camera-app"
+ }
+ url {
+ type: GIT
+ value: "https://github.com/google/jetpack-camera-app"
+ }
+ version: "3ae0900e248937bd7439b3d3c22360f2a7265153"
+ last_upgrade_date { year: 2023 month: 11 day: 17 }
+ license_type: NOTICE
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..6a44f69
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,7 @@
+# Default owners are top 3 active developers of the past 1 or 2 years
+# or people with more than 10 commits last year.
+# Please update this list if you find better owner candidates.
+trevormcguire@google.com
+yasith@google.com
+davidjia@google.com
+kcrevecoeur@google.com
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d4b9422
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+# Jetpack Camera App 📸
+
+This repository contains Jetpack Camera App. It's a work in progress 🚧.
+
+Jetpack Camera App is (will be) a fully functional camera app, focused on features used by
+app developers, and built entirely with CameraX, Kotlin and Jetpack Compose. It follows Android
+design and development best practices and it's intended to be a useful reference for developers.
+
+This repository is currently in early development, and will go through many changes.
+
+# Development Environment ⚒️
+
+This project uses the gradle build system, and can be imported directly into Android Studio.
+
+# Architecture 📐
+
+TBD
+
+# Testing 🧪
+
+TBD
+
+
+## Source Code Headers
+
+Every file containing source code must include copyright and license
+information. This includes any JS/CSS files that you might be serving out to
+browsers. (This is to help well-intentioned people avoid accidental copying that
+doesn't comply with the license.)
+
+Apache header:
+
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/app/Android.bp b/app/Android.bp
new file mode 100644
index 0000000..778e3cd
--- /dev/null
+++ b/app/Android.bp
@@ -0,0 +1,35 @@
+package {
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+android_app {
+ name: "jetpack-camera-app",
+ static_libs:[
+ "androidx.compose.material3_material3",
+ "androidx.compose.ui_ui-tooling-preview",
+ "androidx.compose.ui_ui-tooling",
+ "androidx.lifecycle_lifecycle-viewmodel-compose",
+ "androidx.activity_activity-compose",
+ "androidx.core_core-ktx",
+ "androidx.lifecycle_lifecycle-runtime-ktx",
+ "androidx.navigation_navigation-compose",
+ "hilt_android",
+ "androidx.compose.runtime_runtime",
+ "jetpack-camera-app_data_settings",
+ "jetpack-camera-app_feature_preview",
+ "jetpack-camera-app_feature_settings",
+ ],
+ srcs: [
+ "src/main/**/*.kt",
+ ],
+
+ resource_dirs: [
+ "src/main/res",
+ ],
+ manifest: "src/main/AndroidManifest.xml",
+ sdk_version: "34",
+ min_sdk_version: "21",
+}
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..fe988de
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-kapt")
+ id("com.google.dagger.hilt.android")
+}
+
+android {
+ namespace = "com.google.jetpackcamera"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.google.jetpackcamera"
+ minSdk = 21
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ jvmToolchain(17)
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.4.0"
+ }
+ packagingOptions {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+ // Compose
+ val composeBom = platform("androidx.compose:compose-bom:2023.08.00")
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+
+ // Compose - Material Design 3
+ implementation("androidx.compose.material3:material3")
+
+ // Compose - Android Studio Preview support
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+
+ // Compose - Integration with ViewModels
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
+
+ // Compose - Integration with Activities
+ implementation("androidx.activity:activity-compose")
+
+ // Compose - Testing
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+
+ // Testing
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.3")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
+ androidTestImplementation("androidx.test:rules:1.5.0")
+ androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
+
+ implementation("androidx.core:core-ktx:1.8.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
+
+ // Hilt
+ implementation("com.google.dagger:hilt-android:2.44")
+ kapt("com.google.dagger:hilt-compiler:2.44")
+
+ // Accompanist - Permissions
+ implementation("com.google.accompanist:accompanist-permissions:0.26.5-rc")
+
+ // Jetpack Navigation
+ val nav_version = "2.5.3"
+ implementation("androidx.navigation:navigation-compose:$nav_version")
+
+ // Access Settings data
+ implementation(project(":data:settings"))
+
+ // Camera Preview
+ implementation(project(":feature:preview"))
+
+ // Settings Screen
+ implementation(project(":feature:settings"))
+}
+
+// Allow references to generated code
+kapt {
+ correctErrorTypes = true
+} \ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..ff59496
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.kts.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt
new file mode 100644
index 0000000..5a36f88
--- /dev/null
+++ b/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera
+
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BackgroundDeviceTest {
+ @get:Rule
+ val cameraPermissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(android.Manifest.permission.CAMERA)
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val uiDevice = UiDevice.getInstance(instrumentation)
+
+ private fun backgroundThenForegroundApp() {
+ uiDevice.pressHome()
+ uiDevice.waitForIdle(1500)
+ uiDevice.pressRecentApps()
+ uiDevice.waitForIdle(1500)
+ uiDevice.click(uiDevice.displayWidth / 2, uiDevice.displayHeight / 2)
+ uiDevice.waitForIdle(1500)
+ }
+
+ @Before
+ fun setUp() {
+ ActivityScenario.launch(MainActivity::class.java)
+ uiDevice.waitForIdle(2000)
+ }
+
+ @Test
+ fun background_foreground() {
+ backgroundThenForegroundApp()
+ }
+
+ @Test
+ fun flipCamera_then_background_foreground() {
+ uiDevice.findObject(By.res("QuickSettingDropDown")).click()
+ uiDevice.findObject(By.res("QuickSetFlipCamera")).click()
+ uiDevice.findObject(By.res("QuickSettingDropDown")).click()
+ uiDevice.waitForIdle(2000)
+ backgroundThenForegroundApp()
+ }
+
+ @Test
+ fun setAspectRatio_then_background_foreground() {
+ uiDevice.findObject(By.res("QuickSettingDropDown")).click()
+ uiDevice.findObject(By.res("QuickSetAspectRatio")).click()
+ uiDevice.findObject(By.res("QuickSetAspectRatio1:1")).click()
+ uiDevice.findObject(By.res("QuickSettingDropDown")).click()
+ uiDevice.waitForIdle(2000)
+ backgroundThenForegroundApp()
+ }
+
+ @Test
+ fun toggleCaptureMode_then_background_foreground() {
+ uiDevice.findObject(By.res("ToggleCaptureMode")).click()
+ uiDevice.waitForIdle(2000)
+ backgroundThenForegroundApp()
+ }
+}
diff --git a/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt
new file mode 100644
index 0000000..dd90332
--- /dev/null
+++ b/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera
+
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import com.google.jetpackcamera.settings.model.FlashMode
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+internal class FlashDeviceTest {
+ @get:Rule
+ val cameraPermissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(android.Manifest.permission.CAMERA)
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private var activityScenario: ActivityScenario<MainActivity>? = null
+ private val uiDevice = UiDevice.getInstance(instrumentation)
+
+ @Before
+ fun setUp() {
+ activityScenario = ActivityScenario.launch(MainActivity::class.java)
+ uiDevice.waitForIdle(2000)
+ }
+
+ @Test
+ fun set_flash_on() = runTest {
+ uiDevice.waitForIdle()
+ uiDevice.findObject(By.res("QuickSettingDropDown")).click()
+ uiDevice.findObject(By.res("QuickSetFlash")).click()
+ uiDevice.findObject(By.res("QuickSettingDropDown")).click()
+ assert(
+ UiTestUtil.getPreviewCameraAppSettings(activityScenario!!).flashMode ==
+ FlashMode.ON
+ )
+ }
+
+ @Test
+ fun set_flash_auto() = runTest {
+ uiDevice.waitForIdle()
+ uiDevice.findObject(By.res("QuickSettingDropDown")).click()
+ uiDevice.findObject(By.res("QuickSetFlash")).click()
+ uiDevice.findObject(By.res("QuickSetFlash")).click()
+ uiDevice.findObject(By.res("QuickSettingDropDown")).click()
+ assert(
+ UiTestUtil.getPreviewCameraAppSettings(activityScenario!!).flashMode ==
+ FlashMode.AUTO
+ )
+ }
+
+ @Test
+ fun set_flash_off() = runTest {
+ uiDevice.waitForIdle()
+ assert(
+ UiTestUtil.getPreviewCameraAppSettings(activityScenario!!).flashMode ==
+ FlashMode.OFF
+ )
+ uiDevice.findObject(By.res("QuickSettingDropDown")).click()
+ uiDevice.findObject(By.res("QuickSetFlash")).click()
+ uiDevice.findObject(By.res("QuickSetFlash")).click()
+ uiDevice.findObject(By.res("QuickSetFlash")).click()
+ uiDevice.findObject(By.res("QuickSettingDropDown")).click()
+ assert(
+ UiTestUtil.getPreviewCameraAppSettings(activityScenario!!).flashMode ==
+ FlashMode.OFF
+ )
+ }
+}
diff --git a/app/src/androidTest/java/com/google/jetpackcamera/UiTestUtil.kt b/app/src/androidTest/java/com/google/jetpackcamera/UiTestUtil.kt
new file mode 100644
index 0000000..be57311
--- /dev/null
+++ b/app/src/androidTest/java/com/google/jetpackcamera/UiTestUtil.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera
+
+import androidx.test.core.app.ActivityScenario
+import com.google.jetpackcamera.settings.model.CameraAppSettings
+import java.util.concurrent.atomic.AtomicReference
+
+object UiTestUtil {
+ private fun getActivity(activityScenario: ActivityScenario<MainActivity>): MainActivity {
+ val activityRef: AtomicReference<MainActivity> = AtomicReference<MainActivity>()
+ activityScenario.onActivity(activityRef::set)
+ return activityRef.get()
+ }
+
+ fun getPreviewCameraAppSettings(
+ activityScenario: ActivityScenario<MainActivity>
+ ): CameraAppSettings {
+ return getActivity(
+ activityScenario
+ ).previewViewModel!!.previewUiState.value.currentCameraSettings
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4e5c9ce
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.google.jetpackcamera">
+
+ <uses-feature
+ android:name="android.hardware.camera"
+ android:required="true" />
+ <uses-feature
+ android:name="android.hardware.camera.autofocus"
+ android:required="false" />
+
+ <uses-permission android:name="android.permission.CAMERA" />
+
+ <application
+ android:name=".JetpackCameraApplication"
+ android:allowBackup="true"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.JetpackCamera"
+ tools:targetApi="33">
+ <activity
+ android:name=".MainActivity"
+ android:exported="true"
+ android:screenOrientation="portrait"
+ android:theme="@style/Theme.JetpackCamera">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/app/src/main/java/com/google/jetpackcamera/JetpackCameraApplication.kt b/app/src/main/java/com/google/jetpackcamera/JetpackCameraApplication.kt
new file mode 100644
index 0000000..fc87bfc
--- /dev/null
+++ b/app/src/main/java/com/google/jetpackcamera/JetpackCameraApplication.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+/**
+ * [Application] class for JetpackCameraApp.
+ */
+@HiltAndroidApp(Application::class)
+class JetpackCameraApplication : Hilt_JetpackCameraApplication()
diff --git a/app/src/main/java/com/google/jetpackcamera/MainActivity.kt b/app/src/main/java/com/google/jetpackcamera/MainActivity.kt
new file mode 100644
index 0000000..a499bb9
--- /dev/null
+++ b/app/src/main/java/com/google/jetpackcamera/MainActivity.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.background
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.google.jetpackcamera.MainActivityUiState.Loading
+import com.google.jetpackcamera.MainActivityUiState.Success
+import com.google.jetpackcamera.feature.preview.PreviewViewModel
+import com.google.jetpackcamera.settings.model.DarkMode
+import com.google.jetpackcamera.ui.JcaApp
+import com.google.jetpackcamera.ui.theme.JetpackCameraTheme
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+/**
+ * Activity for the JetpackCameraApp.
+ */
+@AndroidEntryPoint(ComponentActivity::class)
+class MainActivity : Hilt_MainActivity() {
+ private val viewModel: MainActivityViewModel by viewModels()
+
+ @VisibleForTesting
+ var previewViewModel: PreviewViewModel? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ var uiState: MainActivityUiState by mutableStateOf(Loading)
+
+ lifecycleScope.launch {
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.uiState
+ .onEach {
+ uiState = it
+ }
+ .collect()
+ }
+ }
+ setContent {
+ when (uiState) {
+ Loading -> {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Black),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ CircularProgressIndicator(modifier = Modifier.size(50.dp))
+ Text(text = stringResource(R.string.jca_loading), color = Color.White)
+ }
+ }
+
+ is Success -> {
+ // TODO(kimblebee@): add app setting to enable/disable dynamic color
+ JetpackCameraTheme(
+ darkTheme = isInDarkMode(uiState = uiState),
+ dynamicColor = false
+ ) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ JcaApp(onPreviewViewModel = { previewViewModel = it })
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Determines whether the Theme should be in dark, light, or follow system theme
+ */
+@Composable
+private fun isInDarkMode(uiState: MainActivityUiState): Boolean = when (uiState) {
+ Loading -> isSystemInDarkTheme()
+ is Success -> when (uiState.cameraAppSettings.darkMode) {
+ DarkMode.DARK -> true
+ DarkMode.LIGHT -> false
+ DarkMode.SYSTEM -> isSystemInDarkTheme()
+ }
+}
diff --git a/app/src/main/java/com/google/jetpackcamera/MainActivityViewModel.kt b/app/src/main/java/com/google/jetpackcamera/MainActivityViewModel.kt
new file mode 100644
index 0000000..d18dbec
--- /dev/null
+++ b/app/src/main/java/com/google/jetpackcamera/MainActivityViewModel.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.jetpackcamera.MainActivityUiState.Loading
+import com.google.jetpackcamera.MainActivityUiState.Success
+import com.google.jetpackcamera.settings.SettingsRepository
+import com.google.jetpackcamera.settings.model.CameraAppSettings
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@HiltViewModel
+class MainActivityViewModel @Inject constructor(
+ val settingsRepository: SettingsRepository
+) : ViewModel() {
+ val uiState: StateFlow<MainActivityUiState> = settingsRepository.cameraAppSettings.map {
+ Success(it)
+ }.stateIn(
+ scope = viewModelScope,
+ initialValue = Loading,
+ started = SharingStarted.WhileSubscribed(5_000)
+ )
+}
+
+sealed interface MainActivityUiState {
+ object Loading : MainActivityUiState
+ data class Success(val cameraAppSettings: CameraAppSettings) : MainActivityUiState
+}
diff --git a/app/src/main/java/com/google/jetpackcamera/ui/JcaApp.kt b/app/src/main/java/com/google/jetpackcamera/ui/JcaApp.kt
new file mode 100644
index 0000000..8fadcbf
--- /dev/null
+++ b/app/src/main/java/com/google/jetpackcamera/ui/JcaApp.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.ui
+
+import android.Manifest
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+//import com.google.accompanist.permissions.ExperimentalPermissionsApi
+//import com.google.accompanist.permissions.isGranted
+//import com.google.accompanist.permissions.rememberPermissionState
+import com.google.jetpackcamera.feature.preview.PreviewScreen
+import com.google.jetpackcamera.feature.preview.PreviewViewModel
+import com.google.jetpackcamera.settings.SettingsScreen
+import com.google.jetpackcamera.ui.Routes.PREVIEW_ROUTE
+import com.google.jetpackcamera.ui.Routes.SETTINGS_ROUTE
+
+//@OptIn(ExperimentalPermissionsApi::class)
+@Composable
+fun JcaApp(
+ onPreviewViewModel: (PreviewViewModel) -> Unit
+ /*TODO(b/306236646): remove after still capture*/
+) {
+// val permissionState = Manifest.permission.CAMERA
+// rememberPermissionState(permission = Manifest.permission.CAMERA)
+
+// if (permissionState.status.isGranted) {
+ JetpackCameraNavHost(onPreviewViewModel)
+// } else {
+// CameraPermission(
+// modifier = Modifier.fillMaxSize(),
+// cameraPermissionState = permissionState
+// )
+// }
+}
+
+@Composable
+private fun JetpackCameraNavHost(
+ onPreviewViewModel: (PreviewViewModel) -> Unit,
+ navController: NavHostController = rememberNavController()
+) {
+ NavHost(navController = navController, startDestination = PREVIEW_ROUTE) {
+ composable(PREVIEW_ROUTE) {
+ PreviewScreen(
+ onPreviewViewModel = onPreviewViewModel,
+ onNavigateToSettings = { navController.navigate(SETTINGS_ROUTE) }
+ )
+ }
+ composable(SETTINGS_ROUTE) {
+ SettingsScreen(
+ onNavigateToPreview = { navController.navigate(PREVIEW_ROUTE) }
+ )
+ }
+ }
+}
+
diff --git a/app/src/main/java/com/google/jetpackcamera/ui/PermissionsUi.kt b/app/src/main/java/com/google/jetpackcamera/ui/PermissionsUi.kt
new file mode 100644
index 0000000..adb56e1
--- /dev/null
+++ b/app/src/main/java/com/google/jetpackcamera/ui/PermissionsUi.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+//import com.google.accompanist.permissions.ExperimentalPermissionsApi
+//import com.google.accompanist.permissions.PermissionState
+import com.google.jetpackcamera.R
+
+//@OptIn(ExperimentalPermissionsApi::class)
+@Composable
+fun CameraPermission(modifier: Modifier = Modifier) {
+ PermissionTemplate(
+ modifier = modifier,
+// permissionState = cameraPermissionState,
+ painter = painterResource(id = R.drawable.photo_camera),
+ iconAccessibilityText = stringResource(id = R.string.camera_permission_accessibility_text),
+ title = stringResource(id = R.string.camera_permission_screen_title),
+ bodyText = stringResource(id = R.string.camera_permission_required_rationale),
+ requestButtonText = stringResource(R.string.request_permission)
+ )
+}
+
+//@OptIn(ExperimentalPermissionsApi::class)
+@Composable
+fun PermissionTemplate(
+ modifier: Modifier = Modifier,
+// permissionState: PermissionState,
+ onSkipPermission: (() -> Unit)? = null,
+ painter: Painter,
+ iconAccessibilityText: String,
+ title: String,
+ bodyText: String,
+ requestButtonText: String
+) {
+ Column(
+ modifier = modifier.background(MaterialTheme.colorScheme.primary),
+ verticalArrangement = Arrangement.Bottom
+ ) {
+ // permission image / top half
+ PermissionImage(
+ modifier = Modifier
+ .height(IntrinsicSize.Min)
+ .align(Alignment.CenterHorizontally),
+ painter = painter,
+ accessibilityText = iconAccessibilityText
+ )
+ Spacer(modifier = Modifier.fillMaxHeight(.1f))
+ // bottom half
+ Column(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ ) {
+ // text section
+ PermissionText(title = title, bodyText = bodyText)
+ Spacer(modifier = Modifier.fillMaxHeight(.1f))
+ // permission button section
+ PermissionButtonSection(
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.CenterHorizontally)
+ .height(IntrinsicSize.Min),
+// permissionState = permissionState,
+ requestButtonText = requestButtonText,
+ onSkipPermission = onSkipPermission
+ )
+ }
+ Spacer(modifier = Modifier.fillMaxHeight(.2f))
+ }
+}
+
+@Composable
+fun PermissionImage(modifier: Modifier = Modifier, painter: Painter, accessibilityText: String) {
+ Box(modifier = modifier) {
+ Icon(
+ modifier = Modifier
+ .size(300.dp)
+ .align(Alignment.BottomCenter),
+ painter = painter,
+ tint = MaterialTheme.colorScheme.onPrimary,
+ contentDescription = accessibilityText
+ )
+ }
+}
+
+//@OptIn(ExperimentalPermissionsApi::class)
+@Composable
+fun PermissionButtonSection(
+ modifier: Modifier = Modifier,
+// permissionState: PermissionState,
+ requestButtonText: String,
+ onSkipPermission: (() -> Unit)?
+) {
+ Box(modifier = modifier) {
+ // permission buttons
+ Column(
+ modifier = Modifier
+ .align(Alignment.Center)
+ ) {
+ PermissionButton(
+// permissionState = permissionState,
+ requestButtonText = requestButtonText
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+ if (onSkipPermission != null) {
+ Text(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .clickable { onSkipPermission() },
+ text = "Maybe Later",
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.inversePrimary
+ )
+ }
+ }
+ }
+}
+
+//@OptIn(ExperimentalPermissionsApi::class)
+@Composable
+fun PermissionButton(
+ modifier: Modifier = Modifier,
+// permissionState: PermissionState,
+ requestButtonText: String
+) {
+ Button(
+ modifier = modifier,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ ),
+ onClick = {
+// permissionState.launchPermissionRequest()
+ }
+ ) {
+ Text(
+ modifier = Modifier.padding(10.dp),
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.SemiBold,
+ text = requestButtonText
+ )
+ }
+}
+
+@Composable
+fun PermissionText(modifier: Modifier = Modifier, title: String, bodyText: String) {
+ Box(
+ modifier = modifier
+ .height(IntrinsicSize.Min)
+ ) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .align(Alignment.Center)
+ ) {
+ // permission title
+
+ PermissionTitleText(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally),
+ text = title,
+ color = MaterialTheme.colorScheme.onPrimary
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ // permission body text
+ PermissionBodyText(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(horizontal = 50.dp),
+ text = bodyText,
+ color = MaterialTheme.colorScheme.onPrimary
+ )
+ }
+ }
+}
+
+@Composable
+fun PermissionTitleText(modifier: Modifier = Modifier, text: String, color: Color) {
+ Text(
+ modifier = modifier,
+ color = color,
+ text = text,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold
+ )
+}
+
+@Composable
+fun PermissionBodyText(modifier: Modifier = Modifier, text: String, color: Color) {
+ Text(
+ modifier = modifier,
+ color = color,
+ text = text,
+ style = MaterialTheme.typography.bodyLarge,
+ textAlign = TextAlign.Center
+ )
+}
+
diff --git a/app/src/main/java/com/google/jetpackcamera/ui/Routes.kt b/app/src/main/java/com/google/jetpackcamera/ui/Routes.kt
new file mode 100644
index 0000000..b787c8f
--- /dev/null
+++ b/app/src/main/java/com/google/jetpackcamera/ui/Routes.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.ui
+
+object Routes {
+ const val PREVIEW_ROUTE = "preview"
+ const val SETTINGS_ROUTE = "settings"
+}
diff --git a/app/src/main/java/com/google/jetpackcamera/ui/theme/Color.kt b/app/src/main/java/com/google/jetpackcamera/ui/theme/Color.kt
new file mode 100644
index 0000000..d44a9f9
--- /dev/null
+++ b/app/src/main/java/com/google/jetpackcamera/ui/theme/Color.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
diff --git a/app/src/main/java/com/google/jetpackcamera/ui/theme/Theme.kt b/app/src/main/java/com/google/jetpackcamera/ui/theme/Theme.kt
new file mode 100644
index 0000000..b975f0f
--- /dev/null
+++ b/app/src/main/java/com/google/jetpackcamera/ui/theme/Theme.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val DarkColorScheme =
+ darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+ )
+
+private val LightColorScheme =
+ lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+ )
+
+@Composable
+fun JetpackCameraTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme =
+ when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = colorScheme.primary.toArgb()
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
+ }
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
diff --git a/app/src/main/java/com/google/jetpackcamera/ui/theme/Type.kt b/app/src/main/java/com/google/jetpackcamera/ui/theme/Type.kt
new file mode 100644
index 0000000..3f5e579
--- /dev/null
+++ b/app/src/main/java/com/google/jetpackcamera/ui/theme/Type.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography =
+ Typography(
+ bodyLarge =
+ TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+ )
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..96fbfa9
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="85.84757"
+ android:endY="92.4963"
+ android:startX="42.9492"
+ android:startY="49.59793"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000" />
+</vector> \ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..db58d57
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/app/src/main/res/drawable/photo_camera.xml b/app/src/main/res/drawable/photo_camera.xml
new file mode 100644
index 0000000..7d2e784
--- /dev/null
+++ b/app/src/main/res/drawable/photo_camera.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M479.5,693q72.5,0 121.5,-49t49,-121.5q0,-72.5 -49,-121T479.5,353q-72.5,0 -121,48.5t-48.5,121q0,72.5 48.5,121.5t121,49ZM479.5,633q-47.5,0 -78.5,-31.5t-31,-79q0,-47.5 31,-78.5t78.5,-31q47.5,0 79,31t31.5,78.5q0,47.5 -31.5,79t-79,31.5ZM140,840q-24,0 -42,-18t-18,-42v-513q0,-23 18,-41.5t42,-18.5h147l73,-87h240l73,87h147q23,0 41.5,18.5T880,267v513q0,24 -18.5,42T820,840L140,840ZM140,780h680v-513L645,267l-73,-87L388,180l-73,87L140,267v513ZM480,523Z"/>
+</vector>
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..8a9b475
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+ <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon> \ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..8a9b475
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+ <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon> \ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
--- /dev/null
+++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
--- /dev/null
+++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
--- /dev/null
+++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
--- /dev/null
+++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
--- /dev/null
+++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
--- /dev/null
+++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
--- /dev/null
+++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
--- /dev/null
+++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
--- /dev/null
+++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
--- /dev/null
+++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2c3d708
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <string name="app_name">Jetpack Camera</string>
+ <string name="camera_permission_screen_title">Enable Camera</string>
+ <string name="camera_permission_required_rationale">Please provide permission to access to the camera. It is necessary for this app to function.</string>
+ <string name="camera_permission_accessibility_text">A symbol representing a camera</string>
+ <string name="request_permission">Allow Access</string>
+ <string name="jca_loading">Loading App…</string>
+ <string name="camera_not_available">Camera not available</string>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..ec9e86e
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+
+ <style name="Theme.JetpackCamera" parent="android:Theme.Material.Light.NoActionBar" />
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..20311f1
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<full-backup-content>
+ <!--
+ <include domain="sharedpref" path="."/>
+ <exclude domain="sharedpref" path="device.xml"/>
+-->
+</full-backup-content> \ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..ace7997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<data-extraction-rules>
+ <cloud-backup>
+ <!-- TODO: Use <include> and <exclude> to control what is backed up.
+ <include .../>
+ <exclude .../>
+ -->
+ </cloud-backup>
+ <!--
+ <device-transfer>
+ <include .../>
+ <exclude .../>
+ </device-transfer>
+ -->
+</data-extraction-rules> \ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..d22d8a2
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id("com.android.application") version "8.1.1" apply false
+ id("com.android.library") version "8.1.1" apply false
+ id("org.jetbrains.kotlin.android") version "1.8.0" apply false
+ id("com.google.dagger.hilt.android") version "2.44" apply false
+}
+
+tasks.register<Copy>("installGitHooks") {
+ println("Installing git hooks")
+ from(rootProject.rootDir.resolve("hooks/pre-commit"))
+ into(rootProject.rootDir.resolve(".git/hooks"))
+ fileMode = 7 * 64 + 7 * 8 + 5 // 0775 in decimal
+}
+
+gradle.taskGraph.whenReady {
+ allTasks.forEach { task ->
+ if (task != tasks["installGitHooks"]) {
+ task.dependsOn(tasks["installGitHooks"])
+ }
+ }
+} \ No newline at end of file
diff --git a/camera-viewfinder-compose/.gitignore b/camera-viewfinder-compose/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/camera-viewfinder-compose/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/camera-viewfinder-compose/Android.bp b/camera-viewfinder-compose/Android.bp
new file mode 100644
index 0000000..2b55a3f
--- /dev/null
+++ b/camera-viewfinder-compose/Android.bp
@@ -0,0 +1,23 @@
+package {
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+android_library {
+ name: "jetpack-camera-app_camera-viewfinder-compose",
+ srcs: ["src/main/**/*.kt"],
+ static_libs: [
+ "androidx.compose.material3_material3",
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.ui_ui-tooling-preview",
+ "androidx.compose.ui_ui-tooling",
+ "androidx.camera_camera-core",
+ "androidx.camera_camera-viewfinder",
+ "androidx.core_core",
+ ],
+ sdk_version: "34",
+ min_sdk_version: "21",
+ manifest:"src/main/AndroidManifest.xml"
+}
+
diff --git a/camera-viewfinder-compose/build.gradle.kts b/camera-viewfinder-compose/build.gradle.kts
new file mode 100644
index 0000000..b40b70a
--- /dev/null
+++ b/camera-viewfinder-compose/build.gradle.kts
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ namespace = "com.google.jetpackcamera.camerax"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 21
+ targetSdk = 34
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ jvmToolchain(17)
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.4.0"
+ }
+}
+
+dependencies {
+ // Compose
+ val composeBom = platform("androidx.compose:compose-bom:2023.08.00")
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+
+ // Compose - Material Design 3
+ implementation("androidx.compose.material3:material3")
+
+ // Compose - Testing
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+
+ // Compose - Android Studio Preview support
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.ui:ui-tooling")
+
+ // Testing
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.3")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
+
+ // CameraX
+ val camerax_version = "1.4.0-SNAPSHOT"
+ implementation("androidx.camera:camera-core:${camerax_version}")
+ implementation("androidx.camera:camera-view:${camerax_version}")
+
+ // AndroidX Core
+ val core_version = "1.9.0"
+ implementation("androidx.core:core:{$core_version}")
+}
diff --git a/camera-viewfinder-compose/consumer-rules.pro b/camera-viewfinder-compose/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera-viewfinder-compose/consumer-rules.pro
diff --git a/camera-viewfinder-compose/proguard-rules.pro b/camera-viewfinder-compose/proguard-rules.pro
new file mode 100644
index 0000000..ff59496
--- /dev/null
+++ b/camera-viewfinder-compose/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.kts.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/camera-viewfinder-compose/src/main/AndroidManifest.xml b/camera-viewfinder-compose/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..426a891
--- /dev/null
+++ b/camera-viewfinder-compose/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest package="com.google.jetpackcamera.viewfinder">
+
+</manifest>
diff --git a/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/CameraPreview.kt b/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/CameraPreview.kt
new file mode 100644
index 0000000..97009b8
--- /dev/null
+++ b/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/CameraPreview.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.viewfinder
+
+import android.graphics.Bitmap
+import android.util.Log
+import android.view.Surface
+import android.view.View
+import androidx.camera.core.Preview.SurfaceProvider
+import androidx.camera.core.SurfaceRequest
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import com.google.jetpackcamera.viewfinder.surface.CombinedSurface
+import com.google.jetpackcamera.viewfinder.surface.CombinedSurfaceEvent
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.flow.mapNotNull
+
+private const val TAG = "Preview"
+
+enum class ImplementationMode {
+ /**
+ * Use a [SurfaceView] for the preview when possible. If the device
+ * doesn't support [SurfaceView], [PreviewView] will fall back to use a
+ * [TextureView] instead.
+ */
+ PERFORMANCE,
+
+ /**
+ * Use a [TextureView] for the preview.
+ */
+ COMPATIBLE
+}
+
+@Composable
+fun CameraPreview(
+ modifier: Modifier,
+ implementationMode: ImplementationMode = ImplementationMode.COMPATIBLE,
+ onSurfaceProviderReady: (SurfaceProvider) -> Unit = {},
+ onRequestBitmapReady: (() -> Bitmap?) -> Unit,
+ setSurfaceView: (View) -> Unit
+) {
+ Log.d(TAG, "CameraPreview")
+
+ val surfaceRequest by produceState<SurfaceRequest?>(initialValue = null) {
+ onSurfaceProviderReady(
+ SurfaceProvider { request ->
+ value?.willNotProvideSurface()
+ value = request
+ }
+ )
+ }
+
+ PreviewSurface(
+ modifier = modifier,
+ surfaceRequest = surfaceRequest,
+ setView = setSurfaceView,
+ onRequestBitmapReady = onRequestBitmapReady,
+ implementationMode = implementationMode
+ )
+}
+
+@Composable
+fun PreviewSurface(
+ modifier: Modifier,
+ surfaceRequest: SurfaceRequest?,
+ onRequestBitmapReady: (() -> Bitmap?) -> Unit,
+ implementationMode: ImplementationMode = ImplementationMode.COMPATIBLE,
+ setView: (View) -> Unit
+) {
+ Log.d(TAG, "PreviewSurface")
+
+ var surface: Surface? by remember { mutableStateOf(null) }
+
+ LaunchedEffect(surfaceRequest, surface) {
+ Log.d(TAG, "LaunchedEffect")
+ snapshotFlow {
+ if (surfaceRequest == null || surface == null) {
+ null
+ } else {
+ Pair(surfaceRequest, surface)
+ }
+ }.mapNotNull { it }
+ .collect { (request, surface) ->
+ Log.d(TAG, "Collect: Providing surface")
+
+ request.provideSurface(surface!!, Dispatchers.Main.asExecutor()) {}
+ }
+ }
+
+ when (implementationMode) {
+ ImplementationMode.PERFORMANCE -> TODO()
+ ImplementationMode.COMPATIBLE ->
+ CombinedSurface(
+ modifier = modifier,
+ setView = setView,
+ onSurfaceEvent = { event ->
+ surface =
+ when (event) {
+ is CombinedSurfaceEvent.SurfaceAvailable -> {
+ event.surface
+ }
+
+ is CombinedSurfaceEvent.SurfaceDestroyed -> {
+ null
+ }
+ }
+ },
+ surfaceRequest = surfaceRequest,
+ onRequestBitmapReady = onRequestBitmapReady
+ )
+ }
+}
diff --git a/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/CombinedSurface.kt b/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/CombinedSurface.kt
new file mode 100644
index 0000000..0cbb14e
--- /dev/null
+++ b/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/CombinedSurface.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.viewfinder.surface
+
+import android.graphics.Bitmap
+import android.util.Log
+import android.view.Surface
+import android.view.View
+import androidx.camera.core.SurfaceRequest
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+private const val TAG = "CombinedSurface"
+
+@Composable
+fun CombinedSurface(
+ modifier: Modifier,
+ onSurfaceEvent: (CombinedSurfaceEvent) -> Unit,
+ onRequestBitmapReady: (() -> Bitmap?) -> Unit = {},
+ type: SurfaceType = SurfaceType.TEXTURE_VIEW,
+ setView: (View) -> Unit,
+ surfaceRequest: SurfaceRequest?
+) {
+ Log.d(TAG, "PreviewTexture")
+
+ when (type) {
+ SurfaceType.SURFACE_VIEW ->
+ Surface {
+ when (it) {
+ is SurfaceHolderEvent.SurfaceCreated -> {
+ onSurfaceEvent(CombinedSurfaceEvent.SurfaceAvailable(it.holder.surface))
+ }
+
+ is SurfaceHolderEvent.SurfaceDestroyed -> {
+ onSurfaceEvent(CombinedSurfaceEvent.SurfaceDestroyed)
+ }
+
+ is SurfaceHolderEvent.SurfaceChanged -> {
+ // TODO(yasith@)
+ }
+ }
+ }
+
+ SurfaceType.TEXTURE_VIEW ->
+ Texture(
+ modifier = modifier,
+ onSurfaceTextureEvent = {
+ when (it) {
+ is SurfaceTextureEvent.SurfaceTextureAvailable -> {
+ onSurfaceEvent(
+ CombinedSurfaceEvent.SurfaceAvailable(Surface(it.surface))
+ )
+ }
+
+ is SurfaceTextureEvent.SurfaceTextureDestroyed -> {
+ onSurfaceEvent(CombinedSurfaceEvent.SurfaceDestroyed)
+ }
+
+ is SurfaceTextureEvent.SurfaceTextureSizeChanged -> {
+ // TODO(yasith@)
+ }
+
+ is SurfaceTextureEvent.SurfaceTextureUpdated -> {
+ // TODO(yasith@)
+ }
+ }
+ true
+ },
+ onRequestBitmapReady,
+ setView = setView,
+ surfaceRequest = surfaceRequest
+ )
+ }
+}
+
+sealed interface CombinedSurfaceEvent {
+ data class SurfaceAvailable(
+ val surface: Surface
+ ) : CombinedSurfaceEvent
+
+ object SurfaceDestroyed : CombinedSurfaceEvent
+}
+
+enum class SurfaceType {
+ SURFACE_VIEW,
+ TEXTURE_VIEW
+}
diff --git a/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/Surface.kt b/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/Surface.kt
new file mode 100644
index 0000000..b614ea1
--- /dev/null
+++ b/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/Surface.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.viewfinder.surface
+
+import android.util.Log
+import android.view.SurfaceHolder
+import android.view.SurfaceView
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.viewinterop.AndroidView
+
+private const val TAG = "Surface"
+
+@Composable
+fun Surface(onSurfaceHolderEvent: (SurfaceHolderEvent) -> Unit = { _ -> }) {
+ Log.d(TAG, "Surface")
+
+ AndroidView(factory = { context ->
+ SurfaceView(context).apply {
+ layoutParams =
+ LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ holder.addCallback(
+ object : SurfaceHolder.Callback {
+ override fun surfaceCreated(holder: SurfaceHolder) {
+ onSurfaceHolderEvent(SurfaceHolderEvent.SurfaceCreated(holder))
+ }
+
+ override fun surfaceChanged(
+ holder: SurfaceHolder,
+ format: Int,
+ width: Int,
+ height: Int
+ ) {
+ onSurfaceHolderEvent(
+ SurfaceHolderEvent.SurfaceChanged(
+ holder,
+ width,
+ height
+ )
+ )
+ }
+
+ override fun surfaceDestroyed(holder: SurfaceHolder) {
+ onSurfaceHolderEvent(SurfaceHolderEvent.SurfaceDestroyed(holder))
+ }
+ }
+ )
+ }
+ })
+}
+
+sealed interface SurfaceHolderEvent {
+ data class SurfaceCreated(
+ val holder: SurfaceHolder
+ ) : SurfaceHolderEvent
+
+ data class SurfaceChanged(
+ val holder: SurfaceHolder,
+ val width: Int,
+ val height: Int
+ ) : SurfaceHolderEvent
+
+ data class SurfaceDestroyed(
+ val holder: SurfaceHolder
+ ) : SurfaceHolderEvent
+}
diff --git a/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/SurfaceTransformationUtil.kt b/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/SurfaceTransformationUtil.kt
new file mode 100644
index 0000000..d8846ce
--- /dev/null
+++ b/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/SurfaceTransformationUtil.kt
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.viewfinder.surface
+
+import android.annotation.SuppressLint
+import android.graphics.Matrix
+import android.graphics.RectF
+import android.util.Size
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.utils.CameraOrientationUtil
+import androidx.camera.core.impl.utils.TransformUtils
+
+/**
+ * A util class with methods that transform the input viewFinder surface so that its preview fits
+ * the given aspect ratio of its parent view.
+ *
+ * The goal is to transform it in a way so that the entire area of
+ * [SurfaceRequest.TransformationInfo.getCropRect] is 1) visible to end users, and 2)
+ * displayed as large as possible.
+ *
+ * The inputs for the calculation are 1) the dimension of the Surface, 2) the crop rect, 3) the
+ * dimension of the Viewfinder and 4) rotation degrees
+ */
+object SurfaceTransformationUtil {
+ @SuppressLint("RestrictedApi", "WrongConstant")
+ private fun getRemainingRotationDegrees(
+ transformationInfo: SurfaceRequest.TransformationInfo
+ ): Int {
+ return if (!transformationInfo.hasCameraTransform()) {
+ // If the Surface is not connected to the camera, then the SurfaceView/TextureView will
+ // not apply any transformation. In that case, we need to apply the rotation
+ // calculated by CameraX.
+ transformationInfo.rotationDegrees
+ } else if (transformationInfo.targetRotation == -1) {
+ 0
+ } else {
+ // If the Surface is connected to the camera, then the SurfaceView/TextureView
+ // will be the one to apply the camera orientation. In that case, only the Surface
+ // rotation needs to be applied.
+ -CameraOrientationUtil.surfaceRotationToDegrees(transformationInfo.targetRotation)
+ }
+ }
+
+ @SuppressLint("RestrictedApi")
+ fun getTextureViewCorrectionMatrix(
+ transformationInfo: SurfaceRequest.TransformationInfo,
+ resolution: Size
+ ): Matrix {
+ val surfaceRect =
+ RectF(0f, 0f, resolution.width.toFloat(), resolution.height.toFloat())
+ val rotationDegrees: Int = getRemainingRotationDegrees(transformationInfo)
+ return TransformUtils.getRectToRect(surfaceRect, surfaceRect, rotationDegrees)
+ }
+
+ @SuppressLint("RestrictedApi")
+ private fun getRotatedViewportSize(
+ transformationInfo: SurfaceRequest.TransformationInfo
+ ): Size {
+ return if (TransformUtils.is90or270(transformationInfo.rotationDegrees)) {
+ Size(transformationInfo.cropRect.height(), transformationInfo.cropRect.width())
+ } else {
+ Size(transformationInfo.cropRect.width(), transformationInfo.cropRect.height())
+ }
+ }
+
+ @SuppressLint("RestrictedApi")
+ fun isViewportAspectRatioMatchViewFinder(
+ transformationInfo: SurfaceRequest.TransformationInfo,
+ viewFinderSize: Size
+ ): Boolean {
+ // Using viewport rect to check if the viewport is based on the view finder.
+ val rotatedViewportSize: Size = getRotatedViewportSize(transformationInfo)
+ return TransformUtils.isAspectRatioMatchingWithRoundingError(
+ viewFinderSize,
+ true,
+ rotatedViewportSize,
+ false
+ )
+ }
+
+ private fun setMatrixRectToRect(matrix: Matrix, source: RectF, destination: RectF) {
+ val matrixScaleType = Matrix.ScaleToFit.CENTER
+ // android.graphics.Matrix doesn't support fill scale types. The workaround is
+ // mapping inversely from destination to source, then invert the matrix.
+ matrix.setRectToRect(destination, source, matrixScaleType)
+ matrix.invert(matrix)
+ }
+
+ private fun getViewFinderViewportRectForMismatchedAspectRatios(
+ transformationInfo: SurfaceRequest.TransformationInfo,
+ viewFinderSize: Size
+ ): RectF {
+ val viewFinderRect =
+ RectF(
+ 0f,
+ 0f,
+ viewFinderSize.width.toFloat(),
+ viewFinderSize.height.toFloat()
+ )
+ val rotatedViewportSize = getRotatedViewportSize(transformationInfo)
+ val rotatedViewportRect =
+ RectF(
+ 0f,
+ 0f,
+ rotatedViewportSize.width.toFloat(),
+ rotatedViewportSize.height.toFloat()
+ )
+ val matrix = Matrix()
+ setMatrixRectToRect(
+ matrix,
+ rotatedViewportRect,
+ viewFinderRect
+ )
+ matrix.mapRect(rotatedViewportRect)
+ return rotatedViewportRect
+ }
+
+ @SuppressLint("RestrictedApi")
+ fun getSurfaceToViewFinderMatrix(
+ viewFinderSize: Size,
+ transformationInfo: SurfaceRequest.TransformationInfo,
+ isFrontCamera: Boolean
+ ): Matrix {
+ // Get the target of the mapping, the coordinates of the crop rect in view finder.
+ val viewFinderCropRect: RectF =
+ if (isViewportAspectRatioMatchViewFinder(transformationInfo, viewFinderSize)) {
+ // If crop rect has the same aspect ratio as view finder, scale the crop rect to
+ // fill the entire view finder. This happens if the scale type is FILL_* AND a
+ // view-finder-based viewport is used.
+ RectF(
+ 0f,
+ 0f,
+ viewFinderSize.width.toFloat(),
+ viewFinderSize.height.toFloat()
+ )
+ } else {
+ // If the aspect ratios don't match, it could be 1) scale type is FIT_*, 2) the
+ // Viewport is not based on the view finder or 3) both.
+ getViewFinderViewportRectForMismatchedAspectRatios(
+ transformationInfo,
+ viewFinderSize
+ )
+ }
+ val matrix =
+ TransformUtils.getRectToRect(
+ RectF(transformationInfo.cropRect),
+ viewFinderCropRect,
+ transformationInfo.rotationDegrees
+ )
+ if (isFrontCamera && transformationInfo.hasCameraTransform()) {
+ // SurfaceView/TextureView automatically mirrors the Surface for front camera, which
+ // needs to be compensated by mirroring the Surface around the upright direction of the
+ // output image. This is only necessary if the stream has camera transform.
+ // Otherwise, an internal GL processor would have mirrored it already.
+ if (TransformUtils.is90or270(transformationInfo.rotationDegrees)) {
+ // If the rotation is 90/270, the Surface should be flipped vertically.
+ // +---+ 90 +---+ 270 +---+
+ // | ^ | --> | < | | > |
+ // +---+ +---+ +---+
+ matrix.preScale(
+ 1f,
+ -1f,
+ transformationInfo.cropRect.centerX().toFloat(),
+ transformationInfo.cropRect.centerY().toFloat()
+ )
+ } else {
+ // If the rotation is 0/180, the Surface should be flipped horizontally.
+ // +---+ 0 +---+ 180 +---+
+ // | ^ | --> | ^ | | v |
+ // +---+ +---+ +---+
+ matrix.preScale(
+ -1f,
+ 1f,
+ transformationInfo.cropRect.centerX().toFloat(),
+ transformationInfo.cropRect.centerY().toFloat()
+ )
+ }
+ }
+ return matrix
+ }
+
+ fun getTransformedSurfaceRect(
+ resolution: Size,
+ transformationInfo: SurfaceRequest.TransformationInfo,
+ viewFinderSize: Size,
+ isFrontCamera: Boolean
+ ): RectF {
+ val surfaceToViewFinder: Matrix =
+ getSurfaceToViewFinderMatrix(
+ viewFinderSize,
+ transformationInfo,
+ isFrontCamera
+ )
+ val rect = RectF(0f, 0f, resolution.width.toFloat(), resolution.height.toFloat())
+ surfaceToViewFinder.mapRect(rect)
+ return rect
+ }
+}
diff --git a/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/Texture.kt b/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/Texture.kt
new file mode 100644
index 0000000..d3eaff4
--- /dev/null
+++ b/camera-viewfinder-compose/src/main/java/com/google/jetpackcamera/viewfinder/surface/Texture.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.viewfinder.surface
+
+import android.annotation.SuppressLint
+import android.graphics.Bitmap
+import android.graphics.RectF
+import android.graphics.SurfaceTexture
+import android.util.Log
+import android.util.Size
+import android.view.TextureView
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.camera.core.SurfaceRequest
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.viewinterop.AndroidView
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asExecutor
+
+private const val TAG = "Texture"
+
+@SuppressLint("RestrictedApi")
+@Composable
+fun Texture(
+ modifier: Modifier,
+ onSurfaceTextureEvent: (SurfaceTextureEvent) -> Boolean = { _ -> true },
+ onRequestBitmapReady: (() -> Bitmap?) -> Unit,
+ setView: (View) -> Unit,
+ surfaceRequest: SurfaceRequest?
+) {
+ Log.d(TAG, "Texture")
+
+ val resolution = surfaceRequest?.resolution
+ var textureView: TextureView? by remember { mutableStateOf(null) }
+ var parentView: FrameLayout? by remember { mutableStateOf(null) }
+ if (parentView != null && surfaceRequest != null && resolution != null) {
+ surfaceRequest.setTransformationInfoListener(
+ Dispatchers.Main.asExecutor()
+ ) { transformationInfo ->
+ val parentViewSize = Size(parentView!!.width, parentView!!.height)
+ if (parentViewSize.height == 0 || parentViewSize.width == 0) {
+ return@setTransformationInfoListener
+ }
+ val viewFinder = textureView!!
+ val surfaceRectInViewFinder: RectF =
+ SurfaceTransformationUtil.getTransformedSurfaceRect(
+ resolution,
+ transformationInfo,
+ parentViewSize,
+ surfaceRequest.camera.isFrontFacing
+ )
+ if (!transformationInfo.hasCameraTransform()) {
+ viewFinder.layoutParams =
+ FrameLayout.LayoutParams(
+ surfaceRectInViewFinder.width().toInt(),
+ surfaceRectInViewFinder.height().toInt()
+ )
+ } else {
+ viewFinder.layoutParams =
+ FrameLayout.LayoutParams(
+ resolution.width,
+ resolution.height
+ )
+ }
+ // For TextureView, correct the orientation to match the target rotation.
+ val correctionMatrix =
+ SurfaceTransformationUtil.getTextureViewCorrectionMatrix(
+ transformationInfo,
+ resolution
+ )
+ viewFinder.setTransform(correctionMatrix)
+
+ viewFinder.pivotX = 0f
+ viewFinder.pivotY = 0f
+ viewFinder.scaleX = surfaceRectInViewFinder.width() / resolution.width
+ viewFinder.scaleY = surfaceRectInViewFinder.height() / resolution.height
+ viewFinder.translationX = surfaceRectInViewFinder.left - viewFinder.left
+ viewFinder.translationY = surfaceRectInViewFinder.top - viewFinder.top
+ }
+ }
+
+ if (resolution != null) {
+ AndroidView(
+ modifier = modifier.clipToBounds(),
+ factory = { context ->
+ FrameLayout(context).apply {
+ layoutParams =
+ LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ addView(
+ TextureView(context).apply {
+ layoutParams =
+ FrameLayout.LayoutParams(
+ resolution.width,
+ resolution.height
+ )
+ surfaceTextureListener =
+ object : TextureView.SurfaceTextureListener {
+ override fun onSurfaceTextureAvailable(
+ surface: SurfaceTexture,
+ width: Int,
+ height: Int
+ ) {
+ onSurfaceTextureEvent(
+ SurfaceTextureEvent.SurfaceTextureAvailable(
+ surface,
+ width,
+ height
+ )
+ )
+ }
+
+ override fun onSurfaceTextureSizeChanged(
+ surface: SurfaceTexture,
+ width: Int,
+ height: Int
+ ) {
+ onSurfaceTextureEvent(
+ SurfaceTextureEvent.SurfaceTextureSizeChanged(
+ surface,
+ width,
+ height
+ )
+ )
+ }
+
+ override fun onSurfaceTextureDestroyed(
+ surface: SurfaceTexture
+ ): Boolean {
+ return onSurfaceTextureEvent(
+ SurfaceTextureEvent.SurfaceTextureDestroyed(
+ surface
+ )
+ )
+ }
+
+ override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
+ onSurfaceTextureEvent(
+ SurfaceTextureEvent.SurfaceTextureUpdated(
+ surface
+ )
+ )
+ }
+ }
+ }
+ )
+ }
+ },
+ update = {
+ parentView = it
+ textureView = it.getChildAt(0) as TextureView?
+ setView(it)
+ onRequestBitmapReady { textureView!!.bitmap }
+ }
+ )
+ }
+}
+
+sealed interface SurfaceTextureEvent {
+ data class SurfaceTextureAvailable(
+ val surface: SurfaceTexture,
+ val width: Int,
+ val height: Int
+ ) : SurfaceTextureEvent
+
+ data class SurfaceTextureSizeChanged(
+ val surface: SurfaceTexture,
+ val width: Int,
+ val height: Int
+ ) : SurfaceTextureEvent
+
+ data class SurfaceTextureDestroyed(
+ val surface: SurfaceTexture
+ ) : SurfaceTextureEvent
+
+ data class SurfaceTextureUpdated(
+ val surface: SurfaceTexture
+ ) : SurfaceTextureEvent
+}
diff --git a/core/common/.gitignore b/core/common/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/core/common/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/core/common/Android.bp b/core/common/Android.bp
new file mode 100644
index 0000000..552b888
--- /dev/null
+++ b/core/common/Android.bp
@@ -0,0 +1,19 @@
+package {
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+android_library {
+ name: "jetpack-camera-app_core_common",
+ srcs: ["src/main/**/*.kt"],
+ static_libs: [
+ "androidx.core_core-ktx",
+ "hilt_android",
+ "androidx.appcompat_appcompat",
+ "com.google.android.material_material",
+ ],
+ sdk_version: "34",
+ min_sdk_version: "21",
+ manifest:"src/main/AndroidManifest.xml"
+}
diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts
new file mode 100644
index 0000000..9dd6046
--- /dev/null
+++ b/core/common/build.gradle.kts
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-kapt")
+ id("com.google.dagger.hilt.android")
+}
+
+android {
+ namespace = "com.google.jetpackcamera.core.common"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 21
+ targetSdk = 34
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ jvmToolchain(17)
+ }
+}
+
+dependencies {
+
+ implementation("androidx.core:core-ktx:1.8.0")
+ implementation("androidx.appcompat:appcompat:1.6.1")
+ implementation("com.google.android.material:material:1.8.0")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.5")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+
+ // Hilt
+ implementation("com.google.dagger:hilt-android:2.44")
+ kapt("com.google.dagger:hilt-compiler:2.44")
+}
+
+kapt {
+ correctErrorTypes = true
+}
diff --git a/core/common/consumer-rules.pro b/core/common/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/core/common/consumer-rules.pro
diff --git a/core/common/proguard-rules.pro b/core/common/proguard-rules.pro
new file mode 100644
index 0000000..ff59496
--- /dev/null
+++ b/core/common/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.kts.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/core/common/src/main/AndroidManifest.xml b/core/common/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1609b38
--- /dev/null
+++ b/core/common/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest package="com.google.jetpackcamera.core.common">
+
+</manifest>
diff --git a/core/common/src/main/java/com/google/jetpackcamera/core/common/CommonModule.kt b/core/common/src/main/java/com/google/jetpackcamera/core/common/CommonModule.kt
new file mode 100644
index 0000000..1bfea1b
--- /dev/null
+++ b/core/common/src/main/java/com/google/jetpackcamera/core/common/CommonModule.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.core.common
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.CoroutineDispatcher
+
+/**
+ * Dagger [Module] for Common dependencies.
+ */
+@Module
+@InstallIn(SingletonComponent::class)
+class CommonModule {
+ @Provides
+ fun provideDefaultDispatcher(): CoroutineDispatcher = kotlinx.coroutines.Dispatchers.Default
+}
diff --git a/data/settings/.gitignore b/data/settings/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/data/settings/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/data/settings/Android.bp b/data/settings/Android.bp
new file mode 100644
index 0000000..2abb0b4
--- /dev/null
+++ b/data/settings/Android.bp
@@ -0,0 +1,43 @@
+package {
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+java_library {
+ name: "jetpack-camera-app-protos-java-gen",
+ installable: false,
+ proto: {
+ type: "lite",
+ canonical_path_from_root: false,
+ local_include_dirs: ["src/main/proto"],
+ },
+ srcs: [
+ "src/main/proto/**/*.proto",
+ ],
+ min_sdk_version: "21",
+ sdk_version: "34",
+
+ static_libs: [
+ "libprotobuf-java-lite",
+ ]
+}
+
+
+android_library {
+ name: "jetpack-camera-app_data_settings",
+ srcs: [
+ "src/main/**/*.kt",
+ ],
+ static_libs: [
+ "hilt_android",
+ "kotlinx-coroutines-core",
+ "androidx.datastore_datastore",
+ "jetpack-camera-app-protos-java-gen",
+ "libprotobuf-java-lite",
+ ],
+ sdk_version: "34",
+ min_sdk_version: "21",
+ manifest:"src/main/AndroidManifest.xml",
+}
+
diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts
new file mode 100644
index 0000000..5fe0201
--- /dev/null
+++ b/data/settings/build.gradle.kts
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-kapt")
+ id("com.google.dagger.hilt.android")
+ id("com.google.protobuf") version "0.9.1"
+}
+
+android {
+ namespace = "com.google.jetpackcamera.data.settings"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 21
+ targetSdk = 34
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ jvmToolchain(17)
+ }
+}
+
+dependencies {
+ implementation("androidx.test:core-ktx:1.4.0")
+
+ // Testing
+ testImplementation("junit:junit:4.13.2")
+
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
+ androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
+
+ androidTestImplementation("androidx.test.ext:junit:1.1.3")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
+
+
+ // Hilt
+ implementation("com.google.dagger:hilt-android:2.44")
+ kapt("com.google.dagger:hilt-compiler:2.44")
+
+ // proto datastore
+ implementation("androidx.datastore:datastore:1.0.0")
+ implementation("com.google.protobuf:protobuf-kotlin-lite:3.21.12")
+
+}
+
+protobuf {
+ protoc {
+ artifact = "com.google.protobuf:protoc:3.21.12"
+ }
+
+ generateProtoTasks {
+ all().forEach {task ->
+ task.builtins {
+ create("java") {
+ option("lite")
+ }
+ }
+
+ task.builtins {
+ create("kotlin") {
+ option("lite")
+ }
+ }
+ }
+ }
+}
+
+// Allow references to generated code
+kapt {
+ correctErrorTypes = true
+}
diff --git a/data/settings/consumer-rules.pro b/data/settings/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/data/settings/consumer-rules.pro
diff --git a/data/settings/proguard-rules.pro b/data/settings/proguard-rules.pro
new file mode 100644
index 0000000..ff59496
--- /dev/null
+++ b/data/settings/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.kts.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt b/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt
new file mode 100644
index 0000000..ab72c27
--- /dev/null
+++ b/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings
+
+import androidx.datastore.core.DataStore
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.jetpackcamera.settings.test.FakeDataStoreModule
+import com.google.jetpackcamera.settings.test.FakeJcaSettingsSerializer
+import java.io.File
+import junit.framework.TestCase.assertEquals
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+class DataStoreModuleTest {
+ @get:Rule
+ val tempFolder = TemporaryFolder()
+ private lateinit var testFile: File
+
+ @Before
+ fun setUp() {
+ testFile = tempFolder.newFile()
+ }
+
+ @Test
+ fun dataStoreModule_read_can_handle_corrupted_file() = runTest {
+ // should handle exception and replace file information
+ val dataStore: DataStore<JcaSettings> = FakeDataStoreModule.provideDataStore(
+ scope = this,
+ serializer = FakeJcaSettingsSerializer(failReadWithCorruptionException = true),
+ file = testFile
+ )
+ val datastoreValue = dataStore.data.first()
+ advanceUntilIdle()
+
+ assertEquals(datastoreValue, JcaSettings.getDefaultInstance())
+ }
+}
diff --git a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt b/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt
new file mode 100644
index 0000000..3af226d
--- /dev/null
+++ b/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.dataStoreFile
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.jetpackcamera.settings.DataStoreModule.provideDataStore
+import com.google.jetpackcamera.settings.model.CameraAppSettings
+import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS
+import com.google.jetpackcamera.settings.model.DarkMode
+import com.google.jetpackcamera.settings.model.FlashMode
+import java.io.File
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class LocalSettingsRepositoryInstrumentedTest {
+ private val testContext: Context = ApplicationProvider.getApplicationContext()
+ private lateinit var testDataStore: DataStore<JcaSettings>
+ private lateinit var datastoreScope: CoroutineScope
+ private lateinit var repository: LocalSettingsRepository
+
+ @Before
+ fun setup() = runTest(StandardTestDispatcher()) {
+ Dispatchers.setMain(StandardTestDispatcher())
+ testDataStore = provideDataStore(testContext)
+ datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
+
+ testDataStore = DataStoreFactory.create(
+ serializer = JcaSettingsSerializer,
+ scope = datastoreScope
+ ) {
+ testContext.dataStoreFile("test_jca_settings.pb")
+ }
+ repository = LocalSettingsRepository(testDataStore)
+ advanceUntilIdle()
+ }
+
+ @After
+ fun tearDown() {
+ File(
+ ApplicationProvider.getApplicationContext<Context>().filesDir,
+ "datastore"
+ ).deleteRecursively()
+
+ datastoreScope.cancel()
+ }
+
+ @Test
+ fun repository_can_fetch_initial_datastore() = runTest(StandardTestDispatcher()) {
+ // if you've created a new setting value and this test is failing, be sure to check that
+ // JcaSettingsSerializer.kt defaultValue has been properly modified :)
+
+ val cameraAppSettings: CameraAppSettings = repository.getCameraAppSettings()
+
+ advanceUntilIdle()
+ assertTrue(cameraAppSettings == DEFAULT_CAMERA_APP_SETTINGS)
+ }
+
+ @Test
+ fun can_update_dark_mode() = runTest(StandardTestDispatcher()) {
+ val initialDarkModeStatus = repository.getCameraAppSettings().darkMode
+ repository.updateDarkModeStatus(DarkMode.LIGHT)
+ val newDarkModeStatus = repository.getCameraAppSettings().darkMode
+
+ advanceUntilIdle()
+ assertFalse(initialDarkModeStatus == newDarkModeStatus)
+ assertTrue(initialDarkModeStatus == DarkMode.SYSTEM)
+ assertTrue(newDarkModeStatus == DarkMode.LIGHT)
+ }
+
+ @Test
+ fun can_update_default_to_front_camera() = runTest(StandardTestDispatcher()) {
+ // default to front camera starts false
+ val initialFrontCameraDefault = repository.getCameraAppSettings().isFrontCameraFacing
+ repository.updateDefaultToFrontCamera()
+ // default to front camera is now true
+ val frontCameraDefault = repository.getCameraAppSettings().isFrontCameraFacing
+ advanceUntilIdle()
+
+ assertFalse(initialFrontCameraDefault)
+ assertTrue(frontCameraDefault)
+ }
+
+ @Test
+ fun can_update_flash_mode() = runTest(StandardTestDispatcher()) {
+ // default to front camera starts false
+ val initialFlashModeStatus = repository.getCameraAppSettings().flashMode
+ repository.updateFlashModeStatus(FlashMode.ON)
+ // default to front camera is now true
+ val newFlashModeStatus = repository.getCameraAppSettings().flashMode
+ advanceUntilIdle()
+
+ assertEquals(initialFlashModeStatus, FlashMode.OFF)
+ assertEquals(newFlashModeStatus, FlashMode.ON)
+ }
+
+ @Test
+ fun can_update_available_camera_lens() = runTest(StandardTestDispatcher()) {
+ // available cameras start true
+ val initialFrontCamera = repository.getCameraAppSettings().isFrontCameraAvailable
+ val initialBackCamera = repository.getCameraAppSettings().isBackCameraAvailable
+
+ repository.updateAvailableCameraLens(frontLensAvailable = false, backLensAvailable = false)
+ // available cameras now false
+ advanceUntilIdle()
+ val newFrontCamera = repository.getCameraAppSettings().isFrontCameraAvailable
+ val newBackCamera = repository.getCameraAppSettings().isBackCameraAvailable
+
+ assertEquals(true, initialFrontCamera && initialBackCamera)
+ assertEquals(false, newFrontCamera || newBackCamera)
+ }
+}
diff --git a/data/settings/src/main/AndroidManifest.xml b/data/settings/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..da78212
--- /dev/null
+++ b/data/settings/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest package="com.google.jetpackcamera.data.settings">
+
+</manifest>
diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt
new file mode 100644
index 0000000..7f88624
--- /dev/null
+++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
+import androidx.datastore.dataStoreFile
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+
+// with hilt will ensure datastore instance access is unique per file
+@Module
+@InstallIn(SingletonComponent::class)
+object DataStoreModule {
+ private const val FILE_LOCATION = "app_settings.pb"
+
+ @Provides
+ @Singleton
+ fun provideDataStore(@ApplicationContext context: Context): DataStore<JcaSettings> =
+ DataStoreFactory.create(
+ corruptionHandler = ReplaceFileCorruptionHandler { JcaSettings.getDefaultInstance() },
+ // TODO(b/286245619, kimblebee@): Inject coroutine scope once module providing default IO dispatcher scope is implemented
+ scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
+ serializer = JcaSettingsSerializer,
+ produceFile = {
+ context.dataStoreFile(FILE_LOCATION)
+ }
+ )
+}
diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt
new file mode 100644
index 0000000..e607a6b
--- /dev/null
+++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings
+
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.Serializer
+import com.google.protobuf.InvalidProtocolBufferException
+import java.io.InputStream
+import java.io.OutputStream
+
+object JcaSettingsSerializer : Serializer<JcaSettings> {
+
+ override val defaultValue: JcaSettings = JcaSettings.newBuilder()
+ .setDarkModeStatus(DarkMode.DARK_MODE_SYSTEM)
+ .setDefaultFrontCamera(false)
+ .setBackCameraAvailable(true)
+ .setFrontCameraAvailable(true)
+ .setFlashModeStatus(FlashMode.FLASH_MODE_OFF)
+ .setAspectRatioStatus(AspectRatio.ASPECT_RATIO_NINE_SIXTEEN)
+ .setCaptureModeStatus(CaptureMode.CAPTURE_MODE_MULTI_STREAM)
+ .build()
+
+ override suspend fun readFrom(input: InputStream): JcaSettings {
+ try {
+ return JcaSettings.parseFrom(input)
+ } catch (exception: InvalidProtocolBufferException) {
+ throw CorruptionException("Cannot read proto.", exception)
+ }
+ }
+
+ override suspend fun writeTo(t: JcaSettings, output: OutputStream) = t.writeTo(output)
+}
diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt
new file mode 100644
index 0000000..eab14ae
--- /dev/null
+++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings
+
+import androidx.datastore.core.DataStore
+import com.google.jetpackcamera.settings.AspectRatio as AspectRatioProto
+import com.google.jetpackcamera.settings.CaptureMode as CaptureModeProto
+import com.google.jetpackcamera.settings.DarkMode as DarkModeProto
+import com.google.jetpackcamera.settings.FlashMode as FlashModeProto
+import com.google.jetpackcamera.settings.model.AspectRatio
+import com.google.jetpackcamera.settings.model.CameraAppSettings
+import com.google.jetpackcamera.settings.model.CaptureMode
+import com.google.jetpackcamera.settings.model.DarkMode
+import com.google.jetpackcamera.settings.model.FlashMode
+import javax.inject.Inject
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+
+/**
+ * Implementation of [SettingsRepository] with locally stored settings.
+ */
+class LocalSettingsRepository @Inject constructor(
+ private val jcaSettings: DataStore<JcaSettings>
+) : SettingsRepository {
+
+ override val cameraAppSettings = jcaSettings.data
+ .map {
+ CameraAppSettings(
+ isFrontCameraFacing = it.defaultFrontCamera,
+ darkMode = when (it.darkModeStatus) {
+ DarkModeProto.DARK_MODE_DARK -> DarkMode.DARK
+ DarkModeProto.DARK_MODE_LIGHT -> DarkMode.LIGHT
+ DarkModeProto.DARK_MODE_SYSTEM -> DarkMode.SYSTEM
+ else -> DarkMode.SYSTEM
+ },
+ flashMode = when (it.flashModeStatus) {
+ FlashModeProto.FLASH_MODE_AUTO -> FlashMode.AUTO
+ FlashModeProto.FLASH_MODE_ON -> FlashMode.ON
+ FlashModeProto.FLASH_MODE_OFF -> FlashMode.OFF
+ else -> FlashMode.OFF
+ },
+ isFrontCameraAvailable = it.frontCameraAvailable,
+ isBackCameraAvailable = it.backCameraAvailable,
+ aspectRatio = AspectRatio.fromProto(it.aspectRatioStatus),
+ captureMode = when (it.captureModeStatus) {
+ CaptureModeProto.CAPTURE_MODE_SINGLE_STREAM -> CaptureMode.SINGLE_STREAM
+ CaptureModeProto.CAPTURE_MODE_MULTI_STREAM -> CaptureMode.MULTI_STREAM
+ else -> CaptureMode.MULTI_STREAM
+ }
+ )
+ }
+
+ override suspend fun getCameraAppSettings(): CameraAppSettings = cameraAppSettings.first()
+
+ override suspend fun updateDefaultToFrontCamera() {
+ jcaSettings.updateData { currentSettings ->
+ currentSettings.toBuilder()
+ .setDefaultFrontCamera(!currentSettings.defaultFrontCamera)
+ .build()
+ }
+ }
+
+ override suspend fun updateDarkModeStatus(darkMode: DarkMode) {
+ val newStatus = when (darkMode) {
+ DarkMode.DARK -> DarkModeProto.DARK_MODE_DARK
+ DarkMode.LIGHT -> DarkModeProto.DARK_MODE_LIGHT
+ DarkMode.SYSTEM -> DarkModeProto.DARK_MODE_SYSTEM
+ }
+ jcaSettings.updateData { currentSettings ->
+ currentSettings.toBuilder()
+ .setDarkModeStatus(newStatus)
+ .build()
+ }
+ }
+
+ override suspend fun updateFlashModeStatus(flashMode: FlashMode) {
+ val newStatus = when (flashMode) {
+ FlashMode.AUTO -> FlashModeProto.FLASH_MODE_AUTO
+ FlashMode.ON -> FlashModeProto.FLASH_MODE_ON
+ FlashMode.OFF -> FlashModeProto.FLASH_MODE_OFF
+ }
+ jcaSettings.updateData { currentSettings ->
+ currentSettings.toBuilder()
+ .setFlashModeStatus(newStatus)
+ .build()
+ }
+ }
+
+ override suspend fun updateAvailableCameraLens(
+ frontLensAvailable: Boolean,
+ backLensAvailable: Boolean
+ ) {
+ // if a front or back lens is not present, the option to change
+ // the direction of the camera should be disabled
+ jcaSettings.updateData { currentSettings ->
+ currentSettings.toBuilder()
+ .setDefaultFrontCamera(frontLensAvailable)
+ .setFrontCameraAvailable(frontLensAvailable)
+ .setBackCameraAvailable(backLensAvailable)
+ .build()
+ }
+ }
+
+ override suspend fun updateAspectRatio(aspectRatio: AspectRatio) {
+ val newStatus = when (aspectRatio) {
+ AspectRatio.NINE_SIXTEEN -> AspectRatioProto.ASPECT_RATIO_NINE_SIXTEEN
+ AspectRatio.THREE_FOUR -> AspectRatioProto.ASPECT_RATIO_THREE_FOUR
+ AspectRatio.ONE_ONE -> AspectRatioProto.ASPECT_RATIO_ONE_ONE
+ }
+ jcaSettings.updateData { currentSettings ->
+ currentSettings.toBuilder()
+ .setAspectRatioStatus(newStatus)
+ .build()
+ }
+ }
+
+ override suspend fun updateCaptureMode(captureMode: CaptureMode) {
+ val newStatus = when (captureMode) {
+ CaptureMode.MULTI_STREAM -> CaptureModeProto.CAPTURE_MODE_MULTI_STREAM
+ CaptureMode.SINGLE_STREAM -> CaptureModeProto.CAPTURE_MODE_SINGLE_STREAM
+ }
+ jcaSettings.updateData { currentSettings ->
+ currentSettings.toBuilder()
+ .setCaptureModeStatus(newStatus)
+ .build()
+ }
+ }
+}
diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt
new file mode 100644
index 0000000..3c9fb71
--- /dev/null
+++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+/**
+ * Dagger [Module] for settings data layer.
+ */
+@Module
+@InstallIn(SingletonComponent::class)
+interface SettingsModule {
+
+ @Binds
+ fun bindsSettingsRepository(
+ localSettingsRepository: LocalSettingsRepository
+ ): SettingsRepository
+}
diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepository.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepository.kt
new file mode 100644
index 0000000..637a1ab
--- /dev/null
+++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepository.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings
+
+import com.google.jetpackcamera.settings.model.AspectRatio
+import com.google.jetpackcamera.settings.model.CameraAppSettings
+import com.google.jetpackcamera.settings.model.CaptureMode
+import com.google.jetpackcamera.settings.model.DarkMode
+import com.google.jetpackcamera.settings.model.FlashMode
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Data layer for settings.
+ */
+interface SettingsRepository {
+
+ val cameraAppSettings: Flow<CameraAppSettings>
+
+ suspend fun updateDefaultToFrontCamera()
+
+ suspend fun updateDarkModeStatus(darkMode: DarkMode)
+
+ suspend fun updateFlashModeStatus(flashMode: FlashMode)
+
+ // set device values from cameraUseCase
+ suspend fun updateAvailableCameraLens(frontLensAvailable: Boolean, backLensAvailable: Boolean)
+
+ suspend fun updateAspectRatio(aspectRatio: AspectRatio)
+
+ suspend fun updateCaptureMode(captureMode: CaptureMode)
+
+ suspend fun getCameraAppSettings(): CameraAppSettings
+}
diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/AspectRatio.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/AspectRatio.kt
new file mode 100644
index 0000000..6738154
--- /dev/null
+++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/AspectRatio.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings.model
+
+import android.util.Rational
+import com.google.jetpackcamera.settings.AspectRatio as AspectRatioProto
+
+enum class AspectRatio(val ratio: Rational) {
+ THREE_FOUR(Rational(3, 4)),
+ NINE_SIXTEEN(Rational(9, 16)),
+ ONE_ONE(Rational(1, 1));
+
+ companion object {
+
+ /** returns the AspectRatio enum equivalent of a provided AspectRatioProto */
+ fun fromProto(aspectRatioProto: AspectRatioProto): AspectRatio {
+ return when (aspectRatioProto) {
+ AspectRatioProto.ASPECT_RATIO_NINE_SIXTEEN -> AspectRatio.NINE_SIXTEEN
+ AspectRatioProto.ASPECT_RATIO_ONE_ONE -> AspectRatio.ONE_ONE
+
+ // defaults to 3:4 aspect ratio
+ AspectRatioProto.ASPECT_RATIO_THREE_FOUR,
+ AspectRatioProto.ASPECT_RATIO_UNDEFINED,
+ AspectRatioProto.UNRECOGNIZED
+ -> AspectRatio.THREE_FOUR
+ }
+ }
+ }
+}
diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt
new file mode 100644
index 0000000..e16f332
--- /dev/null
+++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings.model
+
+/**
+ * Data layer representation for settings.
+ */
+data class CameraAppSettings(
+ val isFrontCameraFacing: Boolean = false,
+ val isFrontCameraAvailable: Boolean = true,
+ val isBackCameraAvailable: Boolean = true,
+ val darkMode: DarkMode = DarkMode.SYSTEM,
+ val flashMode: FlashMode = FlashMode.OFF,
+ val captureMode: CaptureMode = CaptureMode.MULTI_STREAM,
+ val aspectRatio: AspectRatio = AspectRatio.NINE_SIXTEEN
+)
+
+val DEFAULT_CAMERA_APP_SETTINGS = CameraAppSettings()
diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CaptureMode.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CaptureMode.kt
new file mode 100644
index 0000000..1931d9f
--- /dev/null
+++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CaptureMode.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings.model
+
+enum class CaptureMode {
+ MULTI_STREAM,
+ SINGLE_STREAM
+}
diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/DarkMode.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/DarkMode.kt
new file mode 100644
index 0000000..ad2f627
--- /dev/null
+++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/DarkMode.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings.model
+
+enum class DarkMode {
+ SYSTEM,
+ DARK,
+ LIGHT
+}
diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/FlashMode.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/FlashMode.kt
new file mode 100644
index 0000000..2778740
--- /dev/null
+++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/FlashMode.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings.model
+
+enum class FlashMode {
+ OFF,
+ ON,
+ AUTO
+}
diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeDataStoreModule.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeDataStoreModule.kt
new file mode 100644
index 0000000..1b04f53
--- /dev/null
+++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeDataStoreModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings.test
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
+import com.google.jetpackcamera.settings.JcaSettings
+import java.io.File
+import kotlinx.coroutines.CoroutineScope
+
+/** test implementation of DataStoreModule */
+object FakeDataStoreModule {
+
+ fun provideDataStore(
+ scope: CoroutineScope,
+ serializer: FakeJcaSettingsSerializer,
+ file: File
+ ): DataStore<JcaSettings> = DataStoreFactory.create(
+ corruptionHandler = ReplaceFileCorruptionHandler { JcaSettings.getDefaultInstance() },
+ scope = scope,
+ serializer = serializer,
+ produceFile = { file }
+ )
+}
diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeJcaSettingsSerializer.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeJcaSettingsSerializer.kt
new file mode 100644
index 0000000..2193922
--- /dev/null
+++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeJcaSettingsSerializer.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings.test
+
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.Serializer
+import com.google.jetpackcamera.settings.DarkMode
+import com.google.jetpackcamera.settings.JcaSettings
+import com.google.protobuf.InvalidProtocolBufferException
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+
+class FakeJcaSettingsSerializer(
+ var failReadWithCorruptionException: Boolean = false
+) : Serializer<JcaSettings> {
+
+ override val defaultValue: JcaSettings = JcaSettings.newBuilder()
+ .setDarkModeStatus(DarkMode.DARK_MODE_SYSTEM)
+ .setDefaultFrontCamera(false)
+ .build()
+
+ override suspend fun readFrom(input: InputStream): JcaSettings {
+ if (failReadWithCorruptionException) {
+ throw CorruptionException(
+ "Corruption Exception",
+ IOException()
+ )
+ }
+ try {
+ return JcaSettings.parseFrom(input)
+ } catch (exception: InvalidProtocolBufferException) {
+ throw CorruptionException("Cannot read proto.", exception)
+ }
+ }
+
+ override suspend fun writeTo(t: JcaSettings, output: OutputStream) = t.writeTo(output)
+}
diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeSettingsRepository.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeSettingsRepository.kt
new file mode 100644
index 0000000..fbc40f5
--- /dev/null
+++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeSettingsRepository.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings.test
+
+import com.google.jetpackcamera.settings.SettingsRepository
+import com.google.jetpackcamera.settings.model.AspectRatio
+import com.google.jetpackcamera.settings.model.CameraAppSettings
+import com.google.jetpackcamera.settings.model.CaptureMode
+import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS
+import com.google.jetpackcamera.settings.model.DarkMode
+import com.google.jetpackcamera.settings.model.FlashMode
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+object FakeSettingsRepository : SettingsRepository {
+ var currentCameraSettings: CameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS
+
+ override val cameraAppSettings: Flow<CameraAppSettings> = flow { emit(currentCameraSettings) }
+
+ override suspend fun updateDefaultToFrontCamera() {
+ val newLensFacing = !currentCameraSettings.isFrontCameraFacing
+ currentCameraSettings = currentCameraSettings.copy(isFrontCameraFacing = newLensFacing)
+ }
+
+ override suspend fun updateDarkModeStatus(darkmode: DarkMode) {
+ currentCameraSettings = currentCameraSettings.copy(darkMode = darkmode)
+ }
+
+ override suspend fun updateFlashModeStatus(flashMode: FlashMode) {
+ currentCameraSettings = currentCameraSettings.copy(flashMode = flashMode)
+ }
+
+ override suspend fun getCameraAppSettings(): CameraAppSettings {
+ return currentCameraSettings
+ }
+
+ override suspend fun updateAvailableCameraLens(
+ frontLensAvailable: Boolean,
+ backLensAvailable: Boolean
+ ) {
+ currentCameraSettings = currentCameraSettings.copy(
+ isFrontCameraAvailable = frontLensAvailable,
+ isBackCameraAvailable = backLensAvailable
+ )
+ }
+
+ override suspend fun updateCaptureMode(captureMode: CaptureMode) {
+ currentCameraSettings =
+ currentCameraSettings.copy(captureMode = captureMode)
+ }
+
+ override suspend fun updateAspectRatio(aspectRatio: AspectRatio) {
+ TODO("Not yet implemented")
+ }
+}
diff --git a/data/settings/src/main/proto/com/google/jetpackcamera/settings/aspect_ratio.proto b/data/settings/src/main/proto/com/google/jetpackcamera/settings/aspect_ratio.proto
new file mode 100644
index 0000000..05b7093
--- /dev/null
+++ b/data/settings/src/main/proto/com/google/jetpackcamera/settings/aspect_ratio.proto
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+
+option java_package = "com.google.jetpackcamera.settings";
+option java_multiple_files = true;
+
+enum AspectRatio {
+ ASPECT_RATIO_UNDEFINED = 0;
+ ASPECT_RATIO_THREE_FOUR = 1;
+ ASPECT_RATIO_NINE_SIXTEEN= 2;
+ ASPECT_RATIO_ONE_ONE = 3;
+} \ No newline at end of file
diff --git a/data/settings/src/main/proto/com/google/jetpackcamera/settings/capture_mode.proto b/data/settings/src/main/proto/com/google/jetpackcamera/settings/capture_mode.proto
new file mode 100644
index 0000000..f74868f
--- /dev/null
+++ b/data/settings/src/main/proto/com/google/jetpackcamera/settings/capture_mode.proto
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+
+option java_package = "com.google.jetpackcamera.settings";
+option java_multiple_files = true;
+
+enum CaptureMode {
+ CAPTURE_MODE_UNDEFINED = 0;
+ CAPTURE_MODE_MULTI_STREAM = 1;
+ CAPTURE_MODE_SINGLE_STREAM = 2;
+} \ No newline at end of file
diff --git a/data/settings/src/main/proto/com/google/jetpackcamera/settings/dark_mode.proto b/data/settings/src/main/proto/com/google/jetpackcamera/settings/dark_mode.proto
new file mode 100644
index 0000000..0df6d67
--- /dev/null
+++ b/data/settings/src/main/proto/com/google/jetpackcamera/settings/dark_mode.proto
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+
+option java_package = "com.google.jetpackcamera.settings";
+option java_multiple_files = true;
+
+enum DarkMode {
+ DARK_MODE_SYSTEM = 0;
+ DARK_MODE_LIGHT= 1;
+ DARK_MODE_DARK = 2;
+} \ No newline at end of file
diff --git a/data/settings/src/main/proto/com/google/jetpackcamera/settings/flash_mode.proto b/data/settings/src/main/proto/com/google/jetpackcamera/settings/flash_mode.proto
new file mode 100644
index 0000000..8096b2b
--- /dev/null
+++ b/data/settings/src/main/proto/com/google/jetpackcamera/settings/flash_mode.proto
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+
+option java_package = "com.google.jetpackcamera.settings";
+option java_multiple_files = true;
+
+enum FlashMode{
+ FLASH_MODE_AUTO = 0;
+ FLASH_MODE_ON = 1;
+ FLASH_MODE_OFF = 2;
+} \ No newline at end of file
diff --git a/data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto b/data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto
new file mode 100644
index 0000000..7e27529
--- /dev/null
+++ b/data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+
+import "com/google/jetpackcamera/settings/aspect_ratio.proto";
+import "com/google/jetpackcamera/settings/capture_mode.proto";
+import "com/google/jetpackcamera/settings/dark_mode.proto";
+import "com/google/jetpackcamera/settings/flash_mode.proto";
+
+
+option java_package = "com.google.jetpackcamera.settings";
+option java_multiple_files = true;
+
+message JcaSettings {
+ bool default_front_camera = 2;
+ bool front_camera_available = 3;
+ bool back_camera_available = 4;
+ DarkMode dark_mode_status = 5;
+ FlashMode flash_mode_status = 6;
+ AspectRatio aspect_ratio_status = 7;
+ CaptureMode capture_mode_status = 8;
+} \ No newline at end of file
diff --git a/data/settings/src/test/java/com/google/jetpackcamera/settings/ExampleUnitTest.kt b/data/settings/src/test/java/com/google/jetpackcamera/settings/ExampleUnitTest.kt
new file mode 100644
index 0000000..ec4b827
--- /dev/null
+++ b/data/settings/src/test/java/com/google/jetpackcamera/settings/ExampleUnitTest.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 0000000..87917f8
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,33 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project.
+
+## Before you begin
+
+### Sign our Contributor License Agreement
+
+Contributions to this project must be accompanied by a
+[Contributor License Agreement](https://cla.developers.google.com/about) (CLA).
+You (or your employer) retain the copyright to your contribution; this simply
+gives us permission to use and redistribute your contributions as part of the
+project.
+
+If you or your current employer have already signed the Google CLA (even if it
+was for a different project), you probably don't need to do it again.
+
+Visit <https://cla.developers.google.com/> to see your current agreements or to
+sign a new one.
+
+### Review our Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google/conduct/).
+
+## Contribution process
+
+### Code Reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
diff --git a/docs/code-of-conduct.md b/docs/code-of-conduct.md
new file mode 100644
index 0000000..dc079b4
--- /dev/null
+++ b/docs/code-of-conduct.md
@@ -0,0 +1,93 @@
+# Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of
+experience, education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, or to ban temporarily or permanently any
+contributor for other behaviors that they deem inappropriate, threatening,
+offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+This Code of Conduct also applies outside the project spaces when the Project
+Steward has a reasonable belief that an individual's behavior may have a
+negative impact on the project or its community.
+
+## Conflict Resolution
+
+We do not believe that all conflict is bad; healthy debate and disagreement
+often yield positive results. However, it is never okay to be disrespectful or
+to engage in behavior that violates the project’s code of conduct.
+
+If you see someone violating the code of conduct, you are encouraged to address
+the behavior directly with those involved. Many issues can be resolved quickly
+and easily, and this gives people more control over the outcome of their
+dispute. If you are unable to resolve the matter for any reason, or if the
+behavior is threatening or harassing, report it. We are dedicated to providing
+an environment where participants feel welcome and safe.
+
+Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the
+Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to
+receive and address reported violations of the code of conduct. They will then
+work with a committee consisting of representatives from the Open Source
+Programs Office and the Google Open Source Strategy team. If for any reason you
+are uncomfortable reaching out to the Project Steward, please email
+opensource@google.com.
+
+We will investigate every complaint, but you may not receive a direct response.
+We will use our discretion in determining when and how to follow up on reported
+incidents, which may range from not taking action to permanent expulsion from
+the project and project-sponsored spaces. We will notify the accused of the
+report and provide them an opportunity to discuss it before any action is taken.
+The identity of the reporter will be omitted from the details of the report
+supplied to the accused. In potentially harmful situations, such as ongoing
+harassment or threats to anyone's safety, we may take action without notice.
+
+## Attribution
+
+This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
+available at
+https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
diff --git a/domain/camera/.gitignore b/domain/camera/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/domain/camera/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/domain/camera/Android.bp b/domain/camera/Android.bp
new file mode 100644
index 0000000..b22d7e4
--- /dev/null
+++ b/domain/camera/Android.bp
@@ -0,0 +1,25 @@
+package {
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+android_library {
+ name: "jetpack-camera-app_domain_camera",
+ srcs: ["src/main/**/*.kt"],
+ static_libs: [
+ "androidx.concurrent_concurrent-futures-ktx",
+ "hilt_android",
+ "androidx.camera_camera-core",
+ "androidx.camera_camera-viewfinder",
+ "androidx.camera_camera-camera2",
+ "androidx.camera_camera-lifecycle",
+ "androidx.camera_camera-extensions",
+ "jetpack-camera-app_data_settings",
+ "jetpack-camera-app_core_common",
+ ],
+ sdk_version: "34",
+ min_sdk_version: "21",
+ manifest:"src/main/AndroidManifest.xml"
+}
+
diff --git a/domain/camera/build.gradle.kts b/domain/camera/build.gradle.kts
new file mode 100644
index 0000000..301dab4
--- /dev/null
+++ b/domain/camera/build.gradle.kts
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-kapt")
+ id("com.google.dagger.hilt.android")
+}
+
+android {
+ namespace = "com.google.jetpackcamera.data.camera"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 21
+ targetSdk = 34
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ jvmToolchain(17)
+ }
+}
+
+dependencies {
+ // Testing
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.3")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
+
+ // Futures
+ implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
+
+ // CameraX
+ val camerax_version = "1.4.0-SNAPSHOT"
+ implementation("androidx.camera:camera-core:${camerax_version}")
+ implementation("androidx.camera:camera-camera2:${camerax_version}")
+ implementation("androidx.camera:camera-lifecycle:${camerax_version}")
+ implementation("androidx.camera:camera-video:${camerax_version}")
+
+ implementation("androidx.camera:camera-view:${camerax_version}")
+ implementation("androidx.camera:camera-extensions:${camerax_version}")
+
+ // Hilt
+ implementation("com.google.dagger:hilt-android:2.44")
+ kapt("com.google.dagger:hilt-compiler:2.44")
+
+ // Project dependencies
+ implementation(project(":data:settings"))
+ implementation(project(":core:common"))
+}
+
+// Allow references to generated code
+kapt {
+ correctErrorTypes = true
+}
diff --git a/domain/camera/consumer-rules.pro b/domain/camera/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/domain/camera/consumer-rules.pro
diff --git a/domain/camera/proguard-rules.pro b/domain/camera/proguard-rules.pro
new file mode 100644
index 0000000..ff59496
--- /dev/null
+++ b/domain/camera/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.kts.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/domain/camera/src/main/AndroidManifest.xml b/domain/camera/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2aa7dee
--- /dev/null
+++ b/domain/camera/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.jetpackcamera.domain.camera">
+
+</manifest>
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraModule.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraModule.kt
new file mode 100644
index 0000000..612f520
--- /dev/null
+++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraModule.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.domain.camera
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+/**
+ * Dagger [Module] for camera data layer.
+ */
+@Module
+@InstallIn(SingletonComponent::class)
+interface CameraModule {
+ @Binds
+ fun bindsCameraUseCase(cameraXCameraUseCase: CameraXCameraUseCase): CameraUseCase
+}
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraUseCase.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraUseCase.kt
new file mode 100644
index 0000000..56a3768
--- /dev/null
+++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraUseCase.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.domain.camera
+
+import android.util.Rational
+import android.view.Display
+import androidx.camera.core.Preview
+import com.google.jetpackcamera.settings.model.AspectRatio as SettingsAspectRatio
+import com.google.jetpackcamera.settings.model.CameraAppSettings
+import com.google.jetpackcamera.settings.model.CaptureMode as SettingsCaptureMode
+import com.google.jetpackcamera.settings.model.FlashMode as SettingsFlashMode
+
+/**
+ * Data layer for camera.
+ */
+interface CameraUseCase {
+ /**
+ * Initializes the camera.
+ *
+ * @return list of available lenses.
+ */
+ suspend fun initialize(currentCameraSettings: CameraAppSettings): List<Int>
+
+ /**
+ * Starts the camera with lensFacing with the provided [Preview.SurfaceProvider].
+ *
+ * The camera will run until the calling coroutine is cancelled.
+ */
+ suspend fun runCamera(
+ surfaceProvider: Preview.SurfaceProvider,
+ currentCameraSettings: CameraAppSettings
+ )
+
+ suspend fun takePicture()
+
+ suspend fun startVideoRecording()
+
+ fun stopVideoRecording()
+
+ fun setZoomScale(scale: Float): Float
+
+ fun setFlashMode(flashMode: SettingsFlashMode)
+
+ suspend fun setAspectRatio(aspectRatio: SettingsAspectRatio, isFrontFacing: Boolean)
+
+ suspend fun flipCamera(isFrontFacing: Boolean)
+
+ fun tapToFocus(display: Display, surfaceWidth: Int, surfaceHeight: Int, x: Float, y: Float)
+
+ suspend fun setCaptureMode(captureMode: SettingsCaptureMode)
+
+ companion object {
+ const val INVALID_ZOOM_SCALE = -1f
+ }
+
+ /**
+ * Data class holding information used for configuring [CameraUseCase].
+ */
+ data class Config(
+ val lensFacing: LensFacing = LensFacing.FRONT,
+ val captureMode: CaptureMode = CaptureMode.SINGLE_STREAM,
+ val aspectRatio: AspectRatio = AspectRatio.ASPECT_RATIO_4_3,
+ val flashMode: FlashMode = FlashMode.OFF
+ )
+
+ /**
+ * Represents the lens used by [CameraUseCase].
+ */
+ enum class LensFacing {
+ FRONT,
+ BACK
+ }
+
+ /**
+ * Represents the capture mode used by [CameraUseCase].
+ */
+ enum class CaptureMode {
+ MULTI_STREAM,
+ SINGLE_STREAM
+ }
+
+ /**
+ * Represents the aspect ratio used by [CameraUseCase].
+ */
+ enum class AspectRatio(val rational: Rational) {
+ ASPECT_RATIO_4_3(Rational(4, 3)),
+ ASPECT_RATIO_16_9(Rational(16, 9)),
+ ASPECT_RATIO_1_1(Rational(1, 1))
+ }
+
+ /**
+ * Represents the flash mode used by [CameraUseCase].
+ */
+ enum class FlashMode {
+ OFF,
+ ON,
+ AUTO
+ }
+}
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt
new file mode 100644
index 0000000..e04bdd9
--- /dev/null
+++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.domain.camera
+
+import android.app.Application
+import android.content.ContentValues
+import android.provider.MediaStore
+import android.util.Log
+import android.view.Display
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraSelector.LensFacing
+import androidx.camera.core.DisplayOrientedMeteringPointFactory
+import androidx.camera.core.FocusMeteringAction
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCaseGroup
+import androidx.camera.core.ViewPort
+import androidx.camera.core.ZoomState
+import androidx.camera.lifecycle.ProcessCameraProvider
+//import androidx.camera.video.MediaStoreOutputOptions
+//import androidx.camera.video.Recorder
+//import androidx.camera.video.Recording
+//import androidx.camera.video.VideoCapture
+import androidx.concurrent.futures.await
+import androidx.core.content.ContextCompat
+import com.google.jetpackcamera.domain.camera.CameraUseCase.Companion.INVALID_ZOOM_SCALE
+import com.google.jetpackcamera.settings.SettingsRepository
+import com.google.jetpackcamera.settings.model.AspectRatio
+import com.google.jetpackcamera.settings.model.CameraAppSettings
+import com.google.jetpackcamera.settings.model.CaptureMode
+import com.google.jetpackcamera.settings.model.FlashMode
+import java.util.Date
+import javax.inject.Inject
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+
+private const val TAG = "CameraXCameraUseCase"
+
+/**
+ * CameraX based implementation for [CameraUseCase]
+ */
+class CameraXCameraUseCase
+@Inject
+constructor(
+ private val application: Application,
+ private val defaultDispatcher: CoroutineDispatcher,
+ private val settingsRepository: SettingsRepository
+) : CameraUseCase {
+ private var camera: Camera? = null
+ private lateinit var cameraProvider: ProcessCameraProvider
+
+ // TODO apply flash from settings
+ private val imageCaptureUseCase = ImageCapture.Builder().build()
+
+// private val recorder = Recorder.Builder().setExecutor(
+// defaultDispatcher.asExecutor()
+// ).build()
+// private val videoCaptureUseCase = VideoCapture.withOutput(recorder)
+// private var recording: Recording? = null
+
+ private lateinit var previewUseCase: Preview
+ private lateinit var useCaseGroup: UseCaseGroup
+
+ private lateinit var aspectRatio: AspectRatio
+ private lateinit var captureMode: CaptureMode
+ private lateinit var surfaceProvider: Preview.SurfaceProvider
+ private var isFrontFacing = true
+
+ override suspend fun initialize(currentCameraSettings: CameraAppSettings): List<Int> {
+ this.aspectRatio = currentCameraSettings.aspectRatio
+ this.captureMode = currentCameraSettings.captureMode
+ setFlashMode(currentCameraSettings.flashMode)
+
+ cameraProvider = ProcessCameraProvider.getInstance(application).await()
+ updateUseCaseGroup()
+
+ val availableCameraLens =
+ listOf(
+ CameraSelector.LENS_FACING_BACK,
+ CameraSelector.LENS_FACING_FRONT
+ ).filter { lensFacing ->
+ cameraProvider.hasCamera(cameraLensToSelector(lensFacing))
+ }
+
+ // updates values for available camera lens if necessary
+ coroutineScope {
+ settingsRepository.updateAvailableCameraLens(
+ availableCameraLens.contains(CameraSelector.LENS_FACING_FRONT),
+ availableCameraLens.contains(CameraSelector.LENS_FACING_BACK)
+ )
+ }
+
+ return availableCameraLens
+ }
+
+ override suspend fun runCamera(
+ surfaceProvider: Preview.SurfaceProvider,
+ currentCameraSettings: CameraAppSettings
+ ) = coroutineScope {
+ Log.d(TAG, "startPreview")
+
+ val cameraSelector =
+ cameraLensToSelector(getLensFacing(currentCameraSettings.isFrontCameraFacing))
+
+ previewUseCase.setSurfaceProvider(surfaceProvider)
+ this@CameraXCameraUseCase.surfaceProvider = surfaceProvider
+
+ cameraProvider.runWith(cameraSelector, useCaseGroup) {
+ camera = it
+ awaitCancellation()
+ }
+ }
+
+ override suspend fun takePicture() {
+ val imageDeferred = CompletableDeferred<ImageProxy>()
+
+ imageCaptureUseCase.takePicture(
+ defaultDispatcher.asExecutor(),
+ object : ImageCapture.OnImageCapturedCallback() {
+ override fun onCaptureSuccess(imageProxy: ImageProxy) {
+ Log.d(TAG, "onCaptureSuccess")
+ imageDeferred.complete(imageProxy)
+ }
+
+ override fun onError(exception: ImageCaptureException) {
+ super.onError(exception)
+ Log.d(TAG, "takePicture onError: $exception")
+ }
+ }
+ )
+ }
+
+ override suspend fun startVideoRecording() {
+ Log.d(TAG, "recordVideo")
+ val captureTypeString =
+ when (captureMode) {
+ CaptureMode.MULTI_STREAM -> "MultiStream"
+ CaptureMode.SINGLE_STREAM -> "SingleStream"
+ }
+ val name = "JCA-recording-${Date()}-$captureTypeString.mp4"
+ val contentValues =
+ ContentValues().apply {
+ put(MediaStore.Video.Media.DISPLAY_NAME, name)
+ }
+// val mediaStoreOutput =
+// MediaStoreOutputOptions.Builder(
+// application.contentResolver,
+// MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+// )
+// .setContentValues(contentValues)
+// .build()
+
+// recording =
+// videoCaptureUseCase.output
+// .prepareRecording(application, mediaStoreOutput)
+// .start(ContextCompat.getMainExecutor(application)) { videoRecordEvent ->
+// run {
+// Log.d(TAG, videoRecordEvent.toString())
+// }
+// }
+ }
+
+ override fun stopVideoRecording() {
+ Log.d(TAG, "stopRecording")
+// recording?.stop()
+ }
+
+ override fun setZoomScale(scale: Float): Float {
+ val zoomState = getZoomState() ?: return INVALID_ZOOM_SCALE
+ val finalScale =
+ (zoomState.zoomRatio * scale).coerceIn(
+ zoomState.minZoomRatio,
+ zoomState.maxZoomRatio
+ )
+ camera?.cameraControl?.setZoomRatio(finalScale)
+ return finalScale
+ }
+
+ private fun getZoomState(): ZoomState? = camera?.cameraInfo?.zoomState?.value
+
+ // flips the camera to the designated lensFacing direction
+ override suspend fun flipCamera(isFrontFacing: Boolean) {
+ this.isFrontFacing = isFrontFacing
+ updateUseCaseGroup()
+ rebindUseCases()
+ }
+
+ override fun tapToFocus(
+ display: Display,
+ surfaceWidth: Int,
+ surfaceHeight: Int,
+ x: Float,
+ y: Float
+ ) {
+ if (camera != null) {
+ val meteringPoint =
+ DisplayOrientedMeteringPointFactory(
+ display,
+ camera!!.cameraInfo,
+ surfaceWidth.toFloat(),
+ surfaceHeight.toFloat()
+ )
+ .createPoint(x, y)
+
+ val action = FocusMeteringAction.Builder(meteringPoint).build()
+
+ camera!!.cameraControl.startFocusAndMetering(action)
+ Log.d(TAG, "Tap to focus on: $meteringPoint")
+ }
+ }
+
+ override fun setFlashMode(flashMode: FlashMode) {
+ imageCaptureUseCase.flashMode = when (flashMode) {
+ FlashMode.OFF -> ImageCapture.FLASH_MODE_OFF // 2
+ FlashMode.ON -> ImageCapture.FLASH_MODE_ON // 1
+ FlashMode.AUTO -> ImageCapture.FLASH_MODE_AUTO // 0
+ }
+ Log.d(TAG, "Set flash mode to: ${imageCaptureUseCase.flashMode}")
+ }
+
+ override suspend fun setAspectRatio(aspectRatio: AspectRatio, isFrontFacing: Boolean) {
+ this.aspectRatio = aspectRatio
+ updateUseCaseGroup()
+ rebindUseCases()
+ }
+
+ override suspend fun setCaptureMode(newCaptureMode: CaptureMode) {
+ captureMode = newCaptureMode
+ Log.d(
+ TAG,
+ "Changing CaptureMode: singleStreamCaptureEnabled:" +
+ (captureMode == CaptureMode.SINGLE_STREAM)
+ )
+ updateUseCaseGroup()
+ rebindUseCases()
+ }
+
+ private fun updateUseCaseGroup() {
+ previewUseCase = createPreviewUseCase()
+ if (this::surfaceProvider.isInitialized) {
+ previewUseCase.setSurfaceProvider(surfaceProvider)
+ }
+
+ val useCaseGroupBuilder =
+ UseCaseGroup.Builder()
+ .setViewPort(
+ ViewPort.Builder(aspectRatio.ratio, previewUseCase.targetRotation).build()
+ )
+ .addUseCase(previewUseCase)
+ .addUseCase(imageCaptureUseCase)
+// .addUseCase(videoCaptureUseCase)
+
+ if (captureMode == CaptureMode.SINGLE_STREAM) {
+ useCaseGroupBuilder.addEffect(SingleSurfaceForcingEffect())
+ }
+
+ useCaseGroup = useCaseGroupBuilder.build()
+ }
+
+ private fun createPreviewUseCase(): Preview {
+ val availableCameraInfo = cameraProvider.availableCameraInfos
+ val cameraSelector = if (isFrontFacing) {
+ CameraSelector.DEFAULT_FRONT_CAMERA
+ } else {
+ CameraSelector.DEFAULT_BACK_CAMERA
+ }
+ val isPreviewStabilizationSupported =
+ cameraSelector.filter(availableCameraInfo).firstOrNull()?.let {
+ Preview.getPreviewCapabilities(it).isStabilizationSupported
+ } ?: false
+
+ val previewUseCaseBuilder = Preview.Builder()
+ if (isPreviewStabilizationSupported) {
+ previewUseCaseBuilder.setPreviewStabilizationEnabled(true)
+ }
+ return previewUseCaseBuilder.build()
+ }
+
+ // converts LensFacing from datastore to @LensFacing Int value
+ private fun getLensFacing(isFrontFacing: Boolean): Int = when (isFrontFacing) {
+ true -> CameraSelector.LENS_FACING_FRONT
+ false -> CameraSelector.LENS_FACING_BACK
+ }
+
+ private suspend fun rebindUseCases() {
+ val cameraSelector =
+ cameraLensToSelector(
+ getLensFacing(isFrontFacing)
+ )
+ cameraProvider.unbindAll()
+ cameraProvider.runWith(cameraSelector, useCaseGroup) {
+ camera = it
+ awaitCancellation()
+ }
+ }
+
+ private fun cameraLensToSelector(@LensFacing lensFacing: Int): CameraSelector =
+ when (lensFacing) {
+ CameraSelector.LENS_FACING_FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA
+ CameraSelector.LENS_FACING_BACK -> CameraSelector.DEFAULT_BACK_CAMERA
+ else -> throw IllegalArgumentException("Invalid lens facing type: $lensFacing")
+ }
+}
+
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CoroutineCameraProvider.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CoroutineCameraProvider.kt
new file mode 100644
index 0000000..e55ae35
--- /dev/null
+++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CoroutineCameraProvider.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.domain.camera
+
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.UseCaseGroup
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+
+/**
+ * Runs a camera for the duration of a coroutine.
+ *
+ * The camera selected by [cameraSelector] will run with the provided [useCases] for the
+ * duration that [block] is active. This means that [block] should suspend until the camera
+ * should be closed.
+ */
+suspend fun <R> ProcessCameraProvider.runWith(
+ cameraSelector: CameraSelector,
+ useCases: UseCaseGroup,
+ block: suspend (Camera) -> R
+): R = coroutineScope {
+ val scopedLifecycle = CoroutineLifecycleOwner(coroutineContext)
+ block(this@runWith.bindToLifecycle(scopedLifecycle, cameraSelector, useCases))
+}
+
+/**
+ * A [LifecycleOwner] that follows the lifecycle of a coroutine.
+ *
+ * If the coroutine is active, the owned lifecycle will jump to a
+ * [Lifecycle.State.RESUMED] state. When the coroutine completes, the owned lifecycle will
+ * transition to a [Lifecycle.State.DESTROYED] state.
+ */
+private class CoroutineLifecycleOwner(coroutineContext: CoroutineContext) :
+ LifecycleOwner {
+ private val lifecycleRegistry: LifecycleRegistry =
+ LifecycleRegistry(this).apply {
+ currentState = Lifecycle.State.INITIALIZED
+ }
+
+ init {
+ if (coroutineContext[Job]?.isActive == true) {
+ lifecycleRegistry.currentState = Lifecycle.State.RESUMED
+ coroutineContext[Job]?.invokeOnCompletion {
+ lifecycleRegistry.apply {
+ currentState = Lifecycle.State.STARTED
+ currentState = Lifecycle.State.CREATED
+ currentState = Lifecycle.State.DESTROYED
+ }
+ }
+ } else {
+ lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+ }
+ }
+
+ override public val lifecycle: Lifecycle = lifecycleRegistry
+}
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/EmptySurfaceProcessor.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/EmptySurfaceProcessor.kt
new file mode 100644
index 0000000..9263f80
--- /dev/null
+++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/EmptySurfaceProcessor.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.domain.camera
+
+import android.annotation.SuppressLint
+import android.graphics.SurfaceTexture
+import android.os.Handler
+import android.os.HandlerThread
+import android.view.Surface
+import androidx.camera.core.DynamicRange
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceProcessor
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.newHandlerExecutor
+import androidx.camera.core.processing.OpenGlRenderer
+import androidx.camera.core.processing.ShaderProvider
+import java.util.concurrent.Executor
+
+private const val GL_THREAD_NAME = "EmptySurfaceProcessor"
+
+/**
+ * This is a [SurfaceProcessor] that passes on the same content from the input
+ * surface to the output surface. Used to make a copies of surfaces.
+ */
+@SuppressLint("RestrictedApi")
+class EmptySurfaceProcessor : SurfaceProcessor {
+ private val glThread: HandlerThread = HandlerThread(GL_THREAD_NAME)
+ private var glHandler: Handler
+ var glExecutor: Executor
+ private set
+
+ // Members below are only accessed on GL thread.
+ private val glRenderer: OpenGlRenderer = OpenGlRenderer()
+ private val outputSurfaces: MutableMap<SurfaceOutput, Surface> = mutableMapOf()
+ private val textureTransform: FloatArray = FloatArray(16)
+ private val surfaceTransform: FloatArray = FloatArray(16)
+ private var isReleased = false
+
+ init {
+ glThread.start()
+ glHandler = Handler(glThread.looper)
+ glExecutor = newHandlerExecutor(glHandler)
+ glExecutor.execute {
+ glRenderer.init(
+ DynamicRange.SDR,
+ ShaderProvider.DEFAULT
+ )
+ }
+ }
+
+ override fun onInputSurface(surfaceRequest: SurfaceRequest) {
+ checkGlThread()
+ if (isReleased) {
+ surfaceRequest.willNotProvideSurface()
+ return
+ }
+ val surfaceTexture = SurfaceTexture(glRenderer.textureName)
+ surfaceTexture.setDefaultBufferSize(
+ surfaceRequest.resolution.width,
+ surfaceRequest.resolution.height
+ )
+ val surface = Surface(surfaceTexture)
+ surfaceRequest.provideSurface(surface, glExecutor) {
+ surfaceTexture.setOnFrameAvailableListener(null)
+ surfaceTexture.release()
+ surface.release()
+ }
+ surfaceTexture.setOnFrameAvailableListener({
+ checkGlThread()
+ if (!isReleased) {
+ surfaceTexture.updateTexImage()
+ surfaceTexture.getTransformMatrix(textureTransform)
+ outputSurfaces.forEach { (surfaceOutput, surface) ->
+ run {
+ surfaceOutput.updateTransformMatrix(surfaceTransform, textureTransform)
+ glRenderer.render(surfaceTexture.timestamp, surfaceTransform, surface)
+ }
+ }
+ }
+ }, glHandler)
+ }
+
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+ checkGlThread()
+ if (isReleased) {
+ surfaceOutput.close()
+ return
+ }
+ val surface =
+ surfaceOutput.getSurface(glExecutor) {
+ surfaceOutput.close()
+ outputSurfaces.remove(surfaceOutput)?.let { removedSurface ->
+ glRenderer.unregisterOutputSurface(removedSurface)
+ }
+ }
+ glRenderer.registerOutputSurface(surface)
+ outputSurfaces[surfaceOutput] = surface
+ }
+
+ /**
+ * Releases associated resources.
+ *
+ * Closes output surfaces.
+ * Releases the [OpenGlRenderer].
+ * Quits the GL HandlerThread.
+ */
+ fun release() {
+ glExecutor.execute {
+ releaseInternal()
+ }
+ }
+
+ private fun releaseInternal() {
+ checkGlThread()
+ if (!isReleased) {
+ // Once release is called, we can stop sending frame to output surfaces.
+ for (surfaceOutput in outputSurfaces.keys) {
+ surfaceOutput.close()
+ }
+ outputSurfaces.clear()
+ glRenderer.release()
+ glThread.quitSafely()
+ isReleased = true
+ }
+ }
+
+ private fun checkGlThread() {
+ check(GL_THREAD_NAME == Thread.currentThread().name)
+ }
+}
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/SingleSurfaceForcingEffect.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/SingleSurfaceForcingEffect.kt
new file mode 100644
index 0000000..09c691d
--- /dev/null
+++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/SingleSurfaceForcingEffect.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.domain.camera
+
+import androidx.camera.core.CameraEffect
+
+private const val TARGETS =
+ CameraEffect.PREVIEW or CameraEffect.VIDEO_CAPTURE or CameraEffect.IMAGE_CAPTURE
+
+private val emptySurfaceProcessor = EmptySurfaceProcessor()
+
+/**
+ * [CameraEffect] that applies a no-op effect.
+ *
+ * Essentially copying the camera input to the targets,
+ * Preview, VideoCapture and ImageCapture.
+ *
+ * Used as a workaround to force the above 3 use cases to use a single camera stream.
+ */
+class SingleSurfaceForcingEffect : CameraEffect(
+ TARGETS,
+ emptySurfaceProcessor.glExecutor,
+ emptySurfaceProcessor,
+ {}
+) {
+ // TODO(b/304547401): Invoke this to release the processor properly
+ @SuppressWarnings("unused")
+ fun release() {
+ emptySurfaceProcessor.release()
+ }
+}
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/test/FakeCameraUseCase.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/test/FakeCameraUseCase.kt
new file mode 100644
index 0000000..b57bdb0
--- /dev/null
+++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/test/FakeCameraUseCase.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.domain.camera.test
+
+import android.view.Display
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.Preview
+import com.google.jetpackcamera.domain.camera.CameraUseCase
+import com.google.jetpackcamera.settings.model.AspectRatio
+import com.google.jetpackcamera.settings.model.CameraAppSettings
+import com.google.jetpackcamera.settings.model.CaptureMode
+import com.google.jetpackcamera.settings.model.FlashMode
+
+class FakeCameraUseCase : CameraUseCase {
+ private val availableLenses =
+ listOf(CameraSelector.LENS_FACING_FRONT, CameraSelector.LENS_FACING_BACK)
+ private var initialized = false
+ private var useCasesBinded = false
+
+ var previewStarted = false
+ var numPicturesTaken = 0
+
+ var recordingInProgress = false
+
+ var isLensFacingFront = false
+ private var flashMode = FlashMode.OFF
+ private var aspectRatio = AspectRatio.THREE_FOUR
+
+ override suspend fun initialize(currentCameraSettings: CameraAppSettings): List<Int> {
+ initialized = true
+ flashMode = currentCameraSettings.flashMode
+ isLensFacingFront = currentCameraSettings.isFrontCameraFacing
+ aspectRatio = currentCameraSettings.aspectRatio
+ return availableLenses
+ }
+
+ override suspend fun runCamera(
+ surfaceProvider: Preview.SurfaceProvider,
+ currentCameraSettings: CameraAppSettings
+ ) {
+ val lensFacing =
+ when (currentCameraSettings.isFrontCameraFacing) {
+ true -> CameraSelector.LENS_FACING_FRONT
+ false -> CameraSelector.LENS_FACING_BACK
+ }
+
+ if (!initialized) {
+ throw IllegalStateException("CameraProvider not initialized")
+ }
+ if (!availableLenses.contains(lensFacing)) {
+ throw IllegalStateException("Requested lens not available")
+ }
+ useCasesBinded = true
+ previewStarted = true
+ }
+
+ override suspend fun takePicture() {
+ if (!useCasesBinded) {
+ throw IllegalStateException("Usecases not binded")
+ }
+ numPicturesTaken += 1
+ }
+
+ override suspend fun startVideoRecording() {
+ recordingInProgress = true
+ }
+
+ override fun stopVideoRecording() {
+ recordingInProgress = false
+ }
+
+ override fun setZoomScale(scale: Float): Float {
+ return -1f
+ }
+
+ override fun setFlashMode(flashMode: FlashMode) {
+ this.flashMode = flashMode
+ }
+
+ override suspend fun setAspectRatio(aspectRatio: AspectRatio, isFrontFacing: Boolean) {
+ this.aspectRatio = aspectRatio
+ }
+
+ override suspend fun flipCamera(isFrontFacing: Boolean) {
+ isLensFacingFront = isFrontFacing
+ }
+
+ override fun tapToFocus(
+ display: Display,
+ surfaceWidth: Int,
+ surfaceHeight: Int,
+ x: Float,
+ y: Float
+ ) {
+ TODO("Not yet implemented")
+ }
+
+ override suspend fun setCaptureMode(captureMode: CaptureMode) {
+ TODO("Not yet implemented")
+ }
+}
diff --git a/feature/preview/.gitignore b/feature/preview/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/feature/preview/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/feature/preview/Android.bp b/feature/preview/Android.bp
new file mode 100644
index 0000000..ed5c86e
--- /dev/null
+++ b/feature/preview/Android.bp
@@ -0,0 +1,35 @@
+package {
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+android_library {
+ name: "jetpack-camera-app_feature_preview",
+ srcs: ["src/main/**/*.kt"],
+ resource_dirs: [
+ "src/main/res",
+ ],
+ static_libs: [
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.material3_material3",
+ "androidx.compose.ui_ui-tooling-preview",
+ "hilt_android",
+ "androidx.hilt_hilt-navigation-compose",
+ "androidx.compose.ui_ui-tooling",
+ "kotlinx_coroutines_guava",
+ "androidx.datastore_datastore",
+ "libprotobuf-java-lite",
+ "androidx.camera_camera-core",
+ "androidx.camera_camera-viewfinder",
+ "jetpack-camera-app_data_settings",
+ "jetpack-camera-app_domain_camera",
+ "jetpack-camera-app_camera-viewfinder-compose",
+ "jetpack-camera-app_feature_quicksettings",
+
+ ],
+ sdk_version: "34",
+ min_sdk_version: "21",
+ manifest:"src/main/AndroidManifest.xml"
+}
+
diff --git a/feature/preview/build.gradle.kts b/feature/preview/build.gradle.kts
new file mode 100644
index 0000000..121ac17
--- /dev/null
+++ b/feature/preview/build.gradle.kts
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-kapt")
+ id("com.google.dagger.hilt.android")
+}
+
+android {
+ namespace = "com.google.jetpackcamera.feature.preview"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 21
+ targetSdk = 34
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ jvmToolchain(17)
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.4.0"
+ }
+ testOptions {
+ unitTests {
+ isReturnDefaultValues = true
+ }
+ }
+}
+
+dependencies {
+ // Compose
+ val composeBom = platform("androidx.compose:compose-bom:2023.08.00")
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+
+ // Compose - Material Design 3
+ implementation("androidx.compose.material3:material3")
+
+ // Compose - Android Studio Preview support
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+
+ // Compose - Integration with ViewModels with Navigation and Hilt
+ implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
+
+ // Compose - Testing
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+
+ // Testing
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.3")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
+ testImplementation("org.mockito:mockito-core:5.2.0")
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6")
+
+ // Guava
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.4.1")
+
+ // CameraX
+ val camerax_version = "1.4.0-SNAPSHOT"
+ implementation("androidx.camera:camera-core:${camerax_version}")
+ implementation("androidx.camera:camera-view:${camerax_version}")
+
+ // Hilt
+ implementation("com.google.dagger:hilt-android:2.44")
+ kapt("com.google.dagger:hilt-compiler:2.44")
+
+ // Project dependencies
+ implementation(project(":data:settings"))
+ implementation(project(":domain:camera"))
+ implementation(project(":camera-viewfinder-compose"))
+ implementation(project(":feature:quicksettings"))
+}
+
+// Allow references to generated code
+kapt {
+ correctErrorTypes = true
+} \ No newline at end of file
diff --git a/feature/preview/consumer-rules.pro b/feature/preview/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/feature/preview/consumer-rules.pro
diff --git a/feature/preview/proguard-rules.pro b/feature/preview/proguard-rules.pro
new file mode 100644
index 0000000..ff59496
--- /dev/null
+++ b/feature/preview/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.kts.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/feature/preview/src/main/AndroidManifest.xml b/feature/preview/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1fb3b89
--- /dev/null
+++ b/feature/preview/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest package="com.google.jetpackcamera.feature.preview">
+
+</manifest>
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/MultipleEventsCutter.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/MultipleEventsCutter.kt
new file mode 100644
index 0000000..f5ac412
--- /dev/null
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/MultipleEventsCutter.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.feature.preview
+
+/**
+ * A helper class that prevents multiple clicks.
+ */
+internal class MultipleEventsCutter {
+ private val now: Long
+ get() = System.currentTimeMillis()
+
+ private var lastEventTimeMs: Long = 0
+
+ fun processEvent(event: () -> Unit) {
+ if (now - lastEventTimeMs >= DURATION_BETWEEN_CLICKS_MS) {
+ event.invoke()
+ }
+ lastEventTimeMs = now
+ }
+
+ companion object {
+ private const val DURATION_BETWEEN_CLICKS_MS = 300L
+ }
+}
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt
new file mode 100644
index 0000000..f3366cf
--- /dev/null
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.feature.preview
+
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import androidx.camera.core.Preview.SurfaceProvider
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.google.jetpackcamera.feature.preview.ui.CaptureButton
+import com.google.jetpackcamera.feature.preview.ui.FlipCameraButton
+import com.google.jetpackcamera.feature.preview.ui.PreviewDisplay
+import com.google.jetpackcamera.feature.preview.ui.SettingsNavButton
+import com.google.jetpackcamera.feature.preview.ui.TestingButton
+import com.google.jetpackcamera.feature.preview.ui.ZoomScaleText
+import com.google.jetpackcamera.feature.quicksettings.QuickSettingsScreen
+import com.google.jetpackcamera.settings.model.CaptureMode
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.awaitCancellation
+
+private const val TAG = "PreviewScreen"
+private const val ZOOM_SCALE_SHOW_TIMEOUT_MS = 3000L
+
+/**
+ * Screen used for the Preview feature.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun PreviewScreen(
+ onPreviewViewModel: (PreviewViewModel) -> Unit,
+ onNavigateToSettings: () -> Unit,
+ viewModel: PreviewViewModel = hiltViewModel()
+) {
+ Log.d(TAG, "PreviewScreen")
+
+ val previewUiState: PreviewUiState by viewModel.previewUiState.collectAsState()
+
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ val deferredSurfaceProvider = remember { CompletableDeferred<SurfaceProvider>() }
+
+ val zoomScale by remember { mutableFloatStateOf(1f) }
+
+ var zoomScaleShow by remember { mutableStateOf(false) }
+
+ val zoomHandler = Handler(Looper.getMainLooper())
+
+ onPreviewViewModel(viewModel)
+
+ LaunchedEffect(lifecycleOwner) {
+ val surfaceProvider = deferredSurfaceProvider.await()
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.runCamera(surfaceProvider)
+ try {
+ awaitCancellation()
+ } finally {
+ viewModel.stopCamera()
+ }
+ }
+ }
+ if (previewUiState.cameraState == CameraState.NOT_READY) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Black),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ CircularProgressIndicator(modifier = Modifier.size(50.dp))
+ Text(text = stringResource(R.string.camera_not_ready), color = Color.White)
+ }
+ } else if (previewUiState.cameraState == CameraState.READY) {
+ // display camera feed. this stays behind everything else
+ PreviewDisplay(
+ onFlipCamera = viewModel::flipCamera,
+ onTapToFocus = viewModel::tapToFocus,
+ onZoomChange = { zoomChange: Float ->
+ viewModel.setZoomScale(zoomChange)
+ zoomScaleShow = true
+ zoomHandler.postDelayed({ zoomScaleShow = false }, ZOOM_SCALE_SHOW_TIMEOUT_MS)
+ },
+ aspectRatio = previewUiState.currentCameraSettings.aspectRatio,
+ deferredSurfaceProvider = deferredSurfaceProvider
+ )
+ // overlay
+ Box(
+ modifier = Modifier
+ .semantics {
+ testTagsAsResourceId = true
+ }
+ .fillMaxSize()
+ ) {
+ // hide settings, quickSettings, and quick capture mode button
+ when (previewUiState.videoRecordingState) {
+ VideoRecordingState.ACTIVE -> {}
+ VideoRecordingState.INACTIVE -> {
+ QuickSettingsScreen(
+ modifier = Modifier
+ .align(Alignment.TopCenter),
+ isOpen = previewUiState.quickSettingsIsOpen,
+ toggleIsOpen = { viewModel.toggleQuickSettings() },
+ currentCameraSettings = previewUiState.currentCameraSettings,
+ onLensFaceClick = viewModel::flipCamera,
+ onFlashModeClick = viewModel::setFlash,
+ onAspectRatioClick = {
+ viewModel.setAspectRatio(it)
+ }
+ // onTimerClick = {}/*TODO*/
+ )
+
+ SettingsNavButton(
+ modifier = Modifier
+ .align(Alignment.TopStart)
+ .padding(12.dp),
+ onNavigateToSettings = onNavigateToSettings
+ )
+
+ TestingButton(
+ modifier = Modifier
+ .testTag("ToggleCaptureMode")
+ .align(Alignment.TopEnd)
+ .padding(12.dp),
+ onClick = { viewModel.toggleCaptureMode() },
+ text = stringResource(
+ when (previewUiState.currentCameraSettings.captureMode) {
+ CaptureMode.SINGLE_STREAM -> R.string.capture_mode_single_stream
+ CaptureMode.MULTI_STREAM -> R.string.capture_mode_multi_stream
+ }
+ )
+ )
+ }
+ }
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.align(Alignment.BottomCenter)
+ ) {
+ if (zoomScaleShow) {
+ ZoomScaleText(zoomScale = zoomScale)
+ }
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .height(IntrinsicSize.Min)
+ ) {
+ when (previewUiState.videoRecordingState) {
+ VideoRecordingState.ACTIVE -> {
+ Spacer(
+ modifier = Modifier
+ .fillMaxHeight()
+ .weight(1f)
+ )
+ }
+
+ VideoRecordingState.INACTIVE -> {
+ FlipCameraButton(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxHeight(),
+ onClick = { viewModel.flipCamera() },
+ // enable only when phone has front and rear camera
+ enabledCondition =
+ previewUiState.currentCameraSettings.isBackCameraAvailable &&
+ previewUiState.currentCameraSettings.isFrontCameraAvailable
+ )
+ }
+ }
+ val multipleEventsCutter = remember { MultipleEventsCutter() }
+ /*todo: close quick settings on start record/image capture*/
+ CaptureButton(
+ onClick = {
+ multipleEventsCutter.processEvent { viewModel.captureImage() }
+ },
+ onLongPress = { viewModel.startVideoRecording() },
+ onRelease = { viewModel.stopVideoRecording() },
+ videoRecordingState = previewUiState.videoRecordingState
+ )
+ /* spacer is a placeholder to maintain the proportionate location of this row of
+ UI elements. if you want to add another element, replace it with ONE element.
+ If you want to add multiple components, use a container (Box, Row, Column, etc.)
+ */
+ Spacer(
+ modifier = Modifier
+ .fillMaxHeight()
+ .weight(1f)
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt
new file mode 100644
index 0000000..1c368b2
--- /dev/null
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.feature.preview
+
+import androidx.camera.core.CameraSelector
+import com.google.jetpackcamera.settings.model.CameraAppSettings
+
+/**
+ * Defines the current state of the [PreviewScreen].
+ */
+data class PreviewUiState(
+ val cameraState: CameraState = CameraState.NOT_READY,
+ // "quick" settings
+ val currentCameraSettings: CameraAppSettings,
+ val lensFacing: Int = CameraSelector.LENS_FACING_BACK,
+ val videoRecordingState: VideoRecordingState = VideoRecordingState.INACTIVE,
+ val quickSettingsIsOpen: Boolean = false
+)
+
+/**
+ * Defines the current state of Video Recording
+ */
+enum class VideoRecordingState {
+ /**
+ * Camera is not currently recording a video
+ */
+ INACTIVE,
+
+ /**
+ * Camera is currently recording a video
+ */
+ ACTIVE
+}
+
+/**
+ * Defines the current state of the camera.
+ */
+enum class CameraState {
+ /**
+ * Camera hasn't been initialized.
+ */
+ NOT_READY,
+
+ /**
+ * Camera is open and presenting a preview stream.
+ */
+ READY
+}
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt
new file mode 100644
index 0000000..e7b1544
--- /dev/null
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.feature.preview
+
+import android.util.Log
+import android.view.Display
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.Preview.SurfaceProvider
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.jetpackcamera.domain.camera.CameraUseCase
+import com.google.jetpackcamera.settings.SettingsRepository
+import com.google.jetpackcamera.settings.model.AspectRatio
+import com.google.jetpackcamera.settings.model.CaptureMode
+import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS
+import com.google.jetpackcamera.settings.model.FlashMode
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+private const val TAG = "PreviewViewModel"
+
+/**
+ * [ViewModel] for [PreviewScreen].
+ */
+@HiltViewModel
+class PreviewViewModel @Inject constructor(
+ private val cameraUseCase: CameraUseCase,
+ private val settingsRepository: SettingsRepository
+ // only reads from settingsRepository. do not push changes to repository from here
+) : ViewModel() {
+
+ private val _previewUiState: MutableStateFlow<PreviewUiState> =
+ MutableStateFlow(PreviewUiState(currentCameraSettings = DEFAULT_CAMERA_APP_SETTINGS))
+
+ val previewUiState: StateFlow<PreviewUiState> = _previewUiState
+ private var runningCameraJob: Job? = null
+
+ private var recordingJob: Job? = null
+
+ init {
+ viewModelScope.launch {
+ settingsRepository.cameraAppSettings.collect {
+ // TODO: only update settings that were actually changed
+ // currently resets all "quick" settings to stored settings
+ settings ->
+ _previewUiState
+ .emit(previewUiState.value.copy(currentCameraSettings = settings))
+ }
+ }
+ initializeCamera()
+ }
+
+ private fun initializeCamera() {
+ // TODO(yasith): Handle CameraUnavailableException
+ Log.d(TAG, "initializeCamera")
+ viewModelScope.launch {
+ cameraUseCase.initialize(previewUiState.value.currentCameraSettings)
+ _previewUiState.emit(
+ previewUiState.value.copy(
+ cameraState = CameraState.READY
+ )
+ )
+ }
+ }
+
+ fun runCamera(surfaceProvider: SurfaceProvider) {
+ Log.d(TAG, "runCamera")
+ stopCamera()
+ runningCameraJob = viewModelScope.launch {
+ // TODO(yasith): Handle Exceptions from binding use cases
+ cameraUseCase.runCamera(
+ surfaceProvider,
+ previewUiState.value.currentCameraSettings
+ )
+ }
+ }
+
+ fun stopCamera() {
+ Log.d(TAG, "stopCamera")
+ runningCameraJob?.apply {
+ if (isActive) {
+ cancel()
+ }
+ }
+ }
+
+ fun setFlash(flashMode: FlashMode) {
+ viewModelScope.launch {
+ _previewUiState.emit(
+ previewUiState.value.copy(
+ currentCameraSettings =
+ previewUiState.value.currentCameraSettings.copy(
+ flashMode = flashMode
+ )
+ )
+ )
+ // apply to cameraUseCase
+ cameraUseCase.setFlashMode(previewUiState.value.currentCameraSettings.flashMode)
+ }
+ }
+
+ fun setAspectRatio(aspectRatio: AspectRatio) {
+ stopCamera()
+ runningCameraJob = viewModelScope.launch {
+ _previewUiState.emit(
+ previewUiState.value.copy(
+ currentCameraSettings =
+ previewUiState.value.currentCameraSettings.copy(
+ aspectRatio = aspectRatio
+ )
+ )
+ )
+ cameraUseCase.setAspectRatio(
+ aspectRatio,
+ previewUiState.value
+ .currentCameraSettings.isFrontCameraFacing
+ )
+ }
+ }
+
+ // flips the camera opposite to its current direction
+ fun flipCamera() {
+ flipCamera(
+ !previewUiState.value
+ .currentCameraSettings.isFrontCameraFacing
+ )
+ }
+
+ fun toggleCaptureMode() {
+ val newCaptureMode = when (previewUiState.value.currentCameraSettings.captureMode) {
+ CaptureMode.MULTI_STREAM -> CaptureMode.SINGLE_STREAM
+ CaptureMode.SINGLE_STREAM -> CaptureMode.MULTI_STREAM
+ }
+
+ stopCamera()
+ runningCameraJob = viewModelScope.launch {
+ _previewUiState.emit(
+ previewUiState.value.copy(
+ currentCameraSettings =
+ previewUiState.value.currentCameraSettings.copy(
+ captureMode = newCaptureMode
+ )
+ )
+ )
+ // apply to cameraUseCase
+ cameraUseCase.setCaptureMode(newCaptureMode)
+ }
+ }
+
+ // sets the camera to a designated direction
+ fun flipCamera(isFacingFront: Boolean) {
+ // only flip if 2 directions are available
+ if (previewUiState.value.currentCameraSettings.isBackCameraAvailable &&
+ previewUiState.value.currentCameraSettings.isFrontCameraAvailable
+ ) {
+ stopCamera()
+ runningCameraJob = viewModelScope.launch {
+ _previewUiState.emit(
+ previewUiState.value.copy(
+ currentCameraSettings =
+ previewUiState.value.currentCameraSettings.copy(
+ isFrontCameraFacing = isFacingFront
+ )
+ )
+ )
+ // apply to cameraUseCase
+ cameraUseCase
+ .flipCamera(previewUiState.value.currentCameraSettings.isFrontCameraFacing)
+ }
+ }
+ }
+
+ fun captureImage() {
+ Log.d(TAG, "captureImage")
+ viewModelScope.launch {
+ try {
+ cameraUseCase.takePicture()
+ Log.d(TAG, "cameraUseCase.takePicture success")
+ } catch (exception: ImageCaptureException) {
+ Log.d(TAG, "cameraUseCase.takePicture error")
+ Log.d(TAG, exception.toString())
+ }
+ }
+ }
+
+ fun startVideoRecording() {
+ Log.d(TAG, "startVideoRecording")
+ recordingJob = viewModelScope.launch {
+ try {
+ cameraUseCase.startVideoRecording()
+ _previewUiState.emit(
+ previewUiState.value.copy(
+ videoRecordingState = VideoRecordingState.ACTIVE
+ )
+ )
+ Log.d(TAG, "cameraUseCase.startRecording success")
+ } catch (exception: IllegalStateException) {
+ Log.d(TAG, "cameraUseCase.startVideoRecording error")
+ Log.d(TAG, exception.toString())
+ }
+ }
+ }
+
+ fun stopVideoRecording() {
+ Log.d(TAG, "stopVideoRecording")
+ viewModelScope.launch {
+ _previewUiState.emit(
+ previewUiState.value.copy(
+ videoRecordingState = VideoRecordingState.INACTIVE
+ )
+ )
+ }
+ cameraUseCase.stopVideoRecording()
+ recordingJob?.cancel()
+ }
+
+ fun setZoomScale(scale: Float): Float {
+ return cameraUseCase.setZoomScale(scale = scale)
+ }
+
+ // modify ui values
+ fun toggleQuickSettings() {
+ toggleQuickSettings(!previewUiState.value.quickSettingsIsOpen)
+ }
+
+ private fun toggleQuickSettings(isOpen: Boolean) {
+ viewModelScope.launch {
+ _previewUiState.emit(
+ previewUiState.value.copy(
+ quickSettingsIsOpen = isOpen
+ )
+ )
+ }
+ }
+
+ fun tapToFocus(display: Display, surfaceWidth: Int, surfaceHeight: Int, x: Float, y: Float) {
+ cameraUseCase.tapToFocus(
+ display = display,
+ surfaceWidth = surfaceWidth,
+ surfaceHeight = surfaceHeight,
+ x = x,
+ y = y
+ )
+ }
+}
diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt
new file mode 100644
index 0000000..a952bcf
--- /dev/null
+++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.feature.preview.ui
+
+import android.util.Log
+import android.view.Display
+import android.view.View
+import androidx.camera.core.Preview
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.gestures.rememberTransformableState
+import androidx.compose.foundation.gestures.transformable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.SuggestionChip
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.google.jetpackcamera.feature.preview.R
+import com.google.jetpackcamera.feature.preview.VideoRecordingState
+import com.google.jetpackcamera.settings.model.AspectRatio
+import com.google.jetpackcamera.viewfinder.CameraPreview
+import kotlinx.coroutines.CompletableDeferred
+
+private const val TAG = "PreviewScreen"
+
+/** this is the preview surface display. This view implements gestures tap to focus, pinch to zoom,
+ * and double tap to flip camera */
+@Composable
+fun PreviewDisplay(
+ onTapToFocus: (Display, Int, Int, Float, Float) -> Unit,
+ onFlipCamera: () -> Unit,
+ onZoomChange: (Float) -> Unit,
+ aspectRatio: AspectRatio,
+ deferredSurfaceProvider: CompletableDeferred<Preview.SurfaceProvider>
+) {
+ val transformableState = rememberTransformableState(
+ onTransformation = { zoomChange, _, _ ->
+ onZoomChange(zoomChange)
+ }
+ )
+ val onSurfaceProviderReady: (Preview.SurfaceProvider) -> Unit = {
+ Log.d(TAG, "onSurfaceProviderReady")
+ deferredSurfaceProvider.complete(it)
+ }
+ lateinit var viewInfo: View
+
+ BoxWithConstraints(
+ Modifier
+ .fillMaxSize()
+ .background(Color.Black)
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onDoubleTap = { offset ->
+ // double tap to flip camera
+ Log.d(TAG, "onDoubleTap $offset")
+ onFlipCamera()
+ },
+ onTap = { offset ->
+ // tap to focus
+ try {
+ onTapToFocus(
+ viewInfo.display,
+ viewInfo.width,
+ viewInfo.height,
+ offset.x,
+ offset.y
+ )
+ Log.d(TAG, "onTap $offset")
+ } catch (e: UninitializedPropertyAccessException) {
+ Log.d(TAG, "onTap $offset")
+ e.printStackTrace()
+ }
+ }
+ )
+ },
+
+ contentAlignment = Alignment.Center
+ ) {
+ val maxAspectRatio: Float = maxWidth / maxHeight
+ val aspectRatioFloat: Float = aspectRatio.ratio.toFloat()
+ val shouldUseMaxWidth = maxAspectRatio <= aspectRatioFloat
+ val width = if (shouldUseMaxWidth) maxWidth else maxHeight * aspectRatioFloat
+ val height = if (!shouldUseMaxWidth) maxHeight else maxWidth / aspectRatioFloat
+ Box(
+ modifier = Modifier
+ .width(width)
+ .height(height)
+ .transformable(state = transformableState)
+
+ ) {
+ CameraPreview(
+ modifier = Modifier
+ .fillMaxSize(),
+ onSurfaceProviderReady = onSurfaceProviderReady,
+ onRequestBitmapReady = {
+ it.invoke()
+ },
+ setSurfaceView = { s: View ->
+ viewInfo = s
+ }
+ )
+ }
+ }
+}
+
+/**
+ * A temporary button that can be added to preview for quick testing purposes
+ */
+@Composable
+fun TestingButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: String) {
+ SuggestionChip(
+ onClick = { onClick() },
+ modifier = modifier,
+ label = {
+ Text(text = text)
+ }
+ )
+}
+
+@Composable
+fun FlipCameraButton(
+ modifier: Modifier = Modifier,
+ enabledCondition: Boolean,
+ onClick: () -> Unit
+) {
+ Box(modifier = modifier) {
+ IconButton(
+ modifier = Modifier
+ .align(Alignment.Center)
+ .size(40.dp),
+ onClick = onClick,
+ enabled = enabledCondition
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Refresh,
+ tint = Color.White,
+ contentDescription = stringResource(id = R.string.flip_camera_content_description),
+ modifier = Modifier.size(72.dp)
+ )
+ }
+ }
+}
+
+@Composable
+fun SettingsNavButton(modifier: Modifier, onNavigateToSettings: () -> Unit) {
+ IconButton(
+ modifier = modifier,
+ onClick = onNavigateToSettings
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Settings,
+ tint = Color.White,
+ contentDescription = stringResource(R.string.settings_content_description),
+ modifier = Modifier.size(72.dp)
+ )
+ }
+}
+
+@Composable
+fun ZoomScaleText(zoomScale: Float) {
+ val contentAlpha = animateFloatAsState(
+ targetValue = 10f,
+ label = "zoomScaleAlphaAnimation",
+ animationSpec = tween()
+ )
+ Text(
+ modifier = Modifier.alpha(contentAlpha.value),
+ text = "%.1fx".format(zoomScale),
+ fontSize = 20.sp,
+ color = Color.White
+ )
+}
+
+@Composable
+fun CaptureButton(
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit,
+ onLongPress: () -> Unit,
+ onRelease: () -> Unit,
+ videoRecordingState: VideoRecordingState
+) {
+ Box(
+ modifier = modifier
+ .fillMaxHeight()
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onLongPress = {
+ onLongPress()
+ },
+ onPress = {
+ awaitRelease()
+ onRelease()
+ },
+ onTap = { onClick() }
+ )
+ }
+ .size(120.dp)
+ .padding(18.dp)
+ .border(4.dp, Color.White, CircleShape)
+ ) {
+ Canvas(modifier = Modifier.size(110.dp), onDraw = {
+ drawCircle(
+ color =
+ when (videoRecordingState) {
+ VideoRecordingState.INACTIVE -> Color.Transparent
+ VideoRecordingState.ACTIVE -> Color.Red
+ }
+ )
+ })
+ }
+}
diff --git a/feature/preview/src/main/res/values/strings.xml b/feature/preview/src/main/res/values/strings.xml
new file mode 100644
index 0000000..4d11b0a
--- /dev/null
+++ b/feature/preview/src/main/res/values/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <string name="camera_not_ready">Camera Loading…</string>
+ <string name="settings_content_description">Settings</string>
+ <string name="capture_mode_single_stream">Single Stream</string>
+ <string name="capture_mode_multi_stream">Multi Stream</string>
+ <string name="flip_camera_content_description">Flip Camera</string>
+</resources> \ No newline at end of file
diff --git a/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt b/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt
new file mode 100644
index 0000000..70dc496
--- /dev/null
+++ b/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.feature.preview
+
+import androidx.camera.core.Preview.SurfaceProvider
+import com.google.jetpackcamera.domain.camera.test.FakeCameraUseCase
+import com.google.jetpackcamera.settings.model.FlashMode
+import com.google.jetpackcamera.settings.test.FakeSettingsRepository
+import junit.framework.TestCase.assertEquals
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.mock
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class PreviewViewModelTest {
+
+ private val cameraUseCase = FakeCameraUseCase()
+ private lateinit var previewViewModel: PreviewViewModel
+
+ @Before
+ fun setup() = runTest(StandardTestDispatcher()) {
+ Dispatchers.setMain(StandardTestDispatcher())
+ previewViewModel = PreviewViewModel(cameraUseCase, FakeSettingsRepository)
+ advanceUntilIdle()
+ }
+
+ @Test
+ fun getPreviewUiState() = runTest(StandardTestDispatcher()) {
+ advanceUntilIdle()
+ val uiState = previewViewModel.previewUiState.value
+ assertEquals(CameraState.READY, uiState.cameraState)
+ }
+
+ @Test
+ fun runCamera() = runTest(StandardTestDispatcher()) {
+ val surfaceProvider: SurfaceProvider = mock()
+ previewViewModel.runCamera(surfaceProvider)
+ advanceUntilIdle()
+
+ assertEquals(cameraUseCase.previewStarted, true)
+ }
+
+ @Test
+ fun captureImage() = runTest(StandardTestDispatcher()) {
+ val surfaceProvider: SurfaceProvider = mock()
+ previewViewModel.runCamera(surfaceProvider)
+ previewViewModel.captureImage()
+ advanceUntilIdle()
+ assertEquals(cameraUseCase.numPicturesTaken, 1)
+ }
+
+ @Test
+ fun startVideoRecording() = runTest(StandardTestDispatcher()) {
+ previewViewModel.runCamera(mock())
+ previewViewModel.startVideoRecording()
+ advanceUntilIdle()
+ assertEquals(cameraUseCase.recordingInProgress, true)
+ }
+
+ @Test
+ fun stopVideoRecording() = runTest(StandardTestDispatcher()) {
+ previewViewModel.runCamera(mock())
+ previewViewModel.startVideoRecording()
+ advanceUntilIdle()
+ previewViewModel.stopVideoRecording()
+ assertEquals(cameraUseCase.recordingInProgress, false)
+ }
+
+ @Test
+ fun setFlash() = runTest(StandardTestDispatcher()) {
+ previewViewModel.runCamera(mock())
+ previewViewModel.setFlash(FlashMode.AUTO)
+ advanceUntilIdle()
+ assertEquals(
+ previewViewModel.previewUiState.value.currentCameraSettings.flashMode,
+ FlashMode.AUTO
+ )
+ }
+
+ @Test
+ fun flipCamera() = runTest(StandardTestDispatcher()) {
+ // initial default value should be back
+ previewViewModel.runCamera(mock())
+ assertEquals(
+ previewViewModel.previewUiState.value.currentCameraSettings.isFrontCameraFacing,
+ false
+ )
+ previewViewModel.flipCamera()
+
+ advanceUntilIdle()
+ // ui state and camera should both be true now
+ assertEquals(
+ previewViewModel.previewUiState.value.currentCameraSettings.isFrontCameraFacing,
+ true
+ )
+ assertEquals(true, cameraUseCase.isLensFacingFront)
+ }
+}
diff --git a/feature/quicksettings/.gitignore b/feature/quicksettings/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/feature/quicksettings/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/feature/quicksettings/Android.bp b/feature/quicksettings/Android.bp
new file mode 100644
index 0000000..8b0e3b6
--- /dev/null
+++ b/feature/quicksettings/Android.bp
@@ -0,0 +1,28 @@
+package {
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+android_library {
+ name: "jetpack-camera-app_feature_quicksettings",
+ srcs: ["src/main/**/*.kt"],
+ static_libs: [
+ "androidx.compose.material3_material3",
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.ui_ui-tooling-preview",
+ "androidx.compose.ui_ui-tooling",
+ "androidx.camera_camera-core",
+ "androidx.camera_camera-viewfinder",
+ "kotlinx_coroutines_guava",
+ "jetpack-camera-app_data_settings",
+ ],
+ sdk_version: "34",
+ min_sdk_version: "21",
+ manifest:"src/main/AndroidManifest.xml",
+ resource_dirs: [
+ "src/main/res",
+ ],
+}
+
+
diff --git a/feature/quicksettings/build.gradle.kts b/feature/quicksettings/build.gradle.kts
new file mode 100644
index 0000000..a8da369
--- /dev/null
+++ b/feature/quicksettings/build.gradle.kts
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-kapt")
+}
+
+android {
+ namespace = "com.google.jetpackcamera.quicksettings"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 21
+ targetSdk = 34
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ jvmToolchain(17)
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.4.0"
+ }
+}
+
+dependencies {
+ // Compose
+ val composeBom = platform("androidx.compose:compose-bom:2023.08.00")
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+
+ // Compose - Material Design 3
+ implementation("androidx.compose.material3:material3")
+
+ // Compose - Android Studio Preview support
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+
+ // Compose - Testing
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+
+ // Testing
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.3")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
+
+ // Guava
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.4.1")
+
+ implementation(project(":data:settings"))
+}
+
+// Allow references to generated code
+kapt {
+ correctErrorTypes = true
+} \ No newline at end of file
diff --git a/feature/quicksettings/proguard-rules.pro b/feature/quicksettings/proguard-rules.pro
new file mode 100644
index 0000000..ff59496
--- /dev/null
+++ b/feature/quicksettings/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.kts.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/feature/quicksettings/src/main/AndroidManifest.xml b/feature/quicksettings/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..331bca7
--- /dev/null
+++ b/feature/quicksettings/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest package="com.google.jetpackcamera.quicksettings">
+
+</manifest>
diff --git a/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/QuickSettingsEnums.kt b/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/QuickSettingsEnums.kt
new file mode 100644
index 0000000..99a9315
--- /dev/null
+++ b/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/QuickSettingsEnums.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.feature.quicksettings
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import com.google.jetpackcamera.quicksettings.R
+
+interface QuickSettingsEnum {
+ @DrawableRes
+ fun getDrawableResId(): Int
+
+ @StringRes
+ fun getTextResId(): Int
+
+ @StringRes
+ fun getDescriptionResId(): Int
+}
+
+enum class CameraLensFace : QuickSettingsEnum {
+ FRONT {
+ override fun getDrawableResId(): Int = R.drawable.baseline_cameraswitch_72
+
+ override fun getTextResId(): Int = R.string.quick_settings_front_camera_text
+
+ override fun getDescriptionResId(): Int = R.string.quick_settings_front_camera_description
+ },
+ BACK {
+ override fun getDrawableResId(): Int = R.drawable.baseline_cameraswitch_72
+
+ override fun getTextResId(): Int = R.string.quick_settings_back_camera_text
+
+ override fun getDescriptionResId(): Int = R.string.quick_settings_back_camera_description
+ }
+}
+
+enum class CameraFlashMode : QuickSettingsEnum {
+ OFF {
+ override fun getDrawableResId(): Int = R.drawable.baseline_flash_off_72
+
+ override fun getTextResId(): Int = R.string.quick_settings_flash_off
+
+ override fun getDescriptionResId(): Int = R.string.quick_settings_flash_off_description
+ },
+ AUTO {
+ override fun getDrawableResId(): Int = R.drawable.baseline_flash_auto_72
+
+ override fun getTextResId(): Int = R.string.quick_settings_flash_auto
+
+ override fun getDescriptionResId(): Int = R.string.quick_settings_flash_auto_description
+ },
+ ON {
+ override fun getDrawableResId(): Int = R.drawable.baseline_flash_on_72
+
+ override fun getTextResId(): Int = R.string.quick_settings_flash_on
+
+ override fun getDescriptionResId(): Int = R.string.quick_settings_flash_on_description
+ }
+}
+
+enum class CameraAspectRatio : QuickSettingsEnum {
+ THREE_FOUR {
+ override fun getDrawableResId(): Int = R.drawable.baseline_aspect_ratio_72
+
+ override fun getTextResId(): Int = R.string.quick_settings_aspect_ratio_3_4
+
+ override fun getDescriptionResId(): Int =
+ R.string.quick_settings_aspect_ratio_3_4_description
+ },
+ NINE_SIXTEEN {
+ override fun getDrawableResId(): Int = R.drawable.baseline_aspect_ratio_72
+
+ override fun getTextResId(): Int = R.string.quick_settings_aspect_ratio_9_16
+
+ override fun getDescriptionResId(): Int =
+ R.string.quick_settings_aspect_ratio_9_16_description
+ },
+ ONE_ONE {
+ override fun getDrawableResId(): Int = R.drawable.baseline_aspect_ratio_72
+
+ override fun getTextResId(): Int = R.string.quick_settings_aspect_ratio_1_1
+
+ override fun getDescriptionResId(): Int =
+ R.string.quick_settings_aspect_ratio_1_1_description
+ }
+}
diff --git a/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/QuickSettingsScreen.kt b/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/QuickSettingsScreen.kt
new file mode 100644
index 0000000..b2b9f32
--- /dev/null
+++ b/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/QuickSettingsScreen.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.feature.quicksettings
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.dimensionResource
+import com.google.jetpackcamera.feature.quicksettings.ui.DropDownIcon
+import com.google.jetpackcamera.feature.quicksettings.ui.ExpandedQuickSetRatio
+import com.google.jetpackcamera.feature.quicksettings.ui.QuickFlipCamera
+import com.google.jetpackcamera.feature.quicksettings.ui.QuickSetFlash
+import com.google.jetpackcamera.feature.quicksettings.ui.QuickSetRatio
+import com.google.jetpackcamera.feature.quicksettings.ui.QuickSettingsGrid
+import com.google.jetpackcamera.quicksettings.R
+import com.google.jetpackcamera.settings.model.AspectRatio
+import com.google.jetpackcamera.settings.model.CameraAppSettings
+import com.google.jetpackcamera.settings.model.FlashMode
+
+/**
+ * The UI component for quick settings.
+ */
+@Composable
+fun QuickSettingsScreen(
+ modifier: Modifier = Modifier,
+ currentCameraSettings: CameraAppSettings,
+ isOpen: Boolean = false,
+ toggleIsOpen: () -> Unit,
+ onLensFaceClick: (lensFace: Boolean) -> Unit,
+ onFlashModeClick: (flashMode: FlashMode) -> Unit,
+ onAspectRatioClick: (aspectRation: AspectRatio) -> Unit
+) {
+ var shouldShowQuickSetting by remember {
+ mutableStateOf(IsExpandedQuickSetting.NONE)
+ }
+
+ val backgroundColor =
+ animateColorAsState(
+ targetValue = Color.Black.copy(alpha = if (isOpen) 0.7f else 0f),
+ label = "backgroundColorAnimation"
+ )
+
+ val contentAlpha =
+ animateFloatAsState(
+ targetValue = if (isOpen) 1f else 0f,
+ label = "contentAlphaAnimation",
+ animationSpec = tween()
+ )
+
+ if (isOpen) {
+ Column(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .background(color = backgroundColor.value)
+ .alpha(alpha = contentAlpha.value)
+ .clickable {
+ // if a setting is expanded, click on the background to close it.
+ // if no other settings are expanded, then close the popup
+ when (shouldShowQuickSetting) {
+ IsExpandedQuickSetting.NONE -> toggleIsOpen()
+ else -> shouldShowQuickSetting = IsExpandedQuickSetting.NONE
+ }
+ },
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ ExpandedQuickSettingsUi(
+ currentCameraSettings = currentCameraSettings,
+ shouldShowQuickSetting = shouldShowQuickSetting,
+ setVisibleQuickSetting = { enum: IsExpandedQuickSetting ->
+ shouldShowQuickSetting = enum
+ },
+ onLensFaceClick = onLensFaceClick,
+ onFlashModeClick = onFlashModeClick,
+ onAspectRatioClick = onAspectRatioClick
+ )
+ }
+ } else {
+ shouldShowQuickSetting = IsExpandedQuickSetting.NONE
+ }
+ DropDownIcon(
+ modifier = modifier,
+ toggleDropDown = toggleIsOpen,
+ isOpen = isOpen
+ )
+}
+
+// enum representing which individual quick setting is currently expanded
+private enum class IsExpandedQuickSetting {
+ NONE,
+ ASPECT_RATIO
+}
+
+/**
+ * The UI component for quick settings when it is expanded.
+ */
+@Composable
+private fun ExpandedQuickSettingsUi(
+ currentCameraSettings: CameraAppSettings,
+ onLensFaceClick: (lensFacingFront: Boolean) -> Unit,
+ onFlashModeClick: (flashMode: FlashMode) -> Unit,
+ shouldShowQuickSetting: IsExpandedQuickSetting,
+ setVisibleQuickSetting: (IsExpandedQuickSetting) -> Unit,
+ onAspectRatioClick: (aspectRation: AspectRatio) -> Unit
+) {
+ Column(
+ modifier =
+ Modifier
+ .padding(
+ horizontal = dimensionResource(
+ id = R.dimen.quick_settings_ui_horizontal_padding
+ )
+ )
+ ) {
+ // if no setting is chosen, display the grid of settings
+ // to change the order of display just move these lines of code above or below each other
+ when (shouldShowQuickSetting) {
+ IsExpandedQuickSetting.NONE -> {
+ val displayedQuickSettings: Array<@Composable () -> Unit> =
+ arrayOf(
+ {
+ QuickSetFlash(
+ modifier = Modifier.testTag("QuickSetFlash"),
+ onClick = { f: FlashMode -> onFlashModeClick(f) },
+ currentFlashMode = currentCameraSettings.flashMode
+ )
+ },
+ {
+ QuickFlipCamera(
+ modifier = Modifier.testTag("QuickSetFlipCamera"),
+ flipCamera = { b: Boolean -> onLensFaceClick(b) },
+ currentFacingFront = currentCameraSettings.isFrontCameraFacing
+ )
+ },
+ {
+ QuickSetRatio(
+ modifier = Modifier.testTag("QuickSetAspectRatio"),
+ onClick = {
+ setVisibleQuickSetting(
+ IsExpandedQuickSetting.ASPECT_RATIO
+ )
+ },
+ ratio = currentCameraSettings.aspectRatio,
+ currentRatio = currentCameraSettings.aspectRatio
+ )
+ }
+ )
+ QuickSettingsGrid(quickSettingsButtons = displayedQuickSettings)
+ }
+ // if a setting that can be expanded is selected, show it
+ IsExpandedQuickSetting.ASPECT_RATIO -> {
+ ExpandedQuickSetRatio(
+
+ setRatio = onAspectRatioClick,
+ currentRatio = currentCameraSettings.aspectRatio
+ )
+ }
+ }
+ }
+}
diff --git a/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/ui/QuickSettingsComponents.kt b/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/ui/QuickSettingsComponents.kt
new file mode 100644
index 0000000..420a130
--- /dev/null
+++ b/feature/quicksettings/src/main/java/com/google/jetpackcamera/feature/quicksettings/ui/QuickSettingsComponents.kt
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.feature.quicksettings.ui
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.google.jetpackcamera.feature.quicksettings.CameraAspectRatio
+import com.google.jetpackcamera.feature.quicksettings.CameraFlashMode
+import com.google.jetpackcamera.feature.quicksettings.CameraLensFace
+import com.google.jetpackcamera.feature.quicksettings.QuickSettingsEnum
+import com.google.jetpackcamera.quicksettings.R
+import com.google.jetpackcamera.settings.model.AspectRatio
+import com.google.jetpackcamera.settings.model.FlashMode
+import kotlin.math.min
+
+// completed components ready to go into preview screen
+
+@Composable
+fun ExpandedQuickSetRatio(setRatio: (aspectRatio: AspectRatio) -> Unit, currentRatio: AspectRatio) {
+ val buttons: Array<@Composable () -> Unit> =
+ arrayOf(
+ {
+ QuickSetRatio(
+ onClick = { setRatio(AspectRatio.THREE_FOUR) },
+ ratio = AspectRatio.THREE_FOUR,
+ currentRatio = currentRatio,
+ isHighlightEnabled = true
+ )
+ },
+ {
+ QuickSetRatio(
+ onClick = { setRatio(AspectRatio.NINE_SIXTEEN) },
+ ratio = AspectRatio.NINE_SIXTEEN,
+ currentRatio = currentRatio,
+ isHighlightEnabled = true
+ )
+ },
+ {
+ QuickSetRatio(
+ modifier = Modifier.testTag("QuickSetAspectRatio1:1"),
+ onClick = { setRatio(AspectRatio.ONE_ONE) },
+ ratio = AspectRatio.ONE_ONE,
+ currentRatio = currentRatio,
+ isHighlightEnabled = true
+ )
+ }
+ )
+ ExpandedQuickSetting(quickSettingButtons = buttons)
+}
+
+@Composable
+fun QuickSetRatio(
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit,
+ ratio: AspectRatio,
+ currentRatio: AspectRatio,
+ isHighlightEnabled: Boolean = false
+) {
+ val enum =
+ when (ratio) {
+ AspectRatio.THREE_FOUR -> CameraAspectRatio.THREE_FOUR
+ AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN
+ AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE
+ else -> CameraAspectRatio.ONE_ONE
+ }
+ QuickSettingUiItem(
+ modifier = modifier,
+ enum = enum,
+ onClick = { onClick() },
+ isHighLighted = isHighlightEnabled && (ratio == currentRatio)
+ )
+}
+
+@Composable
+fun QuickSetFlash(
+ modifier: Modifier = Modifier,
+ onClick: (FlashMode) -> Unit,
+ currentFlashMode: FlashMode
+) {
+ val enum = when (currentFlashMode) {
+ FlashMode.OFF -> CameraFlashMode.OFF
+ FlashMode.AUTO -> CameraFlashMode.AUTO
+ FlashMode.ON -> CameraFlashMode.ON
+ }
+ QuickSettingUiItem(
+ modifier = modifier,
+ enum = enum,
+ isHighLighted = currentFlashMode == FlashMode.ON,
+ onClick =
+ {
+ when (currentFlashMode) {
+ FlashMode.OFF -> onClick(FlashMode.ON)
+ FlashMode.ON -> onClick(FlashMode.AUTO)
+ FlashMode.AUTO -> onClick(FlashMode.OFF)
+ }
+ }
+ )
+}
+
+@Composable
+fun QuickFlipCamera(
+ modifier: Modifier = Modifier,
+ flipCamera: (Boolean) -> Unit,
+ currentFacingFront: Boolean
+) {
+ val enum =
+ when (currentFacingFront) {
+ true -> CameraLensFace.FRONT
+ false -> CameraLensFace.BACK
+ }
+ QuickSettingUiItem(
+ modifier = modifier,
+ enum = enum,
+ onClick = { flipCamera(!currentFacingFront) }
+ )
+}
+
+@Composable
+fun DropDownIcon(modifier: Modifier = Modifier, toggleDropDown: () -> Unit, isOpen: Boolean) {
+ Row(
+ modifier = modifier,
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // dropdown icon
+ Icon(
+ painter = painterResource(R.drawable.baseline_expand_more_72),
+ contentDescription = stringResource(R.string.quick_settings_dropdown_description),
+ tint = Color.White,
+ modifier =
+ Modifier
+ .testTag("QuickSettingDropDown")
+ .size(72.dp)
+ .clickable {
+ toggleDropDown()
+ }
+ .scale(1f, if (isOpen) -1f else 1f)
+ )
+ }
+}
+
+// subcomponents used to build completed components
+
+@Composable
+fun QuickSettingUiItem(
+ modifier: Modifier = Modifier,
+ enum: QuickSettingsEnum,
+ onClick: () -> Unit,
+ isHighLighted: Boolean = false
+) {
+ QuickSettingUiItem(
+ modifier = modifier,
+ drawableResId = enum.getDrawableResId(),
+ text = stringResource(id = enum.getTextResId()),
+ accessibilityText = stringResource(id = enum.getDescriptionResId()),
+ onClick = { onClick() },
+ isHighLighted = isHighLighted
+ )
+}
+
+/**
+ * The itemized UI component representing each button in quick settings.
+ */
+@Composable
+fun QuickSettingUiItem(
+ modifier: Modifier = Modifier,
+ @DrawableRes drawableResId: Int,
+ text: String,
+ accessibilityText: String,
+ onClick: () -> Unit,
+ isHighLighted: Boolean = false
+) {
+ Column(
+ modifier =
+ modifier
+ .wrapContentSize()
+ .padding(dimensionResource(id = R.dimen.quick_settings_ui_item_padding))
+ .clickable {
+ onClick()
+ },
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ val tint = if (isHighLighted) Color.Yellow else Color.White
+ Icon(
+ painter = painterResource(drawableResId),
+ contentDescription = accessibilityText,
+ tint = tint,
+ modifier =
+ Modifier
+ .size(dimensionResource(id = R.dimen.quick_settings_ui_item_icon_size))
+ )
+
+ Text(text = text, color = tint)
+ }
+}
+
+/**
+ * Should you want to have an expanded view of a single quick setting
+ */
+@Composable
+fun ExpandedQuickSetting(
+ modifier: Modifier = Modifier,
+ vararg quickSettingButtons: @Composable () -> Unit
+) {
+ val expandedNumOfColumns =
+ min(
+ quickSettingButtons.size,
+ (
+ (
+ LocalConfiguration.current.screenWidthDp.dp - (
+ dimensionResource(
+ id = R.dimen.quick_settings_ui_horizontal_padding
+ ) * 2
+ )
+ ) /
+ (
+ dimensionResource(id = R.dimen.quick_settings_ui_item_icon_size) +
+ (dimensionResource(id = R.dimen.quick_settings_ui_item_padding) * 2)
+ )
+ ).toInt()
+ )
+ LazyVerticalGrid(
+ modifier = modifier.fillMaxWidth(),
+ columns = GridCells.Fixed(count = expandedNumOfColumns)
+ ) {
+ items(quickSettingButtons.size) { i ->
+ quickSettingButtons[i]()
+ }
+ }
+}
+
+/**
+ * Algorithm to determine dimensions of QuickSettings Icon layout
+ */
+@Composable
+fun QuickSettingsGrid(
+ modifier: Modifier = Modifier,
+ vararg quickSettingsButtons: @Composable () -> Unit
+) {
+ val initialNumOfColumns =
+ min(
+ quickSettingsButtons.size,
+ (
+ (
+ LocalConfiguration.current.screenWidthDp.dp - (
+ dimensionResource(
+ id = R.dimen.quick_settings_ui_horizontal_padding
+ ) * 2
+ )
+ ) /
+ (
+ dimensionResource(id = R.dimen.quick_settings_ui_item_icon_size) +
+ (dimensionResource(id = R.dimen.quick_settings_ui_item_padding) * 2)
+ )
+ ).toInt()
+ )
+
+ LazyVerticalGrid(
+ modifier = modifier.fillMaxWidth(),
+ columns = GridCells.Fixed(count = initialNumOfColumns)
+ ) {
+ items(quickSettingsButtons.size) { i ->
+ quickSettingsButtons[i]()
+ }
+ }
+}
diff --git a/feature/quicksettings/src/main/res/drawable/baseline_aspect_ratio_72.xml b/feature/quicksettings/src/main/res/drawable/baseline_aspect_ratio_72.xml
new file mode 100644
index 0000000..abe3cd8
--- /dev/null
+++ b/feature/quicksettings/src/main/res/drawable/baseline_aspect_ratio_72.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector android:height="72dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="72dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M19,12h-2v3h-3v2h5v-5zM7,9h3L10,7L5,7v5h2L7,9zM21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.99h18v14.02z"/>
+</vector>
diff --git a/feature/quicksettings/src/main/res/drawable/baseline_cameraswitch_72.xml b/feature/quicksettings/src/main/res/drawable/baseline_cameraswitch_72.xml
new file mode 100644
index 0000000..f41a44f
--- /dev/null
+++ b/feature/quicksettings/src/main/res/drawable/baseline_cameraswitch_72.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector android:height="72dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="72dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M16,7h-1l-1,-1h-4L9,7H8C6.9,7 6,7.9 6,9v6c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V9C18,7.9 17.1,7 16,7zM12,14c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C14,13.1 13.1,14 12,14z"/>
+ <path android:fillColor="@android:color/white" android:pathData="M8.57,0.51l4.48,4.48V2.04c4.72,0.47 8.48,4.23 8.95,8.95c0,0 2,0 2,0C23.34,3.02 15.49,-1.59 8.57,0.51z"/>
+ <path android:fillColor="@android:color/white" android:pathData="M10.95,21.96C6.23,21.49 2.47,17.73 2,13.01c0,0 -2,0 -2,0c0.66,7.97 8.51,12.58 15.43,10.48l-4.48,-4.48V21.96z"/>
+</vector>
diff --git a/feature/quicksettings/src/main/res/drawable/baseline_expand_more_72.xml b/feature/quicksettings/src/main/res/drawable/baseline_expand_more_72.xml
new file mode 100644
index 0000000..6a0ff27
--- /dev/null
+++ b/feature/quicksettings/src/main/res/drawable/baseline_expand_more_72.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector android:height="72dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="72dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
+</vector>
diff --git a/feature/quicksettings/src/main/res/drawable/baseline_flash_auto_72.xml b/feature/quicksettings/src/main/res/drawable/baseline_flash_auto_72.xml
new file mode 100644
index 0000000..2a5b430
--- /dev/null
+++ b/feature/quicksettings/src/main/res/drawable/baseline_flash_auto_72.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector android:height="72dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="72dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M3,2v12h3v9l7,-12L9,11l4,-9L3,2zM19,2h-2l-3.2,9h1.9l0.7,-2h3.2l0.7,2h1.9L19,2zM16.85,7.65L18,4l1.15,3.65h-2.3z"/>
+</vector>
diff --git a/feature/quicksettings/src/main/res/drawable/baseline_flash_off_72.xml b/feature/quicksettings/src/main/res/drawable/baseline_flash_off_72.xml
new file mode 100644
index 0000000..bf4224d
--- /dev/null
+++ b/feature/quicksettings/src/main/res/drawable/baseline_flash_off_72.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector android:height="72dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="72dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M3.27,3L2,4.27l5,5V13h3v9l3.58,-6.14L17.73,20 19,18.73 3.27,3zM17,10h-4l4,-8H7v2.18l8.46,8.46L17,10z"/>
+</vector>
diff --git a/feature/quicksettings/src/main/res/drawable/baseline_flash_on_72.xml b/feature/quicksettings/src/main/res/drawable/baseline_flash_on_72.xml
new file mode 100644
index 0000000..4eaf409
--- /dev/null
+++ b/feature/quicksettings/src/main/res/drawable/baseline_flash_on_72.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector android:height="72dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="72dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M7,2v11h3v9l7,-12h-4l4,-8z"/>
+</vector>
diff --git a/feature/quicksettings/src/main/res/drawable/baseline_timer_10_72.xml b/feature/quicksettings/src/main/res/drawable/baseline_timer_10_72.xml
new file mode 100644
index 0000000..bcaecdf
--- /dev/null
+++ b/feature/quicksettings/src/main/res/drawable/baseline_timer_10_72.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector android:height="72dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="72dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M0,7.72L0,9.4l3,-1L3,18h2L5,6h-0.25L0,7.72zM23.78,14.37c-0.14,-0.28 -0.35,-0.53 -0.63,-0.74 -0.28,-0.21 -0.61,-0.39 -1.01,-0.53s-0.85,-0.27 -1.35,-0.38c-0.35,-0.07 -0.64,-0.15 -0.87,-0.23 -0.23,-0.08 -0.41,-0.16 -0.55,-0.25 -0.14,-0.09 -0.23,-0.19 -0.28,-0.3 -0.05,-0.11 -0.08,-0.24 -0.08,-0.39 0,-0.14 0.03,-0.28 0.09,-0.41 0.06,-0.13 0.15,-0.25 0.27,-0.34 0.12,-0.1 0.27,-0.18 0.45,-0.24s0.4,-0.09 0.64,-0.09c0.25,0 0.47,0.04 0.66,0.11 0.19,0.07 0.35,0.17 0.48,0.29 0.13,0.12 0.22,0.26 0.29,0.42 0.06,0.16 0.1,0.32 0.1,0.49h1.95c0,-0.39 -0.08,-0.75 -0.24,-1.09 -0.16,-0.34 -0.39,-0.63 -0.69,-0.88 -0.3,-0.25 -0.66,-0.44 -1.09,-0.59C21.49,9.07 21,9 20.46,9c-0.51,0 -0.98,0.07 -1.39,0.21 -0.41,0.14 -0.77,0.33 -1.06,0.57 -0.29,0.24 -0.51,0.52 -0.67,0.84 -0.16,0.32 -0.23,0.65 -0.23,1.01s0.08,0.69 0.23,0.96c0.15,0.28 0.36,0.52 0.64,0.73 0.27,0.21 0.6,0.38 0.98,0.53 0.38,0.14 0.81,0.26 1.27,0.36 0.39,0.08 0.71,0.17 0.95,0.26s0.43,0.19 0.57,0.29c0.13,0.1 0.22,0.22 0.27,0.34 0.05,0.12 0.07,0.25 0.07,0.39 0,0.32 -0.13,0.57 -0.4,0.77 -0.27,0.2 -0.66,0.29 -1.17,0.29 -0.22,0 -0.43,-0.02 -0.64,-0.08 -0.21,-0.05 -0.4,-0.13 -0.56,-0.24 -0.17,-0.11 -0.3,-0.26 -0.41,-0.44 -0.11,-0.18 -0.17,-0.41 -0.18,-0.67h-1.89c0,0.36 0.08,0.71 0.24,1.05 0.16,0.34 0.39,0.65 0.7,0.93 0.31,0.27 0.69,0.49 1.15,0.66 0.46,0.17 0.98,0.25 1.58,0.25 0.53,0 1.01,-0.06 1.44,-0.19 0.43,-0.13 0.8,-0.31 1.11,-0.54 0.31,-0.23 0.54,-0.51 0.71,-0.83 0.17,-0.32 0.25,-0.67 0.25,-1.06 -0.02,-0.4 -0.09,-0.74 -0.24,-1.02zM13.82,7.05c-0.34,-0.4 -0.75,-0.7 -1.23,-0.88 -0.47,-0.18 -1.01,-0.27 -1.59,-0.27 -0.58,0 -1.11,0.09 -1.59,0.27 -0.48,0.18 -0.89,0.47 -1.23,0.88 -0.34,0.41 -0.6,0.93 -0.79,1.59 -0.18,0.65 -0.28,1.45 -0.28,2.39v1.92c0,0.94 0.09,1.74 0.28,2.39 0.19,0.66 0.45,1.19 0.8,1.6 0.34,0.41 0.75,0.71 1.23,0.89 0.48,0.18 1.01,0.28 1.59,0.28 0.59,0 1.12,-0.09 1.59,-0.28 0.48,-0.18 0.88,-0.48 1.22,-0.89 0.34,-0.41 0.6,-0.94 0.78,-1.6 0.18,-0.65 0.28,-1.45 0.28,-2.39v-1.92c0,-0.94 -0.09,-1.74 -0.28,-2.39 -0.18,-0.66 -0.44,-1.19 -0.78,-1.59zM12.9,13.22c0,0.6 -0.04,1.11 -0.12,1.53 -0.08,0.42 -0.2,0.76 -0.36,1.02 -0.16,0.26 -0.36,0.45 -0.59,0.57 -0.23,0.12 -0.51,0.18 -0.82,0.18 -0.3,0 -0.58,-0.06 -0.82,-0.18s-0.44,-0.31 -0.6,-0.57c-0.16,-0.26 -0.29,-0.6 -0.38,-1.02 -0.09,-0.42 -0.13,-0.93 -0.13,-1.53v-2.5c0,-0.6 0.04,-1.11 0.13,-1.52 0.09,-0.41 0.21,-0.74 0.38,-1 0.16,-0.25 0.36,-0.43 0.6,-0.55 0.24,-0.11 0.51,-0.17 0.81,-0.17 0.31,0 0.58,0.06 0.81,0.17 0.24,0.11 0.44,0.29 0.6,0.55 0.16,0.25 0.29,0.58 0.37,0.99 0.08,0.41 0.13,0.92 0.13,1.52v2.51z"/>
+</vector>
diff --git a/feature/quicksettings/src/main/res/drawable/baseline_timer_3_72.xml b/feature/quicksettings/src/main/res/drawable/baseline_timer_3_72.xml
new file mode 100644
index 0000000..0dc5cf7
--- /dev/null
+++ b/feature/quicksettings/src/main/res/drawable/baseline_timer_3_72.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector android:height="72dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="72dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M11.61,12.97c-0.16,-0.24 -0.36,-0.46 -0.62,-0.65 -0.25,-0.19 -0.56,-0.35 -0.93,-0.48 0.3,-0.14 0.57,-0.3 0.8,-0.5 0.23,-0.2 0.42,-0.41 0.57,-0.64 0.15,-0.23 0.27,-0.46 0.34,-0.71 0.08,-0.24 0.11,-0.49 0.11,-0.73 0,-0.55 -0.09,-1.04 -0.28,-1.46 -0.18,-0.42 -0.44,-0.77 -0.78,-1.06 -0.33,-0.28 -0.73,-0.5 -1.2,-0.64 -0.45,-0.13 -0.97,-0.2 -1.53,-0.2 -0.55,0 -1.06,0.08 -1.52,0.24 -0.47,0.17 -0.87,0.4 -1.2,0.69 -0.33,0.29 -0.6,0.63 -0.78,1.03 -0.2,0.39 -0.29,0.83 -0.29,1.29h1.98c0,-0.26 0.05,-0.49 0.14,-0.69 0.09,-0.2 0.22,-0.38 0.38,-0.52 0.17,-0.14 0.36,-0.25 0.58,-0.33 0.22,-0.08 0.46,-0.12 0.73,-0.12 0.61,0 1.06,0.16 1.36,0.47 0.3,0.31 0.44,0.75 0.44,1.32 0,0.27 -0.04,0.52 -0.12,0.74 -0.08,0.22 -0.21,0.41 -0.38,0.57 -0.17,0.16 -0.38,0.28 -0.63,0.37 -0.25,0.09 -0.55,0.13 -0.89,0.13L6.72,11.09v1.57L7.9,12.66c0.34,0 0.64,0.04 0.91,0.11 0.27,0.08 0.5,0.19 0.69,0.35 0.19,0.16 0.34,0.36 0.44,0.61 0.1,0.24 0.16,0.54 0.16,0.87 0,0.62 -0.18,1.09 -0.53,1.42 -0.35,0.33 -0.84,0.49 -1.45,0.49 -0.29,0 -0.56,-0.04 -0.8,-0.13 -0.24,-0.08 -0.44,-0.2 -0.61,-0.36 -0.17,-0.16 -0.3,-0.34 -0.39,-0.56 -0.09,-0.22 -0.14,-0.46 -0.14,-0.72L4.19,14.74c0,0.55 0.11,1.03 0.32,1.45 0.21,0.42 0.5,0.77 0.86,1.05s0.77,0.49 1.24,0.63 0.96,0.21 1.48,0.21c0.57,0 1.09,-0.08 1.58,-0.23 0.49,-0.15 0.91,-0.38 1.26,-0.68 0.36,-0.3 0.64,-0.66 0.84,-1.1 0.2,-0.43 0.3,-0.93 0.3,-1.48 0,-0.29 -0.04,-0.58 -0.11,-0.86 -0.08,-0.25 -0.19,-0.51 -0.35,-0.76zM20.87,14.37c-0.14,-0.28 -0.35,-0.53 -0.63,-0.74 -0.28,-0.21 -0.61,-0.39 -1.01,-0.53s-0.85,-0.27 -1.35,-0.38c-0.35,-0.07 -0.64,-0.15 -0.87,-0.23 -0.23,-0.08 -0.41,-0.16 -0.55,-0.25 -0.14,-0.09 -0.23,-0.19 -0.28,-0.3 -0.05,-0.11 -0.08,-0.24 -0.08,-0.39s0.03,-0.28 0.09,-0.41c0.06,-0.13 0.15,-0.25 0.27,-0.34 0.12,-0.1 0.27,-0.18 0.45,-0.24s0.4,-0.09 0.64,-0.09c0.25,0 0.47,0.04 0.66,0.11 0.19,0.07 0.35,0.17 0.48,0.29 0.13,0.12 0.22,0.26 0.29,0.42 0.06,0.16 0.1,0.32 0.1,0.49h1.95c0,-0.39 -0.08,-0.75 -0.24,-1.09 -0.16,-0.34 -0.39,-0.63 -0.69,-0.88 -0.3,-0.25 -0.66,-0.44 -1.09,-0.59 -0.43,-0.15 -0.92,-0.22 -1.46,-0.22 -0.51,0 -0.98,0.07 -1.39,0.21 -0.41,0.14 -0.77,0.33 -1.06,0.57 -0.29,0.24 -0.51,0.52 -0.67,0.84 -0.16,0.32 -0.23,0.65 -0.23,1.01s0.08,0.68 0.23,0.96c0.15,0.28 0.37,0.52 0.64,0.73 0.27,0.21 0.6,0.38 0.98,0.53 0.38,0.14 0.81,0.26 1.27,0.36 0.39,0.08 0.71,0.17 0.95,0.26s0.43,0.19 0.57,0.29c0.13,0.1 0.22,0.22 0.27,0.34 0.05,0.12 0.07,0.25 0.07,0.39 0,0.32 -0.13,0.57 -0.4,0.77 -0.27,0.2 -0.66,0.29 -1.17,0.29 -0.22,0 -0.43,-0.02 -0.64,-0.08 -0.21,-0.05 -0.4,-0.13 -0.56,-0.24 -0.17,-0.11 -0.3,-0.26 -0.41,-0.44 -0.11,-0.18 -0.17,-0.41 -0.18,-0.67h-1.89c0,0.36 0.08,0.71 0.24,1.05 0.16,0.34 0.39,0.65 0.7,0.93 0.31,0.27 0.69,0.49 1.15,0.66 0.46,0.17 0.98,0.25 1.58,0.25 0.53,0 1.01,-0.06 1.44,-0.19 0.43,-0.13 0.8,-0.31 1.11,-0.54 0.31,-0.23 0.54,-0.51 0.71,-0.83 0.17,-0.32 0.25,-0.67 0.25,-1.06 -0.02,-0.4 -0.09,-0.74 -0.24,-1.02z"/>
+</vector>
diff --git a/feature/quicksettings/src/main/res/drawable/baseline_timer_off_72.xml b/feature/quicksettings/src/main/res/drawable/baseline_timer_off_72.xml
new file mode 100644
index 0000000..8bd1087
--- /dev/null
+++ b/feature/quicksettings/src/main/res/drawable/baseline_timer_off_72.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector android:height="72dp" android:tint="#000000"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="72dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M9,1h6v2h-6z"/>
+ <path android:fillColor="@android:color/white" android:pathData="M13,8v2.17l6.98,6.98C20.63,15.91 21,14.5 21,13c0,-2.12 -0.74,-4.07 -1.97,-5.61l1.42,-1.42c-0.43,-0.51 -0.9,-0.99 -1.41,-1.41l-1.42,1.42C16.07,4.74 14.12,4 12,4c-1.5,0 -2.91,0.37 -4.15,1.02L10.83,8H13z"/>
+ <path android:fillColor="@android:color/white" android:pathData="M2.81,2.81L1.39,4.22l3.4,3.4C3.67,9.12 3,10.98 3,13c0,4.97 4.02,9 9,9c2.02,0 3.88,-0.67 5.38,-1.79l2.4,2.4l1.41,-1.41L2.81,2.81z"/>
+</vector>
diff --git a/feature/quicksettings/src/main/res/values/dimens.xml b/feature/quicksettings/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..24ad7a6
--- /dev/null
+++ b/feature/quicksettings/src/main/res/values/dimens.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <dimen name="quick_settings_ui_horizontal_padding">30dp</dimen>
+ <dimen name="quick_settings_ui_item_icon_size">60dp</dimen>
+ <dimen name="quick_settings_ui_item_padding">20dp</dimen>
+ <dimen name="quick_settings_spacer_height">170dp</dimen>
+</resources> \ No newline at end of file
diff --git a/feature/quicksettings/src/main/res/values/strings.xml b/feature/quicksettings/src/main/res/values/strings.xml
new file mode 100644
index 0000000..e571f3e
--- /dev/null
+++ b/feature/quicksettings/src/main/res/values/strings.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <string name="quick_settings_front_camera_text">FRONT</string>
+ <string name="quick_settings_back_camera_text">BACK</string>
+
+ <string name="quick_settings_aspect_ratio_3_4">3:4</string>
+ <string name="quick_settings_aspect_ratio_9_16">9:16</string>
+ <string name="quick_settings_aspect_ratio_1_1">1:1</string>
+
+ <string name="quick_settings_flash_off">OFF</string>
+ <string name="quick_settings_flash_auto">AUTO</string>
+ <string name="quick_settings_flash_on">ON</string>
+
+ <string name="quick_settings_timer_off">OFF</string>
+ <string name="quick_settings_timer_3"></string>
+ <string name="quick_settings_timer_10"></string>
+
+ <string name="quick_settings_dropdown_description">Quick settings drop down</string>
+
+ <string name="quick_settings_front_camera_description">Front Camera</string>
+ <string name="quick_settings_back_camera_description">Back Camera</string>
+
+ <string name="quick_settings_aspect_ratio_3_4_description">3 to 4 aspect ratio</string>
+ <string name="quick_settings_aspect_ratio_9_16_description">9 to 16 aspect ratio</string>
+ <string name="quick_settings_aspect_ratio_1_1_description">1 to 1 aspect ratio</string>
+
+ <string name="quick_settings_flash_off_description">Flash off</string>
+ <string name="quick_settings_flash_auto_description">Auto flash</string>
+ <string name="quick_settings_flash_on_description">Flash on</string>
+
+ <string name="quick_settings_timer_off_description">Timer off</string>
+ <string name="quick_settings_timer_3_description">3 seconds timer</string>
+ <string name="quick_settings_timer_10_description">10 seconds timer</string>
+</resources> \ No newline at end of file
diff --git a/feature/settings/.gitignore b/feature/settings/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/feature/settings/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/feature/settings/Android.bp b/feature/settings/Android.bp
new file mode 100644
index 0000000..2e21fb8
--- /dev/null
+++ b/feature/settings/Android.bp
@@ -0,0 +1,31 @@
+package {
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+android_library {
+ name: "jetpack-camera-app_feature_settings",
+ srcs: ["src/main/**/*.kt"],
+ resource_dirs: [
+ "src/main/res",
+ ],
+ static_libs: [
+ "androidx.compose.material3_material3",
+ "androidx.compose.material_material-icons-core",
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.ui_ui-tooling-preview",
+ "hilt_android",
+ "androidx.compose.ui_ui-tooling",
+ "kotlinx_coroutines_guava",
+ "androidx.datastore_datastore",
+ "libprotobuf-java-lite",
+ "jetpack-camera-app_data_settings",
+ "androidx.hilt_hilt-navigation-compose",
+
+ ],
+ sdk_version: "34",
+ min_sdk_version: "21",
+ manifest:"src/main/AndroidManifest.xml"
+}
+
diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts
new file mode 100644
index 0000000..d9a1dbb
--- /dev/null
+++ b/feature/settings/build.gradle.kts
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-kapt")
+ id("com.google.dagger.hilt.android")
+}
+
+android {
+ namespace = "com.google.jetpackcamera.settings"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 21
+ targetSdk = 34
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ jvmToolchain(17)
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.4.0"
+ }
+}
+
+dependencies {
+ // Compose
+ val composeBom = platform("androidx.compose:compose-bom:2022.12.00")
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+
+ // Compose - Material Design 3
+ implementation("androidx.compose.material3:material3")
+
+ // Compose - Android Studio Preview support
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+
+ // Compose - Integration with ViewModels with Navigation and Hilt
+ implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
+
+ // Compose - Testing
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+
+ // Testing
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.3")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
+ testImplementation("org.mockito:mockito-core:5.2.0")
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6")
+ implementation("androidx.test:core-ktx:1.5.0")
+
+ // Guava
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.4.1")
+
+ // Hilt
+ implementation("com.google.dagger:hilt-android:2.44")
+ kapt("com.google.dagger:hilt-compiler:2.44")
+
+ // Proto Datastore
+ implementation("androidx.datastore:datastore:1.0.0")
+ implementation("com.google.protobuf:protobuf-kotlin-lite:3.21.12")
+
+ implementation(project(":data:settings"))
+}
+
+// Allow references to generated code
+kapt {
+ correctErrorTypes = true
+} \ No newline at end of file
diff --git a/feature/settings/consumer-rules.pro b/feature/settings/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/feature/settings/consumer-rules.pro
diff --git a/feature/settings/proguard-rules.pro b/feature/settings/proguard-rules.pro
new file mode 100644
index 0000000..ff59496
--- /dev/null
+++ b/feature/settings/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.kts.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt b/feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt
new file mode 100644
index 0000000..d0cd210
--- /dev/null
+++ b/feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.dataStoreFile
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS
+import com.google.jetpackcamera.settings.model.DarkMode
+import java.io.File
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal class CameraAppSettingsViewModelTest {
+ private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
+ private lateinit var testDataStore: DataStore<JcaSettings>
+ private lateinit var datastoreScope: CoroutineScope
+ private lateinit var repository: LocalSettingsRepository
+ private lateinit var settingsViewModel: SettingsViewModel
+
+ @Before
+ fun setup() = runTest(StandardTestDispatcher()) {
+ Dispatchers.setMain(StandardTestDispatcher())
+ datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
+
+ testDataStore = DataStoreFactory.create(
+ serializer = JcaSettingsSerializer,
+ scope = datastoreScope
+ ) {
+ testContext.dataStoreFile("test_jca_settings.pb")
+ }
+ repository = LocalSettingsRepository(testDataStore)
+ settingsViewModel = SettingsViewModel(repository)
+ advanceUntilIdle()
+ }
+
+ @After
+ fun tearDown() {
+ File(
+ ApplicationProvider.getApplicationContext<Context>().filesDir,
+ "datastore"
+ ).deleteRecursively()
+
+ datastoreScope.cancel()
+ }
+
+ @Test
+ fun getSettingsUiState() = runTest(StandardTestDispatcher()) {
+ // giving ViewModel time to call init, otherwise settings will stay disabled
+ delay(100)
+ val uiState = settingsViewModel.settingsUiState.value
+ advanceUntilIdle()
+ assertEquals(
+ uiState,
+ SettingsUiState(cameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS, disabled = false)
+ )
+ }
+
+ @Test
+ fun setDefaultToFrontCamera() = runTest(StandardTestDispatcher()) {
+ val initialFrontCameraValue =
+ settingsViewModel.settingsUiState.value.cameraAppSettings.isFrontCameraFacing
+ settingsViewModel.setDefaultToFrontCamera()
+
+ advanceUntilIdle()
+
+ val newFrontCameraValue =
+ settingsViewModel.settingsUiState.value.cameraAppSettings.isFrontCameraFacing
+
+ assertFalse(initialFrontCameraValue)
+ assertTrue(newFrontCameraValue)
+ }
+
+ @Test
+ fun setDarkMode() = runTest(StandardTestDispatcher()) {
+ val initialDarkMode = settingsViewModel.settingsUiState.value.cameraAppSettings.darkMode
+ settingsViewModel.setDarkMode(DarkMode.DARK)
+ advanceUntilIdle()
+
+ val newDarkMode = settingsViewModel.settingsUiState.value.cameraAppSettings.darkMode
+
+ assertEquals(initialDarkMode, DarkMode.SYSTEM)
+ assertEquals(DarkMode.DARK, newDarkMode)
+ }
+}
diff --git a/feature/settings/src/main/AndroidManifest.xml b/feature/settings/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..cfecf54
--- /dev/null
+++ b/feature/settings/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest package="com.google.jetpackcamera.settings">
+
+</manifest>
diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsScreen.kt
new file mode 100644
index 0000000..c28a558
--- /dev/null
+++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsScreen.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.google.jetpackcamera.settings.ui.AspectRatioSetting
+import com.google.jetpackcamera.settings.ui.CaptureModeSetting
+import com.google.jetpackcamera.settings.ui.DarkModeSetting
+import com.google.jetpackcamera.settings.ui.DefaultCameraFacing
+import com.google.jetpackcamera.settings.ui.FlashModeSetting
+import com.google.jetpackcamera.settings.ui.SectionHeader
+import com.google.jetpackcamera.settings.ui.SettingsPageHeader
+
+/**
+ * Screen used for the Settings feature.
+ */
+@Composable
+fun SettingsScreen(
+ viewModel: SettingsViewModel = hiltViewModel(),
+ onNavigateToPreview: () -> Unit
+) {
+ val settingsUiState by viewModel.settingsUiState.collectAsState()
+
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ ) {
+ SettingsPageHeader(
+ title = stringResource(id = R.string.settings_title),
+ navBack = onNavigateToPreview
+ )
+ SettingsList(uiState = settingsUiState, viewModel = viewModel)
+ }
+}
+
+@Composable
+fun SettingsList(uiState: SettingsUiState, viewModel: SettingsViewModel) {
+ SectionHeader(title = stringResource(id = R.string.section_title_camera_settings))
+
+ DefaultCameraFacing(
+ cameraAppSettings = uiState.cameraAppSettings,
+ onClick = viewModel::setDefaultToFrontCamera
+ )
+
+ FlashModeSetting(
+ currentFlashMode = uiState.cameraAppSettings.flashMode,
+ setFlashMode = viewModel::setFlashMode
+ )
+
+ AspectRatioSetting(
+ currentAspectRatio = uiState.cameraAppSettings.aspectRatio,
+ setAspectRatio = viewModel::setAspectRatio
+ )
+
+ CaptureModeSetting(
+ currentCaptureMode = uiState.cameraAppSettings.captureMode,
+ setCaptureMode = viewModel::setCaptureMode
+ )
+
+ SectionHeader(title = stringResource(id = R.string.section_title_app_settings))
+
+ DarkModeSetting(
+ currentDarkMode = uiState.cameraAppSettings.darkMode,
+ setDarkMode = viewModel::setDarkMode
+ )
+}
diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt
new file mode 100644
index 0000000..85901e2
--- /dev/null
+++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings
+
+import com.google.jetpackcamera.settings.model.CameraAppSettings
+
+/**
+ * Defines the current state of the [SettingsScreen].
+ */
+data class SettingsUiState(
+ val cameraAppSettings: CameraAppSettings,
+ var disabled: Boolean = false
+)
diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsViewModel.kt
new file mode 100644
index 0000000..c0005db
--- /dev/null
+++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsViewModel.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings
+
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.jetpackcamera.settings.model.AspectRatio
+import com.google.jetpackcamera.settings.model.CaptureMode
+import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS
+import com.google.jetpackcamera.settings.model.DarkMode
+import com.google.jetpackcamera.settings.model.FlashMode
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+private const val TAG = "SettingsViewModel"
+
+/**
+ * [ViewModel] for [SettingsScreen].
+ */
+@HiltViewModel
+class SettingsViewModel @Inject constructor(
+ private val settingsRepository: SettingsRepository
+) : ViewModel() {
+
+ private val _settingsUiState: MutableStateFlow<SettingsUiState> =
+ MutableStateFlow(
+ SettingsUiState(
+ DEFAULT_CAMERA_APP_SETTINGS,
+ disabled = true
+ )
+ )
+ val settingsUiState: StateFlow<SettingsUiState> = _settingsUiState
+
+ init {
+ // updates our view model as soon as datastore is updated
+ viewModelScope.launch {
+ settingsRepository.cameraAppSettings.collect { updatedSettings ->
+ _settingsUiState.emit(
+ settingsUiState.value.copy(
+ cameraAppSettings = updatedSettings,
+ disabled = false
+ )
+ )
+
+ Log.d(
+ TAG,
+ "updated setting" +
+ settingsRepository.getCameraAppSettings().captureMode
+ )
+ }
+ }
+ viewModelScope.launch {
+ _settingsUiState.emit(
+ settingsUiState.value.copy(
+ disabled = false
+ )
+ )
+ }
+ }
+
+ fun setDefaultToFrontCamera() {
+ // true means default is front
+ viewModelScope.launch {
+ settingsRepository.updateDefaultToFrontCamera()
+ Log.d(
+ TAG,
+ "set camera default facing: " +
+ settingsRepository.getCameraAppSettings().isFrontCameraFacing
+ )
+ }
+ }
+
+ fun setDarkMode(darkMode: DarkMode) {
+ viewModelScope.launch {
+ settingsRepository.updateDarkModeStatus(darkMode)
+ Log.d(
+ TAG,
+ "set dark mode theme: " +
+ settingsRepository.getCameraAppSettings().darkMode
+ )
+ }
+ }
+
+ fun setFlashMode(flashMode: FlashMode) {
+ viewModelScope.launch {
+ settingsRepository.updateFlashModeStatus(flashMode)
+ }
+ }
+
+ fun setAspectRatio(aspectRatio: AspectRatio) {
+ viewModelScope.launch {
+ settingsRepository.updateAspectRatio(aspectRatio)
+ Log.d(
+ TAG,
+ "set aspect ratio " +
+ "${settingsRepository.getCameraAppSettings().aspectRatio}"
+ )
+ }
+ }
+
+ fun setCaptureMode(captureMode: CaptureMode) {
+ viewModelScope.launch {
+ settingsRepository.updateCaptureMode(captureMode)
+
+ Log.d(
+ TAG,
+ "set default capture mode " +
+ settingsRepository.getCameraAppSettings().captureMode
+ )
+ }
+ }
+}
diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt
new file mode 100644
index 0000000..738d489
--- /dev/null
+++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.settings.ui
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.selection.toggleable
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.google.jetpackcamera.settings.R
+import com.google.jetpackcamera.settings.model.AspectRatio
+import com.google.jetpackcamera.settings.model.CameraAppSettings
+import com.google.jetpackcamera.settings.model.CaptureMode
+import com.google.jetpackcamera.settings.model.DarkMode
+import com.google.jetpackcamera.settings.model.FlashMode
+
+/**
+ * MAJOR SETTING UI COMPONENTS
+ * these are ready to be popped into the ui
+ */
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsPageHeader(modifier: Modifier = Modifier, title: String, navBack: () -> Unit) {
+ TopAppBar(
+ modifier = modifier,
+ title = {
+ Text(title)
+ },
+ navigationIcon = {
+ IconButton(onClick = { navBack() }) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(id = R.string.nav_back_accessibility))
+ }
+ }
+ )
+}
+
+@Composable
+fun SectionHeader(modifier: Modifier = Modifier, title: String) {
+ Text(
+ modifier = modifier
+ .padding(start = 20.dp, top = 10.dp),
+ text = title,
+ color = MaterialTheme.colorScheme.primary,
+ fontSize = 18.sp
+ )
+}
+
+@Composable
+fun DefaultCameraFacing(
+ modifier: Modifier = Modifier,
+ cameraAppSettings: CameraAppSettings,
+ onClick: () -> Unit
+) {
+ SwitchSettingUI(
+ modifier = modifier,
+ title = stringResource(id = R.string.default_facing_camera_title),
+ description = null,
+ leadingIcon = null,
+ onClick = { onClick() },
+ settingValue = cameraAppSettings.isFrontCameraFacing,
+ enabled = cameraAppSettings.isBackCameraAvailable &&
+ cameraAppSettings.isFrontCameraAvailable
+ )
+}
+
+@Composable
+fun DarkModeSetting(
+ modifier: Modifier = Modifier,
+ currentDarkMode: DarkMode,
+ setDarkMode: (DarkMode) -> Unit
+) {
+ BasicPopupSetting(
+ modifier = modifier,
+ title = stringResource(id = R.string.dark_mode_title),
+ leadingIcon = null,
+ description = when (currentDarkMode) {
+ DarkMode.SYSTEM -> stringResource(id = R.string.dark_mode_description_system)
+ DarkMode.DARK -> stringResource(id = R.string.dark_mode_description_dark)
+ DarkMode.LIGHT -> stringResource(id = R.string.dark_mode_description_light)
+ },
+ popupContents = {
+ Column(Modifier.selectableGroup()) {
+ SingleChoiceSelector(
+ text = stringResource(id = R.string.dark_mode_selector_dark),
+ selected = currentDarkMode == DarkMode.DARK,
+ onClick = { setDarkMode(DarkMode.DARK) }
+ )
+ SingleChoiceSelector(
+ text = stringResource(id = R.string.dark_mode_selector_light),
+ selected = currentDarkMode == DarkMode.LIGHT,
+ onClick = { setDarkMode(DarkMode.LIGHT) }
+ )
+ SingleChoiceSelector(
+ text = stringResource(id = R.string.dark_mode_selector_system),
+ selected = currentDarkMode == DarkMode.SYSTEM,
+ onClick = { setDarkMode(DarkMode.SYSTEM) }
+ )
+ }
+ }
+ )
+}
+
+@Composable
+fun FlashModeSetting(
+ modifier: Modifier = Modifier,
+ currentFlashMode: FlashMode,
+ setFlashMode: (FlashMode) -> Unit
+) {
+ BasicPopupSetting(
+ modifier = modifier,
+ title = stringResource(id = R.string.flash_mode_title),
+ leadingIcon = null,
+ description = when (currentFlashMode) {
+ FlashMode.AUTO -> stringResource(id = R.string.flash_mode_description_auto)
+ FlashMode.ON -> stringResource(id = R.string.flash_mode_description_on)
+ FlashMode.OFF -> stringResource(id = R.string.flash_mode_description_off)
+ },
+ popupContents = {
+ Column(Modifier.selectableGroup()) {
+ SingleChoiceSelector(
+ text = stringResource(id = R.string.flash_mode_selector_auto),
+ selected = currentFlashMode == FlashMode.AUTO,
+ onClick = { setFlashMode(FlashMode.AUTO) }
+ )
+ SingleChoiceSelector(
+ text = stringResource(id = R.string.flash_mode_selector_on),
+ selected = currentFlashMode == FlashMode.ON,
+ onClick = { setFlashMode(FlashMode.ON) }
+ )
+ SingleChoiceSelector(
+ text = stringResource(id = R.string.flash_mode_selector_off),
+ selected = currentFlashMode == FlashMode.OFF,
+ onClick = { setFlashMode(FlashMode.OFF) }
+ )
+ }
+ }
+ )
+}
+
+@Composable
+fun AspectRatioSetting(currentAspectRatio: AspectRatio, setAspectRatio: (AspectRatio) -> Unit) {
+ BasicPopupSetting(
+ title = stringResource(id = R.string.aspect_ratio_title),
+ leadingIcon = null,
+ description = when (currentAspectRatio) {
+ AspectRatio.NINE_SIXTEEN -> stringResource(id = R.string.aspect_ratio_description_9_16)
+ AspectRatio.THREE_FOUR -> stringResource(id = R.string.aspect_ratio_description_3_4)
+ AspectRatio.ONE_ONE -> stringResource(id = R.string.aspect_ratio_description_1_1)
+ },
+ popupContents = {
+ Column(Modifier.selectableGroup()) {
+ SingleChoiceSelector(
+ text = stringResource(id = R.string.aspect_ratio_selector_9_16),
+ selected = currentAspectRatio == AspectRatio.NINE_SIXTEEN,
+ onClick = { setAspectRatio(AspectRatio.NINE_SIXTEEN) }
+ )
+ SingleChoiceSelector(
+ text = stringResource(id = R.string.aspect_ratio_selector_3_4),
+ selected = currentAspectRatio == AspectRatio.THREE_FOUR,
+ onClick = { setAspectRatio(AspectRatio.THREE_FOUR) }
+ )
+ SingleChoiceSelector(
+ text = stringResource(id = R.string.aspect_ratio_selector_1_1),
+ selected = currentAspectRatio == AspectRatio.ONE_ONE,
+ onClick = { setAspectRatio(AspectRatio.ONE_ONE) }
+ )
+ }
+ }
+ )
+}
+
+@Composable
+fun CaptureModeSetting(currentCaptureMode: CaptureMode, setCaptureMode: (CaptureMode) -> Unit) {
+ // todo: string resources
+ BasicPopupSetting(
+ title = stringResource(R.string.capture_mode_title),
+ leadingIcon = null,
+ description = when (currentCaptureMode) {
+ CaptureMode.MULTI_STREAM -> stringResource(
+ id = R.string.capture_mode_description_multi_stream
+ )
+ CaptureMode.SINGLE_STREAM -> stringResource(
+ id = R.string.capture_mode_description_single_stream
+ )
+ },
+ popupContents = {
+ Column(Modifier.selectableGroup()) {
+ SingleChoiceSelector(
+ text = stringResource(id = R.string.capture_mode_selector_multi_stream),
+ selected = currentCaptureMode == CaptureMode.MULTI_STREAM,
+ onClick = { setCaptureMode(CaptureMode.MULTI_STREAM) }
+ )
+ SingleChoiceSelector(
+ text = stringResource(id = R.string.capture_mode_description_single_stream),
+ selected = currentCaptureMode == CaptureMode.SINGLE_STREAM,
+ onClick = { setCaptureMode(CaptureMode.SINGLE_STREAM) }
+ )
+ }
+ }
+ )
+}
+
+/**
+ * Setting UI sub-Components
+ * small and whimsical :)
+ * don't use these directly, use them to build the ready-to-use setting components
+ */
+
+/** a composable for creating a simple popup setting **/
+
+@Composable
+fun BasicPopupSetting(
+ modifier: Modifier = Modifier,
+ title: String,
+ description: String?,
+ enabled: Boolean = true,
+ leadingIcon: @Composable (() -> Unit)?,
+ popupContents: @Composable () -> Unit
+) {
+ val popupStatus = remember { mutableStateOf(false) }
+ SettingUI(
+ modifier = modifier.clickable(enabled = enabled) { popupStatus.value = true },
+ title = title,
+ description = description,
+ leadingIcon = leadingIcon,
+ trailingContent = null
+ )
+ if (popupStatus.value) {
+ AlertDialog(
+ onDismissRequest = { popupStatus.value = false },
+ confirmButton = {
+ Text(
+ text = "Close",
+ modifier = Modifier.clickable { popupStatus.value = false }
+ )
+ },
+ title = { Text(text = title) },
+ text = popupContents
+ )
+ }
+}
+
+/**
+ * A composable for creating a setting with a Switch.
+ *
+ * <p> the value should correspond to the setting's UI state value. the switch will only change
+ * appearance if the UI state has been successfully updated
+ */
+@Composable
+fun SwitchSettingUI(
+ modifier: Modifier = Modifier,
+ title: String,
+ description: String?,
+ leadingIcon: @Composable (() -> Unit)?,
+ onClick: () -> Unit,
+ settingValue: Boolean,
+ enabled: Boolean
+) {
+ SettingUI(
+ modifier = modifier.toggleable(
+ enabled = enabled,
+ role = Role.Switch,
+ value = settingValue,
+ onValueChange = { _ -> onClick() }
+ ),
+ title = title,
+ description = description,
+ leadingIcon = leadingIcon,
+ trailingContent = {
+ Switch(
+ enabled = enabled,
+ checked = settingValue,
+ onCheckedChange = {
+ onClick()
+ }
+ )
+ }
+ )
+}
+
+/**
+ * A composable used as a template used to construct other settings components
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingUI(
+ modifier: Modifier = Modifier,
+ title: String,
+ description: String? = null,
+ leadingIcon: @Composable (() -> Unit)?,
+ trailingContent: @Composable (() -> Unit)?
+) {
+ ListItem(
+ modifier = modifier,
+ headlineContent = { Text(title) },
+ supportingContent = when (description) {
+ null -> null
+ else -> {
+ { Text(description) }
+ }
+ },
+ leadingContent = leadingIcon,
+ trailingContent = trailingContent
+ )
+}
+
+/**
+ * A component for a single-choice selector for a multiple choice list
+ */
+@Composable
+fun SingleChoiceSelector(
+ modifier: Modifier = Modifier,
+ text: String,
+ selected: Boolean,
+ onClick: () -> Unit,
+ enabled: Boolean = true
+) {
+ Row(
+ modifier
+ .fillMaxWidth()
+ .selectable(
+ selected = selected,
+ role = Role.RadioButton,
+ onClick = onClick
+ ),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = selected,
+ onClick = onClick,
+ enabled = enabled
+ )
+ Spacer(Modifier.width(8.dp))
+ Text(text)
+ }
+}
diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml
new file mode 100644
index 0000000..3df861f
--- /dev/null
+++ b/feature/settings/src/main/res/values/strings.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <!--
+ settings page text
+ -->
+ <string name="settings_title">Settings</string>
+ <string name="nav_back_accessibility">Button to navigate back out of settings</string>
+
+ <string name="section_title_camera_settings">Default Camera Settings</string>
+ <string name="section_title_app_settings">App Settings</string>
+
+ <string name="default_facing_camera_title">Default to Front Camera</string>
+ <string name="default_facing_camera_description">Default Front</string>
+ <string name="default_facing_camera_description_off">Default Back</string>
+
+ <!-- Dark mode setting strings -->
+ <string name="dark_mode_title">Set Dark Mode</string>
+
+ <string name="dark_mode_selector_dark">On</string>
+ <string name="dark_mode_selector_light">Off</string>
+ <string name="dark_mode_selector_system">System</string>
+
+ <string name="dark_mode_description_dark">On</string>
+ <string name="dark_mode_description_light">Off</string>
+ <string name="dark_mode_description_system">System</string>
+
+ <!-- Flash mode setting strings -->
+ <string name="flash_mode_title">Set Flash Mode</string>
+
+ <string name="flash_mode_selector_auto">Auto</string>
+ <string name="flash_mode_selector_on">On</string>
+ <string name="flash_mode_selector_off">Off</string>
+
+ <string name="flash_mode_description_auto">Flash is set to Auto</string>
+ <string name="flash_mode_description_on">Flash is On</string>
+ <string name="flash_mode_description_off">Flash is Off</string>
+
+ <!-- Capture mode setting strings -->
+ <string name="capture_mode_title">Set Capture Mode</string>
+
+ <string name="capture_mode_selector_multi_stream">Multi Stream Capture</string>
+ <string name="capture_mode_selector_single_stream">Single Stream Capture</string>
+
+ <string name="capture_mode_description_multi_stream">Multi Stream</string>
+ <string name="capture_mode_description_single_stream">Single Stream</string>
+
+ <!-- Aspect Ratio setting strings -->
+ <string name="aspect_ratio_title">Set Aspect Ratio</string>
+
+ <string name="aspect_ratio_selector_9_16">9:16</string>
+ <string name="aspect_ratio_selector_3_4">3:4</string>
+ <string name="aspect_ratio_selector_1_1">1:1</string>
+
+ <string name="aspect_ratio_description_9_16">Aspect Ratio is 9:16</string>
+ <string name="aspect_ratio_description_3_4">Aspect Ratio is 3:4</string>
+ <string name="aspect_ratio_description_1_1">Aspect Ratio is 1:1</string>
+
+</resources> \ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..a2e90d8
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,25 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=true
+android.nonFinalResIds=false \ No newline at end of file
diff --git a/gradle/init.gradle.kts b/gradle/init.gradle.kts
new file mode 100644
index 0000000..38989df
--- /dev/null
+++ b/gradle/init.gradle.kts
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+val ktlintVersion = "1.0.0"
+
+initscript {
+ val spotlessVersion = "6.22.0"
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion")
+ }
+}
+
+rootProject {
+ subprojects {
+ apply<com.diffplug.gradle.spotless.SpotlessPlugin>()
+ extensions.configure<com.diffplug.gradle.spotless.SpotlessExtension> {
+ // limit format enforcement to just the files changed by this feature branch
+ ratchetFrom("origin/main")
+ kotlin {
+ target("**/*.kt")
+ targetExclude("**/build/**/*.kt")
+ ktlint(ktlintVersion)
+ .setEditorConfigPath(rootProject.file(".editorconfig"))
+ licenseHeaderFile(rootProject.file("spotless/copyright.kt"))
+ }
+ format("kts") {
+ target("**/*.kts")
+ targetExclude("**/build/**/*.kts")
+ // Look for the first line that doesn't have a block comment (assumed to be the license)
+ licenseHeaderFile(rootProject.file("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)")
+ }
+ format("xml") {
+ target("**/*.xml")
+ targetExclude("**/build/**/*.xml")
+ // Look for the first XML tag that isn't a comment (<!--) or the xml declaration (<?xml)
+ licenseHeaderFile(rootProject.file("spotless/copyright.xml"), "(<[^!?])")
+ }
+ }
+ }
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..b19396a
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Jan 25 00:53:54 EST 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/hooks/pre-commit b/hooks/pre-commit
new file mode 100644
index 0000000..664ea8b
--- /dev/null
+++ b/hooks/pre-commit
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+#
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+echo "***Applying Spotless***"
+
+./gradlew spotlessApply --init-script gradle/init.gradle.kts
+
+exit $status
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..5e4c892
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ maven {
+ setUrl("https://androidx.dev/snapshots/builds/10955671/artifacts/repository")
+ }
+ google()
+ mavenCentral()
+ }
+}
+rootProject.name = "Jetpack Camera"
+include(":app")
+include(":feature:preview")
+include(":domain:camera")
+include(":camera-viewfinder-compose")
+include(":feature:settings")
+include(":data:settings")
+include(":core:common")
+include(":feature:quicksettings")
diff --git a/spotless/copyright.kt b/spotless/copyright.kt
new file mode 100644
index 0000000..eeed4ac
--- /dev/null
+++ b/spotless/copyright.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */ \ No newline at end of file
diff --git a/spotless/copyright.kts b/spotless/copyright.kts
new file mode 100644
index 0000000..eeed4ac
--- /dev/null
+++ b/spotless/copyright.kts
@@ -0,0 +1,15 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */ \ No newline at end of file
diff --git a/spotless/copyright.xml b/spotless/copyright.xml
new file mode 100644
index 0000000..121891e
--- /dev/null
+++ b/spotless/copyright.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ --> \ No newline at end of file