diff options
author | Inna Palant <ipalant@google.com> | 2024-01-22 21:46:11 +0000 |
---|---|---|
committer | Inna Palant <ipalant@google.com> | 2024-01-22 21:46:11 +0000 |
commit | 1280ae1bb0718dbd855c28e9065fe71d448dd057 (patch) | |
tree | 50ff5afdf8d00fefa49e04d8723f46f77d2d85fc | |
parent | fd636b5f9e1a1755bc14dd6e6cc7b6d7f7f58700 (diff) | |
parent | 368572b731459ece3fb9d70eb11198ea92bc0870 (diff) | |
download | jetpack-camera-app-1280ae1bb0718dbd855c28e9065fe71d448dd057.tar.gz |
Merge remote-tracking branch 'origin/upstream'
Import b/309514655
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) &#36;today.year 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. " /> + <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/)([^,.<>()"\s]+(?:[.,][^,.<>()"\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", + + ] + + @@ -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 @@ -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 Binary files differnew file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differnew file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..62b611d --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 0000000..948a307 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..1b9a695 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 0000000..28d4b77 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..9287f50 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 0000000..aa7d642 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..9126ae3 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp 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 Binary files differnew file mode 100644 index 0000000..e708b1c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar 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 @@ -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 |