├── .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 | 20 | 21 | 133 | 134 | 136 | 137 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 7 | 8 | 13 | 14 | 19 | 20 | 25 | 26 | 31 | 32 | 37 | 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 | --------------------------------------------------------------------------------