├── .github
├── dependabot.yml
└── workflows
│ ├── build.yml
│ └── validate-gradle-wrapper.yml
├── .gitignore
├── .idea
├── .gitignore
├── .name
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── gradle.xml
├── inspectionProfiles
│ ├── Project_Default.xml
│ └── profiles_settings.xml
├── misc.xml
└── vcs.xml
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── lint.xml
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── feature_graphic.png
│ ├── ic_launcher-playstore.png
│ ├── java
│ ├── androidxc
│ │ ├── camera
│ │ │ └── core
│ │ │ │ └── impl
│ │ │ │ └── utils
│ │ │ │ └── Exif.java
│ │ └── exifinterface
│ │ │ └── media
│ │ │ ├── ExifInterface.java
│ │ │ └── ExifInterfaceUtils.java
│ └── app
│ │ └── grapheneos
│ │ └── camera
│ │ ├── ActivityLifeCycleHelper.kt
│ │ ├── App.kt
│ │ ├── AutoFinishOnSleep.kt
│ │ ├── BlurBitmap.kt
│ │ ├── CamConfig.kt
│ │ ├── CapturedItems.kt
│ │ ├── ExifHelper.kt
│ │ ├── GSlideTransformer.kt
│ │ ├── GallerySliderAdapter.kt
│ │ ├── NumInputFilter.kt
│ │ ├── TunePlayer.kt
│ │ ├── analyzer
│ │ └── QRAnalyzer.kt
│ │ ├── capturer
│ │ ├── ImageCapturer.kt
│ │ ├── ImageSaver.kt
│ │ ├── ImageSaverException.kt
│ │ └── VideoCapturer.kt
│ │ ├── ktx
│ │ ├── ApplicationInfo.kt
│ │ ├── PreviewView.kt
│ │ └── SystemSettingsObserver.kt
│ │ ├── notifier
│ │ └── SensorOrientationChangeNotifier.kt
│ │ ├── ui
│ │ ├── BottomTabLayout.kt
│ │ ├── CountDownTimerUI.kt
│ │ ├── CustomGrid.kt
│ │ ├── DialogUtil.kt
│ │ ├── QROverlay.kt
│ │ ├── QRToggle.kt
│ │ ├── SettingsDialog.kt
│ │ ├── SettingsFrameLayout.kt
│ │ ├── ZoomableImageView.kt
│ │ ├── activities
│ │ │ ├── CaptureActivity.kt
│ │ │ ├── InAppGallery.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── MoreSettings.kt
│ │ │ ├── MoreSettingsSecure.kt
│ │ │ ├── QrTile.kt
│ │ │ ├── SecureActivity.kt
│ │ │ ├── SecureCaptureActivity.kt
│ │ │ ├── SecureMainActivity.kt
│ │ │ ├── VideoCaptureActivity.kt
│ │ │ ├── VideoOnlyActivity.kt
│ │ │ └── VideoPlayer.kt
│ │ ├── fragment
│ │ │ └── GallerySlide.kt
│ │ └── seekbar
│ │ │ ├── ExposureBar.kt
│ │ │ └── ZoomBar.kt
│ │ └── util
│ │ ├── BitmapUtils.kt
│ │ ├── CameraControl.kt
│ │ ├── ImageDecoderUtils.kt
│ │ ├── IntentUtils.kt
│ │ ├── PackageManagerUtils.kt
│ │ ├── SharedPrefs.kt
│ │ └── Utils.kt
│ └── res
│ ├── anim
│ ├── slide_down.xml
│ └── slide_up.xml
│ ├── drawable-nodpi
│ ├── aztec.png
│ ├── data_matrix.png
│ ├── pdf417.png
│ └── qr_code.png
│ ├── drawable
│ ├── aspect_ratio.xml
│ ├── auto.xml
│ ├── back.xml
│ ├── back_white.xml
│ ├── camera_shutter.xml
│ ├── camera_shutter_normal.xml
│ ├── camera_shutter_pressed.xml
│ ├── cancel.xml
│ ├── cbutton_bg.xml
│ ├── circle.xml
│ ├── copy_to_clipboard.xml
│ ├── delete.xml
│ ├── done.xml
│ ├── dropdown.xml
│ ├── edit.xml
│ ├── exposure_icon.xml
│ ├── exposure_neg.xml
│ ├── exposure_plus.xml
│ ├── exposure_thumb.xml
│ ├── flash_auto.xml
│ ├── flash_auto_circle.xml
│ ├── flash_off.xml
│ ├── flash_off_circle.xml
│ ├── flash_on.xml
│ ├── flash_on_circle.xml
│ ├── flip_camera.xml
│ ├── focus_ring.xml
│ ├── folder.xml
│ ├── grid_3x3.xml
│ ├── grid_3x3_circle.xml
│ ├── grid_4x4.xml
│ ├── grid_4x4_circle.xml
│ ├── grid_goldenratio.xml
│ ├── grid_goldenratio_circle.xml
│ ├── grid_off.xml
│ ├── grid_off_circle.xml
│ ├── ic_launcher_foreground.xml
│ ├── ic_open_with.xml
│ ├── image_quality.xml
│ ├── info.xml
│ ├── info_adaptable.xml
│ ├── location.xml
│ ├── location_off.xml
│ ├── location_on.xml
│ ├── mic_off.xml
│ ├── mic_on.xml
│ ├── mode_indicator.xml
│ ├── more.xml
│ ├── more_options.xml
│ ├── more_options_raw.xml
│ ├── more_settings_bg.xml
│ ├── option_circle.xml
│ ├── pause.xml
│ ├── play.xml
│ ├── play_button_circle.xml
│ ├── progress_bar_style.xml
│ ├── qr_result_background.xml
│ ├── recording.xml
│ ├── refresh.xml
│ ├── rename.xml
│ ├── retake.xml
│ ├── selfie_preview.xml
│ ├── settings_bg.xml
│ ├── settings_icon.xml
│ ├── settings_normal.xml
│ ├── settings_pressed.xml
│ ├── shade.xml
│ ├── share.xml
│ ├── share_white.xml
│ ├── storage.xml
│ ├── straighten.xml
│ ├── thumb.xml
│ ├── timer.xml
│ ├── torch.xml
│ ├── torch_off.xml
│ ├── torch_off_button.xml
│ ├── torch_off_white.xml
│ ├── torch_on.xml
│ ├── torch_on_button.xml
│ ├── torch_on_white.xml
│ ├── volume_up.xml
│ ├── white_option_circle.xml
│ ├── white_shadow_rect.xml
│ ├── yellow_shadow_rect.xml
│ ├── zoom_in.xml
│ ├── zoom_out.xml
│ └── zsl.xml
│ ├── layout
│ ├── activity_main.xml
│ ├── gallery.xml
│ ├── gallery_placeholder.xml
│ ├── gallery_slide.xml
│ ├── more_settings.xml
│ ├── scan_result_dialog.xml
│ ├── settings.xml
│ ├── video_player.xml
│ └── zoom_bar_thumb.xml
│ ├── menu
│ └── gallery.xml
│ ├── mipmap-anydpi
│ └── ic_launcher.xml
│ ├── raw
│ ├── focus_start.ogg
│ ├── image_shot.ogg
│ ├── keep.xml
│ ├── timer_final_second.ogg
│ ├── timer_increment.ogg
│ ├── video_start.ogg
│ └── video_stop.ogg
│ ├── values
│ ├── arrays.xml
│ ├── colors.xml
│ ├── dimens.xml
│ ├── ic_launcher_background.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── data_extraction_rules.xml
│ └── full_backup_content.xml
├── build.gradle.kts
├── gradle.properties
├── gradle
├── verification-metadata.xml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | target-branch: main
8 | - package-ecosystem: gradle
9 | directory: "/"
10 | schedule:
11 | interval: daily
12 | target-branch: main
13 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build application
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v4
11 | - name: Set up JDK 21
12 | uses: actions/setup-java@v4
13 | with:
14 | distribution: 'temurin'
15 | java-version: 21
16 | cache: gradle
17 | - name: Build with Gradle
18 | run: ./gradlew build --no-daemon
19 |
--------------------------------------------------------------------------------
/.github/workflows/validate-gradle-wrapper.yml:
--------------------------------------------------------------------------------
1 | name: Validate Gradle Wrapper
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | validation:
7 | name: Validation
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 | - uses: gradle/actions/wrapper-validation@v4
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | keystore.properties
28 |
29 | # Proguard folder generated by Eclipse
30 | proguard/
31 |
32 | # Log Files
33 | *.log
34 |
35 | # Android Studio Navigation editor temp files
36 | .navigation/
37 |
38 | # Android Studio captures folder
39 | captures/
40 |
41 | # IntelliJ
42 | *.iml
43 | .idea/workspace.xml
44 | .idea/tasks.xml
45 | .idea/gradle.xml
46 | .idea/misc.xml
47 | .idea/assetWizardSettings.xml
48 | .idea/dictionaries
49 | .idea/libraries
50 | # Android Studio 3 in .gitignore file.
51 | .idea/caches
52 | .idea/modules.xml
53 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
54 | .idea/navEditor.xml
55 |
56 | # Keystore files
57 | *.jks
58 | *.keystore
59 |
60 | # External native build folder generated in Android Studio 2.2 and later
61 | .externalNativeBuild
62 | .cxx/
63 |
64 | # Google Services (e.g. APIs or Firebase)
65 | google-services.json
66 |
67 | # Freeline
68 | freeline.py
69 | freeline/
70 | freeline_project_description.json
71 |
72 | # fastlane
73 | fastlane/report.xml
74 | fastlane/Preview.html
75 | fastlane/screenshots
76 | fastlane/test_output
77 | fastlane/readme.md
78 |
79 | # Version control
80 | vcs.xml
81 |
82 | # lint
83 | lint/intermediates/
84 | lint/generated/
85 | lint/outputs/
86 | lint/tmp/
87 | lint/reports/
88 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | Camera
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | xmlns:android
32 |
33 | ^$
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | xmlns:.*
43 |
44 | ^$
45 |
46 |
47 | BY_NAME
48 |
49 |
50 |
51 |
52 |
53 |
54 | .*:id
55 |
56 | http://schemas.android.com/apk/res/android
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | .*:name
66 |
67 | http://schemas.android.com/apk/res/android
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | name
77 |
78 | ^$
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | style
88 |
89 | ^$
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | .*
99 |
100 | ^$
101 |
102 |
103 | BY_NAME
104 |
105 |
106 |
107 |
108 |
109 |
110 | .*
111 |
112 | http://schemas.android.com/apk/res/android
113 |
114 |
115 | ANDROID_ATTRIBUTE_ORDER
116 |
117 |
118 |
119 |
120 |
121 |
122 | .*
123 |
124 | .*
125 |
126 |
127 | BY_NAME
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright © 2021-2025 GrapheneOS
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is the new GrapheneOS Camera app based on Android's modern CameraX
2 | library. It replaces AOSP Camera for GrapheneOS.
3 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea/workspace.xml
42 | .idea/tasks.xml
43 | .idea/gradle.xml
44 | .idea/misc.xml
45 | .idea/assetWizardSettings.xml
46 | .idea/dictionaries
47 | .idea/libraries
48 | # Android Studio 3 in .gitignore file.
49 | .idea/caches
50 | .idea/modules.xml
51 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
52 | .idea/navEditor.xml
53 |
54 | # Keystore files
55 | *.jks
56 | *.keystore
57 |
58 | # External native build folder generated in Android Studio 2.2 and later
59 | .externalNativeBuild
60 | .cxx/
61 |
62 | # Google Services (e.g. APIs or Firebase)
63 | google-services.json
64 |
65 | # Freeline
66 | freeline.py
67 | freeline/
68 | freeline_project_description.json
69 |
70 | # fastlane
71 | fastlane/report.xml
72 | fastlane/Preview.html
73 | fastlane/screenshots
74 | fastlane/test_output
75 | fastlane/readme.md
76 |
77 | # Version control
78 | vcs.xml
79 |
80 | # lint
81 | lint/intermediates/
82 | lint/generated/
83 | lint/outputs/
84 | lint/tmp/
85 | lint/reports/
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.io.FileInputStream
2 | import java.util.Properties
3 |
4 | val keystorePropertiesFile = rootProject.file("keystore.properties")
5 | val useKeystoreProperties = keystorePropertiesFile.canRead()
6 | val keystoreProperties = Properties()
7 | if (useKeystoreProperties) {
8 | keystoreProperties.load(FileInputStream(keystorePropertiesFile))
9 | }
10 |
11 | plugins {
12 | id("com.android.application")
13 | kotlin("android")
14 | }
15 |
16 | java {
17 | toolchain {
18 | languageVersion.set(JavaLanguageVersion.of(17))
19 | }
20 | }
21 |
22 | android {
23 | if (useKeystoreProperties) {
24 | signingConfigs {
25 | create("release") {
26 | storeFile = rootProject.file(keystoreProperties["storeFile"]!!)
27 | storePassword = keystoreProperties["storePassword"] as String
28 | keyAlias = keystoreProperties["keyAlias"] as String
29 | keyPassword = keystoreProperties["keyPassword"] as String
30 | enableV4Signing = true
31 | }
32 |
33 | create("play") {
34 | storeFile = rootProject.file(keystoreProperties["storeFile"]!!)
35 | storePassword = keystoreProperties["storePassword"] as String
36 | keyAlias = keystoreProperties["uploadKeyAlias"] as String
37 | keyPassword = keystoreProperties["uploadKeyPassword"] as String
38 | }
39 | }
40 | }
41 |
42 | compileSdk = 35
43 | buildToolsVersion = "36.0.0"
44 | ndkVersion = "28.0.13004108"
45 |
46 | namespace = "app.grapheneos.camera"
47 |
48 | defaultConfig {
49 | applicationId = "app.grapheneos.camera"
50 | minSdk = 29
51 | targetSdk = 35
52 | versionCode = 84
53 | versionName = versionCode.toString()
54 | }
55 |
56 | buildTypes {
57 | getByName("release") {
58 | isShrinkResources = true
59 | isMinifyEnabled = true
60 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
61 | if (useKeystoreProperties) {
62 | signingConfig = signingConfigs.getByName("release")
63 | }
64 | resValue("string", "app_name", "Camera")
65 | }
66 |
67 | getByName("debug") {
68 | applicationIdSuffix = ".dev"
69 | resValue("string", "app_name", "Camera d")
70 | // isDebuggable = false
71 | }
72 |
73 | create("play") {
74 | initWith(getByName("release"))
75 | applicationIdSuffix = ".play"
76 | if (useKeystoreProperties) {
77 | signingConfig = signingConfigs.getByName("play")
78 | }
79 | }
80 | }
81 |
82 | buildFeatures {
83 | viewBinding = true
84 | buildConfig = true
85 | }
86 |
87 | androidResources {
88 | localeFilters += listOf("en")
89 | }
90 | }
91 |
92 | dependencies {
93 | implementation("androidx.appcompat:appcompat:1.7.0")
94 | implementation("com.google.android.material:material:1.12.0")
95 | implementation("androidx.constraintlayout:constraintlayout:2.2.1")
96 | implementation("androidx.core:core:1.16.0")
97 |
98 | val cameraVersion = "1.5.0-beta01"
99 | implementation("androidx.camera:camera-core:$cameraVersion")
100 | implementation("androidx.camera:camera-camera2:$cameraVersion")
101 | implementation("androidx.camera:camera-lifecycle:$cameraVersion")
102 | implementation("androidx.camera:camera-video:$cameraVersion")
103 | implementation("androidx.camera:camera-view:$cameraVersion")
104 | implementation("androidx.camera:camera-extensions:$cameraVersion")
105 |
106 | implementation("com.google.zxing:core:3.5.3")
107 | }
108 |
--------------------------------------------------------------------------------
/app/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # work around CameraX 1.5.0-alpha05 regression
2 | -keepclassmembers class androidx.camera.camera2.internal.CameraBurstCaptureCallback {
3 | public *;
4 | }
5 |
--------------------------------------------------------------------------------
/app/src/main/feature_graphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Camera/71928cc12b41aa977bd40f29a13ac39ebcfec078/app/src/main/feature_graphic.png
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Camera/71928cc12b41aa977bd40f29a13ac39ebcfec078/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/androidxc/exifinterface/media/ExifInterfaceUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidxc.exifinterface.media;
18 |
19 | import android.media.MediaDataSource;
20 | import android.media.MediaMetadataRetriever;
21 | import android.os.Build;
22 | import android.system.ErrnoException;
23 | import android.system.Os;
24 | import android.util.Log;
25 |
26 | import androidx.annotation.DoNotInline;
27 | import androidx.annotation.RequiresApi;
28 |
29 | import java.io.Closeable;
30 | import java.io.FileDescriptor;
31 | import java.io.IOException;
32 | import java.io.InputStream;
33 | import java.io.OutputStream;
34 |
35 | class ExifInterfaceUtils {
36 | private static final String TAG = "ExifInterfaceUtils";
37 |
38 | private ExifInterfaceUtils() {
39 | // Prevent instantiation
40 | }
41 | /**
42 | * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
43 | * Returns the total number of bytes transferred.
44 | */
45 | static int copy(InputStream in, OutputStream out) throws IOException {
46 | int total = 0;
47 | byte[] buffer = new byte[8192];
48 | int c;
49 | while ((c = in.read(buffer)) != -1) {
50 | total += c;
51 | out.write(buffer, 0, c);
52 | }
53 | return total;
54 | }
55 |
56 | /**
57 | * Copies the given number of the bytes from {@code in} to {@code out}. Neither stream is
58 | * closed.
59 | */
60 | static void copy(InputStream in, OutputStream out, int numBytes) throws IOException {
61 | int remainder = numBytes;
62 | byte[] buffer = new byte[8192];
63 | while (remainder > 0) {
64 | int bytesToRead = Math.min(remainder, 8192);
65 | int bytesRead = in.read(buffer, 0, bytesToRead);
66 | if (bytesRead != bytesToRead) {
67 | throw new IOException("Failed to copy the given amount of bytes from the input"
68 | + "stream to the output stream.");
69 | }
70 | remainder -= bytesRead;
71 | out.write(buffer, 0, bytesRead);
72 | }
73 | }
74 |
75 | /**
76 | * Convert given int[] to long[]. If long[] is given, just return it.
77 | * Return null for other types of input.
78 | */
79 | static long[] convertToLongArray(Object inputObj) {
80 | if (inputObj instanceof int[]) {
81 | int[] input = (int[]) inputObj;
82 | long[] result = new long[input.length];
83 | for (int i = 0; i < input.length; i++) {
84 | result[i] = input[i];
85 | }
86 | return result;
87 | } else if (inputObj instanceof long[]) {
88 | return (long[]) inputObj;
89 | }
90 | return null;
91 | }
92 |
93 | static boolean startsWith(byte[] cur, byte[] val) {
94 | if (cur == null || val == null) {
95 | return false;
96 | }
97 | if (cur.length < val.length) {
98 | return false;
99 | }
100 | for (int i = 0; i < val.length; i++) {
101 | if (cur[i] != val[i]) {
102 | return false;
103 | }
104 | }
105 | return true;
106 | }
107 |
108 | static String byteArrayToHexString(byte[] bytes) {
109 | StringBuilder sb = new StringBuilder(bytes.length * 2);
110 | for (int i = 0; i < bytes.length; i++) {
111 | sb.append(String.format("%02x", bytes[i]));
112 | }
113 | return sb.toString();
114 | }
115 |
116 | static long parseSubSeconds(String subSec) {
117 | try {
118 | final int len = Math.min(subSec.length(), 3);
119 | long sub = Long.parseLong(subSec.substring(0, len));
120 | for (int i = len; i < 3; i++) {
121 | sub *= 10;
122 | }
123 | return sub;
124 | } catch (NumberFormatException e) {
125 | // Ignored
126 | }
127 | return 0L;
128 | }
129 |
130 |
131 | /**
132 | * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
133 | */
134 | static void closeQuietly(Closeable closeable) {
135 | if (closeable != null) {
136 | try {
137 | closeable.close();
138 | } catch (RuntimeException rethrown) {
139 | throw rethrown;
140 | } catch (Exception ignored) {
141 | }
142 | }
143 | }
144 |
145 | /**
146 | * Closes a file descriptor that has been duplicated.
147 | */
148 | static void closeFileDescriptor(FileDescriptor fd) {
149 | // Os.dup and Os.close was introduced in API 21 so this method shouldn't be called
150 | // in API < 21.
151 | if (Build.VERSION.SDK_INT >= 21) {
152 | try {
153 | Api21Impl.close(fd);
154 | // Catching ErrnoException will raise error in API < 21
155 | } catch (Exception ex) {
156 | Log.e(TAG, "Error closing fd.");
157 | }
158 | } else {
159 | Log.e(TAG, "closeFileDescriptor is called in API < 21, which must be wrong.");
160 | }
161 | }
162 |
163 | @RequiresApi(21)
164 | static class Api21Impl {
165 | private Api21Impl() {}
166 |
167 | @DoNotInline
168 | static FileDescriptor dup(FileDescriptor fileDescriptor) throws ErrnoException {
169 | return Os.dup(fileDescriptor);
170 | }
171 |
172 | @DoNotInline
173 | static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
174 | return Os.lseek(fd, offset, whence);
175 | }
176 |
177 | @DoNotInline
178 | static void close(FileDescriptor fd) throws ErrnoException {
179 | Os.close(fd);
180 | }
181 | }
182 |
183 | @RequiresApi(23)
184 | static class Api23Impl {
185 | private Api23Impl() {}
186 |
187 | @DoNotInline
188 | static void setDataSource(MediaMetadataRetriever retriever, MediaDataSource dataSource) {
189 | retriever.setDataSource(dataSource);
190 | }
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ActivityLifeCycleHelper.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.os.Bundle
6 | import app.grapheneos.camera.ui.activities.MainActivity
7 |
8 | class ActivityLifeCycleHelper(
9 | private val callback: (activity: MainActivity?) -> Unit
10 | ) : Application.ActivityLifecycleCallbacks {
11 |
12 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
13 |
14 | override fun onActivityStarted(activity: Activity) {}
15 |
16 | override fun onActivityResumed(activity: Activity) {
17 | if (activity is MainActivity) {
18 | callback.invoke(activity)
19 | }
20 | }
21 |
22 | override fun onActivityPaused(activity: Activity) {
23 | if (activity is MainActivity) {
24 | callback.invoke(null)
25 | }
26 | }
27 |
28 | override fun onActivityStopped(activity: Activity) {}
29 |
30 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
31 |
32 | override fun onActivityDestroyed(activity: Activity) {}
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/App.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera
2 |
3 | import android.Manifest
4 | import android.app.Application
5 | import android.content.pm.PackageManager
6 | import android.location.Location
7 | import android.location.LocationListener
8 | import android.location.LocationManager
9 | import android.os.Build
10 | import android.os.CountDownTimer
11 | import android.view.WindowManager
12 | import androidx.annotation.RequiresPermission
13 | import androidx.appcompat.app.AppCompatActivity
14 | import app.grapheneos.camera.ktx.isSystemApp
15 | import app.grapheneos.camera.ui.activities.MainActivity
16 | import com.google.android.material.color.DynamicColors
17 |
18 | class App : Application() {
19 |
20 | companion object {
21 | private const val STALE_LOCATION_THRESHOLD = 11 * 1000L
22 | }
23 |
24 | private var activity: MainActivity? = null
25 | private var location: Location? = null
26 |
27 | private var isLocationFetchInProgress = false
28 |
29 | private val locationManager by lazy {
30 | getSystemService(LocationManager::class.java)!!
31 | }
32 |
33 | private val locationListener: LocationListener by lazy {
34 | object : LocationListener {
35 | override fun onLocationChanged(changedLocation: Location) {
36 | location = listOf(location, changedLocation).getOptimalLocation()
37 | }
38 |
39 | override fun onProviderDisabled(provider: String) {
40 | if (!isAnyLocationProvideActive()) {
41 | activity?.indicateLocationProvidedIsDisabled()
42 | }
43 | }
44 |
45 | override fun onLocationChanged(locations: MutableList) {
46 | val location = locations.getOptimalLocation()
47 | if (location != null) {
48 | this@App.location = location
49 | }
50 | }
51 |
52 | override fun onProviderEnabled(provider: String) {}
53 | }
54 | }
55 |
56 | private val autoSleepDuration: Long = 5 * 60 * 1000 // 5 minutes
57 | private val autoSleepTimer = object : CountDownTimer(
58 | autoSleepDuration,
59 | autoSleepDuration / 2
60 | ) {
61 | override fun onTick(milliLeft: Long) {}
62 |
63 | override fun onFinish() {
64 | activity?.enableAutoSleep()
65 | }
66 | }
67 |
68 | private val activityLifeCycleHelper by lazy {
69 | ActivityLifeCycleHelper { activity ->
70 | if (activity != null) activity.disableAutoSleep() else this.activity?.enableAutoSleep()
71 | this.activity = activity
72 | }
73 | }
74 |
75 | fun isAnyLocationProvideActive(): Boolean {
76 | if (!locationManager.isLocationEnabled) return false
77 | val providers = locationManager.allProviders
78 |
79 | providers.forEach {
80 | if (locationManager.isProviderEnabled(it)) return true
81 | }
82 | return false
83 | }
84 |
85 | fun List.getOptimalLocation(): Location? {
86 | if (isNullOrEmpty()) return null
87 |
88 | var optimalLocation: Location? = null
89 | forEach { location ->
90 | if (location != null) {
91 | if (optimalLocation == null) {
92 | optimalLocation = location
93 | return@forEach
94 | }
95 |
96 | val timeDifference = location.time - optimalLocation.time
97 |
98 | // If the location is older than STALE_LOCATION_THRESHOLD ms
99 | if (timeDifference > STALE_LOCATION_THRESHOLD) {
100 | optimalLocation = location
101 | } else {
102 | // Compare their accuracy instead of time if the difference is below
103 | // threshold
104 | if (location.accuracy > optimalLocation.accuracy) {
105 | optimalLocation = location
106 | }
107 | }
108 | }
109 | }
110 | return optimalLocation
111 | }
112 |
113 | override fun onCreate() {
114 | super.onCreate()
115 | registerActivityLifecycleCallbacks(activityLifeCycleHelper)
116 | DynamicColors.applyToActivitiesIfAvailable(this)
117 | }
118 |
119 | @RequiresPermission(allOf = [Manifest.permission.ACCESS_COARSE_LOCATION])
120 | fun requestLocationUpdates(reAttach: Boolean = false) {
121 | if (!isLocationEnabled()) {
122 | activity?.indicateLocationProvidedIsDisabled()
123 | }
124 | if (isLocationFetchInProgress) {
125 | if (!reAttach) return
126 | dropLocationUpdates()
127 | }
128 | isLocationFetchInProgress = true
129 | if (location == null) {
130 | val providers = if (applicationInfo.isSystemApp() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
131 | listOf(LocationManager.FUSED_PROVIDER)
132 | } else {
133 | locationManager.allProviders
134 | }
135 | val locations = providers.map {
136 | locationManager.getLastKnownLocation(it)
137 | }
138 | val fetchedLocation = locations.getOptimalLocation()
139 | if (fetchedLocation != null) {
140 | location = fetchedLocation
141 | }
142 | }
143 |
144 | locationManager.allProviders.forEach { provider ->
145 | locationManager.requestLocationUpdates(
146 | provider,
147 | 2000,
148 | 10f,
149 | locationListener
150 | )
151 | }
152 | }
153 |
154 | fun dropLocationUpdates() {
155 | isLocationFetchInProgress = false
156 | locationManager.removeUpdates(locationListener)
157 | }
158 |
159 | fun getLocation(): Location? = location
160 |
161 | private fun isLocationEnabled(): Boolean = locationManager.isLocationEnabled
162 |
163 | fun shouldAskForLocationPermission() =
164 | checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
165 | checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
166 |
167 | override fun onTerminate() {
168 | super.onTerminate()
169 | unregisterActivityLifecycleCallbacks(activityLifeCycleHelper)
170 | }
171 |
172 | private fun AppCompatActivity.disableAutoSleep() {
173 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
174 | resetPreventScreenFromSleeping()
175 | }
176 |
177 | fun resetPreventScreenFromSleeping() {
178 | autoSleepTimer.cancel()
179 | autoSleepTimer.start()
180 | }
181 |
182 | private fun AppCompatActivity.enableAutoSleep() {
183 | autoSleepTimer.cancel()
184 | window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/AutoFinishOnSleep.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera
2 |
3 | import android.app.Activity
4 | import android.content.BroadcastReceiver
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.content.IntentFilter
8 | import androidx.core.app.ActivityCompat
9 |
10 | // Finishes the passed [activity] and the ones present below it in the stack if the screen
11 | // turns off
12 | class AutoFinishOnSleep(val activity: Activity) {
13 |
14 | companion object {
15 | private const val TAG = "AutoFinishOnSleep"
16 |
17 | private val intentFilter = IntentFilter().apply {
18 | addAction(Intent.ACTION_SCREEN_OFF)
19 | }
20 | }
21 |
22 | private val receiver = object: BroadcastReceiver() {
23 | override fun onReceive(context: Context?, intent: Intent?) {
24 | when (intent?.action) {
25 | Intent.ACTION_SCREEN_OFF -> {
26 | ActivityCompat.finishAffinity(activity)
27 | }
28 | }
29 | }
30 | }
31 |
32 | fun start() {
33 | activity.registerReceiver(receiver, intentFilter)
34 | }
35 |
36 | fun stop() {
37 | activity.unregisterReceiver(receiver)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/BlurBitmap.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera
2 |
3 | import android.graphics.Bitmap
4 | import kotlin.math.abs
5 | import kotlin.math.roundToInt
6 |
7 | object BlurBitmap {
8 | operator fun get(oBitmap: Bitmap): Bitmap {
9 | var sentBitmap = oBitmap
10 | val radius = 4
11 | val width = (sentBitmap.width * 0.1f).roundToInt()
12 | val height = (sentBitmap.height * 0.1f).roundToInt()
13 | sentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false)
14 | val bitmap = sentBitmap.copy(sentBitmap.config!!, true)
15 | val w = bitmap.width
16 | val h = bitmap.height
17 | val pix = IntArray(w * h)
18 | // Log.e("pix", w.toString() + " " + h + " " + pix.size)
19 | bitmap.getPixels(pix, 0, w, 0, 0, w, h)
20 | val wm = w - 1
21 | val hm = h - 1
22 | val wh = w * h
23 | val div = radius + radius + 1
24 | val r = IntArray(wh)
25 | val g = IntArray(wh)
26 | val b = IntArray(wh)
27 | var rSum: Int
28 | var gSum: Int
29 | var bSum: Int
30 | var x: Int
31 | var y: Int
32 | var i: Int
33 | var p: Int
34 | var yp: Int
35 | var yi: Int
36 | val vMin = IntArray(w.coerceAtLeast(h))
37 | var divSum = div + 1 shr 1
38 | divSum *= divSum
39 | val dv = IntArray(256 * divSum)
40 | i = 0
41 | while (i < 256 * divSum) {
42 | dv[i] = i / divSum
43 | i++
44 | }
45 | yi = 0
46 | var yw: Int = yi
47 | val stack = Array(div) { IntArray(3) }
48 | var stackPointer: Int
49 | var stackStart: Int
50 | var sir: IntArray
51 | var rbs: Int
52 | val r1 = radius + 1
53 | var routSum: Int
54 | var goutSum: Int
55 | var boutSum: Int
56 | var rinSum: Int
57 | var ginSum: Int
58 | var binSum: Int
59 | y = 0
60 | while (y < h) {
61 | bSum = 0
62 | gSum = bSum
63 | rSum = gSum
64 | boutSum = rSum
65 | goutSum = boutSum
66 | routSum = goutSum
67 | binSum = routSum
68 | ginSum = binSum
69 | rinSum = ginSum
70 | i = -radius
71 | while (i <= radius) {
72 | p = pix[yi + wm.coerceAtMost(i.coerceAtLeast(0))]
73 | sir = stack[i + radius]
74 | sir[0] = p and 0xff0000 shr 16
75 | sir[1] = p and 0x00ff00 shr 8
76 | sir[2] = p and 0x0000ff
77 | rbs = r1 - abs(i)
78 | rSum += sir[0] * rbs
79 | gSum += sir[1] * rbs
80 | bSum += sir[2] * rbs
81 | if (i > 0) {
82 | rinSum += sir[0]
83 | ginSum += sir[1]
84 | binSum += sir[2]
85 | } else {
86 | routSum += sir[0]
87 | goutSum += sir[1]
88 | boutSum += sir[2]
89 | }
90 | i++
91 | }
92 | stackPointer = radius
93 | x = 0
94 | while (x < w) {
95 | r[yi] = dv[rSum]
96 | g[yi] = dv[gSum]
97 | b[yi] = dv[bSum]
98 | rSum -= routSum
99 | gSum -= goutSum
100 | bSum -= boutSum
101 | stackStart = stackPointer - radius + div
102 | sir = stack[stackStart % div]
103 | routSum -= sir[0]
104 | goutSum -= sir[1]
105 | boutSum -= sir[2]
106 | if (y == 0) {
107 | vMin[x] = (x + radius + 1).coerceAtMost(wm)
108 | }
109 | p = pix[yw + vMin[x]]
110 | sir[0] = p and 0xff0000 shr 16
111 | sir[1] = p and 0x00ff00 shr 8
112 | sir[2] = p and 0x0000ff
113 | rinSum += sir[0]
114 | ginSum += sir[1]
115 | binSum += sir[2]
116 | rSum += rinSum
117 | gSum += ginSum
118 | bSum += binSum
119 | stackPointer = (stackPointer + 1) % div
120 | sir = stack[stackPointer % div]
121 | routSum += sir[0]
122 | goutSum += sir[1]
123 | boutSum += sir[2]
124 | rinSum -= sir[0]
125 | ginSum -= sir[1]
126 | binSum -= sir[2]
127 | yi++
128 | x++
129 | }
130 | yw += w
131 | y++
132 | }
133 | x = 0
134 | while (x < w) {
135 | bSum = 0
136 | gSum = bSum
137 | rSum = gSum
138 | boutSum = rSum
139 | goutSum = boutSum
140 | routSum = goutSum
141 | binSum = routSum
142 | ginSum = binSum
143 | rinSum = ginSum
144 | yp = -radius * w
145 | i = -radius
146 | while (i <= radius) {
147 | yi = 0.coerceAtLeast(yp) + x
148 | sir = stack[i + radius]
149 | sir[0] = r[yi]
150 | sir[1] = g[yi]
151 | sir[2] = b[yi]
152 | rbs = r1 - abs(i)
153 | rSum += r[yi] * rbs
154 | gSum += g[yi] * rbs
155 | bSum += b[yi] * rbs
156 | if (i > 0) {
157 | rinSum += sir[0]
158 | ginSum += sir[1]
159 | binSum += sir[2]
160 | } else {
161 | routSum += sir[0]
162 | goutSum += sir[1]
163 | boutSum += sir[2]
164 | }
165 | if (i < hm) {
166 | yp += w
167 | }
168 | i++
169 | }
170 | yi = x
171 | stackPointer = radius
172 | y = 0
173 | while (y < h) {
174 |
175 | // Preserve alpha channel: ( 0xff000000 & pix[yi] )
176 | pix[yi] =
177 | -0x1000000 and pix[yi] or (dv[rSum] shl 16) or (dv[gSum] shl 8) or dv[bSum]
178 | rSum -= routSum
179 | gSum -= goutSum
180 | bSum -= boutSum
181 | stackStart = stackPointer - radius + div
182 | sir = stack[stackStart % div]
183 | routSum -= sir[0]
184 | goutSum -= sir[1]
185 | boutSum -= sir[2]
186 | if (x == 0) {
187 | vMin[y] = (y + r1).coerceAtMost(hm) * w
188 | }
189 | p = x + vMin[y]
190 | sir[0] = r[p]
191 | sir[1] = g[p]
192 | sir[2] = b[p]
193 | rinSum += sir[0]
194 | ginSum += sir[1]
195 | binSum += sir[2]
196 | rSum += rinSum
197 | gSum += ginSum
198 | bSum += binSum
199 | stackPointer = (stackPointer + 1) % div
200 | sir = stack[stackPointer]
201 | routSum += sir[0]
202 | goutSum += sir[1]
203 | boutSum += sir[2]
204 | rinSum -= sir[0]
205 | ginSum -= sir[1]
206 | binSum -= sir[2]
207 | yi += w
208 | y++
209 | }
210 | x++
211 | }
212 | // Log.e("pix", w.toString() + " " + h + " " + pix.size)
213 | bitmap.setPixels(pix, 0, w, 0, 0, w, h)
214 | return bitmap
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/GSlideTransformer.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera
2 |
3 | import android.view.View
4 | import androidx.viewpager2.widget.ViewPager2
5 | import kotlin.math.abs
6 |
7 | class GSlideTransformer : ViewPager2.PageTransformer {
8 |
9 | companion object {
10 | private const val MIN_SCALE = 0.75f
11 | }
12 |
13 | override fun transformPage(view: View, position: Float) {
14 | view.apply {
15 | val pageWidth = width
16 | when {
17 | position < -1 -> { // [-Infinity,-1)
18 | // This page is way off-screen to the left.
19 | // alpha = 0f
20 | }
21 | position <= 0 -> { // [-1,0]
22 | // Use the default slide transition when moving to the left page
23 | alpha = 1f
24 | translationX = 0f
25 | scaleX = 1f
26 | scaleY = 1f
27 | }
28 | position <= 1 -> { // (0,1]
29 | // Fade the page out.
30 | alpha = 1 - position
31 |
32 | // Counteract the default slide transition
33 | translationX = pageWidth * -position
34 |
35 | // Scale the page down (between MIN_SCALE and 1)
36 | val scaleFactor = (MIN_SCALE + (1 - MIN_SCALE) * (1 - abs(position)))
37 | scaleX = scaleFactor
38 | scaleY = scaleFactor
39 | }
40 | else -> { // (1,+Infinity]
41 | // This page is way off-screen to the right.
42 | alpha = 0f
43 | }
44 | }
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/GallerySliderAdapter.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera
2 |
3 | import android.content.Intent
4 | import android.graphics.Bitmap
5 | import android.graphics.ImageDecoder
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.ImageView
10 | import androidx.recyclerview.widget.RecyclerView
11 | import app.grapheneos.camera.capturer.getVideoThumbnail
12 | import app.grapheneos.camera.databinding.GallerySlideBinding
13 | import app.grapheneos.camera.ui.ZoomableImageView
14 | import app.grapheneos.camera.ui.activities.InAppGallery
15 | import app.grapheneos.camera.ui.activities.VideoPlayer
16 | import app.grapheneos.camera.ui.fragment.GallerySlide
17 | import app.grapheneos.camera.util.executeIfAlive
18 | import kotlin.math.max
19 |
20 | class GallerySliderAdapter(
21 | private val gActivity: InAppGallery,
22 | val items: ArrayList
23 | ) : RecyclerView.Adapter() {
24 |
25 | var atLeastOneBindViewHolderCall = false
26 |
27 | private val layoutInflater: LayoutInflater = LayoutInflater.from(gActivity)
28 |
29 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GallerySlide {
30 | return GallerySlide(GallerySlideBinding.inflate(layoutInflater, parent, false))
31 | }
32 |
33 | override fun getItemId(position: Int): Long {
34 | return items[position].hashCode().toLong()
35 | }
36 |
37 | override fun onBindViewHolder(holder: GallerySlide, position: Int) {
38 | val mediaPreview: ZoomableImageView = holder.binding.slidePreview
39 | // Log.d("GallerySliderAdapter", "postiion $position, preview ${System.identityHashCode(mediaPreview)}")
40 | val playButton: ImageView = holder.binding.playButton
41 | val item = items[position]
42 |
43 | mediaPreview.setGalleryActivity(gActivity)
44 | mediaPreview.disableZooming()
45 | mediaPreview.setOnClickListener(null)
46 | mediaPreview.visibility = View.INVISIBLE
47 | mediaPreview.setImageBitmap(null)
48 |
49 | val placeholderText = holder.binding.placeholderText.root
50 | if (atLeastOneBindViewHolderCall) {
51 | placeholderText.visibility = View.VISIBLE
52 | placeholderText.setText("…")
53 | }
54 | atLeastOneBindViewHolderCall = true
55 |
56 | playButton.visibility = View.GONE
57 |
58 | holder.currentPostion = position
59 |
60 | gActivity.asyncImageLoader.executeIfAlive {
61 | val bitmap: Bitmap? = try {
62 | if (item.type == ITEM_TYPE_VIDEO) {
63 | getVideoThumbnail(gActivity, item.uri)
64 | } else {
65 | val source = ImageDecoder.createSource(gActivity.contentResolver, item.uri)
66 | ImageDecoder.decodeBitmap(source, ImageDownscaler)
67 | }
68 | } catch (e: Exception) { null }
69 |
70 | gActivity.mainExecutor.execute {
71 | if (holder.currentPostion == position) {
72 | if (bitmap != null) {
73 | placeholderText.visibility = View.GONE
74 | mediaPreview.visibility = View.VISIBLE
75 | mediaPreview.setImageBitmap(bitmap)
76 |
77 | if (item.type == ITEM_TYPE_VIDEO) {
78 | playButton.visibility = View.VISIBLE
79 | } else if (item.type == ITEM_TYPE_IMAGE) {
80 | mediaPreview.enableZooming()
81 | }
82 |
83 | mediaPreview.setOnClickListener {
84 | val curItem = getCurrentItem()
85 | if (curItem.type == ITEM_TYPE_VIDEO) {
86 | val intent = Intent(gActivity, VideoPlayer::class.java)
87 | intent.putExtra(VideoPlayer.VIDEO_URI, curItem.uri)
88 | intent.putExtra(VideoPlayer.IN_SECURE_MODE, gActivity.isSecureMode)
89 |
90 | gActivity.startActivity(intent)
91 | }
92 | }
93 | } else {
94 | mediaPreview.visibility = View.INVISIBLE
95 |
96 | val resId = if (item.type == ITEM_TYPE_IMAGE) {
97 | R.string.inaccessible_image
98 | } else { R.string.inaccessible_video }
99 |
100 | placeholderText.visibility = View.VISIBLE
101 | placeholderText.setText(gActivity.getString(resId, item.dateString))
102 | }
103 | } else {
104 | bitmap?.recycle()
105 | }
106 | }
107 | }
108 | }
109 |
110 | fun removeItem(item: CapturedItem) {
111 | removeChildAt(items.indexOf(item))
112 | }
113 |
114 | private fun removeChildAt(index: Int) {
115 | items.removeAt(index)
116 |
117 | // Close gallery if no files are present
118 | if (items.isEmpty()) {
119 | gActivity.showMessage(
120 | gActivity.getString(R.string.existing_no_image)
121 | )
122 | gActivity.finish()
123 | }
124 |
125 | notifyItemRemoved(index)
126 | }
127 |
128 | fun getCurrentItem(): CapturedItem {
129 | return items[gActivity.gallerySlider.currentItem]
130 | }
131 |
132 | override fun getItemCount(): Int {
133 | return items.size
134 | }
135 | }
136 |
137 | object ImageDownscaler : ImageDecoder.OnHeaderDecodedListener {
138 | override fun onHeaderDecoded(decoder: ImageDecoder,
139 | info: ImageDecoder.ImageInfo, source: ImageDecoder.Source) {
140 | val size = info.size
141 | val w = size.width
142 | val h = size.height
143 | // limit the max size of the bitmap to avoid bumping into bitmap size limit
144 | // (100 MB)
145 | val largerSide = max(w, h)
146 | val maxSide = 4500
147 |
148 | if (largerSide > maxSide) {
149 | val ratio = maxSide.toDouble() / largerSide
150 | decoder.setTargetSize((ratio * w).toInt(), (ratio * h).toInt())
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/NumInputFilter.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera
2 |
3 | import android.text.InputFilter
4 | import android.text.Spanned
5 | import app.grapheneos.camera.ui.activities.MoreSettings
6 |
7 | class NumInputFilter(private val settings: MoreSettings) : InputFilter {
8 |
9 | override fun filter(
10 | source: CharSequence,
11 | start: Int,
12 | end: Int,
13 | dest: Spanned,
14 | dstart: Int,
15 | dend: Int
16 | ): CharSequence? {
17 | try {
18 | val input = (dest.subSequence(0, dstart).toString() + source + dest.subSequence(
19 | dend,
20 | dest.length
21 | )).toInt()
22 | if (isInRange(input)) {
23 | return null
24 | } else {
25 | settings.showMessage(settings.getString(
26 | R.string.photo_quality_number_limit, min, max))
27 | }
28 | } catch (e: NumberFormatException) {
29 | e.printStackTrace()
30 | }
31 | return ""
32 | }
33 |
34 | private fun isInRange(value: Int): Boolean {
35 | return value in min..max
36 | }
37 |
38 | companion object {
39 | const val min = 1
40 | const val max = 100
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/TunePlayer.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera
2 |
3 | import android.content.Context
4 | import android.media.MediaPlayer
5 | import android.net.Uri
6 | import android.os.Handler
7 | import android.os.SystemClock
8 | import app.grapheneos.camera.ui.activities.MainActivity
9 |
10 | private fun prepareMediaPlayer(context: Context, resid: Int, listener: MediaPlayer.OnPreparedListener) {
11 | MediaPlayer().apply {
12 | setDataSource(context, Uri.parse("android.resource://" + context.getPackageName() + "/" + resid))
13 | setOnPreparedListener(listener)
14 | prepareAsync()
15 | }
16 | }
17 |
18 | class TunePlayer(val context: MainActivity) {
19 |
20 | private lateinit var shutterPlayer: MediaPlayer
21 |
22 | private lateinit var fSPlayer: MediaPlayer
23 |
24 | private lateinit var tIPlayer: MediaPlayer
25 | private lateinit var tCPlayer: MediaPlayer
26 |
27 | private lateinit var vRecPlayer: MediaPlayer
28 | private lateinit var vStopPlayer: MediaPlayer
29 |
30 | init {
31 | prepareMediaPlayer(context, R.raw.image_shot, { player -> shutterPlayer = player })
32 |
33 | prepareMediaPlayer(context, R.raw.focus_start, { player -> fSPlayer = player })
34 |
35 | prepareMediaPlayer(context, R.raw.timer_increment, { player -> tIPlayer = player })
36 | prepareMediaPlayer(context, R.raw.timer_final_second, { player -> tCPlayer = player })
37 |
38 | prepareMediaPlayer(context, R.raw.video_start, { player -> vRecPlayer = player })
39 | prepareMediaPlayer(context, R.raw.video_stop, { player -> vStopPlayer = player })
40 | }
41 |
42 | private fun shouldNotPlayTune(): Boolean {
43 | return !context.camConfig.enableCameraSounds
44 | }
45 |
46 | fun playShutterSound() {
47 | if (shouldNotPlayTune() || !::shutterPlayer.isInitialized) return
48 | shutterPlayer.seekTo(0)
49 | shutterPlayer.start()
50 | }
51 |
52 | fun playVRStartSound(handler: Handler, onPlayed: Runnable) {
53 | if (shouldNotPlayTune() || !::vRecPlayer.isInitialized) {
54 | onPlayed.run()
55 | return
56 | }
57 | vRecPlayer.seekTo(0)
58 | vRecPlayer.start()
59 | vRecPlayer.setOnCompletionListener({
60 | handler.postDelayed(onPlayed, 10)
61 | })
62 | }
63 |
64 | fun playVRStopSound() {
65 | if (shouldNotPlayTune() || !::vStopPlayer.isInitialized) return
66 | vStopPlayer.seekTo(0)
67 | vStopPlayer.start()
68 | }
69 |
70 | fun playTimerIncrementSound() {
71 | if (shouldNotPlayTune() || !::tIPlayer.isInitialized) return
72 | tIPlayer.seekTo(0)
73 | tIPlayer.start()
74 | }
75 |
76 | fun playTimerFinalSSound() {
77 | if (shouldNotPlayTune() || !::tCPlayer.isInitialized) return
78 | tCPlayer.seekTo(0)
79 | tCPlayer.start()
80 | }
81 |
82 | fun playFocusStartSound() {
83 | if (shouldNotPlayTune() || !::fSPlayer.isInitialized) return
84 | fSPlayer.seekTo(0)
85 | fSPlayer.start()
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/analyzer/QRAnalyzer.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.analyzer
2 |
3 | import android.util.Log
4 | import androidx.camera.core.ImageAnalysis.Analyzer
5 | import androidx.camera.core.ImageProxy
6 | import app.grapheneos.camera.ui.activities.MainActivity
7 | import com.google.zxing.BarcodeFormat
8 | import com.google.zxing.BinaryBitmap
9 | import com.google.zxing.DecodeHintType
10 | import com.google.zxing.MultiFormatReader
11 | import com.google.zxing.PlanarYUVLuminanceSource
12 | import com.google.zxing.ReaderException
13 | import com.google.zxing.common.HybridBinarizer
14 | import java.util.EnumMap
15 | import kotlin.math.roundToInt
16 |
17 | class QRAnalyzer(private val mActivity: MainActivity) : Analyzer {
18 | companion object {
19 | private const val TAG = "QRCodeImageAnalyzer"
20 | }
21 |
22 | private var frameCounter = 0
23 | private var lastFpsTimestamp = System.nanoTime()
24 |
25 | private val reader = MultiFormatReader()
26 | private var imageData = ByteArray(0)
27 |
28 | init {
29 | refreshHints()
30 | }
31 |
32 | fun refreshHints() {
33 | val camConfig = mActivity.camConfig
34 |
35 | val supportedHints: MutableMap = EnumMap(
36 | DecodeHintType::class.java
37 | )
38 |
39 | Log.i(TAG, "allowedFormats: ${camConfig.allowedFormats}")
40 |
41 | supportedHints[DecodeHintType.POSSIBLE_FORMATS] =
42 | if (camConfig.scanAllCodes) {
43 | BarcodeFormat.values().asList()
44 | } else {
45 | camConfig.allowedFormats
46 | }
47 |
48 | reader.setHints(supportedHints)
49 | }
50 |
51 | override fun analyze(image: ImageProxy) {
52 | val plane = image.planes[0]
53 | val byteBuffer = plane.buffer
54 | val rotationDegrees = image.imageInfo.rotationDegrees
55 |
56 | if (imageData.size != byteBuffer.capacity()) {
57 | imageData = ByteArray(byteBuffer.capacity())
58 | }
59 | byteBuffer.get(imageData)
60 |
61 | val previewWidth: Int
62 | val previewHeight: Int
63 |
64 | if (rotationDegrees == 0 || rotationDegrees == 180) {
65 | previewWidth = mActivity.previewView.width
66 | previewHeight = mActivity.previewView.height
67 | } else {
68 | previewWidth = mActivity.previewView.height
69 | previewHeight = mActivity.previewView.width
70 | }
71 |
72 | val scaleFactor = if (previewWidth < previewHeight) {
73 | image.width / previewWidth.toFloat()
74 | } else {
75 | image.height / previewHeight.toFloat()
76 | }
77 |
78 | val size = mActivity.qrOverlay.size * scaleFactor
79 |
80 | val left = (image.width - size) / 2
81 | val top = (image.height - size) / 2
82 |
83 | val source = PlanarYUVLuminanceSource(
84 | imageData,
85 | plane.rowStride, image.height,
86 | left.roundToInt(), top.roundToInt(),
87 | size.roundToInt(), size.roundToInt(),
88 | false
89 | )
90 |
91 | val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
92 | reader.reset()
93 | try {
94 | reader.decodeWithState(binaryBitmap).text?.let {
95 | mActivity.onScanResultSuccess(it)
96 | }
97 | } catch (e: ReaderException) {
98 | val invertedSource = source.invert()
99 | val invertedBinaryBitmap = BinaryBitmap(HybridBinarizer(invertedSource))
100 | reader.reset()
101 | try {
102 | reader.decodeWithState(invertedBinaryBitmap).text?.let {
103 | mActivity.onScanResultSuccess(it)
104 | }
105 | } catch (e: ReaderException) {
106 | }
107 | }
108 |
109 | // Compute the FPS of the entire pipeline
110 | val frameCount = 10
111 | if (++frameCounter % frameCount == 0) {
112 | frameCounter = 0
113 | val now = System.nanoTime()
114 | val delta = now - lastFpsTimestamp
115 | val fps = 1_000_000_000 * frameCount.toFloat() / delta
116 | Log.d(TAG, "Analysis FPS: ${"%.02f".format(fps)}")
117 | lastFpsTimestamp = now
118 | }
119 |
120 | image.close()
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/capturer/ImageSaverException.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.capturer
2 |
3 | class ImageSaverException(val place: Place, cause: Exception? = null) : Exception(cause) {
4 | enum class Place {
5 | IMAGE_EXTRACTION,
6 | IMAGE_CROPPING,
7 | EXIF_PARSING,
8 | FILE_CREATION,
9 | FILE_WRITE,
10 | FILE_WRITE_COMPLETION,
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ktx/ApplicationInfo.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ktx
2 |
3 | import android.content.pm.ApplicationInfo
4 |
5 | fun ApplicationInfo.isSystemApp() : Boolean {
6 | return (flags and ApplicationInfo.FLAG_SYSTEM) != 0
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ktx/PreviewView.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ktx
2 |
3 | import androidx.camera.view.PreviewView
4 | import androidx.constraintlayout.widget.ConstraintLayout
5 | import androidx.core.view.updateLayoutParams
6 |
7 | fun PreviewView.markAs4by3Layout() = applyRatio(3.0, 4.0)
8 |
9 | fun PreviewView.markAs16by9Layout() = applyRatio(9.0, 16.0)
10 |
11 | private fun PreviewView.applyRatio(width: Double, height: Double) {
12 | updateLayoutParams {
13 | dimensionRatio = "H,$width:$height"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ktx/SystemSettingsObserver.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ktx
2 |
3 | import android.content.Context
4 | import android.database.ContentObserver
5 | import android.os.Handler
6 | import android.provider.Settings
7 | import androidx.lifecycle.DefaultLifecycleObserver
8 | import androidx.lifecycle.Lifecycle
9 | import androidx.lifecycle.LifecycleOwner
10 |
11 | class SystemSettingsObserver(
12 | lifecycle: Lifecycle,
13 | private val key: String,
14 | private val context: Context,
15 | private val notifyForDescendants: Boolean = true,
16 | private val callback: () -> Unit
17 | ) : DefaultLifecycleObserver, ContentObserver(Handler(context.mainLooper)) {
18 |
19 | private val contentResolver by lazy { context.applicationContext.contentResolver }
20 |
21 | init {
22 | lifecycle.addObserver(this)
23 | }
24 |
25 | override fun onChange(selfChange: Boolean) {
26 | super.onChange(selfChange)
27 | callback.invoke()
28 | }
29 |
30 | override fun onCreate(owner: LifecycleOwner) {
31 | super.onCreate(owner)
32 | contentResolver.registerContentObserver(
33 | Settings.System.getUriFor(key), notifyForDescendants, this
34 | )
35 | }
36 |
37 | override fun onDestroy(owner: LifecycleOwner) {
38 | super.onDestroy(owner)
39 | contentResolver.unregisterContentObserver(this)
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/notifier/SensorOrientationChangeNotifier.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.notifier
2 |
3 | import android.content.Context
4 | import android.hardware.Sensor
5 | import android.hardware.SensorEvent
6 | import android.hardware.SensorEventListener
7 | import android.hardware.SensorManager
8 | import android.view.View
9 | import app.grapheneos.camera.ui.activities.MainActivity
10 | import java.lang.ref.WeakReference
11 | import kotlin.math.abs
12 | import kotlin.math.atan
13 | import kotlin.math.floor
14 | import kotlin.math.sqrt
15 |
16 | class SensorOrientationChangeNotifier private constructor(
17 | private val mainActivity: MainActivity
18 | ) {
19 |
20 | companion object {
21 | private var mInstance: SensorOrientationChangeNotifier? = null
22 | fun getInstance(mActivity: MainActivity): SensorOrientationChangeNotifier? {
23 | if (mInstance == null) mInstance = SensorOrientationChangeNotifier(mActivity)
24 | return mInstance
25 | }
26 |
27 | fun clearInstance() {
28 | mInstance = null
29 | }
30 |
31 | // Greater the threshold (in degrees), more the chances of the gyroscope being
32 | // visible via the ENTRY_CRITERIA
33 | private const val X_THRESHOLD = 5
34 |
35 | // The gyroscope shall be explicitly made visible only if it's within the ENTRY_
36 | // CRITERIA and if the device isn't moving too fast i.e. (lastX - currentX) is below
37 | // threshold
38 | private const val X_ENTRY_MIN = -8F
39 | private const val X_ENTRY_MAX = 8F
40 |
41 | // If the current angle for a given axis is beyond the EXIT_CRITERIA the listener
42 | // will just hide the gyroscope (and just return the control back from the method as
43 | // executing further statements won't make sense)
44 | private const val X_EXIT_MIN = -45F
45 | private const val X_EXIT_MAX = 45F
46 |
47 |
48 | private const val Z_THRESHOLD = 5
49 |
50 | private const val Z_ENTRY_MIN = -25F
51 | private const val Z_ENTRY_MAX = 25F
52 |
53 | private const val Z_EXIT_MIN = -45F
54 | private const val Z_EXIT_MAX = 45F
55 | }
56 |
57 | var mOrientation = mainActivity.getRotation()
58 | private set
59 |
60 | private val mListeners = ArrayList>(3)
61 | private val mSensorEventListener: NotifierSensorEventListener
62 | private val mSensorManager: SensorManager
63 |
64 | /**
65 | * Call on activity reset()
66 | */
67 | private fun onResume() {
68 | mSensorManager.registerListener(
69 | mSensorEventListener,
70 | mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
71 | SensorManager.SENSOR_DELAY_NORMAL
72 | )
73 | notifyListeners(true)
74 | }
75 |
76 | /**
77 | * Call on activity onPause()
78 | */
79 | private fun onPause() {
80 | mSensorManager.unregisterListener(mSensorEventListener)
81 | }
82 |
83 | inner class NotifierSensorEventListener : SensorEventListener {
84 |
85 | var lastX = 0f
86 | var lastZ = 0f
87 |
88 | private val ALPHA = 0.7f
89 | private val filteredValues = FloatArray(3)
90 |
91 | override fun onSensorChanged(event: SensorEvent) {
92 |
93 | filteredValues[0] = ALPHA * filteredValues[0] + (1 - ALPHA) * event.values[0]
94 | filteredValues[1] = ALPHA * filteredValues[1] + (1 - ALPHA) * event.values[1]
95 | filteredValues[2] = ALPHA * filteredValues[2] + (1 - ALPHA) * event.values[2]
96 |
97 | var x : Float = filteredValues[0]
98 | var y : Float = filteredValues[1]
99 | var z : Float = filteredValues[2]
100 |
101 | var newOrientation = mOrientation
102 | if (x < 5 && x > -5 && y > 5) newOrientation =
103 | 0 else if (x < -5 && y < 5 && y > -5) newOrientation =
104 | 90 else if (x < 5 && x > -5 && y < -5) newOrientation =
105 | 180 else if (x > 5 && y < 5 && y > -5) newOrientation = 270
106 |
107 | if (mOrientation != newOrientation) {
108 | mOrientation = newOrientation
109 | notifyListeners()
110 | }
111 |
112 | if (!mainActivity.camConfig.shouldShowGyroscope()) {
113 | mainActivity.gCircleFrame.visibility = View.GONE
114 | return
115 | }
116 |
117 | if (newOrientation == 90 || newOrientation == 270) {
118 | val t = x
119 | x = y
120 | y = t
121 | }
122 |
123 | x = ((180 / Math.PI) * atan(x / sqrt(y * y + z * z))).toFloat()
124 | z = ((180 / Math.PI) * atan(z / sqrt(y * y + x * x))).toFloat()
125 |
126 | if (z < Z_EXIT_MIN || z > Z_EXIT_MAX) {
127 | mainActivity.gCircleFrame.visibility = View.GONE
128 | return
129 | }
130 |
131 | if (x < X_EXIT_MIN || x > X_EXIT_MAX) {
132 | mainActivity.gCircleFrame.visibility = View.GONE
133 | return
134 | }
135 |
136 | if (x in X_ENTRY_MIN..X_ENTRY_MAX) {
137 | if (abs(x - lastX) < X_THRESHOLD) {
138 | if (z in Z_ENTRY_MIN..Z_ENTRY_MAX) {
139 | if (abs(z - lastZ) < Z_THRESHOLD) {
140 | mainActivity.gCircleFrame.visibility = View.VISIBLE
141 | }
142 | }
143 | }
144 | }
145 |
146 | updateGyro(x, z)
147 | }
148 |
149 | fun updateGyro(xAngle: Float, zAngle: Float) {
150 |
151 | val xAngle = floor(xAngle)
152 | val zAngle = floor(zAngle)
153 |
154 | mainActivity.onDeviceAngleChange(xAngle, zAngle)
155 |
156 | lastX = xAngle
157 | lastZ = zAngle
158 | }
159 |
160 | override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
161 | }
162 |
163 | fun forceUpdateGyro() {
164 | mSensorEventListener.let {
165 | it.updateGyro(it.lastX, it.lastZ)
166 | }
167 | }
168 |
169 | interface Listener {
170 | fun onOrientationChange(orientation: Int)
171 | }
172 |
173 | fun addListener(listener: Listener) {
174 | if (get(listener) == null) // prevent duplications
175 | mListeners.add(WeakReference(listener))
176 | if (mListeners.size == 1) {
177 | onResume() // this is the first client
178 | }
179 | }
180 |
181 | fun remove(listener: Listener) {
182 | val listenerWR = get(listener)
183 | remove(listenerWR)
184 | }
185 |
186 | private fun remove(listenerWR: WeakReference?) {
187 | if (listenerWR != null) mListeners.remove(listenerWR)
188 | if (mListeners.size == 0) {
189 | onPause()
190 | }
191 | }
192 |
193 | private operator fun get(listener: Listener): WeakReference? {
194 | for (existingListener in mListeners) if (existingListener.get() === listener) return existingListener
195 | return null
196 | }
197 |
198 | fun notifyListeners(manualUpdate: Boolean = false) {
199 |
200 | if (manualUpdate) {
201 | mOrientation = mainActivity.getRotation()
202 | }
203 |
204 | val deadLinksArr = ArrayList>()
205 | for (wr in mListeners) {
206 | if (wr.get() == null) deadLinksArr.add(wr) else wr.get()!!
207 | .onOrientationChange(mOrientation)
208 | }
209 |
210 | // remove dead references
211 | for (wr in deadLinksArr) {
212 | mListeners.remove(wr)
213 | }
214 | }
215 |
216 | init {
217 | mSensorEventListener = NotifierSensorEventListener()
218 | mSensorManager = mainActivity.getSystemService(Context.SENSOR_SERVICE) as SensorManager
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/BottomTabLayout.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.ViewGroup
6 | import androidx.core.view.ViewCompat
7 | import app.grapheneos.camera.CameraMode
8 | import com.google.android.material.tabs.TabLayout
9 |
10 | class BottomTabLayout @JvmOverloads constructor(
11 | context: Context, attrs: AttributeSet? = null
12 | ) : TabLayout(context, attrs) {
13 |
14 | private var sp = 0
15 |
16 | private val snapPoints: ArrayList = arrayListOf()
17 |
18 | private lateinit var tabParent: ViewGroup
19 |
20 | val selectedTab: Tab?
21 | get() {
22 | return getTabAt(selectedTabPosition)
23 | }
24 |
25 | override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
26 | super.onLayout(changed, l, t, r, b)
27 |
28 | if (tabCount == 0) return
29 |
30 | tabParent = getChildAt(0) as ViewGroup
31 | val firstTab = tabParent.getChildAt(0)
32 | val lastTab = tabParent.getChildAt(tabParent.childCount - 1)
33 | sp = width / 2 - firstTab.width / 2
34 | getChildAt(0).setPaddingRelative(
35 | sp,
36 | 0,
37 | width / 2 - lastTab.width / 2,
38 | 0
39 | )
40 |
41 | snapPoints.clear()
42 |
43 | for (tabIndex in 0 until tabCount) {
44 | snapPoints.add(calculateScrollXStartForTab(tabIndex))
45 | snapPoints.add(calculateScrollXEndForTab(tabIndex))
46 | }
47 |
48 | centerSelectedTab()
49 | }
50 |
51 | private fun centerSelectedTab() {
52 | getTabAt(selectedTabPosition)?.let {
53 | centerTab(it)
54 | }
55 | }
56 |
57 | override fun onScrollChanged(x: Int, y: Int, oldX: Int, oldY: Int) {
58 | super.onScrollChanged(x, y, oldX, oldY)
59 |
60 | if (snapPoints.last() != 0) {
61 |
62 | for (i in 0 until snapPoints.size step 2) {
63 |
64 | val start = snapPoints[i]
65 | val end = snapPoints[i + 1]
66 |
67 | if (x in start..end) {
68 | val index = i / 2
69 | if (selectedTabPosition != index) {
70 | return selectTab(getTabAt(index))
71 | }
72 | }
73 |
74 | }
75 | }
76 | }
77 |
78 | fun getTabAtX(x: Int): Tab? {
79 | for (i in 0 until snapPoints.size step 2) {
80 |
81 | val start = snapPoints[i]
82 | val end = snapPoints[i + 1]
83 |
84 | if (x in start..end) {
85 | val index = i / 2
86 | if (selectedTabPosition != index) {
87 | return getTabAt(index)
88 | }
89 | }
90 |
91 | }
92 | return null
93 | }
94 |
95 | fun centerTab(tab: Tab) {
96 | if (!this::tabParent.isInitialized) return
97 | val targetScrollX = calculateScrollXForTab(tab.position)
98 |
99 | if (scrollX != targetScrollX)
100 | smoothScrollTo(targetScrollX, 0)
101 | }
102 |
103 | private fun calculateScrollXForTab(position: Int): Int {
104 | val selectedChild = tabParent.getChildAt(position) ?: return 0
105 | val selectedWidth = selectedChild.width
106 |
107 | return selectedChild.left + selectedWidth / 2 - width / 2
108 | }
109 |
110 | private fun calculateScrollXStartForTab(position: Int): Int {
111 | val selectedChild = tabParent.getChildAt(position) ?: return 0
112 | val selectedWidth = selectedChild.width
113 |
114 | return selectedChild.left + selectedWidth / 2 - width / 2
115 | }
116 |
117 | private fun calculateScrollXEndForTab(position: Int): Int {
118 | val selectedChild = tabParent.getChildAt(position) ?: return 0
119 | val selectedWidth = selectedChild.width
120 |
121 | return selectedChild.left + selectedWidth - width / 2
122 | }
123 |
124 | fun getAllModes(): Set {
125 | return IntRange(0, tabCount - 1).map {
126 | getTabAt(it)!!.tag as CameraMode
127 | }.toSet()
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/CountDownTimerUI.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui
2 |
3 | import android.animation.ValueAnimator
4 | import android.content.Context
5 | import android.os.CountDownTimer
6 | import android.util.AttributeSet
7 | import android.view.Gravity
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import android.view.animation.AccelerateDecelerateInterpolator
11 | import androidx.appcompat.widget.AppCompatTextView
12 | import androidx.camera.core.AspectRatio
13 | import app.grapheneos.camera.CamConfig
14 | import app.grapheneos.camera.ui.activities.CaptureActivity
15 | import app.grapheneos.camera.ui.activities.MainActivity
16 |
17 | class CountDownTimerUI @JvmOverloads constructor(
18 | context: Context, attrs: AttributeSet? = null
19 | ) : AppCompatTextView(context, attrs) {
20 |
21 | private lateinit var timer: CountDownTimer
22 | lateinit var mActivity: MainActivity
23 | lateinit var camConfig: CamConfig
24 |
25 | companion object {
26 | private const val textAnimDuration = 700L
27 |
28 | const val startSize = 12f
29 | const val endSize = 100f
30 | }
31 |
32 | var isRunning = false
33 | private set
34 |
35 | fun setMainActivity(mainActivity: MainActivity) {
36 | this.mActivity = mainActivity
37 | camConfig = mainActivity.camConfig
38 | }
39 |
40 | fun startTimer() {
41 | cancelTimer()
42 |
43 | timer = object : CountDownTimer(mActivity.timerDuration * 1000L, 1000L) {
44 | override fun onTick(pendingMs: Long) {
45 | val pendingS = (pendingMs / 1000) + 1
46 |
47 | val scaleAnimation = ValueAnimator.ofFloat(startSize, endSize)
48 | scaleAnimation.interpolator = AccelerateDecelerateInterpolator()
49 | scaleAnimation.duration = textAnimDuration
50 |
51 | scaleAnimation.addUpdateListener { valueAnimator ->
52 | textSize = valueAnimator.animatedValue as Float
53 | }
54 |
55 | val opacityAnimation = ValueAnimator.ofFloat(1f, 0f)
56 | opacityAnimation.interpolator = AccelerateDecelerateInterpolator()
57 | opacityAnimation.duration = textAnimDuration
58 |
59 | opacityAnimation.addUpdateListener { valueAnimator ->
60 | alpha = valueAnimator.animatedValue as Float
61 | }
62 |
63 | scaleAnimation.start()
64 | opacityAnimation.start()
65 |
66 | text = pendingS.toString()
67 |
68 | if (text == "1") {
69 | camConfig.mPlayer.playTimerFinalSSound()
70 | } else {
71 | camConfig.mPlayer.playTimerIncrementSound()
72 | }
73 | }
74 |
75 | override fun onFinish() {
76 | onTimerEnd()
77 | if (mActivity is CaptureActivity) {
78 | (mActivity as CaptureActivity).takePicture()
79 | } else {
80 | mActivity.imageCapturer.takePicture()
81 | }
82 | }
83 |
84 | }
85 |
86 | beforeTimeStarts()
87 |
88 | timer.start()
89 | }
90 |
91 | fun cancelTimer() {
92 | if (::timer.isInitialized) {
93 | timer.cancel()
94 | onTimerEnd(true)
95 | }
96 | }
97 |
98 | private fun beforeTimeStarts() {
99 |
100 | val params: ViewGroup.LayoutParams = layoutParams
101 | params.height = if (camConfig.aspectRatio == AspectRatio.RATIO_4_3) {
102 | mActivity.previewView.width * 4 / 3
103 | } else {
104 | mActivity.previewView.height
105 | }
106 | layoutParams = params
107 |
108 | mActivity.settingsIcon.visibility = View.INVISIBLE
109 | mActivity.thirdOption.visibility = View.INVISIBLE
110 | mActivity.flipCameraCircle.visibility = View.INVISIBLE
111 | mActivity.tabLayout.visibility = View.INVISIBLE
112 | mActivity.cancelButtonView.visibility = View.INVISIBLE
113 | mActivity.cbText.visibility = View.INVISIBLE
114 | mActivity.cbCross.visibility = View.VISIBLE
115 |
116 | visibility = View.VISIBLE
117 | isRunning = true
118 | }
119 |
120 | private fun onTimerEnd(isCancelled: Boolean = false) {
121 | mActivity.settingsIcon.visibility = View.VISIBLE
122 | mActivity.flipCameraCircle.visibility = View.VISIBLE
123 | mActivity.cancelButtonView.visibility = View.VISIBLE
124 | mActivity.cbCross.visibility = View.INVISIBLE
125 |
126 | if (mActivity !is CaptureActivity) {
127 | mActivity.cbText.visibility = View.VISIBLE
128 | mActivity.tabLayout.visibility = View.VISIBLE
129 | mActivity.thirdOption.visibility = View.VISIBLE
130 | } else if (isCancelled) {
131 | mActivity.cbText.visibility = View.VISIBLE
132 | }
133 |
134 | visibility = View.GONE
135 | isRunning = false
136 | }
137 |
138 | init {
139 | gravity = Gravity.CENTER
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/CustomGrid.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.graphics.Color
6 | import android.graphics.Paint
7 | import android.util.AttributeSet
8 | import android.view.View
9 | import androidx.camera.core.AspectRatio
10 | import app.grapheneos.camera.CamConfig
11 | import app.grapheneos.camera.ui.activities.MainActivity
12 |
13 | class CustomGrid @JvmOverloads constructor(
14 | context: Context,
15 | attrs: AttributeSet? = null,
16 | defStyle: Int = 0
17 | ) : View(context, attrs, defStyle) {
18 |
19 | private val paint: Paint = Paint()
20 | private lateinit var mActivity: MainActivity
21 |
22 | fun setMainActivity(mActivity: MainActivity) {
23 | this.mActivity = mActivity
24 | }
25 |
26 | init {
27 | paint.isAntiAlias = true
28 | paint.strokeWidth = 1f
29 | paint.style = Paint.Style.STROKE
30 | paint.color = Color.argb(255, 255, 255, 255)
31 | }
32 |
33 | override fun onDraw(canvas: Canvas) {
34 | val camConfig = mActivity.camConfig
35 |
36 | super.onDraw(canvas)
37 |
38 | if (camConfig.gridType == CamConfig.GridType.NONE) {
39 | return
40 | }
41 |
42 | val previewHeight = if (camConfig.aspectRatio == AspectRatio.RATIO_16_9) {
43 | mActivity.previewView.width * 16 / 9
44 | } else {
45 | mActivity.previewView.width * 4 / 3
46 | }
47 |
48 | if (camConfig.gridType == CamConfig.GridType.GOLDEN_RATIO) {
49 |
50 | val cx = width / 2f
51 | val cy = previewHeight / 2f
52 |
53 | val dxH = width / 8f
54 | val dyH = previewHeight / 8f
55 |
56 | canvas.drawLine(cx - dxH, 0f, cx - dxH, previewHeight.toFloat(), paint)
57 | canvas.drawLine(cx + dxH, 0f, cx + dxH, previewHeight.toFloat(), paint)
58 | canvas.drawLine(0f, cy - dyH, width.toFloat(), cy - dyH, paint)
59 | canvas.drawLine(0f, cy + dyH, width.toFloat(), cy + dyH, paint)
60 |
61 | } else {
62 |
63 | val seed = if (camConfig.gridType == CamConfig.GridType.THREE_BY_THREE) {
64 | 3f
65 | } else {
66 | 4f
67 | }
68 |
69 | canvas.drawLine(
70 | width / seed * 2f,
71 | 0f,
72 | width / seed * 2f,
73 | previewHeight.toFloat(),
74 | paint
75 | )
76 | canvas.drawLine(width / seed, 0f, width / seed, previewHeight.toFloat(), paint)
77 | canvas.drawLine(
78 | 0f, previewHeight / seed * 2f,
79 | width.toFloat(), previewHeight / seed * 2f, paint
80 | )
81 | canvas.drawLine(0f, previewHeight / seed, width.toFloat(), previewHeight / seed, paint)
82 |
83 | if (seed == 4f) {
84 | canvas.drawLine(
85 | width / seed * 3f,
86 | 0f,
87 | width / seed * 3f,
88 | previewHeight.toFloat(),
89 | paint
90 | )
91 | canvas.drawLine(
92 | 0f, previewHeight / seed * 3f,
93 | width.toFloat(), previewHeight / seed * 3f, paint
94 | )
95 | }
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/DialogUtil.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui
2 |
3 | import android.view.WindowManager
4 | import androidx.appcompat.app.AlertDialog
5 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
6 |
7 | /**
8 | * When in an activity where the status bar is hidden, the window layoutInDisplayCutoutMode
9 | * is set to [WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES], and a
10 | * material alert dialog is present that is large enough, the layout of the dialog will appear
11 | * broken and sometimes will shift randomly. These extensions force the dialog window to ignore
12 | * the short edges mode so that it will appear as normal.
13 | */
14 |
15 | fun AlertDialog.ignoreShortEdges() {
16 | window?.attributes?.layoutInDisplayCutoutMode =
17 | WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
18 | }
19 |
20 | fun AlertDialog.showIgnoringShortEdgeMode(): AlertDialog {
21 | ignoreShortEdges()
22 | show()
23 | return this
24 | }
25 |
26 | fun MaterialAlertDialogBuilder.showIgnoringShortEdgeMode(): AlertDialog =
27 | this.create().showIgnoringShortEdgeMode()
28 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/QROverlay.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui
2 |
3 | import android.content.Context
4 | import android.content.res.Resources
5 | import android.graphics.BlendMode
6 | import android.graphics.Canvas
7 | import android.graphics.Color
8 | import android.graphics.Paint
9 | import android.graphics.RectF
10 | import android.util.AttributeSet
11 | import android.view.View
12 |
13 | class QROverlay(context: Context, attrs: AttributeSet) : View(context, attrs) {
14 | companion object {
15 | const val RATIO = 0.6f
16 | }
17 |
18 | private val boxPaint: Paint = Paint().apply {
19 | color = 0Xffffff
20 | style = Paint.Style.STROKE
21 | strokeWidth = 4f * Resources.getSystem().displayMetrics.density
22 | }
23 |
24 | private val scrimPaint: Paint = Paint().apply {
25 | color = Color.parseColor("#99000000")
26 | }
27 |
28 | private val eraserPaint: Paint = Paint().apply {
29 | strokeWidth = boxPaint.strokeWidth
30 | blendMode = BlendMode.CLEAR
31 | }
32 |
33 | private val boxCornerRadius: Float =
34 | 8f * Resources.getSystem().displayMetrics.density
35 |
36 | private var boxRect: RectF? = null
37 |
38 | var size: Float = 0f
39 | private set
40 |
41 | private fun setViewFinder() {
42 | val overlayWidth = width.toFloat()
43 | val overlayHeight = height.toFloat()
44 |
45 | size = overlayHeight.coerceAtMost(overlayWidth) * RATIO
46 |
47 | val cx = overlayWidth / 2
48 | val cy = overlayHeight / 2
49 | boxRect = RectF(cx - size / 2, cy - size / 2, cx + size / 2, cy + size / 2)
50 | }
51 |
52 | override fun draw(canvas: Canvas) {
53 | super.draw(canvas)
54 | setViewFinder()
55 | boxRect?.let {
56 | // Draws the dark background scrim and leaves the box area clear.
57 | canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), scrimPaint)
58 | // As the stroke is always centered, so erase twice with FILL and STROKE respectively to clear
59 | // all area that the box rect would occupy.
60 | eraserPaint.style = Paint.Style.FILL
61 | canvas.drawRoundRect(it, boxCornerRadius, boxCornerRadius, eraserPaint)
62 | eraserPaint.style = Paint.Style.STROKE
63 | canvas.drawRoundRect(it, boxCornerRadius, boxCornerRadius, eraserPaint)
64 | // Draws the box.
65 | canvas.drawRoundRect(it, boxCornerRadius, boxCornerRadius, boxPaint)
66 | }
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/QRToggle.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import app.grapheneos.camera.R
6 | import app.grapheneos.camera.ui.activities.MainActivity
7 | import com.google.android.material.imageview.ShapeableImageView
8 |
9 | class QRToggle @JvmOverloads constructor(
10 | context: Context, attrs: AttributeSet? = null
11 | ) : ShapeableImageView(context, attrs) {
12 |
13 | lateinit var mActivity: MainActivity
14 | lateinit var key: String
15 |
16 | init {
17 | setOnClickListener {
18 | isSelected = !isSelected
19 | }
20 |
21 | refreshToggleUI()
22 | }
23 |
24 | private fun refreshToggleUI() {
25 | alpha = if (isSelected) {
26 | selectedAlpha
27 | } else {
28 | deselectedAlpha
29 | }
30 | }
31 |
32 | override fun setSelected(selected: Boolean) {
33 | super.setSelected(selected)
34 | val camConfig = mActivity.camConfig
35 |
36 | if (!selected && camConfig.allowedFormats.size == 1) {
37 | mActivity.showMessage(mActivity.getString(
38 | R.string.couldnt_exclude_qr_format, key
39 | ))
40 | isSelected = true
41 | } else {
42 | camConfig.setQRScanningFor(key, selected)
43 | }
44 |
45 | refreshToggleUI()
46 | }
47 |
48 | companion object {
49 | private const val selectedAlpha = 1f
50 | private const val deselectedAlpha = 0.3f
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/SettingsFrameLayout.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.util.AttributeSet
6 | import android.widget.FrameLayout
7 | import android.view.MotionEvent
8 |
9 | class SettingsFrameLayout @JvmOverloads constructor(
10 | context: Context, attrs: AttributeSet? = null
11 | ) : FrameLayout(context, attrs) {
12 |
13 | companion object {
14 | private val dummyListener: OnInterceptTouchEventListener =
15 | DummyInterceptTouchEventListener()
16 | }
17 |
18 | private var mDisallowIntercept = false
19 |
20 | interface OnInterceptTouchEventListener {
21 | /**
22 | * If disallowIntercept is true the touch event can't be stolen and the return value
23 | * is ignored.
24 | * @see android.view.ViewGroup.onInterceptTouchEvent
25 | */
26 | fun onInterceptTouchEvent(
27 | view: SettingsFrameLayout?,
28 | ev: MotionEvent?,
29 | disallowIntercept: Boolean
30 | ): Boolean
31 |
32 | /**
33 | * @see android.view.View.onTouchEvent
34 | */
35 | fun onTouchEvent(view: SettingsFrameLayout?, event: MotionEvent?): Boolean
36 | }
37 |
38 | private class DummyInterceptTouchEventListener :
39 | OnInterceptTouchEventListener {
40 | override fun onInterceptTouchEvent(
41 | view: SettingsFrameLayout?,
42 | ev: MotionEvent?,
43 | disallowIntercept: Boolean
44 | ): Boolean {
45 | return false
46 | }
47 |
48 | override fun onTouchEvent(view: SettingsFrameLayout?, event: MotionEvent?): Boolean {
49 | return false
50 | }
51 | }
52 |
53 | private var mInterceptTouchEventListener = dummyListener
54 |
55 | override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
56 | parent.requestDisallowInterceptTouchEvent(disallowIntercept)
57 | mDisallowIntercept = disallowIntercept
58 | }
59 |
60 | fun setOnInterceptTouchEventListener(interceptTouchEventListener: OnInterceptTouchEventListener?) {
61 | mInterceptTouchEventListener = interceptTouchEventListener ?: dummyListener
62 | }
63 |
64 | override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
65 | val stealTouchEvent =
66 | mInterceptTouchEventListener.onInterceptTouchEvent(this, ev, mDisallowIntercept)
67 | return stealTouchEvent && !mDisallowIntercept || super.onInterceptTouchEvent(ev)
68 | }
69 |
70 | @SuppressLint("ClickableViewAccessibility")
71 | override fun onTouchEvent(event: MotionEvent?): Boolean {
72 | val handled = mInterceptTouchEventListener.onTouchEvent(this, event)
73 |
74 | if (event?.action == MotionEvent.ACTION_UP) {
75 | performClick()
76 | }
77 |
78 | return handled || super.onTouchEvent(event)
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/activities/MoreSettingsSecure.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui.activities
2 |
3 | import android.os.Bundle
4 | import app.grapheneos.camera.AutoFinishOnSleep
5 |
6 | class MoreSettingsSecure : MoreSettings() {
7 |
8 | private val autoFinisher = AutoFinishOnSleep(this)
9 |
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 | autoFinisher.start()
13 | }
14 |
15 | override fun onDestroy() {
16 | super.onDestroy()
17 | autoFinisher.stop()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/activities/QrTile.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui.activities
2 |
3 | import android.app.KeyguardManager
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.core.content.getSystemService
7 | import app.grapheneos.camera.CameraMode
8 |
9 | // Requires integration into the OS, see config_defaultQrCodeComponent in frameworks/base.
10 | //
11 | // SystemUI links this activity via a quick tile or a lockscreen shortcut.
12 | // The activity name is historical, changing it would break the SystemUI integration for users on
13 | // older OS versions.
14 | class QrTile : SecureMainActivity() {
15 |
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | camConfig.switchMode(CameraMode.QR_SCAN)
19 | }
20 |
21 | override fun shouldShowCameraModeTabs() = false
22 |
23 | override fun startActivity(intent: Intent, options: Bundle?) {
24 | val keyguardManager = getSystemService()!!
25 |
26 | if (keyguardManager.isKeyguardLocked) {
27 | val cb = object : KeyguardManager.KeyguardDismissCallback() {
28 | override fun onDismissSucceeded() {
29 | super@QrTile.startActivity(intent, options);
30 | }
31 | }
32 | keyguardManager.requestDismissKeyguard(this, cb)
33 | } else {
34 | super.startActivity(intent, options)
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/activities/SecureActivity.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui.activities
2 |
3 | import android.content.SharedPreferences
4 |
5 | interface SecureActivity {
6 | fun getSharedPreferences(name: String, mode: Int): SharedPreferences? = null
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/activities/SecureCaptureActivity.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui.activities
2 |
3 | import android.content.SharedPreferences
4 | import app.grapheneos.camera.util.EphemeralSharedPrefsNamespace
5 | import app.grapheneos.camera.util.getPrefs
6 |
7 | class SecureCaptureActivity : CaptureActivity(), SecureActivity {
8 | val ephemeralPrefsNamespace = EphemeralSharedPrefsNamespace()
9 |
10 | override fun getSharedPreferences(name: String, mode: Int): SharedPreferences {
11 | return ephemeralPrefsNamespace.getPrefs(this, name, mode, cloneOriginal = true)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/activities/SecureMainActivity.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui.activities
2 |
3 | import android.content.SharedPreferences
4 | import android.os.Bundle
5 | import app.grapheneos.camera.AutoFinishOnSleep
6 | import app.grapheneos.camera.CapturedItem
7 | import app.grapheneos.camera.util.EphemeralSharedPrefsNamespace
8 | import app.grapheneos.camera.util.getPrefs
9 |
10 | open class SecureMainActivity : MainActivity(), SecureActivity {
11 | val capturedItems = ArrayList()
12 | val ephemeralPrefsNamespace = EphemeralSharedPrefsNamespace()
13 |
14 | private val autoFinisher = AutoFinishOnSleep(this)
15 |
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | autoFinisher.start()
19 | }
20 |
21 | override fun onDestroy() {
22 | super.onDestroy()
23 | autoFinisher.stop()
24 | }
25 |
26 | override fun getSharedPreferences(name: String, mode: Int): SharedPreferences {
27 | return ephemeralPrefsNamespace.getPrefs(this, name, mode, cloneOriginal = true)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/activities/VideoCaptureActivity.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui.activities
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.provider.MediaStore.EXTRA_OUTPUT
7 | import android.view.View
8 | import android.widget.ImageView
9 | import app.grapheneos.camera.R
10 |
11 | class VideoCaptureActivity : CaptureActivity() {
12 |
13 | private lateinit var whiteOptionCircle: ImageView
14 | private lateinit var playPreview: ImageView
15 |
16 | private var savedUri: Uri? = null
17 |
18 | override fun onCreate(savedInstanceState: Bundle?) {
19 | super.onCreate(savedInstanceState)
20 |
21 | whiteOptionCircle = findViewById(R.id.white_option_circle)
22 | playPreview = findViewById(R.id.play_preview)
23 |
24 | captureButton.setImageResource(R.drawable.recording)
25 |
26 | captureButton.setOnClickListener OnClickListener@{
27 | if (videoCapturer.isRecording) {
28 | videoCapturer.stopRecording()
29 | } else {
30 | videoCapturer.startRecording()
31 | }
32 | }
33 |
34 | playPreview.setOnClickListener {
35 | val i = Intent(
36 | this@VideoCaptureActivity,
37 | VideoPlayer::class.java
38 | )
39 | i.putExtra("videoUri", savedUri)
40 | startActivity(i)
41 | }
42 |
43 | imagePreview.visibility = View.GONE
44 | whiteOptionCircle.visibility = View.GONE
45 | playPreview.visibility = View.VISIBLE
46 |
47 | confirmButton.setOnClickListener {
48 | confirmVideo()
49 | }
50 |
51 | }
52 |
53 | fun afterRecording(savedUri: Uri?) {
54 |
55 | this.savedUri = savedUri
56 |
57 | bitmap = previewView.bitmap!!
58 |
59 | cancelButtonView.visibility = View.VISIBLE
60 |
61 | showPreview()
62 | }
63 |
64 | override fun showPreview() {
65 | super.showPreview()
66 | thirdOption.visibility = View.VISIBLE
67 | }
68 |
69 | private fun confirmVideo() {
70 | if (savedUri == null) {
71 | setResult(RESULT_CANCELED)
72 | } else {
73 | val resultIntent = Intent()
74 | resultIntent.data = savedUri
75 | resultIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
76 | resultIntent.putExtra(
77 | EXTRA_OUTPUT,
78 | savedUri
79 | )
80 | setResult(RESULT_OK, resultIntent)
81 | }
82 | finish()
83 | }
84 |
85 | override fun hidePreview() {
86 | super.hidePreview()
87 | thirdOption.visibility = View.INVISIBLE
88 | }
89 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/activities/VideoOnlyActivity.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui.activities
2 |
3 | import android.os.Bundle
4 | import app.grapheneos.camera.R
5 |
6 | class VideoOnlyActivity : MainActivity() {
7 |
8 | override fun onCreate(savedInstanceState: Bundle?) {
9 | super.onCreate(savedInstanceState)
10 |
11 | captureButton.setImageResource(R.drawable.recording)
12 |
13 | tabLayout.alpha = 0f
14 | tabLayout.isClickable = false
15 | tabLayout.isEnabled = false
16 | // (tabLayout.layoutParams as ViewGroup.MarginLayoutParams).let {
17 | // it.setMargins(it.leftMargin, it.height, it.rightMargin, it.bottomMargin)
18 | // it.height = 0
19 | // }
20 | //
21 | // (previewView.layoutParams as ViewGroup.MarginLayoutParams).let {
22 | // it.setMargins(it.leftMargin, it.topMargin, it.rightMargin, 0)
23 | // }
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/activities/VideoPlayer.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui.activities
2 |
3 | import android.graphics.drawable.ColorDrawable
4 | import android.media.AudioManager
5 | import android.media.MediaMetadataRetriever
6 | import android.net.Uri
7 | import android.os.Bundle
8 | import android.util.Log
9 | import android.widget.FrameLayout
10 | import android.widget.MediaController
11 | import android.widget.RelativeLayout
12 | import androidx.activity.enableEdgeToEdge
13 | import androidx.appcompat.app.AppCompatActivity
14 | import androidx.core.content.ContextCompat
15 | import androidx.core.view.ViewCompat
16 | import androidx.core.view.WindowCompat
17 | import androidx.core.view.WindowInsetsCompat
18 | import androidx.lifecycle.Lifecycle
19 | import app.grapheneos.camera.AutoFinishOnSleep
20 | import app.grapheneos.camera.R
21 | import app.grapheneos.camera.databinding.VideoPlayerBinding
22 | import app.grapheneos.camera.util.getParcelableExtra
23 | import kotlin.concurrent.thread
24 |
25 |
26 | class VideoPlayer : AppCompatActivity() {
27 |
28 | companion object {
29 | const val TAG = "VideoPlayer"
30 | const val IN_SECURE_MODE = "isInSecureMode"
31 | const val VIDEO_URI = "videoUri"
32 | }
33 |
34 | private lateinit var binding: VideoPlayerBinding
35 |
36 | private val autoFinisher = AutoFinishOnSleep(this)
37 |
38 | private var isSecureMode = false
39 |
40 | override fun onCreate(savedInstanceState: Bundle?) {
41 | super.onCreate(savedInstanceState)
42 | enableEdgeToEdge()
43 |
44 | val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
45 | windowInsetsController.isAppearanceLightStatusBars = false
46 | windowInsetsController.isAppearanceLightNavigationBars = false
47 | windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
48 |
49 | val intent = this.intent
50 |
51 | isSecureMode = intent.getBooleanExtra(IN_SECURE_MODE, false)
52 |
53 | if (isSecureMode) {
54 | setShowWhenLocked(true)
55 | setTurnScreenOn(true)
56 | autoFinisher.start()
57 | }
58 |
59 | binding = VideoPlayerBinding.inflate(layoutInflater)
60 | setContentView(binding.root)
61 |
62 | supportActionBar?.let {
63 | it.setBackgroundDrawable(ColorDrawable(ContextCompat.getColor(this, R.color.appbar)))
64 | it.setDisplayShowTitleEnabled(false)
65 | it.setDisplayHomeAsUpEnabled(true)
66 | }
67 |
68 | val uri = getParcelableExtra(intent, VIDEO_URI)!!
69 |
70 | val videoView = binding.videoPlayer
71 |
72 | val mediaController = object : MediaController(this) {
73 | override fun show() {
74 | super.show()
75 | showActionBar()
76 | windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
77 | }
78 |
79 | override fun hide() {
80 | super.hide()
81 | hideActionBar()
82 | windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
83 | }
84 | }
85 |
86 | supportActionBar?.setBackgroundDrawable(null)
87 |
88 | ViewCompat.setOnApplyWindowInsetsListener(binding.shade) { view, insets ->
89 | val systemBars = insets.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.systemBars())
90 | val actionBarHeight = resources.getDimensionPixelSize(R.dimen.action_bar_height)
91 | view.layoutParams =
92 | FrameLayout.LayoutParams(
93 | RelativeLayout.LayoutParams.MATCH_PARENT,
94 | systemBars.top + actionBarHeight
95 | )
96 | view.background = ContextCompat.getDrawable(this@VideoPlayer, R.drawable.shade)
97 | insets
98 | }
99 |
100 | thread {
101 | var hasAudio = true
102 | try {
103 | MediaMetadataRetriever().use {
104 | it.setDataSource(this, uri)
105 | hasAudio = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO) != null
106 | }
107 | } catch (e: Exception) {
108 | Log.d(TAG, "", e)
109 | }
110 |
111 | mainExecutor.execute {
112 | val lifecycleState = lifecycle.currentState
113 |
114 | if (lifecycleState == Lifecycle.State.DESTROYED) {
115 | return@execute
116 | }
117 |
118 | val audioFocus = if (hasAudio) AudioManager.AUDIOFOCUS_GAIN else AudioManager.AUDIOFOCUS_NONE
119 | videoView.setAudioFocusRequest(audioFocus)
120 |
121 | videoView.setOnPreparedListener { _ ->
122 | videoView.setMediaController(mediaController)
123 |
124 | if (lifecycleState == Lifecycle.State.RESUMED) {
125 | videoView.start()
126 | }
127 |
128 | showActionBar()
129 | mediaController.show(0)
130 | }
131 |
132 | videoView.setVideoURI(uri)
133 | }
134 | }
135 | }
136 |
137 | override fun onSupportNavigateUp(): Boolean {
138 | finish()
139 | return true
140 | }
141 |
142 | override fun onResume() {
143 | super.onResume()
144 | showActionBar()
145 | }
146 |
147 | private fun hideActionBar() {
148 | supportActionBar?.hide()
149 | animateShadeToTransparent()
150 | }
151 |
152 | private fun showActionBar() {
153 | supportActionBar?.show()
154 | animateShadeToOriginal()
155 | }
156 |
157 | private fun animateShadeToTransparent() {
158 | if (binding.shade.alpha == 0f) {
159 | return
160 | }
161 |
162 | binding.shade.animate().apply {
163 | duration = 300
164 | alpha(0f)
165 | }
166 | }
167 |
168 | private fun animateShadeToOriginal() {
169 | if (binding.shade.alpha == 1f) {
170 | return
171 | }
172 |
173 | binding.shade.animate().apply {
174 | duration = 300
175 | alpha(1f)
176 | }
177 | }
178 |
179 | override fun onDestroy() {
180 | super.onDestroy()
181 | if (isSecureMode) {
182 | this.autoFinisher.stop()
183 | }
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/fragment/GallerySlide.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui.fragment
2 |
3 | import androidx.recyclerview.widget.RecyclerView
4 | import app.grapheneos.camera.databinding.GallerySlideBinding
5 |
6 | class GallerySlide(val binding: GallerySlideBinding) : RecyclerView.ViewHolder(binding.root) {
7 | @Volatile var currentPostion = 0
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/seekbar/ExposureBar.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui.seekbar
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.graphics.Canvas
6 | import android.os.Handler
7 | import android.os.Looper
8 | import android.util.AttributeSet
9 | import android.util.Log
10 | import android.view.MotionEvent
11 | import android.view.View
12 | import android.view.ViewGroup
13 | import androidx.appcompat.widget.AppCompatSeekBar
14 | import androidx.camera.core.ExposureState
15 | import androidx.transition.Fade
16 | import androidx.transition.Transition
17 | import androidx.transition.TransitionManager
18 | import app.grapheneos.camera.R
19 | import app.grapheneos.camera.ui.activities.MainActivity
20 |
21 | class ExposureBar : AppCompatSeekBar {
22 | constructor(context: Context) : super(context)
23 | constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
24 | context,
25 | attrs,
26 | defStyle
27 | )
28 |
29 | companion object {
30 | private const val PANEL_VISIBILITY_DURATION = 2000L
31 | }
32 |
33 | private val closePanelHandler: Handler = Handler(Looper.getMainLooper())
34 |
35 | private val closePanelRunnable = Runnable {
36 | hidePanel()
37 | }
38 |
39 | private lateinit var mainActivity: MainActivity
40 |
41 | fun setMainActivity(mainActivity: MainActivity) {
42 | this.mainActivity = mainActivity
43 | }
44 |
45 | fun setExposureConfig(exposureState: ExposureState) {
46 | max = exposureState.exposureCompensationRange.upper
47 | min = exposureState.exposureCompensationRange.lower
48 |
49 | incrementProgressBy(exposureState.exposureCompensationIndex)
50 |
51 | Log.i("TAG", "Setting progress from setExposureConfig")
52 | progress = (exposureState.exposureCompensationStep.numerator
53 | / exposureState.exposureCompensationStep.denominator) *
54 | exposureState.exposureCompensationIndex
55 |
56 | onSizeChanged(width, height, 0, 0)
57 | }
58 |
59 | fun showPanel() {
60 | togglePanel(View.VISIBLE)
61 | closePanelHandler.removeCallbacks(closePanelRunnable)
62 | closePanelHandler.postDelayed(closePanelRunnable, PANEL_VISIBILITY_DURATION)
63 | }
64 |
65 | fun hidePanel() {
66 | togglePanel(View.GONE)
67 | }
68 |
69 | private fun togglePanel(visibility: Int) {
70 | val transition: Transition = Fade()
71 | if (visibility == View.GONE) {
72 | transition.duration = 300
73 | } else {
74 | transition.duration = 0
75 | }
76 | transition.addTarget(R.id.exposure_bar)
77 |
78 | TransitionManager.beginDelayedTransition(
79 | mainActivity.window.decorView.rootView as ViewGroup, transition
80 | )
81 |
82 | mainActivity.exposureBarPanel.visibility = visibility
83 | }
84 |
85 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
86 |
87 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
88 | super.onSizeChanged(h, w, oldh, oldw)
89 | }
90 |
91 | @Synchronized
92 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
93 | super.onMeasure(heightMeasureSpec, widthMeasureSpec)
94 | setMeasuredDimension(measuredHeight, measuredWidth)
95 | }
96 |
97 | override fun onDraw(c: Canvas) {
98 | c.rotate(-90f)
99 | c.translate(-height.toFloat(), 0f)
100 | super.onDraw(c)
101 | }
102 |
103 | @SuppressLint("ClickableViewAccessibility")
104 | override fun onTouchEvent(event: MotionEvent): Boolean {
105 | if (!isEnabled) {
106 | return false
107 | }
108 | when (event.action) {
109 | MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP -> {
110 | progress = max - (max * event.y / (height / 2)).toInt()
111 |
112 | Log.i("progress", progress.toString())
113 | Log.i("max", max.toString())
114 |
115 | mainActivity.camConfig.camera?.cameraControl
116 | ?.setExposureCompensationIndex(progress)
117 |
118 | showPanel()
119 |
120 | onSizeChanged(width, height, 0, 0)
121 | }
122 | MotionEvent.ACTION_CANCEL -> {
123 | }
124 | }
125 | return true
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/ui/seekbar/ZoomBar.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.ui.seekbar
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.graphics.Bitmap
6 | import android.graphics.Canvas
7 | import android.graphics.drawable.BitmapDrawable
8 | import android.os.Handler
9 | import android.os.Looper
10 | import android.util.AttributeSet
11 | import android.view.LayoutInflater
12 | import android.view.MotionEvent
13 | import android.view.View
14 | import android.view.ViewGroup
15 | import android.widget.TextView
16 | import androidx.appcompat.widget.AppCompatSeekBar
17 | import androidx.camera.core.ZoomState
18 | import androidx.transition.Fade
19 | import androidx.transition.Transition
20 | import androidx.transition.TransitionManager
21 | import app.grapheneos.camera.CamConfig
22 | import app.grapheneos.camera.R
23 | import app.grapheneos.camera.ui.activities.MainActivity
24 | import kotlin.math.roundToInt
25 |
26 | class ZoomBar : AppCompatSeekBar {
27 | constructor(context: Context) : super(context)
28 | constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
29 | context,
30 | attrs,
31 | defStyle
32 | )
33 |
34 | companion object {
35 | private const val PANEL_VISIBILITY_DURATION = 2000L
36 | }
37 |
38 | private val closePanelHandler: Handler = Handler(Looper.getMainLooper())
39 |
40 | private val closePanelRunnable = Runnable {
41 | hidePanel()
42 | }
43 |
44 | @SuppressLint("InflateParams")
45 | private var thumbView: View = LayoutInflater.from(context)
46 | .inflate(R.layout.zoom_bar_thumb, null, false)
47 |
48 | private lateinit var mainActivity: MainActivity
49 | private lateinit var camConfig: CamConfig
50 |
51 | fun setMainActivity(mainActivity: MainActivity) {
52 | this.mainActivity = mainActivity
53 | camConfig = mainActivity.camConfig
54 | }
55 |
56 | fun showPanel() {
57 | togglePanel(View.VISIBLE)
58 | closePanelHandler.removeCallbacks(closePanelRunnable)
59 | closePanelHandler.postDelayed(closePanelRunnable, PANEL_VISIBILITY_DURATION)
60 | }
61 |
62 | private fun hidePanel() {
63 | togglePanel(View.GONE)
64 | }
65 |
66 | private fun togglePanel(visibility: Int) {
67 | val transition: Transition = Fade()
68 | if (visibility == View.GONE) {
69 | transition.duration = 300
70 | } else {
71 | transition.duration = 0
72 | }
73 | transition.addTarget(R.id.zoom_bar_panel)
74 |
75 | TransitionManager.beginDelayedTransition(
76 | mainActivity.window.decorView.rootView as ViewGroup, transition
77 | )
78 | mainActivity.zoomBarPanel.visibility = visibility
79 | }
80 |
81 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
82 |
83 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
84 | super.onSizeChanged(h, w, oldh, oldw)
85 | }
86 |
87 | fun updateThumb(shouldShowPanel: Boolean = true) {
88 | val zoomState: ZoomState? = camConfig.camera?.cameraInfo?.zoomState
89 | ?.value
90 |
91 | if (shouldShowPanel) {
92 | showPanel()
93 | } else {
94 | hidePanel()
95 | }
96 |
97 | var zoomRatio = 1.0f
98 | var linearZoom = 0.0f
99 |
100 | if (zoomState != null) {
101 | zoomRatio = zoomState.zoomRatio
102 | linearZoom = zoomState.linearZoom
103 | }
104 |
105 | progress = (linearZoom * 100).roundToInt()
106 |
107 | val textView: TextView = thumbView.findViewById(R.id.progress) as TextView
108 | val text = String.format("%.1fx", zoomRatio)
109 |
110 | textView.text = text
111 |
112 | thumbView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
113 | val bitmap = Bitmap.createBitmap(
114 | thumbView.measuredWidth,
115 | thumbView.measuredHeight,
116 | Bitmap.Config.ARGB_8888
117 | )
118 | val canvas = Canvas(bitmap)
119 | thumbView.layout(0, 0, thumbView.measuredWidth, thumbView.measuredHeight)
120 | thumbView.draw(canvas)
121 | thumb = BitmapDrawable(resources, bitmap)
122 | onSizeChanged(width, height, 0, 0)
123 | }
124 |
125 | @Synchronized
126 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
127 | super.onMeasure(heightMeasureSpec, widthMeasureSpec)
128 | setMeasuredDimension(measuredHeight, measuredWidth)
129 | }
130 |
131 | override fun onDraw(c: Canvas) {
132 | c.rotate(-90f)
133 | c.translate(-height.toFloat(), 0f)
134 | super.onDraw(c)
135 | }
136 |
137 | @SuppressLint("ClickableViewAccessibility")
138 | override fun onTouchEvent(event: MotionEvent): Boolean {
139 | if (!isEnabled) {
140 | return false
141 | }
142 | when (event.action) {
143 |
144 | MotionEvent.ACTION_MOVE, MotionEvent.ACTION_DOWN -> {
145 |
146 | var progress = max - (max * event.y / height).toInt()
147 |
148 | if (progress < 1) progress = 1
149 | if (progress > 100) progress = 100
150 |
151 | camConfig.camera?.cameraControl?.setLinearZoom(progress / 100f)
152 |
153 | }
154 | MotionEvent.ACTION_CANCEL -> {
155 | }
156 | }
157 | return true
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/util/BitmapUtils.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.util
2 |
3 | import android.graphics.Bitmap
4 | import android.graphics.RenderEffect
5 | import android.graphics.Shader
6 | import android.os.Build
7 | import android.widget.ImageView
8 | import androidx.annotation.RequiresApi
9 |
10 | fun setBlurBitmapCompat(view: ImageView, bitmap: Bitmap, radius: Float = 4f) {
11 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
12 | val blurRenderEffect = RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP)
13 | view.setImageBitmap(bitmap)
14 | view.setRenderEffect(blurRenderEffect)
15 | return
16 | }
17 | view.setImageBitmap(app.grapheneos.camera.BlurBitmap[bitmap])
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/util/CameraControl.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.util
2 |
3 | import androidx.camera.core.ZoomState
4 | import app.grapheneos.camera.CamConfig
5 |
6 | class CameraControl(private val camConfig: CamConfig) {
7 |
8 | private fun zoomState(): ZoomState? = camConfig.camera?.cameraInfo?.zoomState?.value
9 |
10 | fun zoomIn() = zoomByRatio(1f)
11 |
12 | fun zoomOut() = zoomByRatio(-1f)
13 |
14 | private fun zoomByRatio(zoomValue: Float) {
15 | val zoomState = zoomState() ?: return
16 | val currentZoomRatio = zoomState.zoomRatio
17 | val newZoomRatio = currentZoomRatio + zoomValue
18 |
19 | val zoomTo =
20 | if (newZoomRatio > zoomState.maxZoomRatio) zoomState.maxZoomRatio
21 | else if (newZoomRatio < zoomState.minZoomRatio) zoomState.minZoomRatio
22 | // smoothly transition between wide angle camera to primary one
23 | else if (currentZoomRatio < 1 && newZoomRatio > 1) 1f
24 | else newZoomRatio
25 |
26 | camConfig.camera?.cameraControl?.setZoomRatio(zoomTo)
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/util/ImageDecoderUtils.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.util
2 |
3 | import android.graphics.ImageDecoder
4 | import kotlin.math.max
5 |
6 | class ImageResizer(val targetWidth: Int, val targetHeight: Int) : ImageDecoder.OnHeaderDecodedListener {
7 | override fun onHeaderDecoded(decoder: ImageDecoder, info: ImageDecoder.ImageInfo, source: ImageDecoder.Source) {
8 | val size = info.size
9 | val w = size.width.toDouble()
10 | val h = size.height.toDouble()
11 |
12 | val ratio = max(w / targetWidth, h / targetHeight)
13 | decoder.setTargetSize((w / ratio).toInt(), (h / ratio).toInt())
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/util/IntentUtils.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.util
2 |
3 | import android.content.Intent
4 | import android.os.Build
5 | import android.os.Parcelable
6 | import androidx.core.content.IntentCompat
7 |
8 | inline fun getParcelableExtra(intent: Intent, name: String): T? {
9 | return IntentCompat.getParcelableExtra(intent, name, T::class.java)
10 | }
11 |
12 | inline fun getParcelableArrayListExtra(intent: Intent, name: String): ArrayList? {
13 | return IntentCompat.getParcelableArrayListExtra(intent, name, T::class.java)
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/util/PackageManagerUtils.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.util
2 |
3 | import android.content.Intent
4 | import android.content.pm.PackageManager
5 | import android.content.pm.ResolveInfo
6 | import android.os.Build
7 |
8 | fun PackageManager.resolveActivity(intent: Intent, flags: Long): ResolveInfo? {
9 | return if (Build.VERSION.SDK_INT >= 33) {
10 | resolveActivity(intent, PackageManager.ResolveInfoFlags.of(flags))
11 | } else {
12 | @Suppress("DEPRECATION")
13 | resolveActivity(intent, flags.toInt())
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/util/SharedPrefs.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.util
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import android.os.Build
7 | import android.util.ArrayMap
8 | import java.util.WeakHashMap
9 |
10 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener as ChangeListener
11 |
12 | typealias EphemeralSharedPrefsNamespace = ArrayMap
13 |
14 | fun EphemeralSharedPrefsNamespace.getPrefs(ctx: Context, name: String, mode: Int, cloneOriginal: Boolean): SharedPreferences {
15 | require(mode == Context.MODE_PRIVATE)
16 | synchronized(this) {
17 | return getOrElse(name) {
18 | val prefs = EphemeralSharedPrefs(ctx.applicationInfo.targetSdkVersion)
19 |
20 | if (cloneOriginal) {
21 | val orig = ctx.applicationContext.getSharedPreferences(name, Context.MODE_PRIVATE)
22 | orig.all.forEach { k, v ->
23 | prefs.map[k] = v
24 | }
25 | }
26 |
27 | this[name] = prefs
28 |
29 | prefs
30 | }
31 | }
32 | }
33 |
34 | class EphemeralSharedPrefs(val targetSdk: Int) : SharedPreferences {
35 |
36 | internal val map = HashMap()
37 | // match the "weakly referenced listeners" behavior of the regular SharedPreferences,
38 | // there's no WeakSet, approximate it by using a dummy value
39 | internal val listeners = WeakHashMap()
40 |
41 | override fun getAll(): MutableMap = map
42 |
43 | @Suppress("UNCHECKED_CAST")
44 | private fun get(key: String?, defValue: T?): T? {
45 | synchronized(this) {
46 | return map[key!!] as T ?: defValue
47 | }
48 | }
49 |
50 | override fun getString(key: String?, defValue: String?): String? = get(key, defValue)
51 | override fun getStringSet(key: String?, defValues: MutableSet?): MutableSet? = get(key, defValues)
52 | override fun getInt(key: String?, defValue: Int): Int = get(key, defValue) ?: defValue
53 | override fun getLong(key: String?, defValue: Long): Long = get(key, defValue) ?: defValue
54 | override fun getFloat(key: String?, defValue: Float): Float = get(key, defValue) ?: defValue
55 | override fun getBoolean(key: String?, defValue: Boolean): Boolean = get(key, defValue) ?: defValue
56 |
57 | override fun contains(key: String?): Boolean = map.contains(key)
58 |
59 | override fun edit(): SharedPreferences.Editor = Editor(this)
60 |
61 | override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {
62 | synchronized(this) {
63 | listeners[listener] = this
64 | }
65 | }
66 |
67 | override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {
68 | synchronized(this) {
69 | listeners.remove(listener)
70 | }
71 | }
72 |
73 | private class Editor(val prefs: EphemeralSharedPrefs) : SharedPreferences.Editor {
74 | val map = HashMap()
75 | private val removedKeys = arrayListOf()
76 | val thread = Thread.currentThread()
77 |
78 | private var cleared = false
79 |
80 | private fun checkThread() {
81 | check(Thread.currentThread() === thread)
82 | }
83 |
84 | private fun put(key: String?, value: T?): Editor {
85 | checkThread()
86 | map[key!!] = value
87 | return this
88 | }
89 |
90 | override fun putString(key: String?, value: String?) = put(key, value)
91 | override fun putStringSet(key: String?, values: MutableSet?) = put(key, values)
92 | override fun putInt(key: String?, value: Int) = put(key, value)
93 | override fun putLong(key: String?, value: Long) = put(key, value)
94 | override fun putFloat(key: String?, value: Float) = put(key, value)
95 | override fun putBoolean(key: String?, value: Boolean) = put(key, value)
96 |
97 | override fun remove(key: String?): SharedPreferences.Editor {
98 | checkThread()
99 | removedKeys.add(key!!)
100 | return this
101 | }
102 |
103 | override fun clear(): SharedPreferences.Editor {
104 | checkThread()
105 | cleared = true
106 | return this
107 | }
108 |
109 | override fun commit(): Boolean {
110 | apply()
111 | return true
112 | }
113 |
114 | override fun apply() {
115 | checkThread()
116 | val listeners: Set
117 |
118 | synchronized(prefs) {
119 | listeners = prefs.listeners.keys
120 |
121 | if (cleared) {
122 | prefs.map.clear()
123 | }
124 | removedKeys.forEach { key ->
125 | prefs.map.remove(key)
126 | }
127 | map.forEach { k, v ->
128 | prefs.map[k] = v
129 | }
130 | }
131 |
132 | // notify listeners outside the critical section
133 |
134 | if (cleared) {
135 | // see onSharedPreferenceChanged() doc
136 | if (prefs.targetSdk >= Build.VERSION_CODES.R) {
137 | listeners.forEach {
138 | it.onSharedPreferenceChanged(prefs, null)
139 | }
140 | }
141 | }
142 | removedKeys.forEach { key ->
143 | listeners.forEach {
144 | it.onSharedPreferenceChanged(prefs, key)
145 | }
146 | }
147 | map.forEach { k, _ ->
148 | listeners.forEach {
149 | it.onSharedPreferenceChanged(prefs, k)
150 | }
151 | }
152 | }
153 | }
154 | }
155 |
156 | @SuppressLint("ApplySharedPref")
157 | inline fun SharedPreferences.edit(commit: Boolean = false,
158 | action: SharedPreferences.Editor.() -> Unit) {
159 | val editor = edit()
160 | action(editor)
161 | if (commit) {
162 | editor.commit()
163 | } else {
164 | editor.apply()
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/app/src/main/java/app/grapheneos/camera/util/Utils.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.camera.util
2 |
3 | import android.content.ContentResolver
4 | import android.content.ContentValues
5 | import android.content.Context
6 | import android.net.Uri
7 | import android.os.storage.StorageManager
8 | import android.provider.DocumentsContract
9 | import android.provider.MediaStore
10 | import app.grapheneos.camera.CamConfig
11 | import app.grapheneos.camera.R
12 | import app.grapheneos.camera.capturer.DEFAULT_MEDIA_STORE_CAPTURE_PATH
13 | import app.grapheneos.camera.capturer.SAF_URI_HOST_EXTERNAL_STORAGE
14 | import java.io.ByteArrayOutputStream
15 | import java.io.IOException
16 | import java.io.PrintStream
17 | import java.util.concurrent.ExecutorService
18 | import java.util.concurrent.RejectedExecutionException
19 |
20 | fun Throwable.printStackTraceToString(): String {
21 | val baos = ByteArrayOutputStream(1000)
22 | this.printStackTrace(PrintStream(baos));
23 | return baos.toString()
24 | }
25 |
26 | fun getTreeDocumentUri(treeUri: Uri): Uri {
27 | val treeId = DocumentsContract.getTreeDocumentId(treeUri)
28 | return DocumentsContract.buildDocumentUriUsingTree(treeUri, treeId)
29 | }
30 |
31 | fun ExecutorService.executeIfAlive(r: Runnable) {
32 | try {
33 | execute(r)
34 | } catch (ignored: RejectedExecutionException) {
35 | check(this.isShutdown)
36 | }
37 | }
38 |
39 | fun storageLocationToUiString(ctx: Context, sl: String): String {
40 | if (sl == CamConfig.SettingValues.Default.STORAGE_LOCATION) {
41 | return "${ctx.getString(R.string.main_storage)}/$DEFAULT_MEDIA_STORE_CAPTURE_PATH"
42 | }
43 |
44 | val uri = Uri.parse(sl)
45 | val indexOfId = if (DocumentsContract.isDocumentUri(ctx, uri)) 3 else 1
46 | val locationId = uri.pathSegments[indexOfId]
47 |
48 | if (uri.host == SAF_URI_HOST_EXTERNAL_STORAGE) {
49 | val endOfVolumeId = locationId.lastIndexOf(':')
50 | val volumeId = locationId.substring(0, endOfVolumeId)
51 |
52 | val volumeName = if (volumeId == "primary") {
53 | ctx.getString(R.string.main_storage)
54 | } else {
55 | val sm = ctx.getSystemService(StorageManager::class.java)
56 | sm.storageVolumes.find {
57 | volumeId == it.uuid
58 | }?.getDescription(ctx) ?: volumeId
59 | }
60 |
61 | val path = locationId.substring(endOfVolumeId + 1)
62 |
63 | return "$volumeName/$path"
64 | }
65 |
66 | try {
67 | val docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri))
68 |
69 | val projection = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
70 | ctx.contentResolver.query(docUri, projection, null, null)?.use {
71 | if (it.moveToFirst()) {
72 | return it.getString(0)
73 | }
74 | }
75 | } catch (ignored: Exception) {}
76 |
77 | return locationId
78 | }
79 |
80 | @Throws(IOException::class)
81 | fun removePendingFlagFromUri(contentResolver: ContentResolver, uri: Uri) {
82 | val cv = ContentValues()
83 | cv.put(MediaStore.MediaColumns.IS_PENDING, 0)
84 | if (contentResolver.update(uri, cv, null, null) != 1) {
85 | throw IOException("unable to remove IS_PENDING flag")
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_down.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
15 |
16 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_up.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
15 |
16 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/aztec.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Camera/71928cc12b41aa977bd40f29a13ac39ebcfec078/app/src/main/res/drawable-nodpi/aztec.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/data_matrix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Camera/71928cc12b41aa977bd40f29a13ac39ebcfec078/app/src/main/res/drawable-nodpi/data_matrix.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/pdf417.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Camera/71928cc12b41aa977bd40f29a13ac39ebcfec078/app/src/main/res/drawable-nodpi/pdf417.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/qr_code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Camera/71928cc12b41aa977bd40f29a13ac39ebcfec078/app/src/main/res/drawable-nodpi/qr_code.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/aspect_ratio.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/auto.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/back.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/back_white.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/camera_shutter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/camera_shutter_normal.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
12 |
13 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/camera_shutter_pressed.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
12 |
13 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cancel.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cbutton_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
15 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
8 |
9 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/copy_to_clipboard.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/delete.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/done.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/dropdown.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/edit.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/exposure_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/exposure_neg.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/exposure_plus.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/exposure_thumb.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 |
-
7 |
9 |
10 |
13 |
16 |
17 |
18 | -
22 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/flash_auto.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/flash_auto_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
11 |
12 |
13 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/flash_off.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/flash_off_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
11 |
12 |
13 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/flash_on.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/flash_on_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
11 |
12 |
13 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/flip_camera.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/focus_ring.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/folder.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/grid_3x3.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/grid_3x3_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
11 |
12 |
13 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/grid_4x4.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/grid_4x4_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
11 |
12 |
13 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/grid_goldenratio.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/grid_goldenratio_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
11 |
12 |
13 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/grid_off.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/grid_off_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
11 |
12 |
13 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
16 |
21 |
26 |
29 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_open_with.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/image_quality.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/info.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/info_adaptable.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/location.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
-
6 |
7 |
8 |
13 |
14 |
15 |
17 |
18 |
19 | -
20 |
21 |
-
22 |
23 |
24 |
29 |
30 |
31 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/location_off.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/location_on.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/mic_off.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/mic_on.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/mode_indicator.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/more.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/more_options.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | -
6 |
7 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/more_options_raw.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/more_settings_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/option_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/pause.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/play.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/play_button_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
11 |
14 |
15 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/progress_bar_style.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
7 |
12 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/qr_result_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/recording.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/refresh.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/rename.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/retake.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/selfie_preview.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/settings_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/settings_icon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/settings_normal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
8 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/settings_pressed.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
8 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shade.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/share.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/share_white.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/storage.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/straighten.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/thumb.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
7 |
8 |
11 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/timer.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
12 |
13 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/torch.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
-
6 |
7 |
8 |
13 |
14 |
15 |
18 |
19 |
20 | -
21 |
22 |
-
23 |
24 |
25 |
30 |
31 |
32 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/torch_off.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/torch_off_button.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | -
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/torch_off_white.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/torch_on.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/torch_on_button.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | -
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/torch_on_white.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/volume_up.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/white_option_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/white_shadow_rect.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/yellow_shadow_rect.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/zoom_in.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/zoom_out.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/zsl.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/gallery.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
19 |
20 |
24 |
25 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/gallery_placeholder.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/gallery_slide.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
22 |
23 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/scan_result_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
24 |
25 |
30 |
31 |
40 |
41 |
50 |
51 |
59 |
60 |
61 |
62 |
63 |
64 |
70 |
71 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/video_player.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/zoom_bar_thumb.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/gallery.xml:
--------------------------------------------------------------------------------
1 |
2 |
38 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/raw/focus_start.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Camera/71928cc12b41aa977bd40f29a13ac39ebcfec078/app/src/main/res/raw/focus_start.ogg
--------------------------------------------------------------------------------
/app/src/main/res/raw/image_shot.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Camera/71928cc12b41aa977bd40f29a13ac39ebcfec078/app/src/main/res/raw/image_shot.ogg
--------------------------------------------------------------------------------
/app/src/main/res/raw/keep.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/raw/timer_final_second.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Camera/71928cc12b41aa977bd40f29a13ac39ebcfec078/app/src/main/res/raw/timer_final_second.ogg
--------------------------------------------------------------------------------
/app/src/main/res/raw/timer_increment.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Camera/71928cc12b41aa977bd40f29a13ac39ebcfec078/app/src/main/res/raw/timer_increment.ogg
--------------------------------------------------------------------------------
/app/src/main/res/raw/video_start.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Camera/71928cc12b41aa977bd40f29a13ac39ebcfec078/app/src/main/res/raw/video_start.ogg
--------------------------------------------------------------------------------
/app/src/main/res/raw/video_stop.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Camera/71928cc12b41aa977bd40f29a13ac39ebcfec078/app/src/main/res/raw/video_stop.ogg
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | - Off
6 | - 3s
7 | - 5s
8 | - 8s
9 | - 10s
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #cbe2ff
4 |
5 | #dde2e9
6 | #181c1f
7 |
8 | #2d3034
9 | #77000000
10 |
11 | #ffe8cf
12 |
13 | #ffdf00
14 |
15 | #E1E3E5
16 |
17 | #ff0000
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 10dp
4 | 46dp
5 |
6 | 52dp
7 |
8 | 6dp
9 | 12dp
10 |
11 | 12dp
12 | 14dp
13 |
14 | 4dp
15 | 8dp
16 |
17 | 44dp
18 |
19 | 12dp
20 |
21 | 60dp
22 |
23 | 24dp
24 |
25 | - 0.8
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
17 |
18 |
22 |
23 |
27 |
28 |
43 |
44 |
47 |
48 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/full_backup_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application") version "8.10.1" apply false
3 | id("org.jetbrains.kotlin.android") version "2.1.21" apply false
4 | }
5 |
6 | allprojects {
7 | tasks.withType {
8 | options.compilerArgs.addAll(listOf("-Xlint", "-Xlint:-cast", "-Xlint:-classfile", "-Xlint:-rawtypes", "-Xlint:-serial"))
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2 | android.useAndroidX=true
3 | kotlin.code.style=official
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Camera/71928cc12b41aa977bd40f29a13ac39ebcfec078/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionSha256Sum=845952a9d6afa783db70bb3b0effaae45ae5542ca2bb7929619e8af49cb634cf
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | }
7 | dependencyResolutionManagement {
8 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
9 | repositories {
10 | google()
11 | mavenCentral()
12 | }
13 | }
14 | rootProject.name = "Camera"
15 | include(":app")
16 |
--------------------------------------------------------------------------------