Compare commits
	
		
			222 Commits
		
	
	
		
			1.0-rc2
			...
			5c2b13dc38
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5c2b13dc38 | |||
|  | 63606f8565 | ||
| 9759aab12b | |||
| 31175fb738 | |||
|  | e6d24eef24 | ||
|  | 188d5c770a | ||
| c3fadc7224 | |||
|  | 1e0e5e8aca | ||
| cda5fcd90f | |||
|  | 09ec6500bd | ||
| acebf2c908 | |||
|  | e3b3ac9c7a | ||
| 36d4c1b826 | |||
|  | e0da4230b6 | ||
| ce45d61e33 | |||
|  | 7a1bea8906 | ||
| ee7205ea40 | |||
|  | 4d7d98b7f1 | ||
| cef9be401f | |||
| 13c25015a2 | |||
|  | db3cb482c7 | ||
|  | 2014843301 | ||
| 967167d0da | |||
| 66b304ad61 | |||
|  | b83802d258 | ||
|  | e1aac8994b | ||
| f6adc5fddd | |||
|  | 142f4db1ac | ||
|  | 78eff7783d | ||
|  | 8cbae70e97 | ||
|  | 068720ebad | ||
|  | fd7412b1ee | ||
|  | 8a202ba617 | ||
|  | bc7ad64f4c | ||
|  | e5abad07c4 | ||
|  | 644f8c685d | ||
|  | 8b5c1cd942 | ||
|  | 7e1443deca | ||
|  | 55bf2b8d03 | ||
|  | e79d3124d4 | ||
|  | 3866c314c5 | ||
|  | fd3ad5302d | ||
|  | a64eb719be | ||
|  | e22a17d371 | ||
|  | c6897668c8 | ||
|  | 2dac8329ec | ||
|  | 65171ecbb6 | ||
|  | fb0ee7e6e3 | ||
|  | 8f14ce4c35 | ||
|  | 052e8fc27a | ||
|  | 8fd70fb37f | ||
|  | f2a5ca1804 | ||
|  | 2f07ea6d97 | ||
|  | 0c4bcf198d | ||
|  | 113017de3b | ||
|  | c53df20c4d | ||
|  | f193b972af | ||
|  | 707cd7bd4e | ||
|  | 4aae580c2b | ||
|  | 343b95de8c | ||
|  | 5105597620 | ||
|  | 07cfec550b | ||
|  | d31a4c4c69 | ||
|  | 974f8a6738 | ||
|  | 934215723b | ||
|  | 546fbb55d6 | ||
|  | e1d05ba16f | ||
|  | 0ec542ef25 | ||
|  | b9203a4659 | ||
|  | c6ca8ef754 | ||
|  | 2bc3372db1 | ||
|  | 76cbdb7832 | ||
|  | 87ea1df4cd | ||
|  | ceffc79929 | ||
|  | 0914ebe475 | ||
|  | f57bb2b935 | ||
|  | d896124765 | ||
|  | a3e06eea84 | ||
|  | 01d206fb2b | ||
|  | d6824843f0 | ||
|  | 7635266a78 | ||
|  | 3b27d27c02 | ||
|  | 571f7d60cd | ||
|  | ce23372932 | ||
|  | 1024b8f4aa | ||
|  | 73e2e20398 | ||
|  | a95c6951f2 | ||
|  | 0966aa5054 | ||
|  | 8ca461ee0a | ||
|  | 6c7b1e2675 | ||
|  | 2359fdca81 | ||
|  | 998ed5abc0 | ||
|  | e4452160f6 | ||
|  | 8e67f2d885 | ||
|  | e3943b33ff | ||
|  | e26055afe7 | ||
|  | 587607f7dc | ||
|  | 262dc08881 | ||
|  | cea9c27d57 | ||
|  | 17a6526b29 | ||
|  | a931335a2b | ||
|  | aafd4f76d6 | ||
|  | 5d464826dc | ||
|  | 6eddd15d81 | ||
|  | 6fee471dec | ||
|  | 38727a239d | ||
|  | 83e2cf733e | ||
|  | 3214d772b2 | ||
|  | 40c3f39c49 | ||
|  | 6215ffa7b6 | ||
|  | bf073da67b | ||
|  | 49fe5ca037 | ||
|  | 62f854db27 | ||
|  | aea6fa6c69 | ||
|  | 029a1fcde7 | ||
|  | 92c99bec22 | ||
|  | 663c1236a4 | ||
|  | a9582ffb05 | ||
|  | b11fb89bd9 | ||
|  | 1e6bebe853 | ||
|  | 96904bce79 | ||
|  | 08675a5fc3 | ||
|  | 019046474c | ||
|  | 0e63b6a50d | ||
|  | 608ff610d8 | ||
| a009ce0c15 | |||
|  | 1ba95c54a2 | ||
|  | 51987f54e1 | ||
|  | efb3a436c4 | ||
|  | 8a22d3b66e | ||
|  | c404d498d5 | ||
|  | 10d35956b3 | ||
|  | 1de639dc46 | ||
|  | a297988d33 | ||
|  | e788d064a5 | ||
|  | d6692b5b7c | ||
|  | 5ac4ba1d43 | ||
|  | 50573a37c4 | ||
|  | 2528b7df5d | ||
|  | e6159f6f42 | ||
|  | f2982be549 | ||
|  | 0c5f7a658f | ||
|  | 9b90057f85 | ||
|  | a9192314de | ||
|  | aa08418109 | ||
|  | 5b239ace83 | ||
|  | 555cd8ada2 | ||
|  | b188313eb9 | ||
|  | 74ea62e8cd | ||
|  | a59d79aa0e | ||
|  | e8021f37dd | ||
|  | 4e179d8698 | ||
|  | a91f8545b0 | ||
|  | 94642047fb | ||
|  | e99f615fcd | ||
|  | 3ba61e87f9 | ||
|  | b798200883 | ||
|  | 2998362518 | ||
|  | 73e3add4a8 | ||
|  | 5b43db3ebd | ||
|  | f9535fe2da | ||
|  | aa20ec5a06 | ||
|  | 917a01b2ed | ||
|  | e7f55c2be2 | ||
|  | eb765e09e7 | ||
|  | 1cf4a6bc36 | ||
|  | 7f0212fc5d | ||
|  | 413d8bd7bf | ||
|  | 931adbb4dd | ||
|  | e6b2dfe37a | ||
|  | f73f9b5acf | ||
|  | ba2d0ac024 | ||
|  | f33b4672b0 | ||
|  | b6b69587fa | ||
| 06a006c0a2 | |||
|  | 8ae38f4250 | ||
|  | 3e1252cc0a | ||
|  | e4357a66e0 | ||
|  | 21f2c0d69f | ||
|  | 340789989c | ||
|  | 2428f4e50b | ||
| f4c9eddd22 | |||
|  | 6a5e971619 | ||
|  | 5829f18908 | ||
|  | 426d94ba81 | ||
| a58d208d49 | |||
|  | 069edaf6a2 | ||
| d0aa2fbeb9 | |||
| 17c75f27bc | |||
|  | 1738664f83 | ||
|  | a5b55fe214 | ||
| 0cd90413d1 | |||
|  | 653ee1ccc1 | ||
|  | 9b6a69e227 | ||
|  | e40305b680 | ||
|  | 668e9d653f | ||
| bcfcb85121 | |||
| 49c650c8a9 | |||
|  | b5f52c7e13 | ||
|  | d81922d2c9 | ||
| 84b2c2c455 | |||
| 841c3dea24 | |||
|  | 6596f347a1 | ||
|  | a2dd009533 | ||
|  | 4104104b16 | ||
|  | 0d838b6209 | ||
|  | 1d41671fb6 | ||
|  | 84ebdcc9a8 | ||
|  | 8181da421e | ||
|  | af97381bdb | ||
|  | ba94beab37 | ||
| 19b0f91852 | |||
|  | 3b0bf2097e | ||
| 455c95021b | |||
|  | 78ad7f983f | ||
|  | 0a0b55294f | ||
|  | 040b9c3af4 | ||
|  | 796c76f36c | ||
|  | 517f0240e3 | ||
|  | b02920ca41 | ||
|  | b289648260 | ||
|  | b6de7ca409 | 
							
								
								
									
										
											BIN
										
									
								
								.github/images/apk.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								.github/images/izzy.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB | 
							
								
								
									
										38
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,3 +1,4 @@ | ||||
| #file: noinspection SpellCheckingInspection | ||||
|  | ||||
| name: CI-Android APK | ||||
|  | ||||
| @@ -7,20 +8,32 @@ env: | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ release ] | ||||
|     branches: [ main ] | ||||
|     tags: | ||||
|       - '**' | ||||
|   pull_request: | ||||
|     branches: [ release ] | ||||
|     branches: [ main ] | ||||
|   workflow_dispatch: | ||||
|  | ||||
| # A workflow run is made up of one or more jobs that can run sequentially or in parallel | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       contents: write | ||||
|  | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: gradle/wrapper-validation-action@v2 | ||||
|       - uses: actions/checkout@v5 | ||||
|         with: | ||||
|           submodules: true | ||||
|       - name: set up secrets | ||||
|         run: | | ||||
|           echo "${{ secrets.RELEASE_KEYSTORE }}" > keystore.asc | ||||
|           echo "${{ secrets.RELEASE_KEY}}" > key.asc | ||||
|           gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch keystore.asc > app/keystore.properties | ||||
|           gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch key.asc > app/key.jks | ||||
|  | ||||
|       - uses: gradle/wrapper-validation-action@v3 | ||||
|  | ||||
|       - name: create and checkout branch | ||||
|         if: github.event_name == 'pull_request' | ||||
| @@ -29,7 +42,7 @@ jobs: | ||||
|         run: git checkout -B "$BRANCH" | ||||
|  | ||||
|       - name: set up JDK | ||||
|         uses: actions/setup-java@v4 | ||||
|         uses: actions/setup-java@v5 | ||||
|         with: | ||||
|           java-version: 17 | ||||
|           distribution: "temurin" | ||||
| @@ -38,8 +51,15 @@ jobs: | ||||
|       - name: Build APK | ||||
|         run: ./gradlew assemble | ||||
|  | ||||
|       - name: Upload APK | ||||
|         uses: actions/upload-artifact@v4 | ||||
|       # - name: Upload APK | ||||
|       #   uses: actions/upload-artifact@v4 | ||||
|       #   with: | ||||
|       #     name: app.apk | ||||
|       #     path: app/build/outputs/apk/release/app-release.apk | ||||
|  | ||||
|       - name: Release | ||||
|         uses: softprops/action-gh-release@v2 | ||||
|         if: startsWith(github.ref, 'refs/tags/') | ||||
|         with: | ||||
|           name: app | ||||
|           path: app/build/outputs/apk/release/*.apk | ||||
|           files: | | ||||
|             app/build/outputs/apk/release/app-release.apk | ||||
|   | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -13,3 +13,5 @@ captures/ | ||||
| .externalNativeBuild | ||||
| .cxx | ||||
| local.properties | ||||
| keystore.properties | ||||
| key.jks | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| [submodule "external/KeePassDX"] | ||||
| 	path = external/KeePassDX | ||||
| 	url = https://github.com/Kunzisoft/KeePassDX.git | ||||
							
								
								
									
										24
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | ||||
| This is free and unencumbered software released into the public domain. | ||||
|  | ||||
| Anyone is free to copy, modify, publish, use, compile, sell, or | ||||
| distribute this software, either in source code form or as a compiled | ||||
| binary, for any purpose, commercial or non-commercial, and by any | ||||
| means. | ||||
|  | ||||
| In jurisdictions that recognize copyright laws, the author or authors | ||||
| of this software dedicate any and all copyright interest in the | ||||
| software to the public domain. We make this dedication for the benefit | ||||
| of the public at large and to the detriment of our heirs and | ||||
| successors. We intend this dedication to be an overt act of | ||||
| relinquishment in perpetuity of all present and future rights to this | ||||
| software under copyright law. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | ||||
| IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR | ||||
| OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | ||||
| ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | ||||
| OTHER DEALINGS IN THE SOFTWARE. | ||||
|  | ||||
| For more information, please refer to <https://unlicense.org> | ||||
							
								
								
									
										73
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -0,0 +1,73 @@ | ||||
| <!--suppress ALL --> | ||||
| <div align="center"> | ||||
|   <h1>Keepass Fidelity</h1> | ||||
|   <img width="100px" src="./metadata/en-US/images/icon.png" alt="Logo"> | ||||
|    | ||||
|   <p>A minimalist fidelity/loyalty card plugin</p> | ||||
|    | ||||
|   <img src="https://forthebadge.com/images/badges/built-for-android.svg" alt="Built for Android"> | ||||
|   <img src="https://forthebadge.com/images/badges/built-with-love.svg" alt="Built with love"> | ||||
|   <br> | ||||
|     <a href="https://github.com/choelzl/keepass-fidelity/actions/workflows/build.yml"> | ||||
|     <img src="https://github.com/choelzl/keepass-fidelity/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status"> | ||||
|   </a> | ||||
| </div> | ||||
|  | ||||
| ## 🌄 Screenshots | ||||
|  | ||||
| <div align="center"> | ||||
|   <table> | ||||
|     <tr> | ||||
|       <td style="width: 33%; height: 100px;"><img src="./metadata/en-US/images/phoneScreenshots/launcher.jpg" alt="Launcher" style="width: 100%; height: 100%;"></td> | ||||
|       <td style="width: 33%; height: 100px;"><img src="./metadata/en-US/images/phoneScreenshots/view.jpg" alt="View" style="width: 100%; height: 100%;"></td> | ||||
|       <td style="width: 33%; height: 100px;"><img src="./metadata/en-US/images/phoneScreenshots/edit.jpg" alt="Edit" style="width: 100%; height: 100%;"></td> | ||||
|     </tr> | ||||
|   </table> | ||||
| </div> | ||||
|  | ||||
| ## ⭐ Features | ||||
|  | ||||
| - Search entries in [Keepass2Android](https://github.com/PhilippC/keepass2android/) | ||||
| - Scan & Create entries | ||||
| - Recently used history for fast access | ||||
| - Protect entries from caching | ||||
| - Minimalist design and features | ||||
| - Supported Formats: CODE_39, CODE_93, CODE_128, EAN_8, EAN_13, UPC_A, UPC_E, CODE_QR, PDF_417, AZTEC, CODABAR, DATA_MATRIX, ITF | ||||
|  | ||||
| ## 📳 Installation | ||||
|  | ||||
| <div style="display: flex; justify-content: center; align-items: center; flex-direction: row;"> | ||||
|     <a href="https://apt.izzysoft.de/fdroid/index/apk/net.helcel.fidelity"> | ||||
|         <img width="200" height="80" alt="Izzy Download" src=".github/images/izzy.png"> | ||||
|     </a> | ||||
|     <a href="https://github.com/choelzl/keepass-fidelity/releases/latest"> | ||||
|         <img width="200" height="84" alt="APK Download" src=".github/images/apk.png"> | ||||
|     </a> | ||||
| </div> | ||||
|  | ||||
| ## ⚙️ Permissions | ||||
|  | ||||
| - `CAMERA`: necessary for importing barcodes from camera | ||||
| - `READ_MEDIA_VISUAL_USER_SELECTED`: necessary for the importing barcode from images | ||||
|  | ||||
| ## 📝 Contribute | ||||
|  | ||||
| Keepass-Fidelity is a user-driven project. We welcome any contribution, big or small. | ||||
|  | ||||
| - **🖥️ Development:** Fix bugs, implement features, or research issues. Open a PR for review. | ||||
| - **🍥 Design:** Improve interfaces, including accessibility and usability. | ||||
| - **📂 Issue Reporting:** Report bugs and edge cases with relevant info. | ||||
| - **🌍 Localization:** Translate if it doesn't support your language. | ||||
|  | ||||
| ## ✏️ Acknowledgements | ||||
|  | ||||
| Thanks to all contributors, the developers of our dependencies, and our users. | ||||
|  | ||||
| ## 📝 License | ||||
|  | ||||
| ``` | ||||
| Copyright (C) 2024 Helcel | ||||
|  | ||||
| Licensed under the Unlicense | ||||
| For more information, please refer to <https://unlicense.org> | ||||
| ``` | ||||
							
								
								
									
										112
									
								
								app/build.gradle
									
									
									
									
									
								
							
							
						
						| @@ -1,23 +1,43 @@ | ||||
| plugins { | ||||
|     id 'com.android.application' | ||||
|     id 'org.jetbrains.kotlin.android' | ||||
|     id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.23' | ||||
|     id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.20' | ||||
|     id 'org.jetbrains.kotlin.plugin.compose' version '2.2.20' | ||||
| } | ||||
|  | ||||
|  | ||||
| android { | ||||
|     namespace 'net.helcel.fidelity' | ||||
|     compileSdk 34 | ||||
|     compileSdk 36 | ||||
|  | ||||
|     defaultConfig { | ||||
|         applicationId 'net.helcel.fidelity' | ||||
|         resValue "string", "app_name", "Keepass Fideity" | ||||
|         versionName "1.0d" | ||||
|         buildConfigField("String", "APP_NAME", "\"Keepass Fidelity\"") | ||||
|         manifestPlaceholders["APP_NAME"] = "Keepass Fidelity" | ||||
|         minSdk 28 | ||||
|         targetSdk 34 | ||||
|         versionCode 1 | ||||
|         versionName "1.0" | ||||
|         targetSdk 36 | ||||
|     } | ||||
|  | ||||
|  | ||||
|     signingConfigs { | ||||
|         create("release") { | ||||
|             try { | ||||
|                 def keystorePropertiesFile = rootProject.file("app/keystore.properties") | ||||
|                 def keystoreProperties = new Properties() | ||||
|                 keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) | ||||
|  | ||||
|                 keyAlias keystoreProperties['keyAlias'] | ||||
|                 keyPassword keystoreProperties['keyPassword'] | ||||
|                 storeFile file(keystoreProperties['storeFile']) | ||||
|                 storePassword keystoreProperties['storePassword'] | ||||
|             } catch (FileNotFoundException e) { | ||||
|                 println("File not found: ${e.message}") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     buildTypes { | ||||
|         debug { | ||||
|             debuggable true | ||||
| @@ -27,40 +47,82 @@ android { | ||||
|             shrinkResources false | ||||
|             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' | ||||
|         } | ||||
|         signedRelease { | ||||
|             initWith(buildTypes.release) | ||||
|             matchingFallbacks = ['release'] | ||||
|             signingConfig = signingConfigs.getByName("release") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     compileOptions { | ||||
|         coreLibraryDesugaringEnabled true | ||||
|  | ||||
|         sourceCompatibility JavaVersion.VERSION_17 | ||||
|         targetCompatibility JavaVersion.VERSION_17 | ||||
|         sourceCompatibility JavaVersion.VERSION_21 | ||||
|         targetCompatibility JavaVersion.VERSION_21 | ||||
|         encoding 'utf-8' | ||||
|     } | ||||
|  | ||||
|     kotlinOptions { | ||||
|         jvmTarget = JavaVersion.VERSION_17 | ||||
|     } | ||||
|  | ||||
|     buildFeatures { | ||||
|         viewBinding true | ||||
|         compose true | ||||
|         buildConfig true | ||||
|     } | ||||
|  | ||||
|     dependenciesInfo { | ||||
|         // Disables dependency metadata when building APKs. | ||||
|         includeInApk = false | ||||
|         // Disables dependency metadata when building Android App Bundles. | ||||
|         includeInBundle = false | ||||
|     } | ||||
|     composeOptions { | ||||
|         kotlinCompilerExtensionVersion = "2.2.20" | ||||
|     } | ||||
|     kotlin { | ||||
|         jvmToolchain(21) | ||||
|     } | ||||
|  | ||||
|     lint { | ||||
|         disable 'UsingMaterialAndMaterial3Libraries' | ||||
|         disable 'PreviewAnnotationInFunctionWithParameters' | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| dependencies { | ||||
|  | ||||
|     coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4' | ||||
|     implementation 'androidx.appcompat:appcompat:1.6.1' | ||||
|     implementation 'androidx.core:core-ktx:1.12.0' | ||||
|     implementation 'androidx.compose.ui:ui' | ||||
|     implementation 'androidx.compose.material3:material3:1.4.0' | ||||
|     implementation 'androidx.compose.material:material:1.9.4' | ||||
|     implementation 'androidx.compose.material:material-icons-extended:1.7.8' | ||||
|     implementation 'androidx.navigation:navigation-compose:2.9.5' | ||||
|     implementation 'androidx.preference:preference-ktx:1.2.1' | ||||
|     implementation 'androidx.constraintlayout:constraintlayout:2.1.4' | ||||
|     implementation 'androidx.camera:camera-camera2:1.3.2' | ||||
|     implementation 'androidx.camera:camera-lifecycle:1.3.2' | ||||
|     implementation 'androidx.camera:camera-view:1.3.2' | ||||
|     implementation 'com.google.code.gson:gson:2.10.1' | ||||
|     implementation 'com.google.android.material:material:1.11.0' | ||||
|     implementation 'com.google.zxing:core:3.5.3' | ||||
|     implementation 'com.google.mlkit:barcode-scanning:17.2.0' | ||||
|  | ||||
| } | ||||
|     implementation "androidx.biometric:biometric:1.2.0-alpha05" | ||||
|     implementation "androidx.security:security-crypto:1.1.0" | ||||
|     implementation "androidx.datastore:datastore-preferences:1.1.7" | ||||
|     implementation "androidx.security:security-crypto:1.1.0" | ||||
|  | ||||
|     coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5' | ||||
|  | ||||
|     implementation 'androidx.camera:camera-lifecycle:1.5.1' | ||||
|     implementation 'androidx.camera:camera-view:1.5.1' | ||||
|     runtimeOnly 'androidx.camera:camera-camera2:1.5.1' | ||||
|  | ||||
|     implementation 'com.google.android.material:material:1.13.0' | ||||
|     implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0' | ||||
|     implementation 'com.google.zxing:core:3.5.3' | ||||
|  | ||||
|     implementation project(":database") | ||||
|     implementation project(":crypto") | ||||
|  | ||||
|     implementation platform('androidx.compose:compose-bom:2025.10.01') | ||||
|     implementation 'androidx.compose.ui:ui-tooling:1.9.4' | ||||
|     implementation 'androidx.compose.ui:ui-tooling-preview' | ||||
|  | ||||
| //Submodule | ||||
|     //noinspection NewerVersionAvailable | ||||
|     implementation 'joda-time:joda-time:2.14.0' | ||||
|     implementation 'org.joda:joda-convert:3.0.1' | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										7
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| # Gson uses generic type information stored in a class file when working with | ||||
| # fields. Proguard removes such information by default, keep it. | ||||
| -keepattributes Signature | ||||
|  | ||||
| -keep class org.joda.convert.** { *; } | ||||
| # Optional. For using GSON @Expose annotation | ||||
| -keepattributes AnnotationDefault,RuntimeVisibleAnnotations | ||||
| @@ -1,34 +1,20 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:versionCode="1" | ||||
|     android:versionName="1.0"> | ||||
|  | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <uses-feature android:name="android.hardware.camera" /> | ||||
|     <uses-permission android:name="android.permission.CAMERA" /> | ||||
|     <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" /> | ||||
|  | ||||
|     <application | ||||
|         android:icon="@drawable/logo" | ||||
|         android:label="@string/app_name" | ||||
|         android:icon="@mipmap/ic_launcher_round" | ||||
|         android:label="${APP_NAME}" | ||||
|         android:supportsRtl="true"> | ||||
|         <activity | ||||
|             android:name=".activity.MainActivity" | ||||
|             android:exported="true" | ||||
|             android:theme="@style/Theme.Fidelity"> | ||||
|             android:exported="true"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MAIN" /> | ||||
|                 <category android:name="android.intent.category.LAUNCHER" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|  | ||||
|         <receiver | ||||
|             android:name=".pluginSDK.PluginAccessBroadcastReceiver" | ||||
|             android:exported="true" | ||||
|             tools:ignore="ExportedReceiver"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" /> | ||||
|                 <action android:name="keepass2android.ACTION_RECEIVE_ACCESS" /> | ||||
|                 <action android:name="keepass2android.ACTION_REVOKE_ACCESS" /> | ||||
|             </intent-filter> | ||||
|         </receiver> | ||||
|     </application> | ||||
| </manifest> | ||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										65
									
								
								app/src/main/java/net/helcel/fidelity/activity/Helper.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,65 @@ | ||||
| package net.helcel.fidelity.activity | ||||
|  | ||||
| import android.content.Context | ||||
| import android.os.Build | ||||
| import android.widget.Toast | ||||
| import androidx.compose.foundation.isSystemInDarkTheme | ||||
| import androidx.compose.material.Colors | ||||
| import androidx.compose.material.MaterialTheme | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.res.stringResource | ||||
|  | ||||
| import androidx.compose.material3.darkColorScheme | ||||
| import androidx.compose.material3.dynamicDarkColorScheme | ||||
| import androidx.compose.material3.dynamicLightColorScheme | ||||
| import androidx.compose.material3.lightColorScheme | ||||
| import androidx.preference.PreferenceManager | ||||
| import net.helcel.fidelity.R | ||||
|  | ||||
|  | ||||
| object ToastHelper{ | ||||
|     fun show(context: Context, message: String, duration: Int = Toast.LENGTH_SHORT) { | ||||
|         Toast.makeText(context, message, duration).show() | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun SysTheme( | ||||
|     content: @Composable () -> Unit | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     val prefs = PreferenceManager.getDefaultSharedPreferences(context) | ||||
|     val themeKey = prefs.getString(stringResource(R.string.key_theme), stringResource(R.string.system)) | ||||
|     val darkTheme = when (themeKey) { | ||||
|         stringResource(R.string.system) -> isSystemInDarkTheme() | ||||
|         stringResource(R.string.light) -> false | ||||
|         stringResource(R.string.dark) -> true | ||||
|         else -> isSystemInDarkTheme() | ||||
|     } | ||||
|     val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||||
|         if(darkTheme) dynamicDarkColorScheme(LocalContext.current ) else dynamicLightColorScheme(LocalContext.current ) | ||||
|     } else { | ||||
|         if(darkTheme) darkColorScheme() else lightColorScheme() | ||||
|     } | ||||
|     val m2colors = Colors( | ||||
|         primary = colorScheme.primary, | ||||
|         primaryVariant = colorScheme.primaryContainer, | ||||
|         secondary = colorScheme.secondary, | ||||
|         background = colorScheme.background, | ||||
|         surface = colorScheme.surface, | ||||
|         onPrimary = colorScheme.onPrimary, | ||||
|         onSecondary = colorScheme.onSecondary, | ||||
|         onBackground = colorScheme.onBackground, | ||||
|         onSurface = colorScheme.onSurface, | ||||
|         secondaryVariant = colorScheme.secondary, | ||||
|         error = colorScheme.error, | ||||
|         onError = colorScheme.onError, | ||||
|         isLight = !darkTheme, | ||||
|     ) | ||||
|  | ||||
|     MaterialTheme( | ||||
|         colors = m2colors, | ||||
|         content = content | ||||
|     ) | ||||
| } | ||||
| @@ -1,66 +1,62 @@ | ||||
| package net.helcel.fidelity.activity | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import android.content.pm.ActivityInfo | ||||
| import android.os.Bundle | ||||
| import androidx.activity.addCallback | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import net.helcel.fidelity.R | ||||
| import net.helcel.fidelity.activity.fragment.Launcher | ||||
| import net.helcel.fidelity.activity.fragment.ViewEntry | ||||
| import net.helcel.fidelity.databinding.ActMainBinding | ||||
| import net.helcel.fidelity.pluginSDK.Kp2aControl.getEntryFieldsFromIntent | ||||
| import net.helcel.fidelity.tools.CacheManager | ||||
| import net.helcel.fidelity.tools.KeepassWrapper.bundleCreate | ||||
| import net.helcel.fidelity.tools.KeepassWrapper.entryExtract | ||||
|  | ||||
| @SuppressLint("SourceLockedOrientationActivity") | ||||
| class MainActivity : AppCompatActivity() { | ||||
|  | ||||
|     private lateinit var binding: ActMainBinding | ||||
|     private lateinit var sharedPreferences: SharedPreferences | ||||
| import androidx.activity.compose.BackHandler | ||||
| import androidx.activity.compose.setContent | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import androidx.navigation.compose.NavHost | ||||
| import androidx.navigation.compose.composable | ||||
| import androidx.navigation.compose.rememberNavController | ||||
| import net.helcel.fidelity.activity.fragment.CreateEntryScreen | ||||
| import net.helcel.fidelity.activity.fragment.FileScanner | ||||
| import net.helcel.fidelity.activity.fragment.InitialScreen | ||||
| import net.helcel.fidelity.activity.fragment.LauncherScreen | ||||
| import net.helcel.fidelity.activity.fragment.ScannerScreen | ||||
| import net.helcel.fidelity.activity.fragment.ViewEntryScreen | ||||
| import net.helcel.fidelity.tools.FidelityRepository.entries | ||||
| import net.helcel.fidelity.tools.FidelityRepository.loadEntries | ||||
| import net.helcel.fidelity.tools.KeePassStore.hasCredentials | ||||
|  | ||||
| class MainActivity : FragmentActivity() { | ||||
|  | ||||
|     @SuppressLint("SourceLockedOrientationActivity") | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         sharedPreferences = | ||||
|             this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE) | ||||
|         CacheManager.loadFidelity(sharedPreferences) | ||||
|         actionBar?.hide() | ||||
|         requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT | ||||
|         loadEntries(this.baseContext) | ||||
|  | ||||
|         binding = ActMainBinding.inflate(layoutInflater) | ||||
|         setContentView(binding.root) | ||||
|         setContent { | ||||
|             SysTheme { | ||||
|                 val navController = rememberNavController() | ||||
|                 val context = LocalContext.current | ||||
|  | ||||
|         onBackPressedDispatcher.addCallback(this) { | ||||
|             if (supportFragmentManager.backStackEntryCount > 0) { | ||||
|                 supportFragmentManager.popBackStackImmediate() | ||||
|                 loadLauncher() | ||||
|                 requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT | ||||
|             } else { | ||||
|                 finish() | ||||
|                 BackHandler { | ||||
|                     if (!navController.popBackStack()) finish() | ||||
|                 } | ||||
|                 LaunchedEffect(Unit) { | ||||
|                     if(!hasCredentials(context)) navController.navigate("init") | ||||
|                 } | ||||
|                 NavHost(navController = navController, startDestination = "launcher") { | ||||
|                     composable("exit") { finish() } | ||||
|                     composable("launcher") { LauncherScreen(navController) } | ||||
|                     composable("init"){ InitialScreen (navController)} | ||||
|                     composable("scanCam") { ScannerScreen(navController) } | ||||
|                     composable("scanFile") { FileScanner(navController) } | ||||
|                     composable("edit"){ CreateEntryScreen(navController) } | ||||
|                     composable("view/{entryId}") { e -> | ||||
|                         val entry = entries.find { | ||||
|                             it.uid == (e.arguments?.getString("entryId") ?: "") | ||||
|                         } | ||||
|                         if (entry == null) return@composable navController.navigate("launcher") | ||||
|                         ViewEntryScreen(navController,entry) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (intent.extras != null) | ||||
|             loadViewEntry() | ||||
|         else if (savedInstanceState == null) | ||||
|             loadLauncher() | ||||
|     } | ||||
|  | ||||
|     private fun loadLauncher() { | ||||
|         supportFragmentManager.beginTransaction() | ||||
|             .replace(R.id.container, Launcher()) | ||||
|             .commit() | ||||
|     } | ||||
|  | ||||
|     private fun loadViewEntry() { | ||||
|         val viewEntry = ViewEntry() | ||||
|         val data = getEntryFieldsFromIntent(intent) | ||||
|         viewEntry.arguments = bundleCreate(entryExtract(data)) | ||||
|         supportFragmentManager.beginTransaction() | ||||
|             .replace(R.id.container, viewEntry) | ||||
|             .commit() | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,49 +0,0 @@ | ||||
| package net.helcel.fidelity.activity.adapter | ||||
|  | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.view.ViewGroup.LayoutParams.MATCH_PARENT | ||||
| import android.view.ViewGroup.LayoutParams.WRAP_CONTENT | ||||
| import android.widget.LinearLayout | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import net.helcel.fidelity.databinding.ListItemFidelityBinding | ||||
|  | ||||
| class FidelityListAdapter( | ||||
|     private val triples: ArrayList<Triple<String?, String?, String?>>, | ||||
|     private val onItemClicked: (Triple<String?, String?, String?>) -> Unit | ||||
| ) : | ||||
|     RecyclerView.Adapter<FidelityListAdapter.FidelityViewHolder>() { | ||||
|  | ||||
|     private lateinit var binding: ListItemFidelityBinding | ||||
|  | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FidelityViewHolder { | ||||
|         binding = ListItemFidelityBinding.inflate(LayoutInflater.from(parent.context)) | ||||
|         binding.root.setLayoutParams( | ||||
|             LinearLayout.LayoutParams( | ||||
|                 MATCH_PARENT, WRAP_CONTENT | ||||
|             ) | ||||
|         ) | ||||
|         return FidelityViewHolder(binding.root) | ||||
|     } | ||||
|  | ||||
|     override fun onBindViewHolder(holder: FidelityViewHolder, position: Int) { | ||||
|         val triple = triples[position] | ||||
|         holder.bind(triple) | ||||
|     } | ||||
|  | ||||
|     override fun getItemCount(): Int = triples.size | ||||
|  | ||||
|     inner class FidelityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { | ||||
|  | ||||
|         fun bind(triple: Triple<String?, String?, String?>) { | ||||
|             val text = "${triple.first}" | ||||
|             binding.textView.text = text | ||||
|             binding.card.setOnClickListener { onItemClicked(triple) } | ||||
|         } | ||||
|  | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,162 +1,370 @@ | ||||
| package net.helcel.fidelity.activity.fragment | ||||
|  | ||||
| import android.content.ActivityNotFoundException | ||||
| import android.os.Bundle | ||||
| import android.os.Handler | ||||
| import android.os.Looper | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.view.inputmethod.EditorInfo | ||||
| import android.widget.ArrayAdapter | ||||
| import androidx.core.widget.addTextChangedListener | ||||
| import androidx.fragment.app.Fragment | ||||
| import com.google.android.material.textfield.TextInputEditText | ||||
| import android.graphics.Bitmap | ||||
| import androidx.compose.foundation.Image | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.interaction.MutableInteractionSource | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.Spacer | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.width | ||||
| import androidx.compose.material.Button | ||||
| import androidx.compose.material.Checkbox | ||||
| import androidx.compose.material.CheckboxDefaults | ||||
| import androidx.compose.material.CircularProgressIndicator | ||||
| import androidx.compose.material.DropdownMenuItem | ||||
| import androidx.compose.material.ExperimentalMaterialApi | ||||
| import androidx.compose.material.ExposedDropdownMenuBox | ||||
| import androidx.compose.material.Icon | ||||
| import androidx.compose.material.MaterialTheme | ||||
| import androidx.compose.material.OutlinedTextField | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.material.TextFieldDefaults | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.ArrowDropDown | ||||
| import androidx.compose.material.icons.filled.Camera | ||||
| import androidx.compose.material.icons.filled.FileOpen | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.graphics.asImageBitmap | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.res.stringArrayResource | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.navigation.NavHostController | ||||
| import com.google.zxing.FormatException | ||||
| import com.kunzisoft.keepass.database.element.Entry | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlinx.coroutines.launch | ||||
| import net.helcel.fidelity.R | ||||
| import net.helcel.fidelity.databinding.FragCreateEntryBinding | ||||
| import net.helcel.fidelity.pluginSDK.Kp2aControl | ||||
| import net.helcel.fidelity.activity.ToastHelper | ||||
| import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onCameraScan | ||||
| import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onFileScan | ||||
| import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onSubmit | ||||
| import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onRefresh | ||||
| import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onSave | ||||
| import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode | ||||
| import net.helcel.fidelity.tools.CacheManager | ||||
| import net.helcel.fidelity.tools.ErrorToaster | ||||
| import net.helcel.fidelity.tools.KeepassWrapper | ||||
|  | ||||
| private const val DEBOUNCE_DELAY = 500L | ||||
|  | ||||
| class CreateEntry : Fragment() { | ||||
|  | ||||
|     private val handler = Handler(Looper.getMainLooper()) | ||||
|     private lateinit var binding: FragCreateEntryBinding | ||||
|  | ||||
|     private val resultLauncherAdd = KeepassWrapper.resultLauncher(this) { | ||||
|         val r = KeepassWrapper.entryExtract(it) | ||||
|         if (!KeepassWrapper.isProtected(it)) { | ||||
|             CacheManager.addFidelity(r) | ||||
|         } | ||||
|         startViewEntry(r.first, r.second, r.third) | ||||
|     } | ||||
|  | ||||
|     private var isValidBarcode: Boolean = false | ||||
|  | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
|         container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View { | ||||
|         binding = FragCreateEntryBinding.inflate(layoutInflater) | ||||
|  | ||||
|         val formats = resources.getStringArray(R.array.format_array) | ||||
|         val arrayAdapter = ArrayAdapter(requireContext(), R.layout.list_item_dropdown, formats) | ||||
|         binding.editTextFormat.setAdapter(arrayAdapter) | ||||
|  | ||||
|         val res = KeepassWrapper.bundleExtract(arguments) | ||||
|         binding.editTextCode.setText(res.second) | ||||
|         binding.editTextFormat.setText(res.third, false) | ||||
|  | ||||
|         binding.editTextCode.addTextChangedListener { changeListener() } | ||||
|         binding.editTextFormat.addTextChangedListener { changeListener() } | ||||
|         binding.editTextFormat.addTextChangedListener { binding.editTextFormat.error = null } | ||||
|         binding.btnSave.setOnClickListener { submit() } | ||||
|  | ||||
|         binding.editTextTitle.onDone { submit() } | ||||
|         binding.editTextCode.onDone { submit() } | ||||
| import net.helcel.fidelity.tools.FidelityEntry | ||||
| import net.helcel.fidelity.tools.FidelityRepository | ||||
| import net.helcel.fidelity.tools.FidelityRepository.activeEntry | ||||
| import net.helcel.fidelity.tools.FidelityRepository.addEntry | ||||
|  | ||||
|  | ||||
|         updatePreview() | ||||
|         return binding.root | ||||
|     } | ||||
| @Preview | ||||
| @Composable | ||||
| fun CreateEntryScreen(navController: NavHostController?) { | ||||
|     var entry by remember { activeEntry } | ||||
|     var errorTitle by remember { mutableStateOf("") } | ||||
|     var errorCode by remember { mutableStateOf("") } | ||||
|     var errorFormat by remember { mutableStateOf("") } | ||||
|  | ||||
|     private fun updatePreview() { | ||||
|         try { | ||||
|             val barcodeBitmap = generateBarcode( | ||||
|                 binding.editTextCode.text.toString(), | ||||
|                 binding.editTextFormat.text.toString(), | ||||
|                 600 | ||||
|             ) | ||||
|             binding.imageViewPreview.setImageBitmap(barcodeBitmap) | ||||
|             isValidBarcode = true | ||||
|         } catch (e: FormatException) { | ||||
|             binding.imageViewPreview.setImageBitmap(null) | ||||
|             binding.editTextCode.error = "Invalid format" | ||||
|         } catch (e: IllegalArgumentException) { | ||||
|             binding.imageViewPreview.setImageBitmap(null) | ||||
|             binding.editTextCode.error = e.message | ||||
|         } catch (e: Exception) { | ||||
|             binding.imageViewPreview.setImageBitmap(null) | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|     } | ||||
|     var barcodeBitmap by remember { mutableStateOf<Bitmap?>(null) } | ||||
|     var isValidBarcode by remember { mutableStateOf(false) } | ||||
|     var showDialog by remember { mutableStateOf(false) } | ||||
|     var isLoading by remember { mutableStateOf(false) } | ||||
|     val ctx = LocalContext.current | ||||
|     val scope = rememberCoroutineScope() | ||||
|  | ||||
|     private fun isValidForm(): Boolean { | ||||
|         var valid = true | ||||
|         if (binding.editTextFormat.text.isNullOrEmpty()) { | ||||
|             valid = false | ||||
|             binding.editTextFormat.error = "Format cannot be empty" | ||||
|         } | ||||
|         if (binding.editTextCode.text.isNullOrEmpty()) { | ||||
|             valid = false | ||||
|             binding.editTextCode.error = "Code cannot be empty" | ||||
|         } | ||||
|         if (binding.editTextTitle.text.isNullOrEmpty()) { | ||||
|             valid = false | ||||
|             binding.editTextTitle.error = "Title cannot be empty" | ||||
|         } | ||||
|         return valid | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private fun startViewEntry(title: String?, code: String?, fmt: String?) { | ||||
|         val viewEntryFragment = ViewEntry() | ||||
|         viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt) | ||||
|  | ||||
|         requireActivity().supportFragmentManager.beginTransaction() | ||||
|             .replace(R.id.container, viewEntryFragment).commit() | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private fun changeListener() { | ||||
|     LaunchedEffect(entry) { | ||||
|         isValidBarcode = false | ||||
|         handler.removeCallbacksAndMessages(null) | ||||
|         handler.postDelayed({ | ||||
|             updatePreview() | ||||
|         }, DEBOUNCE_DELAY) | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private fun TextInputEditText.onDone(callback: () -> Unit) { | ||||
|         setOnEditorActionListener { _, actionId, _ -> | ||||
|             if (actionId == EditorInfo.IME_ACTION_DONE) { | ||||
|                 callback.invoke() | ||||
|                 return@setOnEditorActionListener true | ||||
|             } | ||||
|             false | ||||
|         delay(500) | ||||
|         if (entry.code.isEmpty()) return@LaunchedEffect | ||||
|         try { | ||||
|             val bmp = generateBarcode(entry.code, entry.format, 600) | ||||
|             barcodeBitmap = bmp | ||||
|             isValidBarcode = true | ||||
|             errorCode = "" | ||||
|         } catch (_: FormatException) { | ||||
|             barcodeBitmap = null | ||||
|             errorCode = "Invalid Format" | ||||
|         } catch (e: IllegalArgumentException) { | ||||
|             barcodeBitmap = null | ||||
|             errorCode = if (e.message == "com.google.zxing.FormatException") "Invalid Format" | ||||
|             else e.message ?: "Invalid Argument" | ||||
|         } catch (e: Exception) { | ||||
|             barcodeBitmap = null | ||||
|             ToastHelper.show(ctx, e.message ?: e.toString()) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun submit() { | ||||
|         if (!isValidForm() || !isValidBarcode) { | ||||
|             ErrorToaster.formIncomplete(context) | ||||
|         } else { | ||||
|             val kpEntry = KeepassWrapper.entryCreate( | ||||
|                 this, | ||||
|                 binding.editTextTitle.text.toString(), | ||||
|                 binding.editTextCode.text.toString(), | ||||
|                 binding.editTextFormat.text.toString(), | ||||
|                 binding.checkboxProtected.isChecked, | ||||
|     if (showDialog) { | ||||
|         TreeSelectorDialog( | ||||
|             onDismiss = { | ||||
|                 showDialog = false | ||||
|                 if(it!=null){ | ||||
|                     entry = entry.copy(uid = it.nodeId?.id.toString()) | ||||
|                     if(it is Entry){ | ||||
|                         entry = entry.copy(title = it.title) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
|     val formats = stringArrayResource(R.array.format_array) | ||||
|  | ||||
|     Box( | ||||
|         modifier = Modifier | ||||
|             .fillMaxSize() | ||||
|             .background(MaterialTheme.colors.background) | ||||
|     ) { | ||||
|         Column( | ||||
|             modifier = Modifier | ||||
|                 .padding(16.dp, 32.dp), | ||||
|             verticalArrangement = Arrangement.spacedBy(12.dp) | ||||
|         ) | ||||
|         { | ||||
|             OutlinedTextField( | ||||
|                 value = entry.title, | ||||
|                 enabled = entry.uid!=null, | ||||
|                 onValueChange = { | ||||
|                     entry = entry.copy(title = it) | ||||
|                     errorTitle = "" | ||||
|                 }, | ||||
|                 label = { Text(text = "Title") }, | ||||
|                 isError = errorTitle.isNotEmpty(), | ||||
|                 modifier = Modifier.fillMaxWidth(), | ||||
|                 singleLine = true, | ||||
|                 colors = TextFieldDefaults.textFieldColors( | ||||
|                     textColor = if(entry.uid!=null)MaterialTheme.colors.onBackground | ||||
|                     else MaterialTheme.colors.secondary | ||||
|                 ), | ||||
|             ) | ||||
|             try { | ||||
|                 resultLauncherAdd.launch( | ||||
|                     Kp2aControl.getAddEntryIntent( | ||||
|                         kpEntry.first, | ||||
|                         kpEntry.second | ||||
|                     ) | ||||
|             if (errorTitle.isNotEmpty()) { | ||||
|                 Text(errorTitle, color = MaterialTheme.colors.error) | ||||
|             } | ||||
|  | ||||
|             OutlinedTextField( | ||||
|                 value = entry.code, | ||||
|                 onValueChange = { | ||||
|                     entry = entry.copy(code = it) | ||||
|                     errorCode = "" | ||||
|                 }, | ||||
|                 colors = TextFieldDefaults.textFieldColors( | ||||
|                     textColor = MaterialTheme.colors.onBackground | ||||
|                 ), | ||||
|                 label = { Text("Code") }, | ||||
|                 isError = errorCode.isNotEmpty(), | ||||
|                 modifier = Modifier.fillMaxWidth(), | ||||
|                 singleLine = true | ||||
|             ) | ||||
|             if (errorCode.isNotEmpty()) { | ||||
|                 Text(errorCode, color = MaterialTheme.colors.error) | ||||
|             } | ||||
|  | ||||
|             FormatDropdown( | ||||
|                 formats, | ||||
|                 entry.format, | ||||
|                 errorFormat.ifEmpty { null }, | ||||
|             ) { | ||||
|                 entry = entry.copy(format = it) | ||||
|                 errorFormat = "" | ||||
|             } | ||||
|             if (errorFormat.isNotEmpty()) { | ||||
|                 Text(errorFormat, color = MaterialTheme.colors.error) | ||||
|             } | ||||
|  | ||||
|             Row( | ||||
|                 verticalAlignment = Alignment.CenterVertically, | ||||
|                 modifier = Modifier.fillMaxWidth() | ||||
|             ) { | ||||
|                 Checkbox( | ||||
|                     checked = entry.protected, | ||||
|                     onCheckedChange = { | ||||
|                         entry = entry.copy(protected = it) | ||||
|                     }, | ||||
|                     colors = CheckboxDefaults.colors() | ||||
|                 ) | ||||
|             } catch (e: ActivityNotFoundException) { | ||||
|                 ErrorToaster.noKP2AFound(context) | ||||
|             } catch (e: Exception) { | ||||
|                 e.printStackTrace() | ||||
|                 Text("Protected", color = MaterialTheme.colors.onBackground) | ||||
|  | ||||
|                 Spacer(modifier = Modifier.weight(1f)) | ||||
|                 Button(onClick = { onCameraScan(navController!!) }) { | ||||
|                     Icon(Icons.Default.Camera, contentDescription = null) | ||||
|                 } | ||||
|                 Spacer(modifier = Modifier.width(8.dp)) | ||||
|                 Button(onClick = { onFileScan(navController!!) }) { | ||||
|                     Icon(Icons.Default.FileOpen, contentDescription = null) | ||||
|                 } | ||||
|             } | ||||
|             if (barcodeBitmap != null) { | ||||
|                 Image( | ||||
|                     bitmap = barcodeBitmap!!.asImageBitmap(), | ||||
|                     contentDescription = "Barcode preview", | ||||
|                     modifier = Modifier | ||||
|                         .fillMaxWidth() | ||||
|                         .height(150.dp) | ||||
|                 ) | ||||
|                 Spacer(modifier = Modifier.height(8.dp)) | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         Row( | ||||
|             modifier = Modifier | ||||
|                 .align(Alignment.BottomCenter) | ||||
|                 .padding(48.dp), | ||||
|             verticalAlignment = Alignment.CenterVertically | ||||
|         ) { | ||||
|             Button( | ||||
|                 onClick = { | ||||
|                     onSubmitIfValid( | ||||
|                         entry, | ||||
|                         setErrors = { t, c, f -> | ||||
|                             errorTitle = t | ||||
|                             errorCode = c | ||||
|                             errorFormat = f | ||||
|                         }, | ||||
|                         isValidBarcode | ||||
|                     ) { | ||||
|                         if (FidelityRepository.getRoot() == null) { | ||||
|                             isLoading = true | ||||
|                             scope.launch { | ||||
|                                 onRefresh(ctx, navController!!) | ||||
|                                 isLoading = false | ||||
|                                 if(entry.uid!=null){ | ||||
|                                     addEntry(ctx,entry) | ||||
|                                     isLoading = true | ||||
|                                     onSave(ctx,navController) | ||||
|                                     isLoading = false | ||||
|                                     onSubmit(navController) | ||||
|                                 }else { | ||||
|                                     showDialog = true | ||||
|                                 } | ||||
|                             } | ||||
|                         } else { | ||||
|                             if(entry.uid!=null){ | ||||
|                                 addEntry(ctx,entry) | ||||
|                                 isLoading = true | ||||
|                                 scope.launch { | ||||
|                                     onSave(ctx, navController!!) | ||||
|                                     isLoading = false | ||||
|                                     onSubmit(navController) | ||||
|                                 } | ||||
|                             }else { | ||||
|                                 showDialog = true | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 enabled = isValidBarcode.and(entry.uid==null || entry.title.isNotEmpty()), | ||||
|             ) { | ||||
|                 Text(if(entry.uid==null)"Select Entry" else "Save", style = MaterialTheme.typography.h6) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (isLoading) { | ||||
|             Box( | ||||
|                 modifier = Modifier | ||||
|                     .fillMaxSize() | ||||
|                     .background(MaterialTheme.colors.background.copy(alpha = 0.75f)) | ||||
|                     .clickable( | ||||
|                         interactionSource = remember { MutableInteractionSource() }, | ||||
|                         indication = null, | ||||
|                         onClick = { } | ||||
|                     ), | ||||
|                 contentAlignment = Alignment.Center | ||||
|             ) { | ||||
|                 CircularProgressIndicator() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @OptIn(ExperimentalMaterialApi::class) | ||||
| @Composable | ||||
| fun FormatDropdown( | ||||
|     formats: Array<String>, | ||||
|     format: String, | ||||
|     errorFormat: String?, | ||||
|     onFormatChange: (String) -> Unit, | ||||
| ) { | ||||
|     var expanded by remember { mutableStateOf(false) } | ||||
|  | ||||
|     ExposedDropdownMenuBox( | ||||
|         expanded = expanded, | ||||
|         onExpandedChange = { expanded = !expanded } | ||||
|     ) { | ||||
|         OutlinedTextField( | ||||
|             value = format, | ||||
|             onValueChange = {}, | ||||
|             readOnly = true, // important for dropdown | ||||
|             label = { Text("Format", color=MaterialTheme.colors.onBackground) }, | ||||
|             trailingIcon = { | ||||
|                 Icon( | ||||
|                     Icons.Default.ArrowDropDown, | ||||
|                     contentDescription = "Expand", | ||||
|                 ) | ||||
|             }, | ||||
|             colors = TextFieldDefaults.textFieldColors( | ||||
|                 textColor = MaterialTheme.colors.onBackground | ||||
|             ), | ||||
|             isError = errorFormat != null, | ||||
|             modifier = Modifier.fillMaxWidth() | ||||
|         ) | ||||
|  | ||||
|         ExposedDropdownMenu( | ||||
|             expanded = expanded, | ||||
|             onDismissRequest = { expanded = false } | ||||
|         ) { | ||||
|             formats.forEach { option -> | ||||
|                 DropdownMenuItem( | ||||
|                     onClick = { | ||||
|                         onFormatChange(option) | ||||
|                         expanded = false | ||||
|                     } | ||||
|                 ) { | ||||
|                     Text(option) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| private fun onSubmitIfValid( | ||||
|     entry: FidelityEntry, | ||||
|     setErrors: (String, String, String) -> Unit, | ||||
|     isValidBarcode: Boolean, | ||||
|     onValid: (FidelityEntry) -> Unit | ||||
| ) { | ||||
|     var tErr = "" | ||||
|     var cErr = "" | ||||
|     var fErr = "" | ||||
|     if (entry.uid!=null && entry.title.isBlank()) tErr = "Title cannot be empty" | ||||
|     if (entry.code.isBlank()) cErr = "Code cannot be empty" | ||||
|     if (entry.format.isBlank()) fErr = "Format cannot be empty" | ||||
|  | ||||
|     setErrors(tErr, cErr, fErr) | ||||
|  | ||||
|     if (tErr.isEmpty() && cErr.isEmpty() && fErr.isEmpty() && isValidBarcode) { | ||||
|         onValid(entry.copy()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| object CreateEntryEventHandler { | ||||
|     fun onSubmit(navController: NavHostController){ | ||||
|         navController.popBackStack() | ||||
|         activeEntry.value = activeEntry.value.copy(null,"","","",false) | ||||
|     } | ||||
|  | ||||
|     fun onFileScan(navController: NavHostController){ | ||||
|         navController.navigate("scanFile") | ||||
|     } | ||||
|     fun onCameraScan(navController: NavHostController){ | ||||
|         navController.navigate("scanCam") | ||||
|     } | ||||
| } | ||||
| @@ -1,127 +1,345 @@ | ||||
| package net.helcel.fidelity.activity.fragment | ||||
|  | ||||
| import android.content.ActivityNotFoundException | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.recyclerview.widget.ItemTouchHelper | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import net.helcel.fidelity.R | ||||
| import net.helcel.fidelity.activity.adapter.FidelityListAdapter | ||||
| import net.helcel.fidelity.databinding.FragLauncherBinding | ||||
| import net.helcel.fidelity.pluginSDK.Kp2aControl | ||||
| import net.helcel.fidelity.tools.CacheManager | ||||
| import net.helcel.fidelity.tools.ErrorToaster | ||||
| import net.helcel.fidelity.tools.KeepassWrapper | ||||
| import android.content.Context | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.combinedClickable | ||||
| import androidx.compose.foundation.interaction.MutableInteractionSource | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.Spacer | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.foundation.layout.width | ||||
| import androidx.compose.foundation.lazy.grid.GridCells | ||||
| import androidx.compose.foundation.lazy.grid.LazyVerticalGrid | ||||
| import androidx.compose.foundation.lazy.grid.items | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material.DropdownMenu | ||||
| import androidx.compose.material.DropdownMenuItem | ||||
| import androidx.compose.material.ExperimentalMaterialApi | ||||
| import androidx.compose.material.FloatingActionButton | ||||
| import androidx.compose.material.Icon | ||||
| import androidx.compose.material.MaterialTheme | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Add | ||||
| import androidx.compose.material.icons.filled.Edit | ||||
| import androidx.compose.material.icons.filled.HideSource | ||||
| import androidx.compose.material.icons.filled.PushPin | ||||
| import androidx.compose.material.icons.filled.Search | ||||
| import androidx.compose.material3.Card | ||||
| import androidx.compose.material3.CardDefaults | ||||
| import androidx.compose.material3.ExperimentalMaterial3Api | ||||
| import androidx.compose.material3.pulltorefresh.PullToRefreshBox | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.derivedStateOf | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.navigation.NavHostController | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.withContext | ||||
| import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onAdd | ||||
| import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onEdit | ||||
| import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onHide | ||||
| import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onPin | ||||
| import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onQuery | ||||
| import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onRefresh | ||||
| import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onView | ||||
| import net.helcel.fidelity.tools.CredentialResult | ||||
| import net.helcel.fidelity.tools.FidelityEntry | ||||
| import net.helcel.fidelity.tools.FidelityRepository.activeEntry | ||||
| import net.helcel.fidelity.tools.FidelityRepository.end | ||||
| import net.helcel.fidelity.tools.FidelityRepository.entries | ||||
| import net.helcel.fidelity.tools.FidelityRepository.genCredentials | ||||
| import net.helcel.fidelity.tools.FidelityRepository.importDB | ||||
| import net.helcel.fidelity.tools.FidelityRepository.start | ||||
| import net.helcel.fidelity.tools.KeePassStore.loadCredentials | ||||
|  | ||||
|  | ||||
| class Launcher : Fragment() { | ||||
|  | ||||
|     private lateinit var binding: FragLauncherBinding | ||||
|     private lateinit var fidelityListAdapter: FidelityListAdapter | ||||
|  | ||||
|     private val resultLauncherQuery = KeepassWrapper.resultLauncher(this) { | ||||
|         val r = KeepassWrapper.entryExtract(it) | ||||
|         if (!KeepassWrapper.isProtected(it)) { | ||||
|             CacheManager.addFidelity(r) | ||||
|         } | ||||
|         startViewEntry(r.first, r.second, r.third) | ||||
|     } | ||||
|  | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View { | ||||
|         binding = FragLauncherBinding.inflate(layoutInflater) | ||||
|         binding.btnQuery.setOnClickListener { startGetFromKeepass() } | ||||
|         binding.btnAdd.setOnClickListener { | ||||
|             if (binding.menuAdd.visibility == View.GONE) | ||||
|                 showMenuAdd() | ||||
|             else | ||||
|                 hideMenuAdd() | ||||
|         } | ||||
|  | ||||
|         hideMenuAdd() | ||||
|         binding.btnScan.setOnClickListener { | ||||
|             startScanner() | ||||
|             hideMenuAdd() | ||||
|         } | ||||
|  | ||||
|         binding.btnManual.setOnClickListener { | ||||
|             startCreateEntry() | ||||
|             hideMenuAdd() | ||||
|         } | ||||
|  | ||||
|         binding.fidelityList.layoutManager = | ||||
|             LinearLayoutManager(requireContext()) | ||||
|         fidelityListAdapter = FidelityListAdapter(CacheManager.getFidelity()) { | ||||
|             startViewEntry(it.first, it.second, it.third) | ||||
|         } | ||||
|         binding.fidelityList.adapter = fidelityListAdapter | ||||
|  | ||||
|         recyclerSlideHelper().attachToRecyclerView(binding.fidelityList) | ||||
|         return binding.root | ||||
|     } | ||||
|  | ||||
|     private fun hideMenuAdd() { | ||||
|         binding.btnAdd.setImageResource(R.drawable.cross) | ||||
|         binding.menuAdd.visibility = View.GONE | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private fun showMenuAdd() { | ||||
|         binding.btnAdd.setImageResource(R.drawable.minus) | ||||
|         binding.menuAdd.visibility = View.VISIBLE | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private fun startGetFromKeepass() { | ||||
|         try { | ||||
|             this.resultLauncherQuery.launch(Kp2aControl.getQueryEntryForOwnPackageIntent()) | ||||
|         } catch (e: ActivityNotFoundException) { | ||||
|             ErrorToaster.noKP2AFound(requireActivity()) | ||||
| @Preview | ||||
| @OptIn(ExperimentalMaterial3Api::class) | ||||
| @Composable | ||||
| fun LauncherScreen( | ||||
|     navController: NavHostController?, | ||||
| ) { | ||||
|     if(navController==null) return | ||||
|     var isRefreshingState by remember { mutableStateOf(false) } | ||||
|     var showHidden by remember { mutableStateOf(false) } | ||||
|     val context = LocalContext.current | ||||
|     val scope = rememberCoroutineScope() | ||||
|     val sortedEntries = remember(entries) { | ||||
|         derivedStateOf { | ||||
|             entries.filter{showHidden || !it.hidden}.sortedWith( | ||||
|                 compareByDescending<FidelityEntry> { it.pinned } | ||||
|                     .thenBy { it.hidden } | ||||
|                     .thenByDescending { it.lastUse } | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun startFragment(fragment: Fragment) { | ||||
|         requireActivity().supportFragmentManager.beginTransaction() | ||||
|             .addToBackStack("Launcher") | ||||
|             .replace(R.id.container, fragment).commit() | ||||
|     } | ||||
|  | ||||
|     private fun startScanner() { | ||||
|         startFragment(Scanner()) | ||||
|     } | ||||
|     Box(modifier = Modifier | ||||
|         .fillMaxSize() | ||||
|         .background(MaterialTheme.colors.background)) { | ||||
|  | ||||
|     private fun startCreateEntry() { | ||||
|         startFragment(CreateEntry()) | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private fun startViewEntry(title: String?, code: String?, fmt: String?) { | ||||
|         val viewEntryFragment = ViewEntry() | ||||
|         viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt) | ||||
|         startFragment(viewEntryFragment) | ||||
|     } | ||||
|  | ||||
|     private fun recyclerSlideHelper(): ItemTouchHelper { | ||||
|         return ItemTouchHelper(object : ItemTouchHelper.SimpleCallback( | ||||
|             0, ItemTouchHelper.LEFT | ||||
|         PullToRefreshBox( | ||||
|             onRefresh = { | ||||
|                 isRefreshingState = true | ||||
|                 scope.launch { | ||||
|                     onRefresh(context, navController) | ||||
|                     isRefreshingState = false | ||||
|                 } | ||||
|             }, | ||||
|             isRefreshing = isRefreshingState, | ||||
|             modifier = Modifier.fillMaxSize() | ||||
|         ) { | ||||
|             override fun onMove( | ||||
|                 recyclerView: RecyclerView, | ||||
|                 viewHolder: RecyclerView.ViewHolder, | ||||
|                 target: RecyclerView.ViewHolder | ||||
|             ): Boolean = false | ||||
|  | ||||
|             override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { | ||||
|                 val pos = viewHolder.adapterPosition | ||||
|                 CacheManager.rmFidelity(pos) | ||||
|                 fidelityListAdapter.notifyItemRemoved(pos) | ||||
|             LazyVerticalGrid( | ||||
|                 columns = GridCells.Fixed(2), | ||||
|                 modifier = Modifier | ||||
|                     .fillMaxSize() | ||||
|                     .fillMaxSize() | ||||
|                     .padding(16.dp), | ||||
|                 verticalArrangement = Arrangement.spacedBy(8.dp), | ||||
|                 horizontalArrangement = Arrangement.spacedBy(8.dp) | ||||
|             ) { | ||||
|                 items(sortedEntries.value) { entry -> | ||||
|                     FidelityRow(navController, entry) | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|             FloatingActionButton( | ||||
|                 onClick = { onQuery() }, | ||||
|                 modifier = Modifier | ||||
|                     .align(Alignment.BottomCenter) | ||||
|                     .padding(bottom = 16.dp), | ||||
|             ) { | ||||
|                 Icon( | ||||
|                     Icons.Default.Search, | ||||
|                     contentDescription = "Query", | ||||
|                     modifier = Modifier.size(32.dp) | ||||
|                 ) | ||||
|             } | ||||
|             FloatingActionButton( | ||||
|                 onClick = { onAdd(navController) }, modifier = Modifier | ||||
|                     .align(Alignment.BottomEnd) | ||||
|                     .padding(16.dp) | ||||
|             ) { | ||||
|                 Icon(Icons.Default.Add, contentDescription = "Add") | ||||
|             } | ||||
|             FloatingActionButton( | ||||
|                 onClick = { | ||||
|                     showHidden=!showHidden | ||||
|                 }, modifier = Modifier | ||||
|                     .align(Alignment.BottomStart) | ||||
|                     .padding(16.dp).size(24.dp), | ||||
|                 backgroundColor =  if(showHidden) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary, | ||||
|             ) { | ||||
|                 Icon(Icons.Default.HideSource, | ||||
|                     tint= if(showHidden) MaterialTheme.colors.background else MaterialTheme.colors.onSecondary, | ||||
|                     contentDescription = "Show Hidden") | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (isRefreshingState) | ||||
|             Box( | ||||
|                 modifier = Modifier | ||||
|                     .fillMaxSize() | ||||
|                     .background(MaterialTheme.colors.background.copy(alpha = 0.75f)) | ||||
|                     .clickable( | ||||
|                         interactionSource = remember { MutableInteractionSource() }, | ||||
|                         indication = null, | ||||
|                         onClick = { } | ||||
|                     ) | ||||
|             ) | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @OptIn(ExperimentalMaterialApi::class) | ||||
| @Composable | ||||
| fun FidelityRow( | ||||
|     navController: NavHostController, | ||||
|     e: FidelityEntry | ||||
| ) { | ||||
|     var expanded by remember { mutableStateOf(false) } | ||||
|  | ||||
|     Box(modifier = Modifier.fillMaxWidth()) { | ||||
|         Card( | ||||
|             modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .padding(2.dp) | ||||
|                 .combinedClickable( | ||||
|                     onClick = { onView(navController, e) }, | ||||
|                     onLongClick = { expanded = true }, | ||||
|                 ), | ||||
|             shape = RoundedCornerShape(8.dp), | ||||
|             colors = CardDefaults.cardColors( | ||||
|                 containerColor = MaterialTheme.colors.primary, | ||||
|                 contentColor = MaterialTheme.colors.background | ||||
|             ), | ||||
|         ) { | ||||
|             Box(modifier = Modifier.fillMaxSize().padding(2.dp)) { | ||||
|                 Row(modifier = Modifier.padding(14.dp)) { | ||||
|                     Text( | ||||
|                         text = e.title, | ||||
|                         style = MaterialTheme.typography.h6, | ||||
|                         color = MaterialTheme.colors.onPrimary | ||||
|                     ) | ||||
|                 } | ||||
|                 Row(modifier = Modifier.align(Alignment.TopEnd)) { | ||||
|                     if (e.hidden) | ||||
|                         Icon( | ||||
|                             Icons.Default.HideSource, contentDescription = null, | ||||
|                             modifier = Modifier.size(16.dp), | ||||
|                             tint = MaterialTheme.colors.onPrimary | ||||
|                         ) | ||||
|                     if (e.hidden && e.pinned) | ||||
|                         Spacer(modifier = Modifier.width(8.dp)) | ||||
|                     if (e.pinned) | ||||
|                         Icon( | ||||
|                             Icons.Default.PushPin, contentDescription = null, | ||||
|                             modifier = Modifier.size(16.dp), | ||||
|                             tint = MaterialTheme.colors.onPrimary | ||||
|                         ) | ||||
|  | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         DropdownMenu( | ||||
|             modifier = Modifier, | ||||
|             expanded = expanded, | ||||
|             onDismissRequest = { expanded = false } | ||||
|         ) { | ||||
|             DropdownMenuItem(onClick = { | ||||
|                 expanded = false | ||||
|                 onEdit(navController, e) | ||||
|             }) { | ||||
|                 Icon( | ||||
|                     Icons.Default.Edit, | ||||
|                     contentDescription = "edit", | ||||
|                 ) | ||||
|                 Spacer(modifier= Modifier.width(8.dp)) | ||||
|                 Text("Edit") | ||||
|             } | ||||
|             DropdownMenuItem(onClick = { | ||||
|                 expanded = false | ||||
|                 onPin(e) | ||||
|             }) { | ||||
|                 Icon( | ||||
|                     Icons.Default.PushPin, | ||||
|                     contentDescription = "pin", | ||||
|                 ) | ||||
|                 Spacer(modifier= Modifier.width(8.dp)) | ||||
|                 if(e.pinned) Text("Unpin") | ||||
|                 else Text("Pin") | ||||
|             } | ||||
|             DropdownMenuItem(onClick = { | ||||
|                 expanded = false | ||||
|                 onHide(e) | ||||
|             }) { | ||||
|                 Icon( | ||||
|                     Icons.Default.HideSource, | ||||
|                     contentDescription = "hide", | ||||
|                 ) | ||||
|                 Spacer(modifier= Modifier.width(8.dp)) | ||||
|                 if(e.hidden) Text("Unhide") | ||||
|                 else Text("Hide") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| object LauncherEventHandlers { | ||||
|     fun onAdd(navController: NavHostController) { | ||||
|         navController.navigate("edit") | ||||
|     } | ||||
|  | ||||
|     fun onQuery() { | ||||
|         //TODO | ||||
|     } | ||||
|     var CRED: CredentialResult.Success? = null | ||||
|  | ||||
|     suspend fun onSave(context: Context, navController: NavHostController){ | ||||
|         try { | ||||
|             if (CRED == null) { | ||||
|                 val res = loadCredentials(context) | ||||
|                 when (res) { | ||||
|                     CredentialResult.AuthFailed, CredentialResult.NoData -> null | ||||
|                     is CredentialResult.Success -> CRED = res | ||||
|                 } | ||||
|             } | ||||
|             CRED!! | ||||
|             val cred = withContext(Dispatchers.IO) { | ||||
|                 genCredentials(context, CRED!!) | ||||
|             } | ||||
|             if (withContext(Dispatchers.IO) { | ||||
|                     end(context, CRED!!.db, cred) | ||||
|                 }) | ||||
|                 throw Exception("Error in saving") | ||||
|         } catch (e: Exception) { | ||||
|             println(e.toString()) | ||||
|             navController.navigate("init") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun onRefresh(context: Context, navController: NavHostController) { | ||||
|         try { | ||||
|             if (CRED == null) { | ||||
|                 val res = loadCredentials(context) | ||||
|                 when (res) { | ||||
|                     CredentialResult.AuthFailed, CredentialResult.NoData -> null | ||||
|                     is CredentialResult.Success -> CRED = res | ||||
|  | ||||
|                 } | ||||
|             } | ||||
|             CRED!! | ||||
|             val cred = withContext(Dispatchers.IO) { | ||||
|                 genCredentials(context, CRED!!) | ||||
|             } | ||||
|             if (withContext(Dispatchers.IO) { | ||||
|                     start(context, CRED!!.db, cred) | ||||
|                 }) | ||||
|                 importDB(context) | ||||
|         } catch (e: Exception) { | ||||
|             println(e.toString()) | ||||
|             navController.navigate("init") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun onView(navController: NavHostController, entry: FidelityEntry) { | ||||
|         navController.navigate("view/${entry.uid}") | ||||
|         val index = entries.indexOfFirst { it.uid == entry.uid } | ||||
|         if (index != -1) | ||||
|             entries[index] = entry.copy(lastUse = System.currentTimeMillis().toInt()) | ||||
|  | ||||
|     } | ||||
|  | ||||
|     fun onPin(entry: FidelityEntry){ | ||||
|         val index = entries.indexOfFirst { it.uid == entry.uid } | ||||
|         if (index != -1) | ||||
|             entries[index] = entry.copy(pinned = !entry.pinned) | ||||
|     } | ||||
|  | ||||
|     fun onHide(entry: FidelityEntry){ | ||||
|         val index = entries.indexOfFirst { it.uid == entry.uid } | ||||
|         if (index != -1) | ||||
|             entries[index] = entry.copy(hidden = !entry.hidden) | ||||
|     } | ||||
|  | ||||
|     fun onEdit(navController: NavHostController, entry: FidelityEntry){ | ||||
|         activeEntry.value = entry | ||||
|         navController.navigate("edit") | ||||
|     } | ||||
| } | ||||
| @@ -1,115 +1,224 @@ | ||||
| @file:Suppress("PreviewAnnotationInFunctionWithParameters", | ||||
|     "PreviewAnnotationInFunctionWithParameters" | ||||
| ) | ||||
|  | ||||
| package net.helcel.fidelity.activity.fragment | ||||
|  | ||||
| import android.Manifest | ||||
| import android.content.ContentValues | ||||
| import android.content.pm.PackageManager | ||||
| import android.os.Bundle | ||||
| import android.graphics.BitmapFactory | ||||
| import android.util.Log | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.Toast | ||||
| import androidx.activity.compose.BackHandler | ||||
| import androidx.activity.compose.rememberLauncherForActivityResult | ||||
| import androidx.activity.result.PickVisualMediaRequest | ||||
| import androidx.activity.result.contract.ActivityResultContracts | ||||
| import androidx.camera.core.Camera | ||||
| import androidx.camera.core.CameraSelector | ||||
| import androidx.camera.core.Preview | ||||
| import androidx.camera.lifecycle.ProcessCameraProvider | ||||
| import androidx.core.app.ActivityCompat | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.fragment.app.Fragment | ||||
| import net.helcel.fidelity.R | ||||
| import net.helcel.fidelity.databinding.FragScannerBinding | ||||
| import net.helcel.fidelity.tools.BarcodeScanner.getAnalysisUseCase | ||||
| import net.helcel.fidelity.tools.KeepassWrapper | ||||
| import androidx.camera.view.PreviewView | ||||
| import androidx.compose.foundation.Canvas | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.Button | ||||
| import androidx.compose.material.CircularProgressIndicator | ||||
| import androidx.compose.material.Icon | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.FlashOn | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.geometry.Offset | ||||
| import androidx.compose.ui.geometry.Size | ||||
| import androidx.compose.ui.graphics.BlendMode | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.viewinterop.AndroidView | ||||
| import androidx.lifecycle.compose.LocalLifecycleOwner | ||||
| import androidx.navigation.NavController | ||||
| import androidx.navigation.NavHostController | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| import net.helcel.fidelity.activity.fragment.ScannerEventHandler.onResult | ||||
| import net.helcel.fidelity.tools.BarcodeScanner | ||||
| import net.helcel.fidelity.tools.BarcodeScanner.analysisUseCase | ||||
| import net.helcel.fidelity.tools.FidelityRepository.activeEntry | ||||
|  | ||||
| private const val CAMERA_PERMISSION_REQUEST_CODE = 1 | ||||
| @androidx.compose.ui.tooling.preview.Preview | ||||
| @Composable | ||||
| fun ScannerScreen( | ||||
|     navController: NavController | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     val lifecycleOwner = LocalLifecycleOwner.current | ||||
|     val scope = rememberCoroutineScope() | ||||
|  | ||||
| class Scanner : Fragment() { | ||||
|     val cameraProviderFuture = remember { | ||||
|         ProcessCameraProvider.getInstance(context) | ||||
|     } | ||||
|     var camera: Camera? by remember { mutableStateOf(null) } | ||||
|     var torchOn by remember { mutableStateOf(false) } | ||||
|  | ||||
|     private lateinit var binding: FragScannerBinding | ||||
|     val done = remember { mutableStateOf(false) } | ||||
|     val previewView = remember { PreviewView(context) } | ||||
|  | ||||
|     private var code: String = "" | ||||
|     private var fmt: String = "" | ||||
|  | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
|         container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View { | ||||
|         binding = FragScannerBinding.inflate(layoutInflater) | ||||
|         binding.btnScanDone.setOnClickListener { | ||||
|             startCreateEntry() | ||||
|     val permissionLauncher = rememberLauncherForActivityResult( | ||||
|         contract = ActivityResultContracts.RequestPermission(), | ||||
|         onResult = { granted -> | ||||
|             if (granted) { | ||||
|                 val cameraProvider = cameraProviderFuture.get() | ||||
|                 val previewUseCase = Preview.Builder().build().also { | ||||
|                     it.surfaceProvider = previewView.surfaceProvider | ||||
|                 } | ||||
|                 val analysisUseCase = analysisUseCase { detectedCode, detectedFormat -> | ||||
|                     if (detectedCode.isNullOrEmpty() || detectedFormat.isNullOrEmpty()) return@analysisUseCase | ||||
|                     if(done.value) return@analysisUseCase | ||||
|                     scope.launch(Dispatchers.Main) { | ||||
|                         activeEntry.value = | ||||
|                             activeEntry.value.copy(code = detectedCode, format = detectedFormat) | ||||
|                         done.value = true | ||||
|                         onResult(navController) | ||||
|                     } | ||||
|                     return@analysisUseCase | ||||
|                 } | ||||
|                 try { | ||||
|                     cameraProvider.unbindAll() | ||||
|                     camera = cameraProvider.bindToLifecycle( | ||||
|                         lifecycleOwner, | ||||
|                         CameraSelector.DEFAULT_BACK_CAMERA, | ||||
|                         previewUseCase, | ||||
|                         analysisUseCase | ||||
|                     ) | ||||
|                 } catch (e: Exception) { | ||||
|                     Log.e("ScannerScreen", "Camera bind failed: ${e.message}") | ||||
|                 } | ||||
|             } else { | ||||
|                 Toast.makeText(context, "Camera permission denied", Toast.LENGTH_SHORT).show() | ||||
|                 scope.launch(Dispatchers.Main){ | ||||
|                     onResult(navController) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         when (hasCameraPermission()) { | ||||
|             true -> bindCameraUseCases() | ||||
|             else -> requestPermission() | ||||
|         } | ||||
|         binding.btnScanDone.isEnabled = false | ||||
|         return binding.root | ||||
|     ) | ||||
|  | ||||
|     LaunchedEffect(Unit) { | ||||
|         permissionLauncher.launch(Manifest.permission.CAMERA) | ||||
|     } | ||||
|  | ||||
|     private fun startCreateEntry() { | ||||
|         val createEntryFragment = CreateEntry() | ||||
|         createEntryFragment.arguments = | ||||
|             KeepassWrapper.bundleCreate(null, this.code, this.fmt) | ||||
|         requireActivity().supportFragmentManager.beginTransaction() | ||||
|             .replace(R.id.container, createEntryFragment) | ||||
|             .commit() | ||||
|     } | ||||
|  | ||||
|     private fun hasCameraPermission() = | ||||
|         ActivityCompat.checkSelfPermission( | ||||
|             requireContext(), | ||||
|             Manifest.permission.CAMERA | ||||
|         ) == PackageManager.PERMISSION_GRANTED | ||||
|  | ||||
|     private fun requestPermission() { | ||||
|         ActivityCompat.requestPermissions( | ||||
|             requireActivity(), | ||||
|             arrayOf(Manifest.permission.CAMERA), | ||||
|             CAMERA_PERMISSION_REQUEST_CODE | ||||
|     Box(modifier = Modifier.fillMaxSize()) { | ||||
|         AndroidView( | ||||
|             factory = { previewView }, | ||||
|             modifier = Modifier.fillMaxSize() | ||||
|         ) | ||||
|         ActivityCompat.OnRequestPermissionsResultCallback { c, p, i -> | ||||
|             require(c == CAMERA_PERMISSION_REQUEST_CODE) | ||||
|             require(p.contains(Manifest.permission.CAMERA)) | ||||
|             val el = i[p.indexOf(Manifest.permission.CAMERA)] | ||||
|             if (el != PackageManager.PERMISSION_GRANTED) { | ||||
|                 startCreateEntry() | ||||
|             } | ||||
|  | ||||
|         ScannerOverlay( | ||||
|             modifier = Modifier.fillMaxSize() | ||||
|         ) | ||||
|         Button(onClick = { | ||||
|             torchOn = !torchOn | ||||
|             camera?.cameraControl?.enableTorch(torchOn) | ||||
|         }, modifier = Modifier | ||||
|                     .align(Alignment.BottomEnd) | ||||
|                     .padding(16.dp), | ||||
|         ) { | ||||
|             Icon(Icons.Default.FlashOn, contentDescription = null) | ||||
|         } | ||||
|  | ||||
|         if(!done.value) | ||||
|             CircularProgressIndicator( | ||||
|             modifier = Modifier | ||||
|                 .align(Alignment.BottomCenter) // same spot as buttons | ||||
|                 .padding(bottom =80.dp), | ||||
|             ) | ||||
|     } | ||||
| } | ||||
|  | ||||
|     private fun bindCameraUseCases() { | ||||
|         val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) | ||||
|  | ||||
|         cameraProviderFuture.addListener({ | ||||
|             val cameraProvider = cameraProviderFuture.get() | ||||
|             val previewUseCase = Preview.Builder() | ||||
|                 .build() | ||||
|                 .also { | ||||
|                     it.setSurfaceProvider(binding.cameraView.surfaceProvider) | ||||
|                 } | ||||
|             val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA | ||||
|             val analysisUseCase = getAnalysisUseCase { code, format -> | ||||
|                 if (code != null && format != null) { | ||||
|                     this.code = code | ||||
|                     this.fmt = format | ||||
|                     binding.btnScanDone.isEnabled = true | ||||
| @Composable | ||||
| fun ScannerOverlay( | ||||
|     modifier: Modifier = Modifier | ||||
| ) { | ||||
|     Canvas(modifier = modifier.fillMaxSize()) { | ||||
|         val widthF = size.width | ||||
|         val heightF = size.height | ||||
|  | ||||
|         drawRect( | ||||
|             color = Color(0x80000000), // semi-transparent black | ||||
|             size = size | ||||
|         ) | ||||
|  | ||||
|         val squareSize = 0.75f * minOf(widthF, heightF) | ||||
|         val left = (widthF - squareSize) / 2 | ||||
|         val top = (heightF - squareSize) / 2 | ||||
|  | ||||
|         drawRect( | ||||
|             color = Color.Transparent, | ||||
|             topLeft = Offset(left, top), | ||||
|             size = Size(squareSize, squareSize), | ||||
|             blendMode = BlendMode.Clear | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @Composable | ||||
| fun FileScanner(navController: NavHostController) { | ||||
|     val context = LocalContext.current | ||||
|  | ||||
|     rememberCoroutineScope() | ||||
|     val pickImageLauncher = rememberLauncherForActivityResult( | ||||
|         contract = ActivityResultContracts.PickVisualMedia() | ||||
|     ) { uri -> | ||||
|         if (uri == null) { | ||||
|             Toast.makeText(context, "No file selected", Toast.LENGTH_SHORT).show() | ||||
|             onResult(navController) | ||||
|             return@rememberLauncherForActivityResult | ||||
|         } | ||||
|         try { | ||||
|             val inputStream = context.contentResolver.openInputStream(uri) | ||||
|             val bitmap = BitmapFactory.decodeStream(inputStream) | ||||
|             BarcodeScanner.bitmapUseCase(bitmap) { code, format -> | ||||
|                 if (!code.isNullOrEmpty() && !format.isNullOrEmpty()) { | ||||
|                     activeEntry.value = activeEntry.value.copy(code=code, format=format) | ||||
|                     onResult(navController) | ||||
|                 } else { | ||||
|                     binding.btnScanDone.isEnabled = false | ||||
|                     Toast.makeText(context, "No barcode found", Toast.LENGTH_SHORT).show() | ||||
|                     onResult(navController) | ||||
|                 } | ||||
|             } | ||||
|             try { | ||||
|                 cameraProvider.bindToLifecycle( | ||||
|                     this, | ||||
|                     cameraSelector, | ||||
|                     previewUseCase, | ||||
|                     analysisUseCase | ||||
|                 ) | ||||
|             } catch (illegalStateException: IllegalStateException) { | ||||
|                 Log.e(ContentValues.TAG, illegalStateException.message.orEmpty()) | ||||
|             } catch (illegalArgumentException: IllegalArgumentException) { | ||||
|                 Log.e(ContentValues.TAG, illegalArgumentException.message.orEmpty()) | ||||
|             } | ||||
|         }, ContextCompat.getMainExecutor(requireContext())) | ||||
|         } catch (e: Exception) { | ||||
|             e.printStackTrace() | ||||
|             Toast.makeText(context, "Failed to load image", Toast.LENGTH_SHORT).show() | ||||
|             onResult(navController) | ||||
|         } | ||||
|  | ||||
|     } | ||||
| } | ||||
|  | ||||
|     LaunchedEffect(Unit) { | ||||
|         pickImageLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) | ||||
|     } | ||||
|  | ||||
|     BackHandler { | ||||
|         onResult(navController) | ||||
|     } | ||||
|  | ||||
|     Box( | ||||
|         modifier = Modifier.fillMaxSize(), | ||||
|         contentAlignment = Alignment.Center | ||||
|     ) { | ||||
|         CircularProgressIndicator() | ||||
|     } | ||||
| } | ||||
|  | ||||
| object ScannerEventHandler { | ||||
|     fun onResult(navController: NavController) { | ||||
|         navController.popBackStack() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,144 @@ | ||||
| package net.helcel.fidelity.activity.fragment | ||||
|  | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.Spacer | ||||
| import androidx.compose.foundation.layout.fillMaxHeight | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.width | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material.Button | ||||
| import androidx.compose.material.Icon | ||||
| import androidx.compose.material.MaterialTheme | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.automirrored.filled.Undo | ||||
| import androidx.compose.material.icons.filled.ExpandMore | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.window.Dialog | ||||
| import com.kunzisoft.keepass.database.element.Group | ||||
| import com.kunzisoft.keepass.database.element.node.Node | ||||
| import net.helcel.fidelity.tools.FidelityRepository | ||||
|  | ||||
| @Preview | ||||
| @Composable | ||||
| fun TreeSelectorDialog(onDismiss: (Node?) -> Unit = {}) { | ||||
|     Dialog( | ||||
|         onDismissRequest = {onDismiss(null)}, | ||||
|         content = { | ||||
|             Column( | ||||
|                 modifier = Modifier.fillMaxWidth().background( | ||||
|                     MaterialTheme.colors.background, | ||||
|                     RoundedCornerShape(8.dp) | ||||
|                 ) | ||||
|             ) { | ||||
|                 var currentRoot by remember { mutableStateOf(FidelityRepository.getRoot()) } | ||||
|                 var selection by remember { mutableStateOf<Node?>(FidelityRepository.getRoot()) } | ||||
|  | ||||
|                 Column( | ||||
|                     modifier = Modifier.fillMaxWidth().padding(8.dp) | ||||
|                 ) { | ||||
|  | ||||
|                     Row(verticalAlignment = Alignment.CenterVertically) { | ||||
|                         Button( | ||||
|                             onClick = { | ||||
|                                 selection = currentRoot | ||||
|                                 currentRoot = currentRoot?.parent | ||||
|                             }, | ||||
|                             enabled = currentRoot?.parent != null | ||||
|                         ) { | ||||
|                             Icon(Icons.AutoMirrored.Filled.Undo, contentDescription = "up") | ||||
|                         } | ||||
|                         Spacer(modifier = Modifier.width(8.dp)) | ||||
|                         Text( | ||||
|                             currentRoot?.title ?: "?", | ||||
|                             color = MaterialTheme.colors.onBackground, | ||||
|                             style = MaterialTheme.typography.h6 | ||||
|                         ) | ||||
|                         Spacer(modifier = Modifier.width(8.dp)) | ||||
|                     } | ||||
|                     Spacer(modifier = Modifier.height(8.dp)) | ||||
|  | ||||
|                     LazyColumn(modifier = Modifier.fillMaxHeight(0.75f)) { | ||||
|                         items(currentRoot?.getChildGroups() ?: emptyList()) { entry -> | ||||
|                             val isSel = (entry.nodeId == selection?.nodeId) | ||||
|                             Row( | ||||
|                                 modifier = Modifier | ||||
|                                     .fillMaxWidth() | ||||
|                                     .background(color = if (isSel) MaterialTheme.colors.primary else MaterialTheme.colors.background) | ||||
|                                     .clickable { | ||||
|                                         if (entry.getChildEntries().isNotEmpty()) { | ||||
|                                             currentRoot = entry | ||||
|                                             selection = entry | ||||
|                                         } else if (entry.getChildGroups().isNotEmpty()) { | ||||
|                                             currentRoot = entry | ||||
|                                             selection = entry | ||||
|                                         } else { | ||||
|                                             selection = entry | ||||
|                                         } | ||||
|                                     } | ||||
|                                     .padding(8.dp) | ||||
|                             ) { | ||||
|                                 if (entry.getChildEntries().isNotEmpty() || entry.getChildGroups() | ||||
|                                         .isNotEmpty() | ||||
|                                 ) { | ||||
|                                     Icon( | ||||
|                                         imageVector = Icons.Default.ExpandMore, | ||||
|                                         contentDescription = null, | ||||
|                                         tint = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground | ||||
|                                     ) | ||||
|                                 } | ||||
|                                 Text( | ||||
|                                     entry.title, | ||||
|                                     modifier = Modifier.padding(start = 8.dp), | ||||
|                                     color = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|                         items(currentRoot?.getChildEntries() ?: emptyList()) { entry -> | ||||
|                             val isSel = (entry.nodeId == selection?.nodeId) | ||||
|                             Row( | ||||
|                                 modifier = Modifier | ||||
|                                     .fillMaxWidth() | ||||
|                                     .background(color = if (isSel) MaterialTheme.colors.primary else MaterialTheme.colors.background) | ||||
|                                     .clickable { | ||||
|                                         selection = entry | ||||
|                                     } | ||||
|                                     .padding(8.dp) | ||||
|                             ) { | ||||
|                                 Text( | ||||
|                                     entry.title, | ||||
|                                     modifier = Modifier.padding(start = 8.dp), | ||||
|                                     color = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     Spacer(modifier = Modifier.height(8.dp)) | ||||
|                     Button( | ||||
|                         modifier = Modifier.align(Alignment.CenterHorizontally), | ||||
|                         enabled = selection != null, | ||||
|                         onClick = { | ||||
|                             onDismiss(selection) | ||||
|                         }) { | ||||
|                         Text("Select " + if (selection is Group) "Group" else "Entry") | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     ) | ||||
| } | ||||
							
								
								
									
										274
									
								
								app/src/main/java/net/helcel/fidelity/activity/fragment/Setup.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,274 @@ | ||||
| package net.helcel.fidelity.activity.fragment | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import androidx.activity.compose.BackHandler | ||||
| import androidx.activity.compose.rememberLauncherForActivityResult | ||||
| import androidx.activity.result.contract.ActivityResultContracts.OpenDocument | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.Spacer | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.foundation.layout.width | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.foundation.text.KeyboardOptions | ||||
| import androidx.compose.material.Button | ||||
| import androidx.compose.material.Checkbox | ||||
| import androidx.compose.material.CheckboxDefaults | ||||
| import androidx.compose.material.CircularProgressIndicator | ||||
| import androidx.compose.material.MaterialTheme | ||||
| import androidx.compose.material.OutlinedTextField | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.material.TextFieldDefaults | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.text.input.ImeAction | ||||
| import androidx.compose.ui.text.input.KeyboardCapitalization | ||||
| import androidx.compose.ui.text.input.KeyboardType | ||||
| import androidx.compose.ui.text.input.PasswordVisualTransformation | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.navigation.NavHostController | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.withContext | ||||
| import net.helcel.fidelity.activity.ToastHelper | ||||
| import net.helcel.fidelity.activity.fragment.SetupEventHandlers.onOpen | ||||
| import net.helcel.fidelity.tools.CredentialResult | ||||
| import net.helcel.fidelity.tools.FidelityRepository.genCredentials | ||||
| import net.helcel.fidelity.tools.FidelityRepository.start | ||||
| import net.helcel.fidelity.tools.KeePassStore.loadCredentials | ||||
| import net.helcel.fidelity.tools.KeePassStore.packCredentials | ||||
| import net.helcel.fidelity.tools.KeePassStore.saveCredentials | ||||
|  | ||||
|  | ||||
| class GetPersistentContent : OpenDocument() { | ||||
|     @SuppressLint("InlinedApi") | ||||
|     override fun createIntent(context: Context, input: Array<String>): Intent { | ||||
|         return super.createIntent(context, input).apply { | ||||
|             addCategory(Intent.CATEGORY_DEFAULT) | ||||
|             addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) | ||||
|             addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||||
|             addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Preview | ||||
| @Composable | ||||
| fun InitialScreen( | ||||
|     navController: NavHostController? | ||||
| ) { | ||||
|     var loading by remember { mutableStateOf(false) } | ||||
|     var dbFile by remember { mutableStateOf<Uri?>(null) } | ||||
|     var password by remember { mutableStateOf("") } | ||||
|     var keyFile by remember { mutableStateOf<Uri?>(null) } | ||||
|     val context = LocalContext.current | ||||
|     val scope = rememberCoroutineScope() | ||||
|  | ||||
|     val dbFilePickerLauncher = rememberLauncherForActivityResult( | ||||
|         contract = GetPersistentContent(), | ||||
|     ) { | ||||
|         if(it!=null) { | ||||
|             dbFile = it | ||||
|             scope.launch(Dispatchers.IO) { | ||||
|                 context.contentResolver.takePersistableUriPermission( | ||||
|                     it, | ||||
|                     Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     val keyFilePickerLauncher = rememberLauncherForActivityResult( | ||||
|         contract = GetPersistentContent() | ||||
|     ) { | ||||
|         if(it!=null) { | ||||
|             keyFile = it | ||||
|             scope.launch(Dispatchers.IO) { | ||||
|                 context.contentResolver.takePersistableUriPermission( | ||||
|                     it, | ||||
|                     Intent.FLAG_GRANT_READ_URI_PERMISSION | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     BackHandler { | ||||
|         navController!!.navigate("exit") | ||||
|     } | ||||
|     LaunchedEffect(Unit) { | ||||
|         scope.launch(Dispatchers.Main) { | ||||
|             when(val res = loadCredentials(context)) { | ||||
|                 CredentialResult.AuthFailed -> null | ||||
|                 CredentialResult.NoData -> null | ||||
|                 is CredentialResult.Success -> { | ||||
|                     if (res.db != null) dbFile = res.db | ||||
|                     if (res.key != null) keyFile = res.key | ||||
|                     if (res.password != "" && password == "") password = res.password | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     Box(modifier = Modifier | ||||
|         .fillMaxSize() | ||||
|         .background(MaterialTheme.colors.background)) { | ||||
|         Column( | ||||
|             modifier = Modifier | ||||
|                 .fillMaxSize() | ||||
|                 .padding(16.dp) | ||||
|                 .background(MaterialTheme.colors.background), | ||||
|             verticalArrangement = Arrangement.Center | ||||
|         ) { | ||||
|             Text( | ||||
|                 "Keypass Database Setup", | ||||
|                 style = MaterialTheme.typography.h5, | ||||
|                 color = MaterialTheme.colors.onBackground | ||||
|             ) | ||||
|             Spacer(modifier = Modifier.height(16.dp)) | ||||
|  | ||||
|             Row( | ||||
|                 modifier = Modifier.fillMaxWidth(), | ||||
|                 verticalAlignment = Alignment.CenterVertically | ||||
|             ) { | ||||
|                 Text("KDBX Database:", color = MaterialTheme.colors.onBackground) | ||||
|                 Spacer(modifier = Modifier.width(8.dp)) | ||||
|                 Checkbox( | ||||
|                     enabled = !loading, | ||||
|                     modifier = Modifier | ||||
|                         .background( | ||||
|                             MaterialTheme.colors.primary, | ||||
|                             RoundedCornerShape(8.dp) | ||||
|                         ) | ||||
|                         .size(32.dp), | ||||
|                     checked = dbFile != null, | ||||
|                     onCheckedChange = { dbFilePickerLauncher.launch(arrayOf("*/*")) }, | ||||
|                     colors = CheckboxDefaults.colors( | ||||
|                         uncheckedColor = MaterialTheme.colors.primary, | ||||
|                         checkedColor = MaterialTheme.colors.primary, | ||||
|                         checkmarkColor = MaterialTheme.colors.onPrimary | ||||
|                     ), | ||||
|                 ) | ||||
|             } | ||||
|             Spacer(modifier = Modifier.height(8.dp)) | ||||
|             OutlinedTextField( | ||||
|                 enabled = !loading, | ||||
|                 value = password, | ||||
|                 onValueChange = { password = it }, | ||||
|                 label = { Text("Password") }, | ||||
|                 singleLine = true, | ||||
|                 keyboardOptions = KeyboardOptions( | ||||
|                     capitalization = KeyboardCapitalization.Unspecified, | ||||
|                     autoCorrectEnabled = false, | ||||
|                     keyboardType = KeyboardType.Password, | ||||
|                     imeAction = ImeAction.Done | ||||
|                 ), | ||||
|                 colors = TextFieldDefaults.textFieldColors( | ||||
|                     textColor = MaterialTheme.colors.onBackground | ||||
|                 ), | ||||
|                 visualTransformation = PasswordVisualTransformation(), | ||||
|                 modifier = Modifier.fillMaxWidth() | ||||
|             ) | ||||
|             Spacer(modifier = Modifier.height(8.dp)) | ||||
|             Row( | ||||
|                 modifier = Modifier.fillMaxWidth(), | ||||
|                 verticalAlignment = Alignment.CenterVertically | ||||
|             ) { | ||||
|                 Text("KDBX Key File:", color = MaterialTheme.colors.onBackground) | ||||
|                 Spacer(modifier = Modifier.width(8.dp)) | ||||
|                 Checkbox( | ||||
|                     enabled = !loading, | ||||
|                     modifier = Modifier | ||||
|                         .background( | ||||
|                             MaterialTheme.colors.primary, | ||||
|                             RoundedCornerShape(8.dp) | ||||
|                         ) | ||||
|                         .size(32.dp), | ||||
|                     checked = keyFile != null, | ||||
|                     onCheckedChange = { keyFilePickerLauncher.launch(arrayOf("*/*")) }, | ||||
|                     colors = CheckboxDefaults.colors( | ||||
|                         uncheckedColor = MaterialTheme.colors.primary, | ||||
|                         checkedColor = MaterialTheme.colors.primary, | ||||
|                         checkmarkColor = MaterialTheme.colors.onPrimary | ||||
|                     ), | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|  | ||||
|  | ||||
|             Spacer(modifier = Modifier.height(16.dp)) | ||||
|  | ||||
|             Button( | ||||
|                 enabled = !loading && password.isNotBlank() && dbFile != null , | ||||
|                 onClick = { | ||||
|                     loading = true | ||||
|                     scope.launch { | ||||
|                         if(onOpen(context, dbFile!!, password, keyFile)){ | ||||
|                             navController!!.popBackStack() | ||||
|                             navController.navigate("init") | ||||
|                         }else{ | ||||
|                             ToastHelper.show(context, "Auth failed...") | ||||
|                             navController!!.popBackStack() | ||||
|                             navController.navigate("exit") | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 modifier = Modifier.fillMaxWidth() | ||||
|             ) { | ||||
|                 Text("Continue") | ||||
|             } | ||||
|         } | ||||
|         Box(contentAlignment = Alignment.BottomCenter, modifier = Modifier | ||||
|             .fillMaxSize() | ||||
|             .padding(32.dp)){ | ||||
|  | ||||
|             if(loading ) | ||||
|                 CircularProgressIndicator( | ||||
|                     modifier = Modifier | ||||
|                         .align(Alignment.BottomCenter) // same spot as buttons | ||||
|                         .padding(bottom = 80.dp), | ||||
|                 ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| object SetupEventHandlers { | ||||
|     suspend fun onOpen(context: Context, db: Uri, p: String, key: Uri?): Boolean { | ||||
|         try { | ||||
|             val packCred = packCredentials(db, p, key) | ||||
|             withContext(Dispatchers.IO) { | ||||
|                     start(context, db, genCredentials(context, packCred) | ||||
|                     ) | ||||
|             } | ||||
|  | ||||
|             val res = withContext(Dispatchers.Main) { | ||||
|                 saveCredentials(context, packCred) | ||||
|             } | ||||
|             return when (res) { | ||||
|                 CredentialResult.AuthFailed, CredentialResult.NoData -> false | ||||
|                 is CredentialResult.Success -> true | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             ToastHelper.show(context, e.message.toString()) | ||||
|             println("Err${e.toString()}") | ||||
|             println(e.message) | ||||
|             return false | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,86 +1,125 @@ | ||||
| package net.helcel.fidelity.activity.fragment | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.pm.ActivityInfo | ||||
| import android.content.res.Configuration | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL | ||||
| import android.app.Activity | ||||
| import android.graphics.Bitmap | ||||
| import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE | ||||
| import androidx.fragment.app.Fragment | ||||
| import com.google.zxing.FormatException | ||||
| import net.helcel.fidelity.databinding.FragViewEntryBinding | ||||
| import android.widget.Toast | ||||
| import androidx.activity.compose.BackHandler | ||||
| import androidx.compose.foundation.Image | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.interaction.MutableInteractionSource | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.BoxWithConstraints | ||||
| import androidx.compose.foundation.layout.aspectRatio | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.width | ||||
| import androidx.compose.material.CircularProgressIndicator | ||||
| import androidx.compose.material.MaterialTheme | ||||
| import androidx.compose.material.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.SideEffect | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.rotate | ||||
| import androidx.compose.ui.draw.scale | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.graphics.asImageBitmap | ||||
| import androidx.compose.ui.layout.ContentScale | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.navigation.NavHostController | ||||
| import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode | ||||
| import net.helcel.fidelity.tools.ErrorToaster | ||||
| import net.helcel.fidelity.tools.KeepassWrapper | ||||
| import net.helcel.fidelity.tools.FidelityEntry | ||||
| import kotlin.let | ||||
| import kotlin.math.min | ||||
|  | ||||
| @SuppressLint("SourceLockedOrientationActivity") | ||||
| class ViewEntry : Fragment() { | ||||
|  | ||||
|     private lateinit var binding: FragViewEntryBinding | ||||
|     private var title: String? = null | ||||
|     private var code: String? = null | ||||
|     private var fmt: String? = null | ||||
| @Preview | ||||
| @Composable | ||||
| fun PreviewEntryScreen(){ | ||||
|   ViewEntryScreen(null, FidelityEntry("Title","AAA","QR")) | ||||
| } | ||||
|  | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
|         container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View { | ||||
|         binding = FragViewEntryBinding.inflate(layoutInflater) | ||||
|         val res = KeepassWrapper.bundleExtract(arguments) | ||||
|         title = res.first | ||||
|         code = res.second | ||||
|         fmt = res.third | ||||
| @Composable | ||||
| fun ViewEntryScreen( | ||||
|     navController: NavHostController?, | ||||
|     entry: FidelityEntry | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     val activity = context as? Activity | ||||
|     var isFull by remember { mutableStateOf(false) } | ||||
|     var bitmap by remember { mutableStateOf<Bitmap?>(null) } | ||||
|  | ||||
|         updatePreview() | ||||
|         updateLayout() | ||||
|  | ||||
|         binding.imageViewPreview.setOnClickListener { | ||||
|             requireActivity().requestedOrientation = | ||||
|                 if (isLandscape()) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT | ||||
|                 else ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE | ||||
|     SideEffect { | ||||
|         activity?.window?.attributes = activity.window?.attributes?.apply { | ||||
|             screenBrightness = if (isFull) 1f else BRIGHTNESS_OVERRIDE_NONE | ||||
|         } | ||||
|  | ||||
|         return binding.root | ||||
|     } | ||||
|  | ||||
|     private fun updatePreview() { | ||||
|         binding.title.text = title | ||||
|         try { | ||||
|             val barcodeBitmap = generateBarcode( | ||||
|                 code, fmt, 1024 | ||||
|             bitmap = generateBarcode(entry.code, entry.format, 1024) | ||||
|         } catch (_: Exception) { | ||||
|             bitmap = null | ||||
|             Toast.makeText(context, "Invalid barcode format", Toast.LENGTH_SHORT).show() | ||||
|         } | ||||
|     } | ||||
|     BackHandler { | ||||
|         isFull=false | ||||
|         navController!!.popBackStack() | ||||
|     } | ||||
|      | ||||
|  | ||||
|     Box( | ||||
|         modifier = Modifier | ||||
|             .fillMaxSize() | ||||
|             .background(Color.Black) | ||||
|             .clickable( | ||||
|                 onClick = { isFull = !isFull }, | ||||
|                 indication = null, // remove ripple effect | ||||
|                 interactionSource = remember { MutableInteractionSource() } | ||||
|             ), | ||||
|             contentAlignment = Alignment.TopCenter | ||||
|     ) { | ||||
|         if (!isFull) { | ||||
|             Text( | ||||
|                 text = entry.title, | ||||
|                 color = Color.White, | ||||
|                 style = MaterialTheme.typography.h4, | ||||
|                 modifier = Modifier.padding(32.dp) | ||||
|             ) | ||||
|             binding.imageViewPreview.setImageBitmap(barcodeBitmap) | ||||
|         } catch (e: FormatException) { | ||||
|             ErrorToaster.invalidFormat(requireActivity()) | ||||
|             binding.imageViewPreview.setImageBitmap(null) | ||||
|         } catch (e: IllegalArgumentException) { | ||||
|             binding.imageViewPreview.setImageBitmap(null) | ||||
|             ErrorToaster.invalidFormat(requireActivity()) | ||||
|         } catch (e: Exception) { | ||||
|             binding.imageViewPreview.setImageBitmap(null) | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun updateLayout() { | ||||
|         if (isLandscape()) { | ||||
|             binding.title.visibility = View.GONE | ||||
|             setScreenBrightness(BRIGHTNESS_OVERRIDE_FULL) | ||||
|         } else { | ||||
|             binding.title.visibility = View.VISIBLE | ||||
|             setScreenBrightness(BRIGHTNESS_OVERRIDE_NONE) | ||||
|  | ||||
|     BoxWithConstraints( | ||||
|         modifier = Modifier | ||||
|             .fillMaxSize().padding(8.dp), | ||||
|         contentAlignment = Alignment.Center | ||||
|     ) { | ||||
|             bitmap?.let { | ||||
|  | ||||
|  | ||||
|                 val modifier = Modifier | ||||
|                     .fillMaxSize() | ||||
|                     .width(maxWidth) | ||||
|                     .height(maxHeight) | ||||
|                     .padding(16.dp) | ||||
|                     .aspectRatio(it.width.toFloat()/it.height.toFloat()) | ||||
|                     .rotate(if (isFull) 90f else 0f) | ||||
|                     .scale(if(isFull) min(it.width.dp/maxHeight,it.height.dp/maxWidth) else 1f) | ||||
|  | ||||
|                 Image( | ||||
|                     bitmap = it.asImageBitmap(), | ||||
|                     contentDescription = "Barcode", | ||||
|                     modifier = modifier, | ||||
|                     contentScale = ContentScale.Fit, | ||||
|                 ) | ||||
|             } ?: CircularProgressIndicator(color = Color.White) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun isLandscape(): Boolean { | ||||
|         return (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) | ||||
|     } | ||||
|  | ||||
|     private fun setScreenBrightness(brightness: Float?) { | ||||
|         requireActivity().window?.attributes?.screenBrightness = brightness | ||||
|     } | ||||
| } | ||||
| @@ -1,45 +0,0 @@ | ||||
| package net.helcel.fidelity.activity.view | ||||
|  | ||||
| import android.content.Context | ||||
| import android.graphics.Canvas | ||||
| import android.graphics.Color | ||||
| import android.graphics.Paint | ||||
| import android.graphics.PorterDuff | ||||
| import android.graphics.PorterDuffXfermode | ||||
| import android.util.AttributeSet | ||||
| import android.view.View | ||||
|  | ||||
| class ScannerView : View { | ||||
|  | ||||
|     private val overlayPaint = Paint().apply { | ||||
|         color = Color.parseColor("#80000000") // Semi-transparent black | ||||
|         style = Paint.Style.FILL | ||||
|     } | ||||
|  | ||||
|     private val clearPaint = Paint().apply { | ||||
|         xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) | ||||
|     } | ||||
|  | ||||
|     constructor(context: Context) : super(context) | ||||
|     constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) | ||||
|     constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( | ||||
|         context, | ||||
|         attrs, | ||||
|         defStyleAttr | ||||
|     ) | ||||
|  | ||||
|     override fun onDraw(canvas: Canvas) { | ||||
|         super.onDraw(canvas) | ||||
|  | ||||
|         canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), overlayPaint) | ||||
|  | ||||
|         val centerX = width / 2f | ||||
|         val centerY = height / 2f | ||||
|         val squareSize = 0.75f * width.coerceAtMost(height) | ||||
|         canvas.drawRect( | ||||
|             centerX - squareSize / 2, centerY - squareSize / 2, | ||||
|             centerX + squareSize / 2, centerY + squareSize / 2, clearPaint | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -1,94 +0,0 @@ | ||||
| package net.helcel.fidelity.pluginSDK | ||||
|  | ||||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import org.json.JSONArray | ||||
| import org.json.JSONException | ||||
|  | ||||
|  | ||||
| object AccessManager { | ||||
|     private const val PREF_KEY_SCOPE = "scope" | ||||
|     private const val PREF_KEY_TOKEN = "token" | ||||
|  | ||||
|     private fun stringArrayToString(values: ArrayList<String?>): String? { | ||||
|         if (values.isEmpty()) return null | ||||
|         val a = JSONArray() | ||||
|         values.forEach { a.put(it) } | ||||
|         return a.toString() | ||||
|     } | ||||
|  | ||||
|     private fun stringToStringArray(s: String?): ArrayList<String> { | ||||
|         val strings = ArrayList<String>() | ||||
|         if (s.isNullOrEmpty()) return strings | ||||
|  | ||||
|         try { | ||||
|             val a = JSONArray(s) | ||||
|             for (i in 0 until a.length()) | ||||
|                 strings.add(a.optString(i)) | ||||
|         } catch (e: JSONException) { | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|         return strings | ||||
|     } | ||||
|  | ||||
|     fun storeAccessToken( | ||||
|         ctx: Context, | ||||
|         hostPackage: String?, | ||||
|         accessToken: String?, | ||||
|         scopes: ArrayList<String?> | ||||
|     ) { | ||||
|         val prefs = getPrefsForHost(ctx, hostPackage) | ||||
|         val edit = prefs.edit() | ||||
|         edit.putString(PREF_KEY_TOKEN, accessToken) | ||||
|         val scopesString = stringArrayToString(scopes) | ||||
|         edit.putString(PREF_KEY_SCOPE, scopesString) | ||||
|         edit.apply() | ||||
|  | ||||
|         val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE) | ||||
|         if (!hostPrefs.contains(hostPackage)) | ||||
|             hostPrefs.edit().putString(hostPackage, "").apply() | ||||
|     } | ||||
|  | ||||
|     private fun getPrefsForHost( | ||||
|         ctx: Context, | ||||
|         hostPackage: String? | ||||
|     ): SharedPreferences { | ||||
|         return ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE) | ||||
|     } | ||||
|  | ||||
|     fun tryGetAccessToken(ctx: Context, hostPackage: String?, scopes: ArrayList<String?>): String? { | ||||
|         if (hostPackage.isNullOrEmpty()) return null | ||||
|  | ||||
|         val prefs = getPrefsForHost(ctx, hostPackage) | ||||
|         val scopesString = prefs.getString(PREF_KEY_SCOPE, "") | ||||
|         val currentScope = stringToStringArray(scopesString) | ||||
|         if (!isSubset(scopes, currentScope)) | ||||
|             return null | ||||
|         return prefs.getString(PREF_KEY_TOKEN, null) | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private fun isSubset( | ||||
|         requiredScopes: ArrayList<String?>, | ||||
|         availableScopes: ArrayList<String> | ||||
|     ): Boolean { | ||||
|         return availableScopes.containsAll(requiredScopes) | ||||
|     } | ||||
|  | ||||
|     fun removeAccessToken( | ||||
|         ctx: Context, hostPackage: String?, | ||||
|         accessToken: String? | ||||
|     ) { | ||||
|         val prefs = getPrefsForHost(ctx, hostPackage) | ||||
|         if (prefs.getString(PREF_KEY_TOKEN, "") == accessToken) { | ||||
|             val edit = prefs.edit() | ||||
|             edit.clear() | ||||
|             edit.apply() | ||||
|         } | ||||
|  | ||||
|         val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE) | ||||
|         if (hostPrefs.contains(hostPackage)) { | ||||
|             hostPrefs.edit().remove(hostPackage).apply() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| package net.helcel.fidelity.pluginSDK | ||||
|  | ||||
| @Suppress("unused") | ||||
| object KeepassDef { | ||||
|     var TitleField: String = "Title" | ||||
|     var UserNameField: String = "UserName" | ||||
|     var PasswordField: String = "Password" | ||||
|     var UrlField: String = "URL" | ||||
| } | ||||
| @@ -1,49 +0,0 @@ | ||||
| package net.helcel.fidelity.pluginSDK | ||||
|  | ||||
| import android.content.Intent | ||||
| import org.json.JSONException | ||||
| import org.json.JSONObject | ||||
|  | ||||
| object Kp2aControl { | ||||
|  | ||||
|     fun getAddEntryIntent( | ||||
|         fields: HashMap<String?, String?>, | ||||
|         protectedFields: ArrayList<String?>? | ||||
|     ): Intent { | ||||
|         val outputData = JSONObject((fields as Map<*, *>)).toString() | ||||
|         val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK) | ||||
|         startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT) | ||||
|         startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) | ||||
|         startKp2aIntent.putExtra("KP2A_APPTASK", "CreateEntryThenCloseTask") | ||||
|         startKp2aIntent.putExtra("ShowUserNotifications", "false") | ||||
|         startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData) | ||||
|         if (protectedFields != null) | ||||
|             startKp2aIntent.putStringArrayListExtra( | ||||
|                 Strings.EXTRA_PROTECTED_FIELDS_LIST, | ||||
|                 protectedFields | ||||
|             ) | ||||
|         return startKp2aIntent | ||||
|     } | ||||
|  | ||||
|     fun getQueryEntryForOwnPackageIntent(): Intent { | ||||
|         return Intent(Strings.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE) | ||||
|     } | ||||
|  | ||||
|     fun getEntryFieldsFromIntent(intent: Intent?): HashMap<String, String> { | ||||
|         val res = HashMap<String, String>() | ||||
|         try { | ||||
|             val json = JSONObject(intent?.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA) ?: "") | ||||
|             val itr = json.keys() | ||||
|             while (itr.hasNext()) { | ||||
|                 val key = itr.next() | ||||
|                 val value = json[key].toString() | ||||
|                 res[key] = value | ||||
|             } | ||||
|         } catch (e: JSONException) { | ||||
|             e.printStackTrace() | ||||
|         } catch (e: NullPointerException) { | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|         return res | ||||
|     } | ||||
| } | ||||
| @@ -1,51 +0,0 @@ | ||||
| package net.helcel.fidelity.pluginSDK | ||||
|  | ||||
| import android.content.BroadcastReceiver | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
|  | ||||
| class PluginAccessBroadcastReceiver : BroadcastReceiver() { | ||||
|     override fun onReceive(ctx: Context, intent: Intent) { | ||||
|         val action = intent.action ?: return | ||||
|         when (action) { | ||||
|             Strings.ACTION_TRIGGER_REQUEST_ACCESS -> requestAccess(ctx, intent) | ||||
|             Strings.ACTION_RECEIVE_ACCESS -> receiveAccess(ctx, intent) | ||||
|             Strings.ACTION_REVOKE_ACCESS -> revokeAccess(ctx, intent) | ||||
|             else -> {} | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun revokeAccess(ctx: Context, intent: Intent) { | ||||
|         val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER) | ||||
|         val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN) | ||||
|         AccessManager.removeAccessToken(ctx, senderPackage, accessToken) | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private fun receiveAccess(ctx: Context, intent: Intent) { | ||||
|         val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER) | ||||
|         val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN) | ||||
|         AccessManager.storeAccessToken(ctx, senderPackage, accessToken, scopes) | ||||
|     } | ||||
|  | ||||
|     private fun requestAccess(ctx: Context, intent: Intent) { | ||||
|         val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER) | ||||
|         val requestToken = intent.getStringExtra(Strings.EXTRA_REQUEST_TOKEN) | ||||
|         val rpi = Intent(Strings.ACTION_REQUEST_ACCESS) | ||||
|         rpi.setPackage(senderPackage) | ||||
|         rpi.putExtra(Strings.EXTRA_SENDER, ctx.packageName) | ||||
|         rpi.putExtra(Strings.EXTRA_REQUEST_TOKEN, requestToken) | ||||
|  | ||||
|         val token: String? = AccessManager.tryGetAccessToken(ctx, senderPackage, scopes) | ||||
|         rpi.putExtra(Strings.EXTRA_ACCESS_TOKEN, token) | ||||
|  | ||||
|         rpi.putStringArrayListExtra(Strings.EXTRA_SCOPES, scopes) | ||||
|         ctx.sendBroadcast(rpi) | ||||
|     } | ||||
|  | ||||
|     private val scopes: ArrayList<String?> = ArrayList( | ||||
|         listOf( | ||||
|             Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE, | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
| @@ -1,30 +0,0 @@ | ||||
| package net.helcel.fidelity.pluginSDK | ||||
|  | ||||
| @Suppress("unused") | ||||
| object Strings { | ||||
|  | ||||
|     const val SCOPE_DATABASE_ACTIONS = "keepass2android.SCOPE_DATABASE_ACTIONS" | ||||
|     const val SCOPE_CURRENT_ENTRY = "keepass2android.SCOPE_CURRENT_ENTRY" | ||||
|     const val SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE = | ||||
|         "keepass2android.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE" | ||||
|  | ||||
|     const val EXTRA_SCOPES = "keepass2android.EXTRA_SCOPES" | ||||
|     const val EXTRA_PLUGIN_PACKAGE = "keepass2android.EXTRA_PLUGIN_PACKAGE" | ||||
|  | ||||
|     const val EXTRA_SENDER = "keepass2android.EXTRA_SENDER" | ||||
|     const val EXTRA_REQUEST_TOKEN = "keepass2android.EXTRA_REQUEST_TOKEN" | ||||
|     const val ACTION_START_WITH_TASK = "keepass2android.ACTION_START_WITH_TASK" | ||||
|  | ||||
|     const val ACTION_TRIGGER_REQUEST_ACCESS = "keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" | ||||
|     const val ACTION_REQUEST_ACCESS = "keepass2android.ACTION_REQUEST_ACCESS" | ||||
|     const val ACTION_RECEIVE_ACCESS = "keepass2android.ACTION_RECEIVE_ACCESS" | ||||
|     const val ACTION_REVOKE_ACCESS = "keepass2android.ACTION_REVOKE_ACCESS" | ||||
|  | ||||
|  | ||||
|     const val EXTRA_ENTRY_OUTPUT_DATA = "keepass2android.EXTRA_ENTRY_OUTPUT_DATA" | ||||
|     const val EXTRA_PROTECTED_FIELDS_LIST = "keepass2android.EXTRA_PROTECTED_FIELDS_LIST" | ||||
|     const val EXTRA_ACCESS_TOKEN = "keepass2android.EXTRA_ACCESS_TOKEN" | ||||
|  | ||||
|     const val ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE = | ||||
|         "keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE" | ||||
| } | ||||
| @@ -1,6 +1,5 @@ | ||||
| package net.helcel.fidelity.tools | ||||
|  | ||||
| import com.google.mlkit.vision.barcode.common.Barcode | ||||
| import com.google.zxing.BarcodeFormat | ||||
|  | ||||
| object BarcodeFormatConverter { | ||||
| @@ -16,22 +15,40 @@ object BarcodeFormatConverter { | ||||
|             "UPC_A" -> BarcodeFormat.UPC_A | ||||
|             "UPC_E" -> BarcodeFormat.UPC_E | ||||
|             "PDF_417" -> BarcodeFormat.PDF_417 | ||||
|             "AZTEC" -> BarcodeFormat.AZTEC | ||||
|             "CODABAR" -> BarcodeFormat.CODABAR | ||||
|             "MAXICODE" -> BarcodeFormat.MAXICODE | ||||
|             "DATA_MATRIX" -> BarcodeFormat.DATA_MATRIX | ||||
|             "ITF" -> BarcodeFormat.ITF | ||||
|             "RSS_14" -> BarcodeFormat.RSS_14 | ||||
|             "RSS_EXPANDED" -> BarcodeFormat.RSS_EXPANDED | ||||
|             "UPC_EAN" -> BarcodeFormat.UPC_EAN_EXTENSION | ||||
|             else -> throw Exception("Unsupported Format: $f") | ||||
|  | ||||
|  | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun formatToString(f: Int): String { | ||||
|     fun formatToString(f: BarcodeFormat): String { | ||||
|         return when (f) { | ||||
|             Barcode.FORMAT_CODE_128 -> "CODE_128" | ||||
|             Barcode.FORMAT_CODE_39 -> "CODE_39" | ||||
|             Barcode.FORMAT_CODE_93 -> "CODE_93" | ||||
|             Barcode.FORMAT_EAN_8 -> "EAN_8" | ||||
|             Barcode.FORMAT_EAN_13 -> "EAN_13" | ||||
|             Barcode.FORMAT_QR_CODE -> "CODE_QR" | ||||
|             Barcode.FORMAT_UPC_A -> "UPC_A" | ||||
|             Barcode.FORMAT_UPC_E -> "UPC_E" | ||||
|             Barcode.FORMAT_PDF417 -> "PDF_417" | ||||
|             else -> throw Exception("Unsupported Format: $f") | ||||
|             BarcodeFormat.CODE_39 -> "CODE_39" | ||||
|             BarcodeFormat.CODE_93 -> "CODE_93" | ||||
|             BarcodeFormat.CODE_128 -> "CODE_128" | ||||
|             BarcodeFormat.EAN_8 -> "EAN_8" | ||||
|             BarcodeFormat.EAN_13 -> "EAN_13" | ||||
|             BarcodeFormat.QR_CODE -> "CODE_QR" | ||||
|             BarcodeFormat.UPC_A -> "UPC_A" | ||||
|             BarcodeFormat.UPC_E -> "UPC_E" | ||||
|             BarcodeFormat.PDF_417 -> "PDF_417" | ||||
|             BarcodeFormat.AZTEC -> "AZTEC" | ||||
|             BarcodeFormat.CODABAR -> "CODABAR" | ||||
|             BarcodeFormat.MAXICODE -> "MAXICODE" | ||||
|             BarcodeFormat.DATA_MATRIX -> "DATA_MATRIX" | ||||
|             BarcodeFormat.ITF -> "ITF" | ||||
|             BarcodeFormat.RSS_14 -> "RSS_14" | ||||
|             BarcodeFormat.RSS_EXPANDED -> "RSS_EXPANDED" | ||||
|             BarcodeFormat.UPC_EAN_EXTENSION -> "UPC_EAN" | ||||
|             //else -> throw Exception("Unsupported Format: $f") | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,8 @@ import com.google.zxing.MultiFormatWriter | ||||
| import com.google.zxing.WriterException | ||||
| import com.google.zxing.common.BitMatrix | ||||
| import net.helcel.fidelity.tools.BarcodeFormatConverter.stringToFormat | ||||
| import androidx.core.graphics.set | ||||
| import androidx.core.graphics.createBitmap | ||||
|  | ||||
| object BarcodeGenerator { | ||||
|  | ||||
| @@ -31,13 +33,11 @@ object BarcodeGenerator { | ||||
|  | ||||
|  | ||||
|             val bitMatrix: BitMatrix = writer.encode(content, format, width, height) | ||||
|             val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) | ||||
|             val bitmap = createBitmap(width, height) | ||||
|  | ||||
|             for (x in 0 until width) { | ||||
|                 for (y in 0 until height) { | ||||
|                     bitmap.setPixel( | ||||
|                         x, y, getPixelColor(bitMatrix, x, y) | ||||
|                     ) | ||||
|                     bitmap[x, y] = getPixelColor(bitMatrix, x, y) | ||||
|                 } | ||||
|             } | ||||
|             return bitmap | ||||
|   | ||||
| @@ -1,75 +1,61 @@ | ||||
| package net.helcel.fidelity.tools | ||||
|  | ||||
| import android.content.ContentValues | ||||
| import android.util.Log | ||||
| import android.graphics.Bitmap | ||||
| import androidx.annotation.OptIn | ||||
| import androidx.camera.core.ExperimentalGetImage | ||||
| import androidx.camera.core.ImageAnalysis | ||||
| import androidx.camera.core.ImageProxy | ||||
| import com.google.mlkit.vision.barcode.BarcodeScanner | ||||
| import com.google.mlkit.vision.barcode.BarcodeScannerOptions | ||||
| import com.google.mlkit.vision.barcode.BarcodeScanning | ||||
| import com.google.mlkit.vision.barcode.common.Barcode | ||||
| import com.google.mlkit.vision.common.InputImage | ||||
| import com.google.zxing.BinaryBitmap | ||||
| import com.google.zxing.MultiFormatReader | ||||
| import com.google.zxing.NotFoundException | ||||
| import com.google.zxing.RGBLuminanceSource | ||||
| import com.google.zxing.ReaderException | ||||
| import com.google.zxing.common.HybridBinarizer | ||||
| import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString | ||||
| import java.util.concurrent.Executors | ||||
|  | ||||
|  | ||||
| @OptIn(ExperimentalGetImage::class) | ||||
| object BarcodeScanner { | ||||
|  | ||||
|     @OptIn(ExperimentalGetImage::class) | ||||
|     private fun processImageProxy( | ||||
|         barcodeScanner: BarcodeScanner, | ||||
|         imageProxy: ImageProxy, | ||||
|     private fun processImage( | ||||
|         bitmap: Bitmap, | ||||
|         cb: (String?, String?) -> Unit | ||||
|     ) { | ||||
|  | ||||
|         imageProxy.image?.let { image -> | ||||
|             val inputImage = | ||||
|                 InputImage.fromMediaImage( | ||||
|                     image, | ||||
|                     imageProxy.imageInfo.rotationDegrees | ||||
|                 ) | ||||
|  | ||||
|             barcodeScanner.process(inputImage) | ||||
|                 .addOnSuccessListener { barcodeList -> | ||||
|                     val barcode = | ||||
|                         barcodeList.getOrNull(0) | ||||
|                     if (barcode != null) | ||||
|                         cb(barcode.displayValue, formatToString(barcode.format)) | ||||
|                 } | ||||
|                 .addOnFailureListener { | ||||
|                     Log.e(ContentValues.TAG, it.message.orEmpty()) | ||||
|                 }.addOnCompleteListener { | ||||
|                     imageProxy.image?.close() | ||||
|                     imageProxy.close() | ||||
|                 } | ||||
|         val binaryBitmap = createBinaryBitmap(bitmap) | ||||
|         val reader = MultiFormatReader() | ||||
|         try { | ||||
|             val result = reader.decode(binaryBitmap) | ||||
|             cb(result.text, formatToString(result.barcodeFormat)) | ||||
|         } catch (_: NotFoundException) { | ||||
|             cb(null, null) | ||||
|         } catch (_: ReaderException) { | ||||
|             cb(null, null) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun getAnalysisUseCase(cb: (String?, String?) -> Unit): ImageAnalysis { | ||||
|         val options = BarcodeScannerOptions.Builder().setBarcodeFormats( | ||||
|             Barcode.FORMAT_CODE_128, | ||||
|             Barcode.FORMAT_CODE_39, | ||||
|             Barcode.FORMAT_CODE_93, | ||||
|             Barcode.FORMAT_EAN_8, | ||||
|             Barcode.FORMAT_EAN_13, | ||||
|             Barcode.FORMAT_QR_CODE, | ||||
|             Barcode.FORMAT_UPC_A, | ||||
|             Barcode.FORMAT_UPC_E, | ||||
|             Barcode.FORMAT_PDF417 | ||||
|         ).build() | ||||
|         val scanner = BarcodeScanning.getClient(options) | ||||
|         val analysisUseCase = ImageAnalysis.Builder() | ||||
|             .build() | ||||
|     private fun createBinaryBitmap(bitmap: Bitmap): BinaryBitmap { | ||||
|         val pixels = IntArray(bitmap.width * bitmap.height) | ||||
|         bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) | ||||
|         val source = | ||||
|             RGBLuminanceSource(bitmap.width, bitmap.height, pixels) | ||||
|         return BinaryBitmap(HybridBinarizer(source)) | ||||
|     } | ||||
|  | ||||
|     fun analysisUseCase(cb: (String?, String?) -> Unit): ImageAnalysis { | ||||
|         val analysisUseCase = ImageAnalysis.Builder().build() | ||||
|         analysisUseCase.setAnalyzer( | ||||
|             Executors.newSingleThreadExecutor() | ||||
|         ) { imageProxy -> | ||||
|             processImageProxy(scanner, imageProxy, cb) | ||||
|             val bitmap = imageProxy.toBitmap() | ||||
|             imageProxy.close() | ||||
|             bitmapUseCase(bitmap, cb) | ||||
|         } | ||||
|         return analysisUseCase | ||||
|     } | ||||
|  | ||||
|     fun bitmapUseCase(bitmap: Bitmap, cb: (String?, String?) -> Unit) { | ||||
|         processImage(bitmap, cb) | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
							
								
								
									
										142
									
								
								app/src/main/java/net/helcel/fidelity/tools/BiometricStore.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,142 @@ | ||||
| package net.helcel.fidelity.tools | ||||
|  | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| import android.security.keystore.KeyGenParameterSpec | ||||
| import android.security.keystore.KeyProperties | ||||
| import android.util.Base64 | ||||
| import androidx.biometric.BiometricPrompt | ||||
| import androidx.core.content.ContextCompat | ||||
| import javax.crypto.Cipher | ||||
| import androidx.datastore.preferences.core.* | ||||
| import androidx.datastore.preferences.preferencesDataStore | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import com.kunzisoft.keepass.hardware.HardwareKey | ||||
| import com.kunzisoft.keepass.utils.parseUri | ||||
| import kotlinx.coroutines.ExperimentalCoroutinesApi | ||||
| import kotlinx.coroutines.flow.first | ||||
| import kotlinx.coroutines.suspendCancellableCoroutine | ||||
| import java.security.KeyStore | ||||
| import javax.crypto.KeyGenerator | ||||
| import javax.crypto.SecretKey | ||||
|  | ||||
| val Context.securePrefs by preferencesDataStore("keepass_prefs") | ||||
| object KeePassKeys { | ||||
|     val DB_FILE_PATH = stringPreferencesKey("db_file_path") | ||||
|     val PASSWORD = stringPreferencesKey("password_enc") | ||||
|     val KEY_FILE_PATH = stringPreferencesKey("key_file_path") | ||||
|     val IV = stringPreferencesKey("iv") | ||||
| } | ||||
|  | ||||
| sealed class CredentialResult { | ||||
|     data class Success(val db: Uri?, val password: String, val key: Uri?) : CredentialResult() | ||||
|     object NoData : CredentialResult() | ||||
|     object AuthFailed : CredentialResult() | ||||
| } | ||||
|  | ||||
| private const val KEY_ALIAS = "keepass_bio_key" | ||||
|  | ||||
| fun getOrCreateBiometricKey(): SecretKey { | ||||
|     val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } | ||||
|     keyStore.getKey(KEY_ALIAS, null)?.let { return it as SecretKey } | ||||
|     val keyGenerator = | ||||
|         KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") | ||||
|     val spec = KeyGenParameterSpec.Builder( | ||||
|         KEY_ALIAS, | ||||
|         KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT | ||||
|     ).apply { | ||||
|         setBlockModes(KeyProperties.BLOCK_MODE_GCM) | ||||
|         setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) | ||||
|         setUserAuthenticationRequired(true) | ||||
|         setInvalidatedByBiometricEnrollment(true) | ||||
|     }.build() | ||||
|  | ||||
|     keyGenerator.init(spec) | ||||
|     return keyGenerator.generateKey() | ||||
| } | ||||
|  | ||||
| fun getCipherForDecryption(key: SecretKey, iv: ByteArray?): Cipher { | ||||
|     val cipher = Cipher.getInstance("AES/GCM/NoPadding") | ||||
|     if(iv==null) cipher.init(Cipher.ENCRYPT_MODE, key) | ||||
|     else cipher.init(Cipher.DECRYPT_MODE, key, javax.crypto.spec.GCMParameterSpec(128, iv)) | ||||
|     return cipher | ||||
| } | ||||
| object KeePassStore { | ||||
|     suspend fun saveCredentials( | ||||
|         context: Context, cred: CredentialResult.Success | ||||
|     ): CredentialResult { | ||||
|         val cipher = showBiometricPrompt(context as FragmentActivity, true) | ||||
|             ?: return CredentialResult.AuthFailed | ||||
|         val encPasswordB = cipher.doFinal(cred.password.toByteArray(Charsets.UTF_8)) | ||||
|         context.securePrefs.edit { prefs -> | ||||
|             prefs[KeePassKeys.DB_FILE_PATH] = cred.db.toString() | ||||
|             prefs[KeePassKeys.PASSWORD] = Base64.encodeToString(encPasswordB, Base64.DEFAULT) | ||||
|             prefs[KeePassKeys.IV] = Base64.encodeToString(cipher.iv, Base64.DEFAULT) | ||||
|             cred.key?.let { prefs[KeePassKeys.KEY_FILE_PATH] = it.toString() } | ||||
|         } | ||||
|         return cred | ||||
|     } | ||||
|  | ||||
|     suspend fun hasCredentials(context: Context): Boolean { | ||||
|         val prefs = context.securePrefs.data.first() | ||||
|         return prefs[KeePassKeys.DB_FILE_PATH] != null && | ||||
|                 prefs[KeePassKeys.PASSWORD] != null | ||||
|     } | ||||
|  | ||||
|     fun packCredentials(dbFilePath:Uri?, password: String, keyFilePath: Uri?): CredentialResult.Success { | ||||
|         return CredentialResult.Success(dbFilePath, password, keyFilePath) | ||||
|     } | ||||
|  | ||||
|     suspend fun loadCredentials(context: Context): CredentialResult { | ||||
|         val prefs = context.securePrefs.data.first { true } | ||||
|         val dbFilePath = prefs[KeePassKeys.DB_FILE_PATH] ?: return CredentialResult.NoData | ||||
|         val encryptedBase64 = prefs[KeePassKeys.PASSWORD] ?: return CredentialResult.NoData | ||||
|         val keyFilePath = prefs[KeePassKeys.KEY_FILE_PATH] | ||||
|         val cipher = showBiometricPrompt(context as FragmentActivity, false) | ||||
|             ?: return CredentialResult.AuthFailed | ||||
|         val decrypted = cipher.doFinal(Base64.decode(encryptedBase64, Base64.DEFAULT)) | ||||
|         return packCredentials( | ||||
|             dbFilePath.parseUri(), | ||||
|             String(decrypted, Charsets.UTF_8), | ||||
|             keyFilePath?.parseUri() | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @OptIn(ExperimentalCoroutinesApi::class) | ||||
| suspend fun showBiometricPrompt(activity: FragmentActivity, enc: Boolean): Cipher? { | ||||
|     val prefs = activity.securePrefs.data.first() | ||||
|     return suspendCancellableCoroutine { cont -> | ||||
|         val executor = ContextCompat.getMainExecutor(activity) | ||||
|         val biometricPrompt = BiometricPrompt( | ||||
|             activity, | ||||
|             executor, | ||||
|             object : BiometricPrompt.AuthenticationCallback() { | ||||
|                 override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { cont.resume(result.cryptoObject?.cipher) {} } | ||||
|                 override fun onAuthenticationError(code: Int, msg: CharSequence) { cont.resume(null) {} } | ||||
|                 override fun onAuthenticationFailed() { cont.resume(null) {} } | ||||
|             } | ||||
|         ) | ||||
|         val iv = if(enc) null else prefs[KeePassKeys.IV]?.let { Base64.decode(it, Base64.DEFAULT) } | ||||
|         if (!enc && iv == null) { cont.resume(null) {} } | ||||
|         val cipher = getCipherForDecryption(getOrCreateBiometricKey(), iv) | ||||
|         val promptInfo = BiometricPrompt.PromptInfo.Builder() | ||||
|             .setTitle("Unlock KeePass") | ||||
|             .setSubtitle("Authenticate to access your KeePass database") | ||||
|             .setNegativeButtonText("Cancel") | ||||
|             .build() | ||||
|  | ||||
|         biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| fun retrieveResponseFromChallenge( | ||||
|     hardwareKey: HardwareKey, | ||||
|     seed: ByteArray?, | ||||
| ): ByteArray { | ||||
|     val response: ByteArray = "".toByteArray() | ||||
|     return response | ||||
| } | ||||
| @@ -1,50 +0,0 @@ | ||||
| package net.helcel.fidelity.tools | ||||
|  | ||||
| import android.content.SharedPreferences | ||||
| import com.google.gson.Gson | ||||
| import com.google.gson.reflect.TypeToken | ||||
|  | ||||
|  | ||||
| object CacheManager { | ||||
|  | ||||
|     const val PREF_NAME = "FIDELITY" | ||||
|     private const val ENTRY_KEY = "FIDELITY" | ||||
|     private var data: ArrayList<Triple<String?, String?, String?>> = ArrayList() | ||||
|     private var pref: SharedPreferences? = null | ||||
|  | ||||
|     fun addFidelity(item: Triple<String?, String?, String?>) { | ||||
|         val exists = data.find { it.first == item.first } | ||||
|         if (exists != null) | ||||
|             data.remove(exists) | ||||
|  | ||||
|         data.add(0, item) | ||||
|         saveFidelity() | ||||
|     } | ||||
|  | ||||
|     fun rmFidelity(idx: Int) { | ||||
|         data.removeAt(idx) | ||||
|         saveFidelity() | ||||
|     } | ||||
|  | ||||
|     private fun saveFidelity() { | ||||
|         val editor = pref?.edit() | ||||
|         val gson = Gson() | ||||
|         val json = gson.toJson(data) | ||||
|         editor?.putString(ENTRY_KEY, json) | ||||
|         editor?.apply() | ||||
|     } | ||||
|  | ||||
|     fun loadFidelity(pref: SharedPreferences) { | ||||
|         this.pref = pref | ||||
|         val gson = Gson() | ||||
|         val json = pref.getString(ENTRY_KEY, null) | ||||
|         val type = object : TypeToken<List<Triple<String, String, Int>>>() {}.type | ||||
|         data = gson.fromJson(json, type) ?: ArrayList() | ||||
|  | ||||
|     } | ||||
|  | ||||
|     fun getFidelity(): ArrayList<Triple<String?, String?, String?>> { | ||||
|         return data | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| package net.helcel.fidelity.tools | ||||
|  | ||||
| import android.content.Context | ||||
| import android.widget.Toast | ||||
|  | ||||
| object ErrorToaster { | ||||
|     private fun helper(activity: Context?, message: String, length: Int) { | ||||
|         if (activity != null) | ||||
|             Toast.makeText(activity, message, length).show() | ||||
|     } | ||||
|  | ||||
|     fun noKP2AFound(activity: Context?) { | ||||
|         helper(activity, "KeePass2Android Not Installed", Toast.LENGTH_LONG) | ||||
|     } | ||||
|  | ||||
|     fun formIncomplete(activity: Context?) { | ||||
|         helper(activity, "Form Incomplete", Toast.LENGTH_SHORT) | ||||
|     } | ||||
|  | ||||
|     fun invalidFormat(activity: Context?) { | ||||
|         helper(activity, "Invalid Format", Toast.LENGTH_SHORT) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										185
									
								
								app/src/main/java/net/helcel/fidelity/tools/Keepass.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,185 @@ | ||||
| package net.helcel.fidelity.tools | ||||
|  | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| import androidx.compose.runtime.mutableStateListOf | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import kotlinx.serialization.Serializable | ||||
| import java.io.ByteArrayInputStream | ||||
| import kotlinx.serialization.json.Json | ||||
| import androidx.core.content.edit | ||||
| import com.kunzisoft.keepass.database.element.Database | ||||
| import com.kunzisoft.keepass.database.element.Field | ||||
| import com.kunzisoft.keepass.database.element.Group | ||||
| import com.kunzisoft.keepass.database.element.MasterCredential | ||||
| import com.kunzisoft.keepass.database.element.binary.BinaryData | ||||
| import com.kunzisoft.keepass.database.element.node.NodeIdUUID | ||||
| import com.kunzisoft.keepass.database.element.security.ProtectedString | ||||
| import com.kunzisoft.keepass.hardware.HardwareKey | ||||
| import com.kunzisoft.keepass.utils.getBinaryDir | ||||
| import kotlinx.serialization.builtins.ListSerializer | ||||
| import java.io.File | ||||
| import java.util.UUID | ||||
|  | ||||
| object FidelityKeepassFields { | ||||
|     const val FIDELITYFORMAT = "FidelityFormat" | ||||
|     const val FIDELITYCODE = "FidelityCode" | ||||
| } | ||||
|  | ||||
| @Serializable | ||||
| data class FidelityEntry( | ||||
|     val uid: String? = null, | ||||
|     val title: String = "", | ||||
|     val code: String = "", | ||||
|     val format: String = "", | ||||
|     val protected: Boolean = false, | ||||
|  | ||||
|     val hidden: Boolean = false, | ||||
|     val pinned: Boolean = false, | ||||
|     val lastUse: Int = 0, | ||||
| ) | ||||
|  | ||||
| object FidelityRepository { | ||||
|     private var db: Database = Database() | ||||
|     private var binaryDir: File? = null | ||||
|     val entries = mutableStateListOf<FidelityEntry>() | ||||
|     val activeEntry = mutableStateOf(FidelityEntry()) | ||||
|  | ||||
|  | ||||
|     fun getRoot(): Group? { | ||||
|         return db.rootGroup | ||||
|     } | ||||
|  | ||||
|     fun start(ctx: Context, uri: Uri?, c: MasterCredential): Boolean { | ||||
|         if (binaryDir == null) binaryDir = ctx.getBinaryDir() | ||||
|         if (uri == null) return false | ||||
|         try { | ||||
|             val bitStream = | ||||
|                 ByteArrayInputStream(ctx.contentResolver.openInputStream(uri)?.readBytes()) | ||||
|             db.loadData( | ||||
|                 bitStream, c, | ||||
|                 { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) }, | ||||
|                 false, binaryDir!!, | ||||
|                 { BinaryData.canMemoryBeAllocatedInRAM(ctx, it) }, | ||||
|                 false, null | ||||
|             ) | ||||
|             return true | ||||
|         } catch (e: Exception) { | ||||
|             println(e) | ||||
|             return false | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun end(ctx: Context, uri: Uri?, c: MasterCredential): Boolean { | ||||
|         if (uri == null) return false | ||||
|         db.saveData( | ||||
|             File(binaryDir, db.binaryCache.hashCode().toString()),{  ctx.contentResolver.openOutputStream(uri) }, | ||||
|             false, c, { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) }) | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     fun genCredentials( | ||||
|         ctx: Context, | ||||
|         cred: CredentialResult.Success, | ||||
|         hardwareKey: HardwareKey? = null | ||||
|     ): MasterCredential { | ||||
|         return MasterCredential( | ||||
|             cred.password, | ||||
|             cred.key?.let { ctx.contentResolver.openInputStream(cred.key)?.readBytes() }, | ||||
|             hardwareKey | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     fun importDB(context: Context) { | ||||
|         val seenID= arrayListOf<String>() | ||||
|         fun importDBRec(group: Group) { | ||||
|             group.getChildEntries().forEach { | ||||
|                 val fields = it.getExtraFields() | ||||
|                 val code = fields.firstOrNull { e -> e.name == FidelityKeepassFields.FIDELITYCODE } | ||||
|                 val format = | ||||
|                     fields.firstOrNull { e -> e.name == FidelityKeepassFields.FIDELITYFORMAT } | ||||
|                 if (code == null || format == null) return@forEach | ||||
|  | ||||
|                 val newEntry = FidelityEntry( | ||||
|                     uid=it.nodeId.id.toString(), | ||||
|                     title=it.title, | ||||
|                     code=code.protectedValue.stringValue, | ||||
|                     format=format.protectedValue.stringValue, | ||||
|                     protected=code.protectedValue.isProtected, | ||||
|                 ) | ||||
|                 val idx = entries.indexOfFirst { e -> e.uid == newEntry.uid } | ||||
|                 seenID.add(newEntry.uid!!) | ||||
|                 if (idx >= 0) { | ||||
|                     val oldEntry = entries[idx] | ||||
|                     entries[idx] = newEntry.copy( | ||||
|                         pinned = oldEntry.pinned, | ||||
|                         hidden = oldEntry.hidden, | ||||
|                         lastUse = oldEntry.lastUse | ||||
|                     ) | ||||
|                 } else { | ||||
|                     entries.add(newEntry) | ||||
|                 } | ||||
|  | ||||
|  | ||||
|             } | ||||
|             group.getChildGroups().forEach { importDBRec(it) } | ||||
|         } | ||||
|         if (db.rootGroup != null) | ||||
|             importDBRec(db.rootGroup!!) | ||||
|         entries.removeAll { !seenID.contains(it.uid)} | ||||
|         val distinct = entries.distinctBy { it.uid } | ||||
|         entries.clear() | ||||
|         entries.addAll(distinct) | ||||
|         saveEntries(context) | ||||
|     } | ||||
|  | ||||
|     fun saveEntries(context: Context) { | ||||
|         val prefs = context.getSharedPreferences("fidelity_prefs", Context.MODE_PRIVATE) | ||||
|         prefs.edit { putString("entries", Json.encodeToString( | ||||
|             ListSerializer(FidelityEntry.serializer()), | ||||
|             entries | ||||
|         )) } | ||||
|     } | ||||
|  | ||||
|     fun loadEntries(context: Context) { | ||||
|         val prefs = context.getSharedPreferences("fidelity_prefs", Context.MODE_PRIVATE) | ||||
|         try { | ||||
|             val json = prefs.getString("entries", null) ?: return | ||||
|             val list = Json.decodeFromString( | ||||
|                 ListSerializer(FidelityEntry.serializer()), | ||||
|                 json | ||||
|             ) | ||||
|  | ||||
|             entries.clear() | ||||
|             entries.addAll(list) | ||||
|         }catch(_: Exception){ | ||||
|             prefs.edit{ putString("entries",Json.encodeToString( | ||||
|                 ListSerializer(FidelityEntry.serializer()),emptyList())) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun addEntry(ctx: Context, entry: FidelityEntry) { | ||||
|         val dbEntry = db.getEntryById(NodeIdUUID(UUID.fromString(entry.uid))) ?: db.createEntry() | ||||
|         val dbParent = db.getGroupById(NodeIdUUID(UUID.fromString(entry.uid))) | ||||
|         dbEntry?.apply { | ||||
|             putExtraField( | ||||
|                 Field( | ||||
|                     FidelityKeepassFields.FIDELITYCODE, | ||||
|                     ProtectedString(entry.protected, entry.code) | ||||
|                 ) | ||||
|             ) | ||||
|             putExtraField( | ||||
|                 Field( | ||||
|                     FidelityKeepassFields.FIDELITYFORMAT, | ||||
|                     ProtectedString(string= entry.format) | ||||
|                 ) | ||||
|             ) | ||||
|             if(dbParent!=null) title = entry.title | ||||
|             dbParent?.addChildEntry(dbEntry) | ||||
|         } | ||||
|         entries.removeIf {it.uid == entry.uid} | ||||
|         entries.add(entry.copy(uid=dbEntry?.nodeId?.id.toString())) | ||||
|         saveEntries(ctx) | ||||
|     } | ||||
| } | ||||
| @@ -1,85 +0,0 @@ | ||||
| package net.helcel.fidelity.tools | ||||
|  | ||||
| import android.app.Activity | ||||
| import android.content.Intent | ||||
| import android.os.Bundle | ||||
| import androidx.activity.result.ActivityResultLauncher | ||||
| import androidx.activity.result.contract.ActivityResultContracts | ||||
| import androidx.fragment.app.Fragment | ||||
| import net.helcel.fidelity.pluginSDK.KeepassDef | ||||
| import net.helcel.fidelity.pluginSDK.Kp2aControl | ||||
|  | ||||
| object KeepassWrapper { | ||||
|  | ||||
|     private const val CODE_FIELD: String = "FidelityCode" | ||||
|     private const val FORMAT_FIELD: String = "FidelityFormat" | ||||
|     private const val PROTECT_CODE_FIELD: String = "FidelityProtectedCode" | ||||
|  | ||||
|     fun entryCreate( | ||||
|         fragment: Fragment, | ||||
|         title: String, | ||||
|         code: String, | ||||
|         format: String, | ||||
|         protectCode: Boolean, | ||||
|     ): Pair<HashMap<String?, String?>, ArrayList<String?>> { | ||||
|  | ||||
|         val fields = HashMap<String?, String?>() | ||||
|         val protected = ArrayList<String?>() | ||||
|         fields[KeepassDef.TitleField] = title | ||||
|         fields[KeepassDef.UrlField] = | ||||
|             "androidapp://" + fragment.requireActivity().packageName | ||||
|         fields[CODE_FIELD] = code | ||||
|         fields[FORMAT_FIELD] = format | ||||
|         fields[PROTECT_CODE_FIELD] = protectCode.toString() | ||||
|         protected.add(CODE_FIELD) | ||||
|  | ||||
|         return Pair(fields, protected) | ||||
|     } | ||||
|  | ||||
|  | ||||
|     fun resultLauncher( | ||||
|         fragment: Fragment, | ||||
|         callback: (HashMap<String, String>) -> Unit | ||||
|     ): ActivityResultLauncher<Intent> { | ||||
|         return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> | ||||
|             if (result.resultCode == Activity.RESULT_OK) { | ||||
|                 val credentials = Kp2aControl.getEntryFieldsFromIntent(result.data) | ||||
|                 callback(credentials) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun entryExtract(map: HashMap<String, String>): Triple<String?, String?, String?> { | ||||
|         return Triple( | ||||
|             map[KeepassDef.TitleField], | ||||
|             map[CODE_FIELD], | ||||
|             map[FORMAT_FIELD] | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     fun bundleCreate(title: String?, code: String?, fmt: String?): Bundle { | ||||
|         val data = Bundle() | ||||
|         data.putString("title", title) | ||||
|         data.putString("code", code) | ||||
|         data.putString("fmt", fmt) | ||||
|         return data | ||||
|     } | ||||
|  | ||||
|     fun bundleCreate(triple: Triple<String?, String?, String?>): Bundle { | ||||
|         return bundleCreate(triple.first, triple.second, triple.third) | ||||
|     } | ||||
|  | ||||
|     fun bundleExtract(data: Bundle?): Triple<String?, String?, String?> { | ||||
|         return Triple( | ||||
|             data?.getString("title"), | ||||
|             data?.getString("code"), | ||||
|             data?.getString("fmt") | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     fun isProtected(map: HashMap<String, String>): Boolean { | ||||
|         return map[PROTECT_CODE_FIELD].toBoolean() | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
							
								
								
									
										58
									
								
								app/src/main/res/drawable/ic_launcher_foreground.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,58 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportWidth="128" | ||||
|     android:viewportHeight="128"> | ||||
|   <group android:scaleX="1.2833333" | ||||
|       android:scaleY="1.2833333" | ||||
|       android:translateX="-16.612345" | ||||
|       android:translateY="-16.612345"> | ||||
|       <group | ||||
|           android:translateX="34" | ||||
|           android:translateY="26"> | ||||
|           <group | ||||
|               android:scaleX="0.8" | ||||
|               android:scaleY="1.0" | ||||
|               android:translateX="0" | ||||
|               android:translateY="0"> | ||||
|               <path | ||||
|                   android:fillColor="@color/blue" | ||||
|                   android:pathData="M59.959,52.794H12.041c-0.552,0 -1,-0.448 -1,-1v-29.547c0,-0.552 0.448,-1 1,-1h47.918c0.552,0 1,0.448 1,1v29.547C60.959,52.347 60.511,52.794 59.959,52.794z" | ||||
|                   android:strokeWidth="2" | ||||
|                   android:strokeColor="#000000" /> | ||||
|           </group> | ||||
|           <group | ||||
|               android:scaleX="0.4" | ||||
|               android:scaleY="0.5" | ||||
|               android:translateX="27" | ||||
|               android:translateY="15.75"> | ||||
|               <path | ||||
|                   android:fillColor="@color/red" | ||||
|                   android:pathData="M46.5,56l-10,-11l-10,11l0,-45l20,0z" | ||||
|                   android:strokeLineCap="round" | ||||
|                   android:strokeLineJoin="round" /> | ||||
|               <path | ||||
|                   android:fillColor="@color/red2" | ||||
|                   android:fillAlpha="1.0" | ||||
|                   android:pathData="M41.5,11l0,39l5,6l0,-45z" | ||||
|                   android:strokeColor="#00000000" /> | ||||
|  | ||||
|           </group> | ||||
|           <group | ||||
|               android:scaleX="0.75" | ||||
|               android:scaleY="0.75" | ||||
|               android:translateX="6" | ||||
|               android:translateY="10"> | ||||
|               <path | ||||
|                   android:fillColor="#00000000" | ||||
|                   android:pathData="M9,21V52 M12,21V52 M20,21V50 M28,21V50 M15,50V21H17V50H15 M23,50V21H25V50H23 M31,50V21H32V50H31 M35,21V52 M38,21V52" | ||||
|                   android:strokeWidth="2" | ||||
|                   android:strokeColor="#000" | ||||
|                   android:strokeLineCap="round" | ||||
|                   android:strokeLineJoin="round" /> | ||||
|           </group> | ||||
|  | ||||
|       </group> | ||||
|   </group> | ||||
| </vector> | ||||
| @@ -1,21 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <item | ||||
|         android:width="128dp" | ||||
|         android:height="128dp" | ||||
|         android:gravity="center" | ||||
|         android:drawable="@drawable/card" /> | ||||
|     <item | ||||
|         android:width="64dp" | ||||
|         android:height="64dp" | ||||
|         android:drawable="@drawable/barcode" | ||||
|         android:gravity="center" | ||||
|         android:right="32dp" /> | ||||
|     <item | ||||
|         android:width="52dp" | ||||
|         android:height="52dp" | ||||
|         android:drawable="@drawable/bookmark" | ||||
|         android:gravity="center" | ||||
|         android:left="72dp" | ||||
|         android:bottom="20dp" /> | ||||
| </layer-list> | ||||
							
								
								
									
										53
									
								
								app/src/main/res/drawable/logo_g.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,53 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="200dp" | ||||
|     android:height="200dp" | ||||
|     android:viewportWidth="128" | ||||
|     android:viewportHeight="128"> | ||||
|     <group | ||||
|         android:translateX="34" | ||||
|         android:translateY="26"> | ||||
|         <group | ||||
|             android:scaleX="0.8" | ||||
|             android:scaleY="1.0" | ||||
|             android:translateX="0" | ||||
|             android:translateY="0"> | ||||
|             <path | ||||
|                 android:fillColor="@color/blue" | ||||
|                 android:pathData="M59.959,52.794H12.041c-0.552,0 -1,-0.448 -1,-1v-29.547c0,-0.552 0.448,-1 1,-1h47.918c0.552,0 1,0.448 1,1v29.547C60.959,52.347 60.511,52.794 59.959,52.794z" | ||||
|                 android:strokeWidth="2" | ||||
|                 android:strokeColor="#000000" /> | ||||
|         </group> | ||||
|         <group | ||||
|             android:scaleX="0.4" | ||||
|             android:scaleY="0.5" | ||||
|             android:translateX="27" | ||||
|             android:translateY="15.75"> | ||||
|             <path | ||||
|                 android:fillColor="@color/red" | ||||
|                 android:pathData="M46.5,56l-10,-11l-10,11l0,-45l20,0z" | ||||
|                 android:strokeLineCap="round" | ||||
|                 android:strokeLineJoin="round" /> | ||||
|             <path | ||||
|                 android:fillColor="@color/red2" | ||||
|                 android:fillAlpha="1.0" | ||||
|                 android:pathData="M41.5,11l0,39l5,6l0,-45z" | ||||
|                 android:strokeColor="#00000000" /> | ||||
|  | ||||
|         </group> | ||||
|         <group | ||||
|             android:scaleX="0.75" | ||||
|             android:scaleY="0.75" | ||||
|             android:translateX="6" | ||||
|             android:translateY="10"> | ||||
|             <path | ||||
|                 android:fillColor="#00000000" | ||||
|                 android:pathData="M9,21V52 M12,21V52 M20,21V50 M28,21V50 M15,50V21H17V50H15 M23,50V21H25V50H23 M31,50V21H32V50H31 M35,21V52 M38,21V52" | ||||
|                 android:strokeWidth="2" | ||||
|                 android:strokeColor="#000" | ||||
|                 android:strokeLineCap="round" | ||||
|                 android:strokeLineJoin="round" /> | ||||
|         </group> | ||||
|  | ||||
|     </group> | ||||
| </vector> | ||||
							
								
								
									
										22
									
								
								app/src/main/res/drawable/open.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="72dp" | ||||
|     android:height="72dp" | ||||
|     android:viewportWidth="58" | ||||
|     android:viewportHeight="58"> | ||||
|   <group android:translateX="-10" android:translateY="-8"> | ||||
|   <path | ||||
|       android:pathData="m57.008,20.304v-3.356l-27.338,-0.002c-0.198,0 -0.359,-0.165 -0.359,-0.368l-0.069,-1.517c-0.116,-1.788 -1.34,-3.003 -2.997,-3.003h-11.287c-1.657,0 -3,1.343 -3,3v40.943" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="m17.027,55.568c-0.59,1.954 -2.972,4.139 -4.646,4.394l44.665,0.011c1.657,0 2.323,-0.439 3,-3s7,-31.657 7,-31.657c0,-0.552 -0.448,-1 -1,-1H24.965c-0.552,0 -1,0.448 -1,1 0,0 -6.348,28.299 -6.938,30.253Z" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   </group> | ||||
| </vector> | ||||
							
								
								
									
										302
									
								
								app/src/main/res/drawable/qr.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,302 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="72dp" | ||||
|     android:height="72dp" | ||||
|     android:viewportWidth="58" | ||||
|     android:viewportHeight="58"> | ||||
|   <group android:translateX="-8" android:translateY="-8"> | ||||
|   <path | ||||
|       android:pathData="M20,20h4v4h-4z" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M20,48h4v4h-4z" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M48,20h4v4h-4z" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M18,40m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M16,38m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M20,38m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M34,46m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M40,38m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M40,28m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M32,16m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M46,32m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M52,32m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M52,44m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M54,48m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M56,56m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M32,56m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M44,56m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M46,54m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M44,52m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M16,32m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M40,54m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" | ||||
|       android:fillColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M12,12h48v48h-48z" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M16,16h12v12h-12z" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M20,20h4v4h-4z" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M16,44h12v12h-12z" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M20,48h4v4h-4z" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M44,16h12v12h-12z" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M48,20h4v4h-4z" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000"/> | ||||
|   <path | ||||
|       android:pathData="M18,36V34H26" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M20,34V32" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M24,34V40" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M24,38H26" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M38,32V30" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M56,34H54" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M42,42H44V40" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M28,32H30" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M34,32H40" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M38,16V20H36V28" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M36,26H32V28" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M36,20H32" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M36,22H34V18" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M28,36H36" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M30,36V40H28" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M34,36V38" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M32,44V42H38V48H42V46H50V56" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M36,40V44H42M46,40H42V48H44" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M48,34V38H50V42H48V46" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M50,38V36H52" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M52,50H48V52" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M32,52H34V54H36V50" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M56,32V38H54" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M44,36V34" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M56,42V44" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M54,52H56" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   <path | ||||
|       android:pathData="M40,22V24" | ||||
|       android:strokeLineJoin="round" | ||||
|       android:strokeWidth="2" | ||||
|       android:fillColor="#00000000" | ||||
|       android:strokeColor="#000" | ||||
|       android:strokeLineCap="round"/> | ||||
|   </group> | ||||
| </vector> | ||||
| @@ -1,16 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:fitsSystemWindows="true" | ||||
|     android:orientation="vertical" | ||||
|     tools:context=".activity.MainActivity"> | ||||
|  | ||||
|     <FrameLayout | ||||
|         android:id="@+id/container" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         tools:ignore="MergeRootFrame" /> | ||||
|  | ||||
| </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||||
| @@ -1,113 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:orientation="vertical" | ||||
|     tools:context=".activity.fragment.CreateEntry"> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:orientation="vertical" | ||||
|         android:padding="16dp"> | ||||
|  | ||||
|         <com.google.android.material.textfield.TextInputLayout | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginTop="16dp" | ||||
|             android:hint="@string/title"> | ||||
|  | ||||
|             <com.google.android.material.textfield.TextInputEditText | ||||
|                 android:id="@+id/editTextTitle" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:imeOptions="actionNext" | ||||
|                 android:inputType="text" | ||||
|                 android:maxLines="1" | ||||
|                 android:minLines="1" /> | ||||
|  | ||||
|         </com.google.android.material.textfield.TextInputLayout> | ||||
|  | ||||
|  | ||||
|         <androidx.constraintlayout.widget.ConstraintLayout | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginTop="16dp"> | ||||
|  | ||||
|             <com.google.android.material.textfield.TextInputLayout | ||||
|                 android:id="@+id/codeInputLayout" | ||||
|                 android:layout_width="0dp" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:hint="@string/code" | ||||
|                 app:layout_constraintBottom_toBottomOf="parent" | ||||
|                 app:layout_constraintEnd_toStartOf="@+id/checkboxProtected" | ||||
|                 app:layout_constraintStart_toStartOf="parent" | ||||
|                 app:layout_constraintTop_toTopOf="parent"> | ||||
|  | ||||
|                 <com.google.android.material.textfield.TextInputEditText | ||||
|                     android:id="@+id/editTextCode" | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:imeOptions="actionDone" | ||||
|                     android:inputType="text" | ||||
|                     android:maxLines="1" | ||||
|                     android:minLines="1" /> | ||||
|             </com.google.android.material.textfield.TextInputLayout> | ||||
|  | ||||
|  | ||||
|             <com.google.android.material.checkbox.MaterialCheckBox | ||||
|                 android:id="@+id/checkboxProtected" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:button="@drawable/lock_checkbox" | ||||
|                 android:scaleX="0.40" | ||||
|                 android:scaleY="0.40" | ||||
|                 app:layout_constraintBottom_toBottomOf="parent" | ||||
|                 app:layout_constraintEnd_toEndOf="parent" | ||||
|                 app:layout_constraintStart_toEndOf="@+id/codeInputLayout" | ||||
|                 app:layout_constraintTop_toTopOf="parent" /> | ||||
|         </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|  | ||||
|         <com.google.android.material.textfield.TextInputLayout | ||||
|             style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginTop="16dp" | ||||
|             android:hint="@string/format" | ||||
|             android:labelFor="@id/edit_text_format"> | ||||
|  | ||||
|             <AutoCompleteTextView | ||||
|                 android:id="@+id/edit_text_format" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="match_parent" | ||||
|                 android:layout_weight="1" | ||||
|                 android:focusable="false" | ||||
|                 android:inputType="none" /> | ||||
|         </com.google.android.material.textfield.TextInputLayout> | ||||
|  | ||||
|  | ||||
|         <ImageView | ||||
|             android:id="@+id/imageViewPreview" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginTop="16dp" | ||||
|             android:contentDescription="@string/barcode_preview" | ||||
|             android:scaleType="fitCenter" /> | ||||
|  | ||||
|     </LinearLayout> | ||||
|  | ||||
|  | ||||
|     <com.google.android.material.floatingactionbutton.FloatingActionButton | ||||
|         android:id="@+id/btnSave" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_alignParentEnd="true" | ||||
|         android:layout_alignParentBottom="true" | ||||
|         android:layout_margin="24dp" | ||||
|         android:contentDescription="@string/save" | ||||
|         app:fabCustomSize="46dp" | ||||
|         app:maxImageSize="32dp" | ||||
|         app:srcCompat="@drawable/save" /> | ||||
| </RelativeLayout> | ||||
| @@ -1,83 +0,0 @@ | ||||
| <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:orientation="vertical" | ||||
|     tools:context=".activity.fragment.Launcher"> | ||||
|  | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/fidelityList" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:layout_margin="24dp" /> | ||||
|  | ||||
|  | ||||
|     <com.google.android.material.floatingactionbutton.FloatingActionButton | ||||
|         android:id="@+id/btnQuery" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_alignParentBottom="true" | ||||
|         android:layout_gravity="center" | ||||
|         android:layout_margin="24dp" | ||||
|         android:contentDescription="@string/query" | ||||
|         app:fabCustomSize="46dp" | ||||
|         app:maxImageSize="32dp" | ||||
|         app:srcCompat="@drawable/search" /> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_alignParentEnd="true" | ||||
|         android:layout_alignParentBottom="true" | ||||
|         android:layout_margin="16dp" | ||||
|         android:background="@android:color/transparent" | ||||
|         android:orientation="vertical" | ||||
|         tools:ignore="RelativeOverlap"> | ||||
|  | ||||
|  | ||||
|         <LinearLayout | ||||
|             android:id="@+id/menuAdd" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:background="@android:color/transparent" | ||||
|             android:orientation="vertical" | ||||
|             android:visibility="gone" | ||||
|             tools:visibility="visible"> | ||||
|  | ||||
|  | ||||
|             <com.google.android.material.floatingactionbutton.FloatingActionButton | ||||
|                 android:id="@+id/btnScan" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_margin="8dp" | ||||
|                 android:contentDescription="@string/scan" | ||||
|                 app:fabCustomSize="46dp" | ||||
|                 app:maxImageSize="32dp" | ||||
|                 app:srcCompat="@drawable/camera" /> | ||||
|  | ||||
|             <com.google.android.material.floatingactionbutton.FloatingActionButton | ||||
|                 android:id="@+id/btnManual" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_margin="8dp" | ||||
|                 android:contentDescription="@string/manual" | ||||
|                 app:fabCustomSize="46dp" | ||||
|                 app:maxImageSize="32dp" | ||||
|                 app:srcCompat="@drawable/edit" /> | ||||
|  | ||||
|  | ||||
|         </LinearLayout> | ||||
|  | ||||
|         <com.google.android.material.floatingactionbutton.FloatingActionButton | ||||
|             android:id="@+id/btnAdd" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_gravity="bottom|end" | ||||
|             android:layout_margin="8dp" | ||||
|             android:contentDescription="@string/expand" | ||||
|             app:fabCustomSize="46dp" | ||||
|             app:maxImageSize="32dp" /> | ||||
|     </LinearLayout> | ||||
|  | ||||
| </RelativeLayout> | ||||
| @@ -1,36 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:orientation="vertical" | ||||
|     tools:context=".activity.fragment.Scanner"> | ||||
|  | ||||
|     <androidx.camera.view.PreviewView | ||||
|         android:id="@+id/cameraView" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" /> | ||||
|  | ||||
|     <net.helcel.fidelity.activity.view.ScannerView | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" /> | ||||
|  | ||||
|     <com.google.android.material.floatingactionbutton.FloatingActionButton | ||||
|         android:id="@+id/btnScanDone" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_alignParentBottom="true" | ||||
|         android:layout_centerHorizontal="true" | ||||
|         android:layout_margin="24dp" | ||||
|         android:contentDescription="@string/manual" /> | ||||
|  | ||||
|     <com.google.android.material.progressindicator.CircularProgressIndicator | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_alignParentBottom="true" | ||||
|         android:layout_centerHorizontal="true" | ||||
|         android:layout_margin="28dp" | ||||
|         android:indeterminate="true" /> | ||||
|  | ||||
|  | ||||
| </RelativeLayout> | ||||
| @@ -1,39 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:orientation="vertical" | ||||
|     android:padding="16dp" | ||||
|     tools:context=".activity.fragment.ViewEntry"> | ||||
|  | ||||
|     <com.google.android.material.textview.MaterialTextView | ||||
|         android:id="@+id/title" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginStart="16dp" | ||||
|         android:layout_marginEnd="16dp" | ||||
|         android:hint="@string/title" | ||||
|         android:textAlignment="center" | ||||
|         android:textSize="42sp" | ||||
|         app:layout_constraintBottom_toTopOf="@id/imageViewPreview" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         app:layout_constraintVertical_bias="0.0" /> | ||||
|  | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/imageViewPreview" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="0dp" | ||||
|         android:contentDescription="@string/barcode_preview" | ||||
|         android:scaleType="fitCenter" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toBottomOf="@id/title" /> | ||||
|  | ||||
|  | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| @@ -1,8 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <TextView xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:padding="15dp" | ||||
|     android:text="" | ||||
|     android:textSize="18sp" | ||||
|     android:textStyle="bold" /> | ||||
| @@ -1,30 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     android:id="@+id/card" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_marginStart="16dp" | ||||
|     android:layout_marginTop="8dp" | ||||
|     android:layout_marginEnd="16dp" | ||||
|     android:layout_marginBottom="8dp" | ||||
|     app:cardCornerRadius="8dp" | ||||
|     app:cardElevation="4dp" | ||||
|     app:cardMaxElevation="4dp" | ||||
|     app:cardPreventCornerOverlap="false" | ||||
|     app:cardUseCompatPadding="true"> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:orientation="vertical" | ||||
|         android:padding="16dp"> | ||||
|  | ||||
|         <com.google.android.material.textview.MaterialTextView | ||||
|             android:id="@+id/textView" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:textSize="18sp" | ||||
|             android:textStyle="bold" /> | ||||
|     </LinearLayout> | ||||
| </com.google.android.material.card.MaterialCardView> | ||||
							
								
								
									
										5
									
								
								app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background android:drawable="@color/ic_launcher_background"/> | ||||
|     <foreground android:drawable="@drawable/ic_launcher_foreground"/> | ||||
| </adaptive-icon> | ||||
							
								
								
									
										5
									
								
								app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background android:drawable="@color/ic_launcher_background"/> | ||||
|     <foreground android:drawable="@drawable/ic_launcher_foreground"/> | ||||
| </adaptive-icon> | ||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-hdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-mdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 10 KiB | 
							
								
								
									
										14
									
								
								app/src/main/res/values/colors.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <color name="black">#FF000000</color> | ||||
|     <color name="darkgray">#FF0C1D2E</color> | ||||
|     <color name="gray">#425F7C</color> | ||||
|     <color name="lightgray">#FF93A9BE</color> | ||||
|     <color name="white">#FFF0F3F7</color> | ||||
|  | ||||
|     <color name="blue">#7DB9F5</color> | ||||
|     <color name="blue2">#3193F5</color> | ||||
|     <color name="red">#F57D7D</color> | ||||
|     <color name="red2">#F53131</color> | ||||
|  | ||||
| </resources> | ||||
| @@ -1,3 +0,0 @@ | ||||
| <resources> | ||||
|      | ||||
| </resources> | ||||
							
								
								
									
										4
									
								
								app/src/main/res/values/ic_launcher_background.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <color name="ic_launcher_background">#0C1D2E</color> | ||||
| </resources> | ||||
| @@ -1,10 +1,10 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources xmlns:tools="http://schemas.android.com/tools"> | ||||
|     <string name="kp2aplugin_title" tools:keep="@string/kp2aplugin_title">Fidelity</string> | ||||
|     <string name="kp2aplugin_shortdesc" tools:keep="@string/kp2aplugin_shortdesc">Fidelity adds an interface to manage fidelity cards and other barcodes to Keepass2Android</string> | ||||
|     <string name="kp2aplugin_author" tools:keep="@string/kp2aplugin_author">Soraefir</string> | ||||
|  | ||||
|     <string name="app_name">Keepass Fidelity</string> | ||||
| <resources> | ||||
|     <string name="key_theme">App theme</string> | ||||
|     <string name="system">System</string> | ||||
|     <string name="light">Light</string> | ||||
|     <string name="dark">Dark</string> | ||||
|     <string name="key_stats">Statistics</string> | ||||
|  | ||||
|     <string name="barcode_preview">barcode preview</string> | ||||
|     <string name="expand">Expand</string> | ||||
| @@ -15,15 +15,20 @@ | ||||
|     <string name="code">Code</string> | ||||
|     <string name="format">Format</string> | ||||
|     <string name="save">Save</string> | ||||
|     <string name="open">Open</string> | ||||
|     <string-array name="format_array"> | ||||
|         <item>CODE_128</item> | ||||
|         <item>CODE_39</item> | ||||
|         <item>CODE_93</item> | ||||
|         <item>CODE_128</item> | ||||
|         <item>EAN_8</item> | ||||
|         <item>EAN_13</item> | ||||
|         <item>CODE_QR</item> | ||||
|         <item>UPC_A</item> | ||||
|         <item>UPC_E</item> | ||||
|         <item>PDF_417</item> | ||||
|         <item>AZTEC</item> | ||||
|         <item>CODABAR</item> | ||||
|         <item>DATA_MATRIX</item> | ||||
|         <item>ITF</item> | ||||
|     </string-array> | ||||
| </resources> | ||||
| @@ -1,12 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|  | ||||
|     <style name="Theme.Fidelity" parent="Theme.MaterialComponents.DayNight.NoActionBar"> | ||||
|  | ||||
|         <item name="colorPrimary">#7DB9F5</item> | ||||
|         <item name="colorPrimaryVariant">#7DB9F5</item> | ||||
|         <item name="colorSecondary">#7DB9F5</item> | ||||
|         <item name="colorSecondaryVariant">#7DB9F5</item> | ||||
|         <item name="colorOnPrimary">#030B12</item> | ||||
|     </style> | ||||
| </resources> | ||||
							
								
								
									
										15
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						| @@ -1,7 +1,16 @@ | ||||
| // Top-level build file where you can add configuration options common to all sub-projects/modules. | ||||
|  | ||||
| buildscript { | ||||
| //    ext.kotlin_version = '1.8.20' | ||||
| //    ext.android_core_version = '1.10.1' | ||||
| //    ext.android_appcompat_version = '1.6.1' | ||||
| //    ext.android_material_version = '1.9.0' | ||||
|     ext.android_test_version = '1.5.2' | ||||
| } | ||||
|  | ||||
| plugins { | ||||
|     id 'com.android.application' version '8.3.1' apply false | ||||
|     id 'com.android.library' version '8.3.1' apply false | ||||
|     id 'org.jetbrains.kotlin.android' version '1.9.23' apply false | ||||
|     id 'com.android.application' version '8.13.0' apply false | ||||
|     id 'com.android.library' version '8.13.0' apply false | ||||
|     id 'org.jetbrains.kotlin.android' version '2.2.20' apply false | ||||
|     id 'com.autonomousapps.dependency-analysis' version '3.2.0' apply true | ||||
| } | ||||
							
								
								
									
										1
									
								
								external/KeePassDX
									
									
									
									
										vendored
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,6 +1,6 @@ | ||||
| distributionBase=GRADLE_USER_HOME | ||||
| distributionPath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip | ||||
| networkTimeout=10000 | ||||
| validateDistributionUrl=true | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
|   | ||||
							
								
								
									
										15
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,7 +1,7 @@ | ||||
| #!/bin/sh | ||||
|  | ||||
| # | ||||
| # Copyright © 2015-2021 the original authors. | ||||
| # Copyright © 2015 the original authors. | ||||
| # | ||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| # you may not use this file except in compliance with the License. | ||||
| @@ -15,6 +15,8 @@ | ||||
| # See the License for the specific language governing permissions and | ||||
| # limitations under the License. | ||||
| # | ||||
| # SPDX-License-Identifier: Apache-2.0 | ||||
| # | ||||
|  | ||||
| ############################################################################## | ||||
| # | ||||
| @@ -55,7 +57,7 @@ | ||||
| #       Darwin, MinGW, and NonStop. | ||||
| # | ||||
| #   (3) This script is generated from the Groovy template | ||||
| #       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt | ||||
| #       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt | ||||
| #       within the Gradle project. | ||||
| # | ||||
| #       You can find Gradle at https://github.com/gradle/gradle/. | ||||
| @@ -84,7 +86,7 @@ done | ||||
| # shellcheck disable=SC2034 | ||||
| APP_BASE_NAME=${0##*/} | ||||
| # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) | ||||
| APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit | ||||
| APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit | ||||
|  | ||||
| # Use the maximum available, or set MAX_FD != -1 to use that value. | ||||
| MAX_FD=maximum | ||||
| @@ -112,7 +114,6 @@ case "$( uname )" in                #( | ||||
|   NONSTOP* )        nonstop=true ;; | ||||
| esac | ||||
|  | ||||
| CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | ||||
|  | ||||
|  | ||||
| # Determine the Java command to use to start the JVM. | ||||
| @@ -170,7 +171,6 @@ fi | ||||
| # For Cygwin or MSYS, switch paths to Windows format before running java | ||||
| if "$cygwin" || "$msys" ; then | ||||
|     APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) | ||||
|     CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) | ||||
|  | ||||
|     JAVACMD=$( cygpath --unix "$JAVACMD" ) | ||||
|  | ||||
| @@ -203,15 +203,14 @@ fi | ||||
| DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' | ||||
|  | ||||
| # Collect all arguments for the java command: | ||||
| #   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, | ||||
| #   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, | ||||
| #     and any embedded shellness will be escaped. | ||||
| #   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be | ||||
| #     treated as '${Hostname}' itself on the command line. | ||||
|  | ||||
| set -- \ | ||||
|         "-Dorg.gradle.appname=$APP_BASE_NAME" \ | ||||
|         -classpath "$CLASSPATH" \ | ||||
|         org.gradle.wrapper.GradleWrapperMain \ | ||||
|         -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ | ||||
|         "$@" | ||||
|  | ||||
| # Stop when "xargs" is not available. | ||||
|   | ||||
							
								
								
									
										5
									
								
								gradlew.bat
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -13,6 +13,8 @@ | ||||
| @rem See the License for the specific language governing permissions and | ||||
| @rem limitations under the License. | ||||
| @rem | ||||
| @rem SPDX-License-Identifier: Apache-2.0 | ||||
| @rem | ||||
|  | ||||
| @if "%DEBUG%"=="" @echo off | ||||
| @rem ########################################################################## | ||||
| @@ -68,11 +70,10 @@ goto fail | ||||
| :execute | ||||
| @rem Setup the command line | ||||
|  | ||||
| set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | ||||
|  | ||||
|  | ||||
| @rem Execute Gradle | ||||
| "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* | ||||
| "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* | ||||
|  | ||||
| :end | ||||
| @rem End local scope for the variables with windows NT shell | ||||
|   | ||||
							
								
								
									
										1
									
								
								metadata/en-US/full_description.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <p><i>Keepass-Fidelity</i> adds an interface to view/save barcodes (QR included) to Keepass through the plugin interface of the Keepass2Android app.</p><p><br></p><ul><li><b>Launcher:</b> view and launch recent entries (a per entry flag can disable this behaviour)</li><li><b>View:</b> view entries from the history or queried from Keepass2Android</li><li><b>Create:</b> add entries from the camera, an image of by filling out a form. The entry is then created in the Keepass2Android app</li><li><b>Data:</b> the app uses the following data Title (entry name), barcode type (QR, UPC, ...), barcode content (number/text content) and a "secure" flag (enable/disable caching the entry).</li></ul> | ||||
							
								
								
									
										
											BIN
										
									
								
								metadata/en-US/images/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										
											BIN
										
									
								
								metadata/en-US/images/phoneScreenshots/edit.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 34 KiB | 
							
								
								
									
										
											BIN
										
									
								
								metadata/en-US/images/phoneScreenshots/launcher.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 16 KiB | 
							
								
								
									
										
											BIN
										
									
								
								metadata/en-US/images/phoneScreenshots/view.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 25 KiB | 
							
								
								
									
										1
									
								
								metadata/en-US/permission_description.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <ul><li><b>CAMERA:</b> necessary for importing barcodes from camera</li><li><b>READ_MEDIA_VISUAL_USER_SELECTED:</b> necessary for the importing barcode from images</li></ul> | ||||
							
								
								
									
										1
									
								
								metadata/en-US/short_description.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| Fidelity (Membership/Loyalty) Card plugin for Keepass2Android | ||||
| @@ -14,5 +14,11 @@ dependencyResolutionManagement { | ||||
|         maven { url 'https://jitpack.io' } | ||||
|     } | ||||
| } | ||||
| include(":database") | ||||
| project(":database").projectDir = file("external/KeePassDX/database") | ||||
|  | ||||
| include(":crypto") | ||||
| project(":crypto").projectDir = file("external/KeePassDX/crypto") | ||||
|  | ||||
| rootProject.name = "Fidelity" | ||||
| include ':app' | ||||
|   | ||||