Compare commits
	
		
			191 Commits
		
	
	
		
			1.0-rc6
			...
			6b722e9ca4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 6b722e9ca4 | ||
|  | 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 | 
							
								
								
									
										
											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 | 
							
								
								
									
										33
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,3 +1,4 @@ | |||||||
|  | #file: noinspection SpellCheckingInspection | ||||||
|  |  | ||||||
| name: CI-Android APK | name: CI-Android APK | ||||||
|  |  | ||||||
| @@ -7,11 +8,11 @@ env: | |||||||
|  |  | ||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     branches: [ release ] |     branches: [ main ] | ||||||
|     tags: |     tags: | ||||||
|       - '**' |       - '**' | ||||||
|   pull_request: |   pull_request: | ||||||
|     branches: [ release ] |     branches: [ main ] | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|  |  | ||||||
| # A workflow run is made up of one or more jobs that can run sequentially or in parallel | # A workflow run is made up of one or more jobs that can run sequentially or in parallel | ||||||
| @@ -22,8 +23,17 @@ jobs: | |||||||
|       contents: write |       contents: write | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v5 | ||||||
|       - uses: gradle/wrapper-validation-action@v2 |         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 |       - name: create and checkout branch | ||||||
|         if: github.event_name == 'pull_request' |         if: github.event_name == 'pull_request' | ||||||
| @@ -32,7 +42,7 @@ jobs: | |||||||
|         run: git checkout -B "$BRANCH" |         run: git checkout -B "$BRANCH" | ||||||
|  |  | ||||||
|       - name: set up JDK |       - name: set up JDK | ||||||
|         uses: actions/setup-java@v4 |         uses: actions/setup-java@v5 | ||||||
|         with: |         with: | ||||||
|           java-version: 17 |           java-version: 17 | ||||||
|           distribution: "temurin" |           distribution: "temurin" | ||||||
| @@ -41,14 +51,15 @@ jobs: | |||||||
|       - name: Build APK |       - name: Build APK | ||||||
|         run: ./gradlew assemble |         run: ./gradlew assemble | ||||||
|  |  | ||||||
|       - name: Upload APK |       # - name: Upload APK | ||||||
|         uses: actions/upload-artifact@v4 |       #   uses: actions/upload-artifact@v4 | ||||||
|         with: |       #   with: | ||||||
|           name: app.apk |       #     name: app.apk | ||||||
|           path: app/build/outputs/apk/release/app-release-unsigned.apk |       #     path: app/build/outputs/apk/release/app-release.apk | ||||||
|  |  | ||||||
|       - name: Release |       - name: Release | ||||||
|         uses: softprops/action-gh-release@v2 |         uses: softprops/action-gh-release@v2 | ||||||
|  |         if: startsWith(github.ref, 'refs/tags/') | ||||||
|         with: |         with: | ||||||
|           files: | |           files: | | ||||||
|             app/build/outputs/apk/release/app-release-unsigned.apk |             app/build/outputs/apk/release/app-release.apk | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -13,3 +13,5 @@ captures/ | |||||||
| .externalNativeBuild | .externalNativeBuild | ||||||
| .cxx | .cxx | ||||||
| local.properties | 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> | ||||||
|  | ``` | ||||||
							
								
								
									
										108
									
								
								app/build.gradle
									
									
									
									
									
								
							
							
						
						| @@ -1,23 +1,43 @@ | |||||||
| plugins { | plugins { | ||||||
|     id 'com.android.application' |     id 'com.android.application' | ||||||
|     id 'org.jetbrains.kotlin.android' |     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 { | android { | ||||||
|     namespace 'net.helcel.fidelity' |     namespace 'net.helcel.fidelity' | ||||||
|     compileSdk 34 |     compileSdk 36 | ||||||
|  |  | ||||||
|     defaultConfig { |     defaultConfig { | ||||||
|         applicationId 'net.helcel.fidelity' |         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 |         minSdk 28 | ||||||
|         targetSdk 34 |         targetSdk 36 | ||||||
|         versionCode 1 |  | ||||||
|         versionName "1.0" |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     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 { |     buildTypes { | ||||||
|         debug { |         debug { | ||||||
|             debuggable true |             debuggable true | ||||||
| @@ -27,40 +47,82 @@ android { | |||||||
|             shrinkResources false |             shrinkResources false | ||||||
|             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' |             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' | ||||||
|         } |         } | ||||||
|  |         signedRelease { | ||||||
|  |             initWith(buildTypes.release) | ||||||
|  |             matchingFallbacks = ['release'] | ||||||
|  |             signingConfig = signingConfigs.getByName("release") | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     compileOptions { |     compileOptions { | ||||||
|         coreLibraryDesugaringEnabled true |         coreLibraryDesugaringEnabled true | ||||||
|  |  | ||||||
|         sourceCompatibility JavaVersion.VERSION_17 |         sourceCompatibility JavaVersion.VERSION_21 | ||||||
|         targetCompatibility JavaVersion.VERSION_17 |         targetCompatibility JavaVersion.VERSION_21 | ||||||
|         encoding 'utf-8' |         encoding 'utf-8' | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     kotlinOptions { |  | ||||||
|         jvmTarget = JavaVersion.VERSION_17 |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     buildFeatures { |     buildFeatures { | ||||||
|         viewBinding true |         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 { | dependencies { | ||||||
|  |     implementation 'androidx.compose.ui:ui' | ||||||
|     coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4' |     implementation 'androidx.compose.material3:material3:1.4.0' | ||||||
|     implementation 'androidx.appcompat:appcompat:1.6.1' |     implementation 'androidx.compose.material:material:1.9.2' | ||||||
|     implementation 'androidx.core:core-ktx:1.12.0' |     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.preference:preference-ktx:1.2.1' | ||||||
|     implementation 'androidx.constraintlayout:constraintlayout:2.1.4' |  | ||||||
|     implementation 'androidx.camera:camera-camera2:1.3.2' |     implementation "androidx.biometric:biometric:1.2.0-alpha05" | ||||||
|     implementation 'androidx.camera:camera-lifecycle:1.3.2' |     implementation "androidx.security:security-crypto:1.1.0" | ||||||
|     implementation 'androidx.camera:camera-view:1.3.2' |     implementation "androidx.datastore:datastore-preferences:1.1.7" | ||||||
|     implementation 'com.google.code.gson:gson:2.10.1' |     implementation "androidx.security:security-crypto:1.1.0" | ||||||
|     implementation 'com.google.android.material:material:1.11.0' |  | ||||||
|  |     coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5' | ||||||
|  |  | ||||||
|  |     implementation 'androidx.camera:camera-lifecycle:1.5.0' | ||||||
|  |     implementation 'androidx.camera:camera-view:1.5.0' | ||||||
|  |     runtimeOnly 'androidx.camera:camera-camera2:1.5.0' | ||||||
|  |  | ||||||
|  |     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 'com.google.zxing:core:3.5.3' | ||||||
|     implementation 'com.google.mlkit:barcode-scanning:17.2.0' |  | ||||||
|  |     implementation project(":database") | ||||||
|  |     implementation project(":crypto") | ||||||
|  |  | ||||||
|  |     implementation platform('androidx.compose:compose-bom:2025.09.01') | ||||||
|  |     implementation 'androidx.compose.ui:ui-tooling:1.9.2' | ||||||
|  |     implementation 'androidx.compose.ui:ui-tooling-preview' | ||||||
|  |  | ||||||
|  | //Submodule | ||||||
|  |     //noinspection NewerVersionAvailable | ||||||
|  |     implementation 'joda-time:joda-time:2.14.0' | ||||||
|  |     implementation 'org.joda:joda-convert:2.2.3' | ||||||
|  |  | ||||||
| } | } | ||||||
							
								
								
									
										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"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|     xmlns:tools="http://schemas.android.com/tools" |  | ||||||
|     android:versionCode="1" |  | ||||||
|     android:versionName="1.0"> |  | ||||||
|  |  | ||||||
|     <uses-feature android:name="android.hardware.camera" /> |     <uses-feature android:name="android.hardware.camera" /> | ||||||
|     <uses-permission android:name="android.permission.CAMERA" /> |     <uses-permission android:name="android.permission.CAMERA" /> | ||||||
|  |     <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" /> | ||||||
|  |  | ||||||
|     <application |     <application | ||||||
|         android:icon="@drawable/logo" |         android:icon="@mipmap/ic_launcher_round" | ||||||
|         android:label="@string/app_name" |         android:label="${APP_NAME}" | ||||||
|         android:supportsRtl="true"> |         android:supportsRtl="true"> | ||||||
|         <activity |         <activity | ||||||
|             android:name=".activity.MainActivity" |             android:name=".activity.MainActivity" | ||||||
|             android:exported="true" |             android:exported="true"> | ||||||
|             android:theme="@style/Theme.Fidelity"> |  | ||||||
|             <intent-filter> |             <intent-filter> | ||||||
|                 <action android:name="android.intent.action.MAIN" /> |                 <action android:name="android.intent.action.MAIN" /> | ||||||
|                 <category android:name="android.intent.category.LAUNCHER" /> |                 <category android:name="android.intent.category.LAUNCHER" /> | ||||||
|             </intent-filter> |             </intent-filter> | ||||||
|         </activity> |         </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> |     </application> | ||||||
| </manifest> | </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 | package net.helcel.fidelity.activity | ||||||
|  |  | ||||||
| import android.annotation.SuppressLint | import android.annotation.SuppressLint | ||||||
| import android.content.Context |  | ||||||
| import android.content.SharedPreferences |  | ||||||
| import android.content.pm.ActivityInfo | import android.content.pm.ActivityInfo | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| import androidx.activity.addCallback | import androidx.activity.compose.BackHandler | ||||||
| import androidx.appcompat.app.AppCompatActivity | import androidx.activity.compose.setContent | ||||||
| import net.helcel.fidelity.R | import androidx.compose.runtime.LaunchedEffect | ||||||
| import net.helcel.fidelity.activity.fragment.Launcher | import androidx.compose.ui.platform.LocalContext | ||||||
| import net.helcel.fidelity.activity.fragment.ViewEntry | import androidx.fragment.app.FragmentActivity | ||||||
| import net.helcel.fidelity.databinding.ActMainBinding | import androidx.navigation.compose.NavHost | ||||||
| import net.helcel.fidelity.pluginSDK.Kp2aControl.getEntryFieldsFromIntent | import androidx.navigation.compose.composable | ||||||
| import net.helcel.fidelity.tools.CacheManager | import androidx.navigation.compose.rememberNavController | ||||||
| import net.helcel.fidelity.tools.KeepassWrapper.bundleCreate | import net.helcel.fidelity.activity.fragment.CreateEntryScreen | ||||||
| import net.helcel.fidelity.tools.KeepassWrapper.entryExtract | import net.helcel.fidelity.activity.fragment.FileScanner | ||||||
|  | import net.helcel.fidelity.activity.fragment.InitialScreen | ||||||
| @SuppressLint("SourceLockedOrientationActivity") | import net.helcel.fidelity.activity.fragment.LauncherScreen | ||||||
| class MainActivity : AppCompatActivity() { | import net.helcel.fidelity.activity.fragment.ScannerScreen | ||||||
|  | import net.helcel.fidelity.activity.fragment.ViewEntryScreen | ||||||
|     private lateinit var binding: ActMainBinding | import net.helcel.fidelity.tools.FidelityRepository.entries | ||||||
|     private lateinit var sharedPreferences: SharedPreferences | import net.helcel.fidelity.tools.FidelityRepository.loadEntries | ||||||
|  | import net.helcel.fidelity.tools.KeePassStore.hasCredentials | ||||||
|  |  | ||||||
|  | class MainActivity : FragmentActivity() { | ||||||
|  |  | ||||||
|  |     @SuppressLint("SourceLockedOrientationActivity") | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|         super.onCreate(savedInstanceState) |         super.onCreate(savedInstanceState) | ||||||
|         sharedPreferences = |         actionBar?.hide() | ||||||
|             this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE) |  | ||||||
|         CacheManager.loadFidelity(sharedPreferences) |  | ||||||
|  |  | ||||||
|         binding = ActMainBinding.inflate(layoutInflater) |  | ||||||
|         setContentView(binding.root) |  | ||||||
|  |  | ||||||
|         onBackPressedDispatcher.addCallback(this) { |  | ||||||
|             if (supportFragmentManager.backStackEntryCount > 0) { |  | ||||||
|                 supportFragmentManager.popBackStackImmediate() |  | ||||||
|                 loadLauncher() |  | ||||||
|         requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT |         requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT | ||||||
|             } else { |         loadEntries(this.baseContext) | ||||||
|                 finish() |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (intent.extras != null) |         setContent { | ||||||
|             loadViewEntry() |             SysTheme { | ||||||
|         else if (savedInstanceState == null) |                 val navController = rememberNavController() | ||||||
|             loadLauncher() |                 val context = LocalContext.current | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun loadLauncher() { |                 BackHandler { | ||||||
|         supportFragmentManager.beginTransaction() |                     if (!navController.popBackStack()) finish() | ||||||
|             .replace(R.id.container, Launcher()) |                 } | ||||||
|             .commit() |                 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) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     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 | package net.helcel.fidelity.activity.fragment | ||||||
|  |  | ||||||
| import android.content.ActivityNotFoundException | import android.graphics.Bitmap | ||||||
| import android.os.Bundle | import androidx.compose.foundation.Image | ||||||
| import android.os.Handler | import androidx.compose.foundation.background | ||||||
| import android.os.Looper | import androidx.compose.foundation.clickable | ||||||
| import android.view.LayoutInflater | import androidx.compose.foundation.interaction.MutableInteractionSource | ||||||
| import android.view.View | import androidx.compose.foundation.layout.Arrangement | ||||||
| import android.view.ViewGroup | import androidx.compose.foundation.layout.Box | ||||||
| import android.view.inputmethod.EditorInfo | import androidx.compose.foundation.layout.Column | ||||||
| import android.widget.ArrayAdapter | import androidx.compose.foundation.layout.Row | ||||||
| import androidx.core.widget.addTextChangedListener | import androidx.compose.foundation.layout.Spacer | ||||||
| import androidx.fragment.app.Fragment | import androidx.compose.foundation.layout.fillMaxSize | ||||||
| import com.google.android.material.textfield.TextInputEditText | 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.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.R | ||||||
| import net.helcel.fidelity.databinding.FragCreateEntryBinding | import net.helcel.fidelity.activity.ToastHelper | ||||||
| import net.helcel.fidelity.pluginSDK.Kp2aControl | 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.BarcodeGenerator.generateBarcode | ||||||
| import net.helcel.fidelity.tools.CacheManager | import net.helcel.fidelity.tools.FidelityEntry | ||||||
| import net.helcel.fidelity.tools.ErrorToaster | import net.helcel.fidelity.tools.FidelityRepository | ||||||
| import net.helcel.fidelity.tools.KeepassWrapper | import net.helcel.fidelity.tools.FidelityRepository.activeEntry | ||||||
|  | import net.helcel.fidelity.tools.FidelityRepository.addEntry | ||||||
| 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() } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|         updatePreview() | @Preview | ||||||
|         return binding.root | @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() { |     var barcodeBitmap by remember { mutableStateOf<Bitmap?>(null) } | ||||||
|         try { |     var isValidBarcode by remember { mutableStateOf(false) } | ||||||
|             val barcodeBitmap = generateBarcode( |     var showDialog by remember { mutableStateOf(false) } | ||||||
|                 binding.editTextCode.text.toString(), |     var isLoading by remember { mutableStateOf(false) } | ||||||
|                 binding.editTextFormat.text.toString(), |     val ctx = LocalContext.current | ||||||
|                 600 |     val scope = rememberCoroutineScope() | ||||||
|             ) |  | ||||||
|             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() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun isValidForm(): Boolean { |     LaunchedEffect(entry) { | ||||||
|         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() { |  | ||||||
|         isValidBarcode = false |         isValidBarcode = false | ||||||
|         handler.removeCallbacksAndMessages(null) |         delay(500) | ||||||
|         handler.postDelayed({ |         if (entry.code.isEmpty()) return@LaunchedEffect | ||||||
|             updatePreview() |  | ||||||
|         }, DEBOUNCE_DELAY) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     private fun TextInputEditText.onDone(callback: () -> Unit) { |  | ||||||
|         setOnEditorActionListener { _, actionId, _ -> |  | ||||||
|             if (actionId == EditorInfo.IME_ACTION_DONE) { |  | ||||||
|                 callback.invoke() |  | ||||||
|                 return@setOnEditorActionListener true |  | ||||||
|             } |  | ||||||
|             false |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     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, |  | ||||||
|             ) |  | ||||||
|         try { |         try { | ||||||
|                 resultLauncherAdd.launch( |             val bmp = generateBarcode(entry.code, entry.format, 600) | ||||||
|                     Kp2aControl.getAddEntryIntent( |             barcodeBitmap = bmp | ||||||
|                         kpEntry.first, |             isValidBarcode = true | ||||||
|                         kpEntry.second |             errorCode = "" | ||||||
|                     ) |         } catch (_: FormatException) { | ||||||
|                 ) |             barcodeBitmap = null | ||||||
|             } catch (e: ActivityNotFoundException) { |             errorCode = "Invalid Format" | ||||||
|                 ErrorToaster.noKP2AFound(context) |         } catch (e: IllegalArgumentException) { | ||||||
|  |             barcodeBitmap = null | ||||||
|  |             errorCode = if (e.message == "com.google.zxing.FormatException") "Invalid Format" | ||||||
|  |             else e.message ?: "Invalid Argument" | ||||||
|         } catch (e: Exception) { |         } catch (e: Exception) { | ||||||
|                 e.printStackTrace() |             barcodeBitmap = null | ||||||
|             } |             ToastHelper.show(ctx, e.message ?: e.toString()) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     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 | ||||||
|  |                 ), | ||||||
|  |             ) | ||||||
|  |             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() | ||||||
|  |                 ) | ||||||
|  |                 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 | package net.helcel.fidelity.activity.fragment | ||||||
|  |  | ||||||
| import android.content.ActivityNotFoundException | import android.content.Context | ||||||
| import android.os.Bundle | import androidx.compose.foundation.background | ||||||
| import android.view.LayoutInflater | import androidx.compose.foundation.clickable | ||||||
| import android.view.View | import androidx.compose.foundation.combinedClickable | ||||||
| import android.view.ViewGroup | import androidx.compose.foundation.interaction.MutableInteractionSource | ||||||
| import androidx.fragment.app.Fragment | import androidx.compose.foundation.layout.Arrangement | ||||||
| import androidx.recyclerview.widget.ItemTouchHelper | import androidx.compose.foundation.layout.Box | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager | import androidx.compose.foundation.layout.Row | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.compose.foundation.layout.Spacer | ||||||
| import net.helcel.fidelity.R | import androidx.compose.foundation.layout.fillMaxSize | ||||||
| import net.helcel.fidelity.activity.adapter.FidelityListAdapter | import androidx.compose.foundation.layout.fillMaxWidth | ||||||
| import net.helcel.fidelity.databinding.FragLauncherBinding | import androidx.compose.foundation.layout.padding | ||||||
| import net.helcel.fidelity.pluginSDK.Kp2aControl | import androidx.compose.foundation.layout.size | ||||||
| import net.helcel.fidelity.tools.CacheManager | import androidx.compose.foundation.layout.width | ||||||
| import net.helcel.fidelity.tools.ErrorToaster | import androidx.compose.foundation.lazy.grid.GridCells | ||||||
| import net.helcel.fidelity.tools.KeepassWrapper | 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 | ||||||
|  |  | ||||||
|  | @Preview | ||||||
| class Launcher : Fragment() { | @OptIn(ExperimentalMaterial3Api::class) | ||||||
|  | @Composable | ||||||
|     private lateinit var binding: FragLauncherBinding | fun LauncherScreen( | ||||||
|     private lateinit var fidelityListAdapter: FidelityListAdapter |     navController: NavHostController?, | ||||||
|  | ) { | ||||||
|     private val resultLauncherQuery = KeepassWrapper.resultLauncher(this) { |     if(navController==null) return | ||||||
|         val r = KeepassWrapper.entryExtract(it) |     var isRefreshingState by remember { mutableStateOf(false) } | ||||||
|         if (!KeepassWrapper.isProtected(it)) { |     var showHidden by remember { mutableStateOf(false) } | ||||||
|             CacheManager.addFidelity(r) |     val context = LocalContext.current | ||||||
|         } |     val scope = rememberCoroutineScope() | ||||||
|         startViewEntry(r.first, r.second, r.third) |     val sortedEntries = remember(entries) { | ||||||
|     } |         derivedStateOf { | ||||||
|  |             entries.filter{showHidden || !it.hidden}.sortedWith( | ||||||
|     override fun onCreateView( |                 compareByDescending<FidelityEntry> { it.pinned } | ||||||
|         inflater: LayoutInflater, container: ViewGroup?, |                     .thenBy { it.hidden } | ||||||
|         savedInstanceState: Bundle? |                     .thenByDescending { it.lastUse } | ||||||
|     ): 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()) |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private fun startFragment(fragment: Fragment) { |  | ||||||
|         requireActivity().supportFragmentManager.beginTransaction() |     Box(modifier = Modifier | ||||||
|             .addToBackStack("Launcher") |         .fillMaxSize() | ||||||
|             .replace(R.id.container, fragment).commit() |         .background(MaterialTheme.colors.background)) { | ||||||
|  |  | ||||||
|  |         PullToRefreshBox( | ||||||
|  |             onRefresh = { | ||||||
|  |                 isRefreshingState = true | ||||||
|  |                 scope.launch { | ||||||
|  |                     onRefresh(context, navController) | ||||||
|  |                     isRefreshingState = false | ||||||
|                 } |                 } | ||||||
|  |             }, | ||||||
|     private fun startScanner() { |             isRefreshing = isRefreshingState, | ||||||
|         startFragment(Scanner()) |             modifier = Modifier.fillMaxSize() | ||||||
|     } |  | ||||||
|  |  | ||||||
|     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 |  | ||||||
|         ) { |         ) { | ||||||
|             override fun onMove( |             LazyVerticalGrid( | ||||||
|                 recyclerView: RecyclerView, |                 columns = GridCells.Fixed(2), | ||||||
|                 viewHolder: RecyclerView.ViewHolder, |                 modifier = Modifier | ||||||
|                 target: RecyclerView.ViewHolder |                     .fillMaxSize() | ||||||
|             ): Boolean = false |                     .fillMaxSize() | ||||||
|  |                     .padding(16.dp), | ||||||
|             override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { |                 verticalArrangement = Arrangement.spacedBy(8.dp), | ||||||
|                 val pos = viewHolder.adapterPosition |                 horizontalArrangement = Arrangement.spacedBy(8.dp) | ||||||
|                 CacheManager.rmFidelity(pos) |             ) { | ||||||
|                 fidelityListAdapter.notifyItemRemoved(pos) |                 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 | package net.helcel.fidelity.activity.fragment | ||||||
|  |  | ||||||
| import android.Manifest | import android.Manifest | ||||||
| import android.content.ContentValues | import android.graphics.BitmapFactory | ||||||
| import android.content.pm.PackageManager |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.util.Log | import android.util.Log | ||||||
| import android.view.LayoutInflater | import android.widget.Toast | ||||||
| import android.view.View | import androidx.activity.compose.BackHandler | ||||||
| import android.view.ViewGroup | 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.CameraSelector | ||||||
| import androidx.camera.core.Preview | import androidx.camera.core.Preview | ||||||
| import androidx.camera.lifecycle.ProcessCameraProvider | import androidx.camera.lifecycle.ProcessCameraProvider | ||||||
| import androidx.core.app.ActivityCompat | import androidx.camera.view.PreviewView | ||||||
| import androidx.core.content.ContextCompat | import androidx.compose.foundation.Canvas | ||||||
| import androidx.fragment.app.Fragment | import androidx.compose.foundation.layout.Box | ||||||
| import net.helcel.fidelity.R | import androidx.compose.foundation.layout.fillMaxSize | ||||||
| import net.helcel.fidelity.databinding.FragScannerBinding | import androidx.compose.foundation.layout.padding | ||||||
| import net.helcel.fidelity.tools.BarcodeScanner.getAnalysisUseCase | import androidx.compose.material.Button | ||||||
| import net.helcel.fidelity.tools.KeepassWrapper | 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) | ||||||
|     private lateinit var binding: FragScannerBinding |  | ||||||
|  |  | ||||||
|     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() |  | ||||||
|         } |  | ||||||
|         when (hasCameraPermission()) { |  | ||||||
|             true -> bindCameraUseCases() |  | ||||||
|             else -> requestPermission() |  | ||||||
|         } |  | ||||||
|         binding.btnScanDone.isEnabled = false |  | ||||||
|         return binding.root |  | ||||||
|     } |     } | ||||||
|  |     var camera: Camera? by remember { mutableStateOf(null) } | ||||||
|  |     var torchOn by remember { mutableStateOf(false) } | ||||||
|  |  | ||||||
|     private fun startCreateEntry() { |     val done = remember { mutableStateOf(false) } | ||||||
|         val createEntryFragment = CreateEntry() |     val previewView = remember { PreviewView(context) } | ||||||
|         createEntryFragment.arguments = |  | ||||||
|             KeepassWrapper.bundleCreate(null, this.code, this.fmt) |  | ||||||
|         requireActivity().supportFragmentManager.beginTransaction() |  | ||||||
|             .replace(R.id.container, createEntryFragment) |  | ||||||
|             .commit() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun hasCameraPermission() = |     val permissionLauncher = rememberLauncherForActivityResult( | ||||||
|         ActivityCompat.checkSelfPermission( |         contract = ActivityResultContracts.RequestPermission(), | ||||||
|             requireContext(), |         onResult = { granted -> | ||||||
|             Manifest.permission.CAMERA |             if (granted) { | ||||||
|         ) == PackageManager.PERMISSION_GRANTED |  | ||||||
|  |  | ||||||
|     private fun requestPermission() { |  | ||||||
|         ActivityCompat.requestPermissions( |  | ||||||
|             requireActivity(), |  | ||||||
|             arrayOf(Manifest.permission.CAMERA), |  | ||||||
|             CAMERA_PERMISSION_REQUEST_CODE |  | ||||||
|         ) |  | ||||||
|         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() |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun bindCameraUseCases() { |  | ||||||
|         val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) |  | ||||||
|  |  | ||||||
|         cameraProviderFuture.addListener({ |  | ||||||
|                 val cameraProvider = cameraProviderFuture.get() |                 val cameraProvider = cameraProviderFuture.get() | ||||||
|             val previewUseCase = Preview.Builder() |                 val previewUseCase = Preview.Builder().build().also { | ||||||
|                 .build() |                     it.surfaceProvider = previewView.surfaceProvider | ||||||
|                 .also { |  | ||||||
|                     it.setSurfaceProvider(binding.cameraView.surfaceProvider) |  | ||||||
|                 } |                 } | ||||||
|             val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA |                 val analysisUseCase = analysisUseCase { detectedCode, detectedFormat -> | ||||||
|             val analysisUseCase = getAnalysisUseCase { code, format -> |                     if (detectedCode.isNullOrEmpty() || detectedFormat.isNullOrEmpty()) return@analysisUseCase | ||||||
|                 if (code != null && format != null) { |                     if(done.value) return@analysisUseCase | ||||||
|                     this.code = code |                     scope.launch(Dispatchers.Main) { | ||||||
|                     this.fmt = format |                         activeEntry.value = | ||||||
|                     binding.btnScanDone.isEnabled = true |                             activeEntry.value.copy(code = detectedCode, format = detectedFormat) | ||||||
|  |                         done.value = true | ||||||
|                 } else { |                         onResult(navController) | ||||||
|                     binding.btnScanDone.isEnabled = false |  | ||||||
|                     } |                     } | ||||||
|  |                     return@analysisUseCase | ||||||
|                 } |                 } | ||||||
|                 try { |                 try { | ||||||
|                 cameraProvider.bindToLifecycle( |                     cameraProvider.unbindAll() | ||||||
|                     this, |                     camera = cameraProvider.bindToLifecycle( | ||||||
|                     cameraSelector, |                         lifecycleOwner, | ||||||
|  |                         CameraSelector.DEFAULT_BACK_CAMERA, | ||||||
|                         previewUseCase, |                         previewUseCase, | ||||||
|                         analysisUseCase |                         analysisUseCase | ||||||
|                     ) |                     ) | ||||||
|             } catch (illegalStateException: IllegalStateException) { |                 } catch (e: Exception) { | ||||||
|                 Log.e(ContentValues.TAG, illegalStateException.message.orEmpty()) |                     Log.e("ScannerScreen", "Camera bind failed: ${e.message}") | ||||||
|             } catch (illegalArgumentException: IllegalArgumentException) { |  | ||||||
|                 Log.e(ContentValues.TAG, illegalArgumentException.message.orEmpty()) |  | ||||||
|                 } |                 } | ||||||
|         }, ContextCompat.getMainExecutor(requireContext())) |             } else { | ||||||
|  |                 Toast.makeText(context, "Camera permission denied", Toast.LENGTH_SHORT).show() | ||||||
|  |                 scope.launch(Dispatchers.Main){ | ||||||
|  |                     onResult(navController) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     LaunchedEffect(Unit) { | ||||||
|  |         permissionLauncher.launch(Manifest.permission.CAMERA) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Box(modifier = Modifier.fillMaxSize()) { | ||||||
|  |         AndroidView( | ||||||
|  |             factory = { previewView }, | ||||||
|  |             modifier = Modifier.fillMaxSize() | ||||||
|  |         ) | ||||||
|  |         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), | ||||||
|  |             ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @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 { | ||||||
|  |                     Toast.makeText(context, "No barcode found", Toast.LENGTH_SHORT).show() | ||||||
|  |                     onResult(navController) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } 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 | package net.helcel.fidelity.activity.fragment | ||||||
|  |  | ||||||
| import android.annotation.SuppressLint | import android.app.Activity | ||||||
| import android.content.pm.ActivityInfo | import android.graphics.Bitmap | ||||||
| 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.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE | import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE | ||||||
| import androidx.fragment.app.Fragment | import android.widget.Toast | ||||||
| import com.google.zxing.FormatException | import androidx.activity.compose.BackHandler | ||||||
| import net.helcel.fidelity.databinding.FragViewEntryBinding | 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.BarcodeGenerator.generateBarcode | ||||||
| import net.helcel.fidelity.tools.ErrorToaster | import net.helcel.fidelity.tools.FidelityEntry | ||||||
| import net.helcel.fidelity.tools.KeepassWrapper | import kotlin.let | ||||||
|  | import kotlin.math.min | ||||||
|  |  | ||||||
| @SuppressLint("SourceLockedOrientationActivity") |  | ||||||
| class ViewEntry : Fragment() { |  | ||||||
|  |  | ||||||
|     private lateinit var binding: FragViewEntryBinding | @Preview | ||||||
|     private var title: String? = null | @Composable | ||||||
|     private var code: String? = null | fun PreviewEntryScreen(){ | ||||||
|     private var fmt: String? = null |   ViewEntryScreen(null, FidelityEntry("Title","AAA","QR")) | ||||||
|  | } | ||||||
|  |  | ||||||
|     override fun onCreateView( | @Composable | ||||||
|         inflater: LayoutInflater, | fun ViewEntryScreen( | ||||||
|         container: ViewGroup?, |     navController: NavHostController?, | ||||||
|         savedInstanceState: Bundle? |     entry: FidelityEntry | ||||||
|     ): View { | ) { | ||||||
|         binding = FragViewEntryBinding.inflate(layoutInflater) |     val context = LocalContext.current | ||||||
|         val res = KeepassWrapper.bundleExtract(arguments) |     val activity = context as? Activity | ||||||
|         title = res.first |     var isFull by remember { mutableStateOf(false) } | ||||||
|         code = res.second |     var bitmap by remember { mutableStateOf<Bitmap?>(null) } | ||||||
|         fmt = res.third |  | ||||||
|  |  | ||||||
|         updatePreview() |     SideEffect { | ||||||
|         updateLayout() |         activity?.window?.attributes = activity.window?.attributes?.apply { | ||||||
|  |             screenBrightness = if (isFull) 1f else BRIGHTNESS_OVERRIDE_NONE | ||||||
|         binding.imageViewPreview.setOnClickListener { |  | ||||||
|             requireActivity().requestedOrientation = |  | ||||||
|                 if (isLandscape()) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT |  | ||||||
|                 else ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return binding.root |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun updatePreview() { |  | ||||||
|         binding.title.text = title |  | ||||||
|         try { |         try { | ||||||
|             val barcodeBitmap = generateBarcode( |             bitmap = generateBarcode(entry.code, entry.format, 1024) | ||||||
|                 code, fmt, 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) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun isLandscape(): Boolean { |     BoxWithConstraints( | ||||||
|         return (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) |         modifier = Modifier | ||||||
|     } |             .fillMaxSize().padding(8.dp), | ||||||
|  |         contentAlignment = Alignment.Center | ||||||
|  |     ) { | ||||||
|  |             bitmap?.let { | ||||||
|  |  | ||||||
|     private fun setScreenBrightness(brightness: Float?) { |  | ||||||
|         requireActivity().window?.attributes?.screenBrightness = brightness |                 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) | ||||||
|         } |         } | ||||||
| } | } | ||||||
| @@ -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 | package net.helcel.fidelity.tools | ||||||
|  |  | ||||||
| import com.google.mlkit.vision.barcode.common.Barcode |  | ||||||
| import com.google.zxing.BarcodeFormat | import com.google.zxing.BarcodeFormat | ||||||
|  |  | ||||||
| object BarcodeFormatConverter { | object BarcodeFormatConverter { | ||||||
| @@ -16,22 +15,40 @@ object BarcodeFormatConverter { | |||||||
|             "UPC_A" -> BarcodeFormat.UPC_A |             "UPC_A" -> BarcodeFormat.UPC_A | ||||||
|             "UPC_E" -> BarcodeFormat.UPC_E |             "UPC_E" -> BarcodeFormat.UPC_E | ||||||
|             "PDF_417" -> BarcodeFormat.PDF_417 |             "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") |             else -> throw Exception("Unsupported Format: $f") | ||||||
|  |  | ||||||
|  |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun formatToString(f: Int): String { |     fun formatToString(f: BarcodeFormat): String { | ||||||
|         return when (f) { |         return when (f) { | ||||||
|             Barcode.FORMAT_CODE_128 -> "CODE_128" |             BarcodeFormat.CODE_39 -> "CODE_39" | ||||||
|             Barcode.FORMAT_CODE_39 -> "CODE_39" |             BarcodeFormat.CODE_93 -> "CODE_93" | ||||||
|             Barcode.FORMAT_CODE_93 -> "CODE_93" |             BarcodeFormat.CODE_128 -> "CODE_128" | ||||||
|             Barcode.FORMAT_EAN_8 -> "EAN_8" |             BarcodeFormat.EAN_8 -> "EAN_8" | ||||||
|             Barcode.FORMAT_EAN_13 -> "EAN_13" |             BarcodeFormat.EAN_13 -> "EAN_13" | ||||||
|             Barcode.FORMAT_QR_CODE -> "CODE_QR" |             BarcodeFormat.QR_CODE -> "CODE_QR" | ||||||
|             Barcode.FORMAT_UPC_A -> "UPC_A" |             BarcodeFormat.UPC_A -> "UPC_A" | ||||||
|             Barcode.FORMAT_UPC_E -> "UPC_E" |             BarcodeFormat.UPC_E -> "UPC_E" | ||||||
|             Barcode.FORMAT_PDF417 -> "PDF_417" |             BarcodeFormat.PDF_417 -> "PDF_417" | ||||||
|             else -> throw Exception("Unsupported Format: $f") |             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.WriterException | ||||||
| import com.google.zxing.common.BitMatrix | import com.google.zxing.common.BitMatrix | ||||||
| import net.helcel.fidelity.tools.BarcodeFormatConverter.stringToFormat | import net.helcel.fidelity.tools.BarcodeFormatConverter.stringToFormat | ||||||
|  | import androidx.core.graphics.set | ||||||
|  | import androidx.core.graphics.createBitmap | ||||||
|  |  | ||||||
| object BarcodeGenerator { | object BarcodeGenerator { | ||||||
|  |  | ||||||
| @@ -31,13 +33,11 @@ object BarcodeGenerator { | |||||||
|  |  | ||||||
|  |  | ||||||
|             val bitMatrix: BitMatrix = writer.encode(content, format, width, height) |             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 (x in 0 until width) { | ||||||
|                 for (y in 0 until height) { |                 for (y in 0 until height) { | ||||||
|                     bitmap.setPixel( |                     bitmap[x, y] = getPixelColor(bitMatrix, x, y) | ||||||
|                         x, y, getPixelColor(bitMatrix, x, y) |  | ||||||
|                     ) |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             return bitmap |             return bitmap | ||||||
|   | |||||||
| @@ -1,75 +1,61 @@ | |||||||
| package net.helcel.fidelity.tools | package net.helcel.fidelity.tools | ||||||
|  |  | ||||||
| import android.content.ContentValues | import android.graphics.Bitmap | ||||||
| import android.util.Log |  | ||||||
| import androidx.annotation.OptIn | import androidx.annotation.OptIn | ||||||
| import androidx.camera.core.ExperimentalGetImage | import androidx.camera.core.ExperimentalGetImage | ||||||
| import androidx.camera.core.ImageAnalysis | import androidx.camera.core.ImageAnalysis | ||||||
| import androidx.camera.core.ImageProxy | import com.google.zxing.BinaryBitmap | ||||||
| import com.google.mlkit.vision.barcode.BarcodeScanner | import com.google.zxing.MultiFormatReader | ||||||
| import com.google.mlkit.vision.barcode.BarcodeScannerOptions | import com.google.zxing.NotFoundException | ||||||
| import com.google.mlkit.vision.barcode.BarcodeScanning | import com.google.zxing.RGBLuminanceSource | ||||||
| import com.google.mlkit.vision.barcode.common.Barcode | import com.google.zxing.ReaderException | ||||||
| import com.google.mlkit.vision.common.InputImage | import com.google.zxing.common.HybridBinarizer | ||||||
| import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString | import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString | ||||||
| import java.util.concurrent.Executors | import java.util.concurrent.Executors | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalGetImage::class) | ||||||
| object BarcodeScanner { | object BarcodeScanner { | ||||||
|  |  | ||||||
|     @OptIn(ExperimentalGetImage::class) |     private fun processImage( | ||||||
|     private fun processImageProxy( |         bitmap: Bitmap, | ||||||
|         barcodeScanner: BarcodeScanner, |  | ||||||
|         imageProxy: ImageProxy, |  | ||||||
|         cb: (String?, String?) -> Unit |         cb: (String?, String?) -> Unit | ||||||
|     ) { |     ) { | ||||||
|  |         val binaryBitmap = createBinaryBitmap(bitmap) | ||||||
|         imageProxy.image?.let { image -> |         val reader = MultiFormatReader() | ||||||
|             val inputImage = |         try { | ||||||
|                 InputImage.fromMediaImage( |             val result = reader.decode(binaryBitmap) | ||||||
|                     image, |             cb(result.text, formatToString(result.barcodeFormat)) | ||||||
|                     imageProxy.imageInfo.rotationDegrees |         } catch (_: NotFoundException) { | ||||||
|                 ) |             cb(null, null) | ||||||
|  |         } catch (_: ReaderException) { | ||||||
|             barcodeScanner.process(inputImage) |             cb(null, null) | ||||||
|                 .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() |  | ||||||
|                 } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun getAnalysisUseCase(cb: (String?, String?) -> Unit): ImageAnalysis { |     private fun createBinaryBitmap(bitmap: Bitmap): BinaryBitmap { | ||||||
|         val options = BarcodeScannerOptions.Builder().setBarcodeFormats( |         val pixels = IntArray(bitmap.width * bitmap.height) | ||||||
|             Barcode.FORMAT_CODE_128, |         bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) | ||||||
|             Barcode.FORMAT_CODE_39, |         val source = | ||||||
|             Barcode.FORMAT_CODE_93, |             RGBLuminanceSource(bitmap.width, bitmap.height, pixels) | ||||||
|             Barcode.FORMAT_EAN_8, |         return BinaryBitmap(HybridBinarizer(source)) | ||||||
|             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() |  | ||||||
|  |  | ||||||
|  |     fun analysisUseCase(cb: (String?, String?) -> Unit): ImageAnalysis { | ||||||
|  |         val analysisUseCase = ImageAnalysis.Builder().build() | ||||||
|         analysisUseCase.setAnalyzer( |         analysisUseCase.setAnalyzer( | ||||||
|             Executors.newSingleThreadExecutor() |             Executors.newSingleThreadExecutor() | ||||||
|         ) { imageProxy -> |         ) { imageProxy -> | ||||||
|             processImageProxy(scanner, imageProxy, cb) |             val bitmap = imageProxy.toBitmap() | ||||||
|  |             imageProxy.close() | ||||||
|  |             bitmapUseCase(bitmap, cb) | ||||||
|         } |         } | ||||||
|         return analysisUseCase |         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"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <resources xmlns:tools="http://schemas.android.com/tools"> | <resources> | ||||||
|     <string name="kp2aplugin_title" tools:keep="@string/kp2aplugin_title">Fidelity</string> |     <string name="key_theme">App theme</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="system">System</string> | ||||||
|     <string name="kp2aplugin_author" tools:keep="@string/kp2aplugin_author">Soraefir</string> |     <string name="light">Light</string> | ||||||
|  |     <string name="dark">Dark</string> | ||||||
|     <string name="app_name">Keepass Fidelity</string> |     <string name="key_stats">Statistics</string> | ||||||
|  |  | ||||||
|     <string name="barcode_preview">barcode preview</string> |     <string name="barcode_preview">barcode preview</string> | ||||||
|     <string name="expand">Expand</string> |     <string name="expand">Expand</string> | ||||||
| @@ -15,15 +15,20 @@ | |||||||
|     <string name="code">Code</string> |     <string name="code">Code</string> | ||||||
|     <string name="format">Format</string> |     <string name="format">Format</string> | ||||||
|     <string name="save">Save</string> |     <string name="save">Save</string> | ||||||
|  |     <string name="open">Open</string> | ||||||
|     <string-array name="format_array"> |     <string-array name="format_array"> | ||||||
|         <item>CODE_128</item> |  | ||||||
|         <item>CODE_39</item> |         <item>CODE_39</item> | ||||||
|         <item>CODE_93</item> |         <item>CODE_93</item> | ||||||
|  |         <item>CODE_128</item> | ||||||
|         <item>EAN_8</item> |         <item>EAN_8</item> | ||||||
|         <item>EAN_13</item> |         <item>EAN_13</item> | ||||||
|         <item>CODE_QR</item> |         <item>CODE_QR</item> | ||||||
|         <item>UPC_A</item> |         <item>UPC_A</item> | ||||||
|         <item>UPC_E</item> |         <item>UPC_E</item> | ||||||
|         <item>PDF_417</item> |         <item>PDF_417</item> | ||||||
|  |         <item>AZTEC</item> | ||||||
|  |         <item>CODABAR</item> | ||||||
|  |         <item>DATA_MATRIX</item> | ||||||
|  |         <item>ITF</item> | ||||||
|     </string-array> |     </string-array> | ||||||
| </resources> | </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> |  | ||||||
							
								
								
									
										17
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						| @@ -1,7 +1,16 @@ | |||||||
| // Top-level build file where you can add configuration options common to all sub-projects/modules. | // Top-level build file where you can add configuration options common to all sub-projects/modules. | ||||||
|  |  | ||||||
| plugins { | buildscript { | ||||||
|     id 'com.android.application' version '8.3.1' apply false | //    ext.kotlin_version = '1.8.20' | ||||||
|     id 'com.android.library' version '8.3.1' apply false | //    ext.android_core_version = '1.10.1' | ||||||
|     id 'org.jetbrains.kotlin.android' version '1.9.23' apply false | //    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.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.0.4' 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 | distributionBase=GRADLE_USER_HOME | ||||||
| distributionPath=wrapper/dists | 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 | networkTimeout=10000 | ||||||
| validateDistributionUrl=true | validateDistributionUrl=true | ||||||
| zipStoreBase=GRADLE_USER_HOME | zipStoreBase=GRADLE_USER_HOME | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,7 +1,7 @@ | |||||||
| #!/bin/sh | #!/bin/sh | ||||||
|  |  | ||||||
| # | # | ||||||
| # Copyright © 2015-2021 the original authors. | # Copyright © 2015 the original authors. | ||||||
| # | # | ||||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | # Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| # you may not use this file except in compliance with 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 | # See the License for the specific language governing permissions and | ||||||
| # limitations under the License. | # limitations under the License. | ||||||
| # | # | ||||||
|  | # SPDX-License-Identifier: Apache-2.0 | ||||||
|  | # | ||||||
|  |  | ||||||
| ############################################################################## | ############################################################################## | ||||||
| # | # | ||||||
| @@ -55,7 +57,7 @@ | |||||||
| #       Darwin, MinGW, and NonStop. | #       Darwin, MinGW, and NonStop. | ||||||
| # | # | ||||||
| #   (3) This script is generated from the Groovy template | #   (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. | #       within the Gradle project. | ||||||
| # | # | ||||||
| #       You can find Gradle at https://github.com/gradle/gradle/. | #       You can find Gradle at https://github.com/gradle/gradle/. | ||||||
| @@ -84,7 +86,7 @@ done | |||||||
| # shellcheck disable=SC2034 | # shellcheck disable=SC2034 | ||||||
| APP_BASE_NAME=${0##*/} | APP_BASE_NAME=${0##*/} | ||||||
| # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) | # 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. | # Use the maximum available, or set MAX_FD != -1 to use that value. | ||||||
| MAX_FD=maximum | MAX_FD=maximum | ||||||
| @@ -112,7 +114,6 @@ case "$( uname )" in                #( | |||||||
|   NONSTOP* )        nonstop=true ;; |   NONSTOP* )        nonstop=true ;; | ||||||
| esac | esac | ||||||
|  |  | ||||||
| CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # Determine the Java command to use to start the JVM. | # 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 | # For Cygwin or MSYS, switch paths to Windows format before running java | ||||||
| if "$cygwin" || "$msys" ; then | if "$cygwin" || "$msys" ; then | ||||||
|     APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) |     APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) | ||||||
|     CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) |  | ||||||
|  |  | ||||||
|     JAVACMD=$( cygpath --unix "$JAVACMD" ) |     JAVACMD=$( cygpath --unix "$JAVACMD" ) | ||||||
|  |  | ||||||
| @@ -203,15 +203,14 @@ fi | |||||||
| DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' | ||||||
|  |  | ||||||
| # Collect all arguments for the java command: | # 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. | #     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 | #   * 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. | #     treated as '${Hostname}' itself on the command line. | ||||||
|  |  | ||||||
| set -- \ | set -- \ | ||||||
|         "-Dorg.gradle.appname=$APP_BASE_NAME" \ |         "-Dorg.gradle.appname=$APP_BASE_NAME" \ | ||||||
|         -classpath "$CLASSPATH" \ |         -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ | ||||||
|         org.gradle.wrapper.GradleWrapperMain \ |  | ||||||
|         "$@" |         "$@" | ||||||
|  |  | ||||||
| # Stop when "xargs" is not available. | # 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 See the License for the specific language governing permissions and | ||||||
| @rem limitations under the License. | @rem limitations under the License. | ||||||
| @rem | @rem | ||||||
|  | @rem SPDX-License-Identifier: Apache-2.0 | ||||||
|  | @rem | ||||||
|  |  | ||||||
| @if "%DEBUG%"=="" @echo off | @if "%DEBUG%"=="" @echo off | ||||||
| @rem ########################################################################## | @rem ########################################################################## | ||||||
| @@ -68,11 +70,10 @@ goto fail | |||||||
| :execute | :execute | ||||||
| @rem Setup the command line | @rem Setup the command line | ||||||
|  |  | ||||||
| set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @rem Execute Gradle | @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 | :end | ||||||
| @rem End local scope for the variables with windows NT shell | @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' } |         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" | rootProject.name = "Fidelity" | ||||||
| include ':app' | include ':app' | ||||||
|   | |||||||