Compare commits

...
Author SHA1 Message Date
Renovate Bot eed33c5543 Update plugin org.jetbrains.kotlin.android to v2.4.20 2026-09-21 02:02:13 +00:00
sora 2a9a0463b9 Update .github/workflows/build.yml 2026-09-20 08:59:35 +02:00
bot 29006aae25 Merge pull request 'Update plugin com.android.library to v9.4.1' (#332) from renovate/com.android.library-9.x into main 2026-09-20 04:02:43 +02:00
Renovate Bot 0994a7ba72 Update plugin com.android.library to v9.4.1 2026-09-20 02:02:41 +00:00
bot 391e530953 Merge pull request 'Update dependency joda-time:joda-time to v2.14.4' (#335) from renovate/joda-time-joda-time-2.x into main 2026-09-20 04:02:32 +02:00
Renovate Bot 6ab13de711 Update dependency joda-time:joda-time to v2.14.4 2026-09-20 02:02:29 +00:00
sora 28b0d15511 Update README.md 2026-09-19 18:23:40 +02:00
sora 598293d0ac Upload files to ".github/images" 2026-09-19 18:20:59 +02:00
sora 9791607581 Update .github/workflows/build.yml 2026-09-19 12:17:34 +02:00
sora b311932e27 Add PRIVACY.md 2026-09-19 11:40:00 +02:00
bot 8f249e351d Merge pull request 'Update plugin com.android.application to v9.4.1' (#331) from renovate/com.android.application-9.x into main 2026-09-19 04:02:51 +02:00
Renovate Bot 9797e249f3 Update plugin com.android.application to v9.4.1 2026-09-19 02:02:50 +00:00
sora 978cc062d2 Update .github/workflows/build.yml 2026-09-18 23:27:11 +02:00
sora 5b5a204bf4 Update .github/workflows/build.yml 2026-09-18 22:39:09 +02:00
sora 5ccb4aa900 Update .github/workflows/build.yml 2026-09-18 22:37:16 +02:00
sora fa658c525c Update .github/workflows/build.yml 2026-09-18 22:30:28 +02:00
sora 8596906ef1 Update .github/workflows/build.yml 2026-09-18 22:19:11 +02:00
sora 7ab553e85c Add app/src/main/assets/adi-registration.properties 2026-09-18 11:57:18 +02:00
bot f3b5d4197a Merge pull request 'Update dependency org.robolectric:robolectric to v4.17' (#330) from renovate/org.robolectric-robolectric-4.x into main 2026-09-17 04:01:50 +02:00
Renovate Bot d3c3d49d24 Update dependency org.robolectric:robolectric to v4.17 2026-09-17 02:01:47 +00:00
sora 5a76638f27 Integrate Keepass2android functionality with standalone 2026-09-16 23:04:06 +02:00
sora d05b34573e Re-add keepass2android plugin 2026-09-16 22:01:33 +02:00
sora 732f9558f6 update build and gradle 2026-09-16 22:00:19 +02:00
bot 0d0b2e7091 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.19.2' (#329) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-09-15 04:02:17 +02:00
Renovate Bot 6340a7b686 Update plugin com.autonomousapps.dependency-analysis to v3.19.2 2026-09-15 02:02:14 +00:00
bot 1e30b1a930 Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.09.00' (#328) from renovate/androidx.compose-compose-bom-2026.x into main 2026-09-11 04:02:25 +02:00
bot d39502a5a2 Merge pull request 'Update dependency androidx.navigation:navigation-compose to v2.10.1' (#327) from renovate/androidx.navigation-navigation-compose-2.x into main 2026-09-11 04:02:21 +02:00
Renovate Bot fee79f0030 Update dependency androidx.compose:compose-bom to v2026.09.00 2026-09-11 02:02:21 +00:00
Renovate Bot 06cc53f28b Update dependency androidx.navigation:navigation-compose to v2.10.1 2026-09-11 02:02:19 +00:00
bot 1ce8ac4a92 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.12.1' (#326) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2026-09-10 04:03:37 +02:00
bot 6d95a794cf Merge pull request 'Update dependency androidx.compose.material:material to v1.12.1' (#325) from renovate/androidx.compose.material-material-1.x into main 2026-09-10 04:03:17 +02:00
Renovate Bot d8d8bc72ed Update dependency androidx.compose.ui:ui-tooling to v1.12.1 2026-09-10 02:03:13 +00:00
Renovate Bot 639b16e317 Update dependency androidx.compose.material:material to v1.12.1 2026-09-10 02:03:06 +00:00
bot dcff45dcc4 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.serialization to v2.4.20' (#324) from renovate/org.jetbrains.kotlin.plugin.serialization-2.x into main 2026-09-09 04:01:50 +02:00
Renovate Bot d75ed155c8 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.4.20 2026-09-09 02:01:47 +00:00
bot 790a3e91bb Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.compose to v2.4.20' (#323) from renovate/org.jetbrains.kotlin.plugin.compose-2.x into main 2026-09-08 04:01:47 +02:00
bot d8f4774757 Merge pull request 'Update plugin org.jetbrains.kotlin.android to v2.4.20' (#322) from renovate/org.jetbrains.kotlin.android-2.x into main 2026-09-08 04:01:44 +02:00
Renovate Bot c462813cc4 Update plugin org.jetbrains.kotlin.plugin.compose to v2.4.20 2026-09-08 02:01:43 +00:00
Renovate Bot f5d7de95a2 Update plugin org.jetbrains.kotlin.android to v2.4.20 2026-09-08 02:01:41 +00:00
bot d83a7f81e6 Merge pull request 'Update plugin com.android.library to v9.4.0' (#321) from renovate/com.android.library-9.x into main 2026-09-03 04:03:09 +02:00
Renovate Bot 8e8d0f5398 Update plugin com.android.library to v9.4.0 2026-09-03 02:02:28 +00:00
bot 0d3c732999 Merge pull request 'Update plugin com.android.application to v9.4.0' (#320) from renovate/com.android.application-9.x into main 2026-09-02 04:07:33 +02:00
Renovate Bot b5b09d6cfe Update plugin com.android.application to v9.4.0 2026-09-02 02:07:00 +00:00
bot fa57d697ff Merge pull request 'Update actions/setup-java action to v6' (#315) from renovate/actions-setup-java-6.x into main 2026-08-29 04:03:04 +02:00
Renovate Bot 622af28e49 Update actions/setup-java action to v6 2026-08-29 02:03:02 +00:00
bot 1dc05e561e Merge pull request 'Update plugin com.android.library to v9.3.2' (#313) from renovate/com.android.library-9.x into main 2026-08-29 04:02:53 +02:00
Renovate Bot 4c779e5e80 Update plugin com.android.library to v9.3.2 2026-08-29 02:02:51 +00:00
bot 776fe395d8 Merge pull request 'Update dependency androidx.navigation:navigation-compose to v2.10.0' (#319) from renovate/androidx.navigation-navigation-compose-2.x into main 2026-08-28 04:02:23 +02:00
Renovate Bot 1703171b1a Update dependency androidx.navigation:navigation-compose to v2.10.0 2026-08-28 02:02:21 +00:00
bot e20f80c97e Merge pull request 'Update dependency androidx.camera:camera-view to v1.6.2' (#318) from renovate/androidx.camera-camera-view-1.x into main 2026-08-28 04:02:20 +02:00
Renovate Bot 28fbc9f050 Update dependency androidx.camera:camera-view to v1.6.2 2026-08-28 02:02:16 +00:00
bot 5f33dd6168 Merge pull request 'Update dependency androidx.camera:camera-lifecycle to v1.6.2' (#317) from renovate/androidx.camera-camera-lifecycle-1.x into main 2026-08-27 04:02:23 +02:00
bot e3812710b6 Merge pull request 'Update dependency androidx.camera:camera-camera2 to v1.6.2' (#316) from renovate/androidx.camera-camera-camera2-1.x into main 2026-08-27 04:02:19 +02:00
Renovate Bot 1b6140c7f7 Update dependency androidx.camera:camera-lifecycle to v1.6.2 2026-08-27 02:02:18 +00:00
Renovate Bot 191ba40623 Update dependency androidx.camera:camera-camera2 to v1.6.2 2026-08-27 02:02:15 +00:00
bot 4e81d7a625 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.19.1' (#314) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-08-26 04:03:55 +02:00
Renovate Bot fec817623f Update plugin com.autonomousapps.dependency-analysis to v3.19.1 2026-08-26 02:03:53 +00:00
bot 20d7916c16 Merge pull request 'Update plugin com.android.application to v9.3.2' (#312) from renovate/com.android.application-9.x into main 2026-08-25 04:02:26 +02:00
Renovate Bot 3928ce7a05 Update plugin com.android.application to v9.3.2 2026-08-25 02:02:23 +00:00
bot a9c7a7ca7d Merge pull request 'Update dependency gradle to v9.7.1' (#311) from renovate/gradle-9.x into main 2026-08-20 04:02:02 +02:00
Renovate Bot 93471f0827 Update dependency gradle to v9.7.1 2026-08-20 02:01:58 +00:00
bot ba70aa5982 Merge pull request 'Update dependency androidx.compose.material:material to v1.12.0' (#309) from renovate/androidx.compose.material-material-1.x into main 2026-08-15 04:04:32 +02:00
Renovate Bot 319e09df22 Update dependency androidx.compose.material:material to v1.12.0 2026-08-15 02:04:29 +00:00
bot a49f5413a5 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.12.0' (#310) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2026-08-14 04:03:40 +02:00
Renovate Bot 7846320df7 Update dependency androidx.compose.ui:ui-tooling to v1.12.0 2026-08-14 02:03:37 +00:00
bot 1b3f5ef7cf Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.08.00' (#308) from renovate/androidx.compose-compose-bom-2026.x into main 2026-08-13 04:02:15 +02:00
Renovate Bot 730d5a3650 Update dependency androidx.compose:compose-bom to v2026.08.00 2026-08-13 02:02:12 +00:00
bot c147eafe90 Merge pull request 'Update dependency gradle to v9.7.0' (#307) from renovate/gradle-9.x into main 2026-08-07 04:02:18 +02:00
Renovate Bot dba7b7110f Update dependency gradle to v9.7.0 2026-08-07 02:02:14 +00:00
bot 1d199b7581 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.18.0' (#306) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-07-28 04:01:58 +02:00
Renovate Bot ff63b537f6 Update plugin com.autonomousapps.dependency-analysis to v3.18.0 2026-07-28 02:01:53 +00:00
bot 302b5bc1bd Merge pull request 'Update dependency joda-time:joda-time to v2.14.3' (#305) from renovate/joda-time-joda-time-2.x into main 2026-07-27 04:01:42 +02:00
Renovate Bot 156757d23a Update dependency joda-time:joda-time to v2.14.3 2026-07-27 02:01:37 +00:00
bot 5d39635c66 Merge pull request 'Update plugin com.android.library to v9.3.1' (#304) from renovate/com.android.library-9.x into main 2026-07-25 04:02:21 +02:00
Renovate Bot d93307086b Update plugin com.android.library to v9.3.1 2026-07-25 02:02:16 +00:00
bot e08cbe4f7b Merge pull request 'Update plugin com.android.application to v9.3.1' (#303) from renovate/com.android.application-9.x into main 2026-07-24 04:03:43 +02:00
Renovate Bot e768e92515 Update plugin com.android.application to v9.3.1 2026-07-24 02:03:37 +00:00
bot 78c5ca5a88 Merge pull request 'Update plugin com.android.application to v9.3.0' (#300) from renovate/com.android.application-9.x into main 2026-07-19 04:02:49 +02:00
Renovate Bot 21fe4a66b1 Update plugin com.android.application to v9.3.0 2026-07-18 02:03:14 +00:00
bot fd4a521d0f Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.17.0' (#302) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-07-17 04:02:12 +02:00
bot ed1f05eed5 Merge pull request 'Update plugin com.android.library to v9.3.0' (#301) from renovate/com.android.library-9.x into main 2026-07-17 04:02:07 +02:00
Renovate Bot b8232c9a19 Update plugin com.autonomousapps.dependency-analysis to v3.17.0 2026-07-17 02:02:05 +00:00
Renovate Bot af52816559 Update plugin com.android.library to v9.3.0 2026-07-17 02:02:04 +00:00
bot 90ba769c44 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.serialization to v2.4.10' (#299) from renovate/org.jetbrains.kotlin.plugin.serialization-2.x into main 2026-07-16 04:01:58 +02:00
Renovate Bot 505ec55c50 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.4.10 2026-07-16 02:01:55 +00:00
bot a352ebd21d Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.compose to v2.4.10' (#298) from renovate/org.jetbrains.kotlin.plugin.compose-2.x into main 2026-07-15 04:02:37 +02:00
bot ee686c4e0c Merge pull request 'Update plugin org.jetbrains.kotlin.android to v2.4.10' (#297) from renovate/org.jetbrains.kotlin.android-2.x into main 2026-07-15 04:02:34 +02:00
Renovate Bot cefc7032bd Update plugin org.jetbrains.kotlin.plugin.compose to v2.4.10 2026-07-15 02:02:32 +00:00
Renovate Bot 26c1142375 Update plugin org.jetbrains.kotlin.android to v2.4.10 2026-07-15 02:02:29 +00:00
bot 20ef7ed04a Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.16.1' (#296) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-07-08 04:02:24 +02:00
Renovate Bot 7f9749ef07 Update plugin com.autonomousapps.dependency-analysis to v3.16.1 2026-07-08 02:02:21 +00:00
bot a907102c19 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.11.4' (#295) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2026-07-03 04:02:25 +02:00
Renovate Bot 8c5f0f4d15 Update dependency androidx.compose.ui:ui-tooling to v1.11.4 2026-07-03 02:02:22 +00:00
bot 367697b66b Merge pull request 'Update dependency androidx.compose.material:material to v1.11.4' (#294) from renovate/androidx.compose.material-material-1.x into main 2026-07-02 04:03:48 +02:00
bot 000ad7f979 Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.06.01' (#293) from renovate/androidx.compose-compose-bom-2026.x into main 2026-07-02 04:03:45 +02:00
Renovate Bot e320c22bce Update dependency androidx.compose.material:material to v1.11.4 2026-07-02 02:03:43 +00:00
Renovate Bot 72087c72e6 Update dependency androidx.compose:compose-bom to v2026.06.01 2026-07-02 02:03:41 +00:00
bot 5ef09f0f9b Merge pull request 'Update dependency gradle to v9.6.1' (#292) from renovate/gradle-9.x into main 2026-06-27 04:02:41 +02:00
Renovate Bot 4997107aa4 Update dependency gradle to v9.6.1 2026-06-27 02:02:39 +00:00
bot 7665723ea6 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.16.0' (#291) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-06-26 04:02:03 +02:00
Renovate Bot 62dc4cf0d5 Update plugin com.autonomousapps.dependency-analysis to v3.16.0 2026-06-26 02:02:01 +00:00
bot bdb15389d8 Merge pull request 'Update actions/checkout action to v7' (#290) from renovate/actions-checkout-7.x into main 2026-06-20 04:04:30 +02:00
Renovate Bot d2ca79ac57 Update actions/checkout action to v7 2026-06-20 02:04:27 +00:00
bot 3034f08bf4 Merge pull request 'Update dependency gradle to v9.6.0' (#289) from renovate/gradle-9.x into main 2026-06-19 04:03:32 +02:00
Renovate Bot 529fb2cf16 Update dependency gradle to v9.6.0 2026-06-19 02:03:19 +00:00
bot 27ead287ae Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.06.00' (#288) from renovate/androidx.compose-compose-bom-2026.x into main 2026-06-19 04:03:01 +02:00
Renovate Bot a6dce9a524 Update dependency androidx.compose:compose-bom to v2026.06.00 2026-06-19 02:02:57 +00:00
bot 45bbc6d90b Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.11.3' (#287) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2026-06-18 04:02:16 +02:00
bot fb62271321 Merge pull request 'Update dependency androidx.compose.material:material to v1.11.3' (#286) from renovate/androidx.compose.material-material-1.x into main 2026-06-18 04:02:14 +02:00
Renovate Bot 57885f9882 Update dependency androidx.compose.ui:ui-tooling to v1.11.3 2026-06-18 02:02:13 +00:00
Renovate Bot a04f0aa75a Update dependency androidx.compose.material:material to v1.11.3 2026-06-18 02:02:11 +00:00
bot 695f748be5 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.15.0' (#285) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-06-11 04:02:54 +02:00
Renovate Bot c85e9b6dfa Update plugin com.autonomousapps.dependency-analysis to v3.15.0 2026-06-11 02:02:46 +00:00
bot 283fa26763 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.serialization to v2.4.0' (#284) from renovate/org.jetbrains.kotlin.plugin.serialization-2.x into main 2026-06-05 04:15:35 +02:00
Renovate Bot cc1272f297 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.4.0 2026-06-05 02:07:02 +00:00
bot 4510ff6f57 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.compose to v2.4.0' (#283) from renovate/org.jetbrains.kotlin.plugin.compose-2.x into main 2026-06-04 04:12:36 +02:00
bot 155595eca1 Merge pull request 'Update plugin org.jetbrains.kotlin.android to v2.4.0' (#282) from renovate/org.jetbrains.kotlin.android-2.x into main 2026-06-04 04:12:33 +02:00
Renovate Bot 25c6456bff Update plugin org.jetbrains.kotlin.plugin.compose to v2.4.0 2026-06-04 02:12:31 +00:00
Renovate Bot c153710f3d Update plugin org.jetbrains.kotlin.android to v2.4.0 2026-06-04 02:12:28 +00:00
bot 2154b74413 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.14.1' (#281) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-06-02 04:04:00 +02:00
Renovate Bot 132d1317a0 Update plugin com.autonomousapps.dependency-analysis to v3.14.1 2026-06-02 02:03:51 +00:00
sora 1468124c23 Update README.md 2026-05-29 18:49:59 +02:00
sora dae511c2a2 Update README.md 2026-05-29 18:49:47 +02:00
bot b89d49d407 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.14.0' (#280) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-05-29 04:02:42 +02:00
Renovate Bot 0fee2c31ab Update plugin com.autonomousapps.dependency-analysis to v3.14.0 2026-05-29 02:02:33 +00:00
soraefir eba6b40779 Add NDK version 2026-05-24 13:26:39 +02:00
sora 93442645fc Update README.md 2026-05-24 11:59:19 +02:00
sora f71399f6fa Update .github/workflows/build.yml 2026-05-23 15:44:10 +02:00
sora 0593eb8fbf Update .github/workflows/build.yml 2026-05-23 14:53:48 +02:00
sora 54dadd5bf2 Update app/build.gradle 2026-05-23 14:52:39 +02:00
sora c191eb6248 Update metadata/en-US/short_description.txt 2026-05-23 14:52:09 +02:00
sora 35366a5a09 Update metadata/en-US/full_description.txt 2026-05-23 14:51:50 +02:00
sora 07eb4eefdd Update README.md 2026-05-23 14:50:37 +02:00
soraefir 2a76ef340b Fix proguard 2026-05-23 14:12:11 +02:00
soraefir 249b281ae1 Fixes and updates 2026-05-23 14:01:32 +02:00
bot 82ab401862 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.13.0' (#279) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-05-22 04:01:56 +02:00
Renovate Bot 1cc4533617 Update plugin com.autonomousapps.dependency-analysis to v3.13.0 2026-05-22 02:01:48 +00:00
bot 9b903ce12b Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.12.2' (#278) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-05-21 04:02:34 +02:00
bot 122d03c971 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.11.2' (#277) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2026-05-21 04:02:27 +02:00
Renovate Bot 25c993354e Update plugin com.autonomousapps.dependency-analysis to v3.12.2 2026-05-21 02:02:27 +00:00
Renovate Bot 6eefb809d8 Update dependency androidx.compose.ui:ui-tooling to v1.11.2 2026-05-21 02:02:24 +00:00
bot 3ae56fdbdf Merge pull request 'Update dependency androidx.compose.material:material to v1.11.2' (#276) from renovate/androidx.compose.material-material-1.x into main 2026-05-20 04:02:21 +02:00
bot e4a7413534 Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.05.01' (#275) from renovate/androidx.compose-compose-bom-2026.x into main 2026-05-20 04:02:18 +02:00
Renovate Bot 6c8cfaf128 Update dependency androidx.compose.material:material to v1.11.2 2026-05-20 02:02:17 +00:00
Renovate Bot 3c71988d12 Update dependency androidx.compose:compose-bom to v2026.05.01 2026-05-20 02:02:15 +00:00
bot beca592f01 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.12.0' (#274) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-05-15 04:02:35 +02:00
Renovate Bot ecfaa44756 Update plugin com.autonomousapps.dependency-analysis to v3.12.0 2026-05-15 02:02:27 +00:00
bot 1e12c6e0db Merge pull request 'Update dependency com.google.android.material:material to v1.14.0' (#273) from renovate/com.google.android.material-material-1.x into main 2026-05-14 04:02:59 +02:00
Renovate Bot cb71570834 Update dependency com.google.android.material:material to v1.14.0 2026-05-14 02:02:55 +00:00
bot 1a2d785d82 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.11.0' (#272) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-05-13 04:03:20 +02:00
bot 8e0c8b7eaa Merge pull request 'Update dependency gradle to v9.5.1' (#271) from renovate/gradle-9.x into main 2026-05-13 04:03:15 +02:00
Renovate Bot f96363306d Update plugin com.autonomousapps.dependency-analysis to v3.11.0 2026-05-13 02:03:14 +00:00
Renovate Bot ab32a2ecd2 Update dependency gradle to v9.5.1 2026-05-13 02:03:11 +00:00
bot 85ae80696e Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.05.00' (#270) from renovate/androidx.compose-compose-bom-2026.x into main 2026-05-09 04:03:16 +02:00
Renovate Bot 41bba56d5b Update dependency androidx.compose:compose-bom to v2026.05.00 2026-05-09 02:03:15 +00:00
bot 6e1466f1a3 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.11.1' (#269) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2026-05-09 04:03:07 +02:00
Renovate Bot d082c6b1bd Update dependency androidx.compose.ui:ui-tooling to v1.11.1 2026-05-09 02:03:05 +00:00
bot dfa97108d4 Merge pull request 'Update dependency androidx.compose.material:material to v1.11.1' (#268) from renovate/androidx.compose.material-material-1.x into main 2026-05-08 04:03:25 +02:00
bot ef1f4d071e Merge pull request 'Update dependency androidx.camera:camera-view to v1.6.1' (#267) from renovate/androidx.camera-camera-view-1.x into main 2026-05-08 04:03:22 +02:00
Renovate Bot 9a64cd2cb6 Update dependency androidx.compose.material:material to v1.11.1 2026-05-08 02:03:21 +00:00
Renovate Bot 9971360e7e Update dependency androidx.camera:camera-view to v1.6.1 2026-05-08 02:03:20 +00:00
bot 7c30df4436 Merge pull request 'Update plugin com.android.library to v9.2.1' (#264) from renovate/com.android.library-9.x into main 2026-05-07 04:03:38 +02:00
bot 6bde0f2129 Merge pull request 'Update dependency androidx.camera:camera-lifecycle to v1.6.1' (#266) from renovate/androidx.camera-camera-lifecycle-1.x into main 2026-05-07 04:03:35 +02:00
Renovate Bot 28d23034b0 Update plugin com.android.library to v9.2.1 2026-05-07 02:03:34 +00:00
bot 962bf1ca7c Merge pull request 'Update dependency androidx.camera:camera-camera2 to v1.6.1' (#265) from renovate/androidx.camera-camera-camera2-1.x into main 2026-05-07 04:03:32 +02:00
Renovate Bot 9364cced34 Update dependency androidx.camera:camera-lifecycle to v1.6.1 2026-05-07 02:03:32 +00:00
Renovate Bot 9916804e36 Update dependency androidx.camera:camera-camera2 to v1.6.1 2026-05-07 02:03:30 +00:00
bot 7764b1aa06 Merge pull request 'Update plugin com.android.application to v9.2.1' (#263) from renovate/com.android.application-9.x into main 2026-05-06 10:01:12 +02:00
Renovate Bot 355a5342f4 Update plugin com.android.application to v9.2.1 2026-05-06 08:01:07 +00:00
bot 29b28d4935 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.10.0' (#262) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-04-30 04:02:22 +02:00
Renovate Bot a7833beb8d Update plugin com.autonomousapps.dependency-analysis to v3.10.0 2026-04-30 02:02:15 +00:00
bot 1984406048 Merge pull request 'Update dependency gradle to v9.5.0' (#261) from renovate/gradle-9.x into main 2026-04-29 04:03:16 +02:00
Renovate Bot 5224337b13 Update dependency gradle to v9.5.0 2026-04-29 02:03:13 +00:00
bot 1d15f25cd7 Merge pull request 'Update dependency joda-time:joda-time to v2.14.2' (#260) from renovate/joda-time-joda-time-2.x into main 2026-04-29 04:03:09 +02:00
Renovate Bot d7ffbc40ae Update dependency joda-time:joda-time to v2.14.2 2026-04-29 02:03:04 +00:00
bot 3f1d809855 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.11.0' (#259) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2026-04-26 04:03:13 +02:00
Renovate Bot db9bd281d7 Update dependency androidx.compose.ui:ui-tooling to v1.11.0 2026-04-26 02:03:11 +00:00
bot 1b217ef01d Merge pull request 'Update dependency androidx.compose.material:material to v1.11.0' (#258) from renovate/androidx.compose.material-material-1.x into main 2026-04-25 04:13:26 +02:00
Renovate Bot 6e3487f905 Update dependency androidx.compose.material:material to v1.11.0 2026-04-25 02:13:23 +00:00
bot e25d6da3aa Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.serialization to v2.3.21' (#257) from renovate/org.jetbrains.kotlin.plugin.serialization-2.x into main 2026-04-25 04:13:10 +02:00
Renovate Bot 57ab225705 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.3.21 2026-04-25 02:13:06 +00:00
bot 43147f0600 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.compose to v2.3.21' (#256) from renovate/org.jetbrains.kotlin.plugin.compose-2.x into main 2026-04-24 04:13:59 +02:00
bot bdd1fe2323 Merge pull request 'Update plugin org.jetbrains.kotlin.android to v2.3.21' (#255) from renovate/org.jetbrains.kotlin.android-2.x into main 2026-04-24 04:13:50 +02:00
Renovate Bot de62af173f Update plugin org.jetbrains.kotlin.plugin.compose to v2.3.21 2026-04-24 02:13:49 +00:00
Renovate Bot c9c5009092 Update plugin org.jetbrains.kotlin.android to v2.3.21 2026-04-24 02:06:37 +00:00
bot fa8e9f390f Merge pull request 'Update plugin com.android.library to v9.2.0' (#252) from renovate/com.android.library-9.x into main 2026-04-23 04:02:48 +02:00
bot 7ce9d0f36a Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.04.01' (#254) from renovate/androidx.compose-compose-bom-2026.x into main 2026-04-23 04:02:44 +02:00
Renovate Bot 6066b25d15 Update plugin com.android.library to v9.2.0 2026-04-23 02:02:44 +00:00
bot 9262c737c3 Merge pull request 'Update dependency androidx.navigation:navigation-compose to v2.9.8' (#253) from renovate/androidx.navigation-navigation-compose-2.x into main 2026-04-23 04:02:42 +02:00
Renovate Bot 5b03dc4c7b Update dependency androidx.compose:compose-bom to v2026.04.01 2026-04-23 02:02:42 +00:00
Renovate Bot f32ae1a8e8 Update dependency androidx.navigation:navigation-compose to v2.9.8 2026-04-23 02:02:40 +00:00
bot 4220a334fd Merge pull request 'Update plugin com.android.application to v9.2.0' (#251) from renovate/com.android.application-9.x into main 2026-04-22 04:02:34 +02:00
Renovate Bot c2be61f1a6 Update plugin com.android.application to v9.2.0 2026-04-22 02:02:31 +00:00
bot c8bae4838e Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.9.0' (#250) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-04-21 04:02:45 +02:00
Renovate Bot e60c4cd35b Update plugin com.autonomousapps.dependency-analysis to v3.9.0 2026-04-21 02:02:38 +00:00
bot c230c8ea72 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.8.0' (#249) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-04-20 04:02:40 +02:00
Renovate Bot 4e2a3de7d2 Update plugin com.autonomousapps.dependency-analysis to v3.8.0 2026-04-20 02:02:34 +00:00
bot 1b7f068976 Merge pull request 'Update plugin com.android.library to v9.1.1' (#248) from renovate/com.android.library-9.x into main 2026-04-18 04:05:07 +02:00
Renovate Bot 4e24f9e73e Update plugin com.android.library to v9.1.1 2026-04-15 02:01:43 +00:00
bot 4b471f5e56 Merge pull request 'Update plugin com.android.application to v9.1.1' (#247) from renovate/com.android.application-9.x into main 2026-04-14 04:02:03 +02:00
Renovate Bot 463007c80e Update plugin com.android.application to v9.1.1 2026-04-14 02:02:00 +00:00
bot aedb9d0ab4 Merge pull request 'Update softprops/action-gh-release action to v3' (#246) from renovate/softprops-action-gh-release-3.x into main 2026-04-13 04:02:33 +02:00
Renovate Bot a48fc9a374 Update softprops/action-gh-release action to v3 2026-04-13 02:02:30 +00:00
bot 541c79db7f Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.7.0' (#245) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-04-11 04:03:08 +02:00
Renovate Bot 7cad346bb7 Update plugin com.autonomousapps.dependency-analysis to v3.7.0 2026-04-11 02:03:02 +00:00
bot dc5237652e Merge pull request 'Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.11.0' (#244) from renovate/org.jetbrains.kotlinx-kotlinx-serialization-json-1.x into main 2026-04-10 04:02:26 +02:00
Renovate Bot f0bd4100dc Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.11.0 2026-04-10 02:02:21 +00:00
sora 9903790e87 Update .github/workflows/build.yml 2026-04-04 12:08:52 +02:00
sora 06053b7ec8 Update .github/workflows/build.yml 2026-04-04 12:05:40 +02:00
bot ab6073d415 Merge pull request 'Update dependency androidx.camera:camera-view to v1.6.0' (#243) from renovate/androidx.camera-camera-view-1.x into main 2026-03-28 03:02:44 +01:00
Renovate Bot 4f1897af2f Update dependency androidx.camera:camera-view to v1.6.0 2026-03-28 02:02:43 +00:00
bot 867e438857 Merge pull request 'Update dependency androidx.camera:camera-lifecycle to v1.6.0' (#242) from renovate/androidx.camera-camera-lifecycle-1.x into main 2026-03-28 03:02:36 +01:00
Renovate Bot b41ff5868e Update dependency androidx.camera:camera-lifecycle to v1.6.0 2026-03-28 02:02:34 +00:00
bot ca84a36630 Merge pull request 'Update dependency androidx.camera:camera-camera2 to v1.6.0' (#241) from renovate/androidx.camera-camera-camera2-1.x into main 2026-03-27 03:01:19 +01:00
bot 5736164692 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.10.6' (#240) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2026-03-27 03:01:18 +01:00
Renovate Bot 864927e8eb Update dependency androidx.camera:camera-camera2 to v1.6.0 2026-03-27 02:01:16 +00:00
Renovate Bot 8d4267608b Update dependency androidx.compose.ui:ui-tooling to v1.10.6 2026-03-27 02:01:14 +00:00
bot 7e41b10394 Merge pull request 'Update dependency androidx.compose.material:material to v1.10.6' (#239) from renovate/androidx.compose.material-material-1.x into main 2026-03-26 03:01:23 +01:00
bot 0f9bfa8f29 Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.03.01' (#238) from renovate/androidx.compose-compose-bom-2026.x into main 2026-03-26 03:01:22 +01:00
Renovate Bot 2322a02e0d Update dependency androidx.compose.material:material to v1.10.6 2026-03-26 02:01:21 +00:00
Renovate Bot b73cd5e9f7 Update dependency androidx.compose:compose-bom to v2026.03.01 2026-03-26 02:01:19 +00:00
bot bcf848f667 Merge pull request 'Update dependency gradle to v9.4.1' (#237) from renovate/gradle-9.x into main 2026-03-20 03:02:34 +01:00
Renovate Bot 01a627af79 Update dependency gradle to v9.4.1 2026-03-20 02:02:31 +00:00
bot e542c38eb9 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.serialization to v2.3.20' (#236) from renovate/org.jetbrains.kotlin.plugin.serialization-2.x into main 2026-03-18 03:09:11 +01:00
Renovate Bot f1fdd91025 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.3.20 2026-03-18 02:09:08 +00:00
bot 741c8eeb80 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.compose to v2.3.20' (#235) from renovate/org.jetbrains.kotlin.plugin.compose-2.x into main 2026-03-17 03:10:24 +01:00
bot f3c5b80122 Merge pull request 'Update plugin org.jetbrains.kotlin.android to v2.3.20' (#234) from renovate/org.jetbrains.kotlin.android-2.x into main 2026-03-17 03:10:19 +01:00
Renovate Bot dd03b7797f Update plugin org.jetbrains.kotlin.plugin.compose to v2.3.20 2026-03-17 02:10:18 +00:00
Renovate Bot a705efbeb9 Update plugin org.jetbrains.kotlin.android to v2.3.20 2026-03-17 02:10:16 +00:00
bot 6865c0513f Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.03.00' (#233) from renovate/androidx.compose-compose-bom-2026.x into main 2026-03-13 03:01:31 +01:00
bot 385484eb5c Merge pull request 'Update dependency androidx.datastore:datastore-preferences to v1.2.1' (#232) from renovate/androidx.datastore-datastore-preferences-1.x into main 2026-03-13 03:01:30 +01:00
Renovate Bot 8cc07d2570 Update dependency androidx.compose:compose-bom to v2026.03.00 2026-03-13 02:01:29 +00:00
Renovate Bot e876d470ed Update dependency androidx.datastore:datastore-preferences to v1.2.1 2026-03-13 02:01:26 +00:00
bot f2a8a9b179 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.10.5' (#231) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2026-03-12 03:01:41 +01:00
bot a03e9553c3 Merge pull request 'Update dependency androidx.compose.material:material to v1.10.5' (#230) from renovate/androidx.compose.material-material-1.x into main 2026-03-12 03:01:38 +01:00
Renovate Bot 6f5fc5733e Update dependency androidx.compose.ui:ui-tooling to v1.10.5 2026-03-12 02:01:37 +00:00
Renovate Bot 0cff562c76 Update dependency androidx.compose.material:material to v1.10.5 2026-03-12 02:01:35 +00:00
bot a9e5e0c969 Merge pull request 'Update dependency joda-time:joda-time to v2.14.1' (#229) from renovate/joda-time-joda-time-2.x into main 2026-03-09 03:01:38 +01:00
Renovate Bot ee1ef7b2de Update dependency joda-time:joda-time to v2.14.1 2026-03-09 02:01:32 +00:00
bot b22c24aa54 Merge pull request 'Update plugin com.android.library to v9.1.0' (#227) from renovate/com.android.library-9.x into main 2026-03-05 03:02:34 +01:00
bot 0ceee3ab67 Merge pull request 'Update dependency gradle to v9.4.0' (#228) from renovate/gradle-9.x into main 2026-03-05 03:02:30 +01:00
Renovate Bot 5c4b20d57c Update plugin com.android.library to v9.1.0 2026-03-05 02:02:30 +00:00
Renovate Bot 5c4b3c7df0 Update dependency gradle to v9.4.0 2026-03-05 02:02:28 +00:00
bot 933578fca0 Merge pull request 'Update plugin com.android.application to v9.1.0' (#226) from renovate/com.android.application-9.x into main 2026-03-04 03:01:52 +01:00
Renovate Bot ec12827b21 Update plugin com.android.application to v9.1.0 2026-03-04 02:01:49 +00:00
bot 3655987082 Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.02.01' (#225) from renovate/androidx.compose-compose-bom-2026.x into main 2026-02-28 03:02:30 +01:00
Renovate Bot 28ec153a0c Update dependency androidx.compose:compose-bom to v2026.02.01 2026-02-28 02:02:28 +00:00
bot f3d7059221 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.6.1' (#224) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2026-02-27 03:01:51 +01:00
Renovate Bot c83ef3d3c4 Update plugin com.autonomousapps.dependency-analysis to v3.6.1 2026-02-27 02:01:43 +00:00
bot 4dfb171d41 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.10.4' (#223) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2026-02-26 03:01:50 +01:00
bot ee6a8dc35a Merge pull request 'Update dependency androidx.compose.material:material to v1.10.4' (#222) from renovate/androidx.compose.material-material-1.x into main 2026-02-26 03:01:47 +01:00
Renovate Bot 8e22babdc2 Update dependency androidx.compose.ui:ui-tooling to v1.10.4 2026-02-26 02:01:45 +00:00
Renovate Bot a770f0d381 Update dependency androidx.compose.material:material to v1.10.4 2026-02-26 02:01:43 +00:00
bot 7976c05e76 Merge pull request 'Update plugin com.android.library to v9.0.1' (#221) from renovate/com.android.library-9.x into main 2026-02-14 03:03:02 +01:00
Renovate Bot af8d718058 Update plugin com.android.library to v9.0.1 2026-02-14 02:03:01 +00:00
bot ccf9da6e98 Merge pull request 'Update plugin com.android.application to v9.0.1' (#220) from renovate/com.android.application-9.x into main 2026-02-14 03:02:54 +01:00
Renovate Bot 6c27af1404 Update plugin com.android.application to v9.0.1 2026-02-14 02:02:52 +00:00
bot daef958340 Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.02.00' (#219) from renovate/androidx.compose-compose-bom-2026.x into main 2026-02-13 03:02:10 +01:00
Renovate Bot 1cea5b6c3f Update dependency androidx.compose:compose-bom to v2026.02.00 2026-02-13 02:02:07 +00:00
bot 4d52634e18 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.10.3' (#218) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2026-02-12 03:02:13 +01:00
bot 6f6d220f4f Merge pull request 'Update dependency androidx.compose.material:material to v1.10.3' (#217) from renovate/androidx.compose.material-material-1.x into main 2026-02-12 03:02:11 +01:00
Renovate Bot 34858d23d5 Update dependency androidx.compose.ui:ui-tooling to v1.10.3 2026-02-12 02:02:10 +00:00
Renovate Bot 8ddd042579 Update dependency androidx.compose.material:material to v1.10.3 2026-02-12 02:02:08 +00:00
bot 7136f8c883 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.serialization to v2.3.10' (#216) from renovate/org.jetbrains.kotlin.plugin.serialization-2.x into main 2026-02-07 03:10:06 +01:00
Renovate Bot 372274cfc3 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.3.10 2026-02-07 02:10:04 +00:00
bot 54ff955670 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.compose to v2.3.10' (#215) from renovate/org.jetbrains.kotlin.plugin.compose-2.x into main 2026-02-06 03:09:47 +01:00
bot f4f18258d1 Merge pull request 'Update plugin org.jetbrains.kotlin.android to v2.3.10' (#214) from renovate/org.jetbrains.kotlin.android-2.x into main 2026-02-06 03:09:44 +01:00
Renovate Bot b43d1a53e2 Update plugin org.jetbrains.kotlin.plugin.compose to v2.3.10 2026-02-06 02:09:44 +00:00
Renovate Bot 63c1bd1d7e Update plugin org.jetbrains.kotlin.android to v2.3.10 2026-02-06 02:09:41 +00:00
bot 4e33f88f4f Merge pull request 'Update dependency gradle to v9.3.1' (#213) from renovate/gradle-9.x into main 2026-02-01 03:03:57 +01:00
Renovate Bot 9893c8ddae Update dependency gradle to v9.3.1 2026-02-01 02:03:48 +00:00
bot 3a5a9ca273 Merge pull request 'Update dependency androidx.navigation:navigation-compose to v2.9.7' (#212) from renovate/androidx.navigation-navigation-compose-2.x into main 2026-02-01 03:03:21 +01:00
Renovate Bot 2b4823206c Update dependency androidx.navigation:navigation-compose to v2.9.7 2026-02-01 02:03:16 +00:00
bot 60bc918153 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.10.2' (#211) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2026-01-31 03:03:31 +01:00
Renovate Bot 9454b612ce Update dependency androidx.compose.ui:ui-tooling to v1.10.2 2026-01-31 02:03:30 +00:00
bot f5f472d39e Merge pull request 'Update dependency androidx.compose.material:material to v1.10.2' (#210) from renovate/androidx.compose.material-material-1.x into main 2026-01-31 03:03:22 +01:00
Renovate Bot 58182a51db Update dependency androidx.compose.material:material to v1.10.2 2026-01-31 02:03:17 +00:00
bot 1db246eab9 Merge pull request 'Update dependency androidx.compose:compose-bom to v2026.01.01' (#209) from renovate/androidx.compose-compose-bom-2026.x into main 2026-01-30 03:02:04 +01:00
bot 67ade9909c Merge pull request 'Update dependency androidx.camera:camera-view to v1.5.3' (#208) from renovate/androidx.camera-camera-view-1.x into main 2026-01-30 03:02:03 +01:00
Renovate Bot e412cee462 Update dependency androidx.compose:compose-bom to v2026.01.01 2026-01-30 02:02:01 +00:00
Renovate Bot 46c9aedb7c Update dependency androidx.camera:camera-view to v1.5.3 2026-01-30 02:01:59 +00:00
bot 089de53398 Merge pull request 'Update dependency androidx.camera:camera-lifecycle to v1.5.3' (#207) from renovate/androidx.camera-camera-lifecycle-1.x into main 2026-01-29 03:02:09 +01:00
bot dcc18bc26e Merge pull request 'Update dependency androidx.camera:camera-camera2 to v1.5.3' (#206) from renovate/androidx.camera-camera-camera2-1.x into main 2026-01-29 03:02:09 +01:00
Renovate Bot 89d9f0274e Update dependency androidx.camera:camera-lifecycle to v1.5.3 2026-01-29 02:02:07 +00:00
Renovate Bot 126cfe08a3 Update dependency androidx.camera:camera-camera2 to v1.5.3 2026-01-29 02:02:05 +00:00
bot 7866c074ed Merge pull request 'Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.10.0' (#205) from renovate/org.jetbrains.kotlinx-kotlinx-serialization-json-1.x into main 2026-01-22 03:01:46 +01:00
Renovate Bot d8d91f091a Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.10.0 2026-01-22 02:01:42 +00:00
bot 2b3d01a680 Merge pull request 'Update plugin com.android.library to v9' (#204) from renovate/com.android.library-9.x into main 2026-01-17 03:04:49 +01:00
Renovate Bot 99ffa3226e Update plugin com.android.library to v9 2026-01-17 02:04:48 +00:00
bot a0af4a3d24 Merge pull request 'Update dependency gradle to v9.3.0' (#203) from renovate/gradle-9.x into main 2026-01-17 03:04:39 +01:00
Renovate Bot aacedd40e0 Update dependency gradle to v9.3.0 2026-01-17 02:04:36 +00:00
bot d9e0cb0502 Merge pull request 'Update plugin com.android.application to v9' (#202) from renovate/com.android.application-9.x into main 2026-01-16 03:02:17 +01:00
bot 08cfbc1e53 Merge pull request 'Update dependency androidx.compose:compose-bom to v2026' (#201) from renovate/androidx.compose-compose-bom-2026.x into main 2026-01-16 03:02:16 +01:00
Renovate Bot f24e15a9f3 Update plugin com.android.application to v9 2026-01-16 02:02:14 +00:00
Renovate Bot adcd5f50f3 Update dependency androidx.compose:compose-bom to v2026 2026-01-16 02:02:12 +00:00
bot bb4dd58bbb Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.10.1' (#200) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2026-01-15 03:03:04 +01:00
bot 25c7ddae1c Merge pull request 'Update dependency androidx.compose.material:material to v1.10.1' (#199) from renovate/androidx.compose.material-material-1.x into main 2026-01-15 03:03:03 +01:00
Renovate Bot 40f28ceca9 Update dependency androidx.compose.ui:ui-tooling to v1.10.1 2026-01-15 02:03:01 +00:00
Renovate Bot 6622ef0d94 Update dependency androidx.compose.material:material to v1.10.1 2026-01-15 02:02:59 +00:00
bot 950678e279 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.serialization to v2.3.0' (#198) from renovate/org.jetbrains.kotlin.plugin.serialization-2.x into main 2025-12-18 03:09:21 +01:00
bot 5544289d0f Merge pull request 'Update dependency androidx.compose:compose-bom to v2025.12.01' (#197) from renovate/androidx.compose-compose-bom-2025.x into main 2025-12-18 03:01:18 +01:00
Renovate Bot d7f32d0841 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.3.0 2025-12-18 02:01:16 +00:00
Renovate Bot b1fc257786 Update dependency androidx.compose:compose-bom to v2025.12.01 2025-12-18 02:01:13 +00:00
bot 140ef2cb64 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.compose to v2.3.0' (#196) from renovate/org.jetbrains.kotlin.plugin.compose-2.x into main 2025-12-17 03:10:37 +01:00
bot 55b730f73e Merge pull request 'Update plugin org.jetbrains.kotlin.android to v2.3.0' (#195) from renovate/org.jetbrains.kotlin.android-2.x into main 2025-12-17 03:10:34 +01:00
Renovate Bot 41d6ba0e87 Update plugin org.jetbrains.kotlin.plugin.compose to v2.3.0 2025-12-17 02:10:34 +00:00
Renovate Bot 8124ff0f55 Update plugin org.jetbrains.kotlin.android to v2.3.0 2025-12-17 02:10:31 +00:00
bot 0c862b4756 Merge pull request 'Update plugin com.android.library to v8.13.2' (#194) from renovate/com.android.library-8.x into main 2025-12-14 03:02:15 +01:00
Renovate Bot 6453e56e18 Update plugin com.android.library to v8.13.2 2025-12-13 02:02:24 +00:00
bot ddf56d86ba Merge pull request 'Update plugin com.android.application to v8.13.2' (#193) from renovate/com.android.application-8.x into main 2025-12-12 03:02:22 +01:00
Renovate Bot 5cc072921d Update plugin com.android.application to v8.13.2 2025-12-12 02:02:19 +00:00
bot 247da0b8b6 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.10.0' (#192) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2025-12-06 03:02:41 +01:00
Renovate Bot 993524e641 Update dependency androidx.compose.ui:ui-tooling to v1.10.0 2025-12-06 02:02:39 +00:00
bot c17026509c Merge pull request 'Update dependency androidx.camera:camera-view to v1.5.2' (#191) from renovate/androidx.camera-camera-view-1.x into main 2025-12-06 03:02:30 +01:00
Renovate Bot 595524f2d1 Update dependency androidx.camera:camera-view to v1.5.2 2025-12-06 02:02:27 +00:00
bot f322e35fcb Merge pull request 'Update dependency androidx.camera:camera-lifecycle to v1.5.2' (#190) from renovate/androidx.camera-camera-lifecycle-1.x into main 2025-12-05 03:01:42 +01:00
bot 045ad6eb89 Merge pull request 'Update dependency androidx.camera:camera-camera2 to v1.5.2' (#189) from renovate/androidx.camera-camera-camera2-1.x into main 2025-12-05 03:01:40 +01:00
Renovate Bot 8c5f9b988d Update dependency androidx.camera:camera-lifecycle to v1.5.2 2025-12-05 02:01:39 +00:00
Renovate Bot 4803c2d59a Update dependency androidx.camera:camera-camera2 to v1.5.2 2025-12-05 02:01:37 +00:00
bot ce827e9c0d Merge pull request 'Update dependency androidx.compose.material:material to v1.10.0' (#188) from renovate/androidx.compose.material-material-1.x into main 2025-12-04 03:02:31 +01:00
bot 4ee6bf0bcf Merge pull request 'Update dependency androidx.compose:compose-bom to v2025.12.00' (#187) from renovate/androidx.compose-compose-bom-2025.x into main 2025-12-04 03:02:26 +01:00
Renovate Bot 80fbb71bfd Update dependency androidx.compose.material:material to v1.10.0 2025-12-04 02:02:25 +00:00
Renovate Bot c4d5eb2831 Update dependency androidx.compose:compose-bom to v2025.12.00 2025-12-04 02:02:23 +00:00
bot 958f23803e Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.5.1' (#186) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2025-11-27 03:02:29 +01:00
Renovate Bot 78afa1163a Update plugin com.autonomousapps.dependency-analysis to v3.5.1 2025-11-27 02:02:22 +00:00
bot 767723f642 Merge pull request 'Update actions/checkout action to v6' (#185) from renovate/actions-checkout-6.x into main 2025-11-22 03:03:08 +01:00
Renovate Bot 43e462256b Update actions/checkout action to v6 2025-11-22 02:03:06 +00:00
bot 9986b58fc4 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.5.0' (#184) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2025-11-22 03:02:57 +01:00
Renovate Bot 86cd3e89eb Update plugin com.autonomousapps.dependency-analysis to v3.5.0 2025-11-22 02:02:50 +00:00
41 changed files with 1643 additions and 447 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

+58 -13
View File
@@ -23,9 +23,16 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v7
with:
submodules: true
fetch-depth: 0
- name: Make script executable
run: chmod +x ./patch_submodule.sh
- name: Run patch submodule script
run: ./patch_submodule.sh
- name: set up secrets
run: |
echo "${{ secrets.RELEASE_KEYSTORE }}" > keystore.asc
@@ -33,7 +40,10 @@ jobs:
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: Generate Dynamic Release Notes
run: |
mkdir -p tmp/whatsnew
echo "Various improvements for you to experience..." > tmp/whatsnew/whatsnew-en-US
- name: create and checkout branch
if: github.event_name == 'pull_request'
@@ -42,24 +52,59 @@ jobs:
run: git checkout -B "$BRANCH"
- name: set up JDK
uses: actions/setup-java@v5
uses: actions/setup-java@v6
with:
java-version: 17
java-version: 21
distribution: "temurin"
cache: 'gradle'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
# Scoped to :app so the upstream KeePassDX submodule tests do not gate this build.
- name: Run unit tests
run: ./gradlew :app:testDebugUnitTest
- name: Build APK
run: ./gradlew assemble
run: |
VERSION_CODE=$(git rev-list --count HEAD)
VERSION_BASE=$(git describe --tags --abbrev=0 | sed 's/^v//')
VERSION_DEV=$(git rev-list --count $(git describe --tags --abbrev=0)..HEAD)
if [ $VERSION_DEV -gt 0 ]; then
VERSION_NAME="${VERSION_BASE}.${VERSION_DEV}"
else
VERSION_NAME="${VERSION_BASE}"
fi
./gradlew bundleSignedRelease assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
# ./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
# ./gradlew bundleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
# - name: Upload APK
# uses: actions/upload-artifact@v4
# with:
# name: app.apk
# path: app/build/outputs/apk/release/app-release.apk
- name: Upload APK
uses: actions/upload-artifact@v7
with:
path: app/build/outputs/apk/signedRelease/app-signedRelease.apk
compression-level: 0
archive: false
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
app/build/outputs/apk/release/app-release.apk
app/build/outputs/apk/signedRelease/app-signedRelease.apk
prerelease: |
if [[ ${{ github.ref }} =~ ^refs/tags/[0-9]+\.[0-9]+[a-z]+$ ]]; then
echo "false"
else
echo "true"
fi
- name: Upload to Google Play
uses: r0adkll/upload-google-play@v1
if: startsWith(github.ref, 'refs/tags/')
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: net.helcel.fidelity
releaseFiles: app/build/outputs/bundle/signedRelease/app-signedRelease.aab
tracks: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'alpha' }}
status: completed
whatsNewDirectory: tmp/whatsnew
+77
View File
@@ -0,0 +1,77 @@
# Privacy Policy for Keepass Fidelity
**App:** Keepass Fidelity (`net.helcel.fidelity`)
**Developer:** Helcel
**Effective date:** 19 September 2026
## What we collect
Nothing. We operate no servers and receive no data from the app. Keepass
Fidelity has no internet access, no account, and contains no analytics,
advertising, or tracking libraries.
## Your data
Your loyalty cards (names and barcodes) are stored in the
KeePass database you choose, not in a separate store controlled by us:
- **Standalone mode:** the app opens a `.kdbx` file you select, using the
bundled KeePassDX engine. The file stays where you put it, encrypted by
KeePass with your password or key file.
- **Keepass2Android plugin mode:** entries are read and created through the
Keepass2Android app installed on your device. That app holds the database and
its own privacy policy applies to it.
Your database password is used only to unlock your database on the device. It is
never stored anywhere by us and never leaves your device.
If your database file is kept in a folder synchronised by another app or cloud
service, that service handles the file under its own privacy policy.
## On-device cache
For quick access, the app keeps a recently-used history and may cache entry data
in its private storage. You can mark individual entries to be excluded from
caching, and the cache can be cleared from the app or by clearing the app's data
in Android Settings.
## Permissions
- `CAMERA` — scanning a barcode to create an entry. Camera frames are processed
on the device and are not stored or transmitted.
- `READ_MEDIA_VISUAL_USER_SELECTED` — reading a barcode from an image you
explicitly select. Only the images you pick are accessed, and only while
importing.
## Retention and deletion
Cached data and history remain until you clear them or uninstall the app.
Uninstalling does not delete your KeePass database, which is your own file:
delete it yourself, or remove entries through the app or any KeePass client.
## Children
Keepass Fidelity is suitable for all ages and collects no data from any user,
including children.
## Security
Your cards are protected by KeePass encryption, which depends on the strength of
the password or key file you choose. On-device cache and history are held in the
app's private storage, protected by the Android sandbox.
## Open source
Keepass Fidelity is released into the public domain under the Unlicense. The
full source is available at https://github.com/helcel-net/keepass-fidelity, so
these statements can be independently verified.
## Changes
Any change affecting privacy will be published in this document before or with
the release that introduces it.
## Contact
Email: net.helcel+privacy@google.com
Issues: https://github.com/helcel-net/keepass-fidelity/issues
+29 -7
View File
@@ -3,13 +3,15 @@
<h1>Keepass Fidelity</h1>
<img width="100px" src="./metadata/en-US/images/icon.png" alt="Logo">
<p>A minimalist fidelity/loyalty card plugin</p>
<p>A minimalist fidelity/loyalty card app with Keepass Database storage</p>
<a href="https://ko-fi.com/I2I615VP5M"><img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="ko-fi"></a>
<br>
<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 href="https://github.com/helcel-net/keepass-fidelity/actions/workflows/build.yml">
<img src="https://github.com/helcel-net/keepass-fidelity/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status">
</a>
</div>
@@ -27,7 +29,10 @@
## ⭐ Features
- Search entries in [Keepass2Android](https://github.com/PhilippC/keepass2android/)
- Two storage modes, chosen on first start (switchable later from the launcher):
- **Standalone**: opens a `.kdbx` database directly (bundled KeePassDX engine)
- **Keepass2Android plugin**: queries and creates entries through the installed Keepass2Android app
- Search entries in Keepass Database
- Scan & Create entries
- Recently used history for fast access
- Protect entries from caching
@@ -38,13 +43,23 @@
<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">
<img width="200" height="84" alt="Izzy Download" src=".github/images/izzy.png">
</a>
<a href="https://github.com/choelzl/keepass-fidelity/releases/latest">
<a href="https://github.com/helcel-net/keepass-fidelity/releases/latest">
<img width="200" height="84" alt="APK Download" src=".github/images/apk.png">
</a>
<a future-href="https://f-droid.org/en/packages/net.helcel.fidelity">
<img width="200" height="84" alt="Fdroid Download" src=".github/images/fdroid.png">
</a>
<a href="https://play.google.com/store/apps/details?id=net.helcel.fidelity">
<img width="200" height="84" alt="GooglePlay Download" src=".github/images/playstore.png">
</a>
</div>
Note:
- Fdroid: requested, see https://gitlab.com/fdroid/rfp/-/work_items/4411
- PlayStore: alpha test restriction by Google. To access, join https://groups.google.com/g/helcel-android-test
## ⚙️ Permissions
- `CAMERA`: necessary for importing barcodes from camera
@@ -63,10 +78,17 @@ Keepass-Fidelity is a user-driven project. We welcome any contribution, big or s
Thanks to all contributors, the developers of our dependencies, and our users.
## Signinig key
```
net.helcel.fidelity
D1:96:54:FE:1D:79:16:76:47:D7:A6:12:0E:F6:AB:7D:56:9D:45:AD:FD:41:EF:D6:FA:80:9D:26:52:73:F6
```
## 📝 License
```
Copyright (C) 2024 Helcel
Copyright (C) 2026 Helcel
Licensed under the Unlicense
For more information, please refer to <https://unlicense.org>
+79 -24
View File
@@ -1,27 +1,30 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.21'
id 'org.jetbrains.kotlin.plugin.compose' version '2.2.21'
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.20'
id 'org.jetbrains.kotlin.plugin.compose' version '2.4.20'
id 'jacoco'
}
android {
namespace 'net.helcel.fidelity'
compileSdk 36
compileSdk = 37
ndkVersion "25.2.9519653" //Match this to KeypassDX crypto NDK version
defaultConfig {
applicationId 'net.helcel.fidelity'
versionName "1.0d"
// CI derives both from git (see .github/workflows/build.yml); the fallbacks are for local builds.
versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1.3c"
versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1
buildConfigField("String", "APP_NAME", "\"Keepass Fidelity\"")
manifestPlaceholders["APP_NAME"] = "Keepass Fidelity"
minSdk 28
targetSdk 36
minSdk = 28
targetSdk = 37
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
create("release") {
register("release") {
try {
def keystorePropertiesFile = rootProject.file("app/keystore.properties")
def keystoreProperties = new Properties()
@@ -41,10 +44,13 @@ android {
buildTypes {
debug {
debuggable true
initWith(buildTypes.release)
signingConfig signingConfigs.debug
enableUnitTestCoverage true
}
release {
minifyEnabled true
shrinkResources false
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
signedRelease {
@@ -78,6 +84,25 @@ android {
composeOptions {
kotlinCompilerExtensionVersion = "2.2.20"
}
testOptions {
unitTests {
includeAndroidResources = true
all {
maxParallelForks = 2
forkEvery = 50
maxHeapSize = "1024m"
// Robolectric loads classes through its own sandbox classloader, which leaves them
// without a code-source location. Without this JaCoCo skips them entirely and the
// report only sees the handful of plain JVM classes.
jacoco {
includeNoLocationClasses = true
excludes = ["jdk.internal.*"]
}
}
}
}
kotlin {
jvmToolchain(21)
}
@@ -86,43 +111,73 @@ android {
disable 'UsingMaterialAndMaterial3Libraries'
disable 'PreviewAnnotationInFunctionWithParameters'
}
}
dependencies {
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material3:material3:1.4.0'
implementation 'androidx.compose.material:material:1.9.5'
implementation 'androidx.compose.material:material:1.12.1'
implementation 'androidx.compose.material:material-icons-extended:1.7.8'
implementation 'androidx.navigation:navigation-compose:2.9.6'
implementation 'androidx.navigation:navigation-compose:2.10.1'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation "androidx.biometric:biometric:1.2.0-alpha05"
implementation "androidx.security:security-crypto:1.1.0"
implementation "androidx.datastore:datastore-preferences:1.2.0"
implementation "androidx.datastore:datastore-preferences:1.2.1"
implementation "androidx.security:security-crypto:1.1.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5'
implementation 'androidx.camera:camera-lifecycle:1.5.1'
implementation 'androidx.camera:camera-view:1.5.1'
runtimeOnly 'androidx.camera:camera-camera2:1.5.1'
implementation 'androidx.camera:camera-lifecycle:1.6.2'
implementation 'androidx.camera:camera-view:1.6.2'
runtimeOnly 'androidx.camera:camera-camera2:1.6.2'
implementation 'com.google.android.material:material:1.13.0'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0'
implementation 'com.google.android.material:material:1.14.0'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0'
implementation 'com.google.zxing:core:3.5.4'
implementation project(":database")
implementation project(":crypto")
implementation platform('androidx.compose:compose-bom:2025.11.01')
implementation 'androidx.compose.ui:ui-tooling:1.9.5'
implementation platform('androidx.compose:compose-bom:2026.09.00')
implementation 'androidx.compose.ui:ui-tooling:1.12.1'
implementation 'androidx.compose.ui:ui-tooling-preview'
//Submodule
//noinspection NewerVersionAvailable
implementation 'joda-time:joda-time:2.14.0'
implementation 'joda-time:joda-time:2.14.4'
implementation 'org.joda:joda-convert:3.0.1'
testImplementation 'junit:junit:4.13.2'
testImplementation 'io.mockk:mockk:1.14.11'
testImplementation 'org.robolectric:robolectric:4.17'
testImplementation 'androidx.test:core:1.7.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0'
testImplementation 'androidx.compose.ui:ui-test-junit4'
androidTestImplementation platform('androidx.compose:compose-bom:2026.09.00')
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
androidTestImplementation 'io.mockk:mockk-android:1.14.11'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
}
tasks.register('jacocoTestReport', JacocoReport) {
dependsOn 'testDebugUnitTest'
reports {
xml.required = true
html.required = true
}
def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*']
def debugTree = fileTree(dir: "${project.layout.buildDirectory.asFile.get()}/intermediates/built_in_kotlinc/debug/compileDebugKotlin/classes", excludes: fileFilter)
def mainSrc = "${project.projectDir}/src/main/java"
sourceDirectories.from = files([mainSrc])
classDirectories.from = files([debugTree])
executionData.from = fileTree(dir: project.layout.buildDirectory.asFile.get(), includes: [
'jacoco/testDebugUnitTest.exec', 'outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec'
])
}
+1
View File
@@ -3,5 +3,6 @@
-keepattributes Signature
-keep class org.joda.convert.** { *; }
-dontwarn org.threeten.bp.**
# Optional. For using GSON @Expose annotation
-keepattributes AnnotationDefault,RuntimeVisibleAnnotations
+27 -2
View File
@@ -1,20 +1,45 @@
<?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">
<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<!-- Keepass2Android plugin mode: make the KP2A activities resolvable from this app -->
<queries>
<intent>
<action android:name="keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE" />
</intent>
<intent>
<action android:name="keepass2android.ACTION_START_WITH_TASK" />
</intent>
<package android:name="keepass2android.keepass2android" />
<package android:name="keepass2android.keepass2android_nonet" />
</queries>
<application
android:icon="@mipmap/ic_launcher_round"
android:label="${APP_NAME}"
android:supportsRtl="true">
<activity
android:name=".activity.MainActivity"
android:exported="true">
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".pluginSDK.PluginAccessBroadcastReceiver"
android:exported="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" />
<action android:name="keepass2android.ACTION_RECEIVE_ACCESS" />
<action android:name="keepass2android.ACTION_REVOKE_ACCESS" />
</intent-filter>
</receiver>
</application>
</manifest>
@@ -0,0 +1 @@
CUE6FCSBR6JIAAAAAAAAAAAAAA
@@ -1,11 +1,19 @@
package net.helcel.fidelity.activity
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.FragmentActivity
import androidx.navigation.compose.NavHost
@@ -15,20 +23,32 @@ import net.helcel.fidelity.activity.fragment.CreateEntryScreen
import net.helcel.fidelity.activity.fragment.FileScanner
import net.helcel.fidelity.activity.fragment.InitialScreen
import net.helcel.fidelity.activity.fragment.LauncherScreen
import net.helcel.fidelity.activity.fragment.ModeSelectScreen
import net.helcel.fidelity.activity.fragment.ScannerScreen
import net.helcel.fidelity.activity.fragment.ViewEntryScreen
import net.helcel.fidelity.tools.FidelityRepository.entries
import net.helcel.fidelity.tools.AppMode
import net.helcel.fidelity.tools.AppModeStore
import net.helcel.fidelity.tools.FidelityEntry
import net.helcel.fidelity.tools.FidelityRepository.cacheEntry
import net.helcel.fidelity.tools.FidelityRepository.findEntry
import net.helcel.fidelity.tools.FidelityRepository.loadEntries
import net.helcel.fidelity.tools.FidelityRepository.transientEntry
import net.helcel.fidelity.tools.KeePassStore.hasCredentials
import net.helcel.fidelity.tools.Kp2a
class MainActivity : FragmentActivity() {
// Entry pushed by Keepass2Android when the user opens this app from an entry.
private val incomingEntry = mutableStateOf<FidelityEntry?>(null)
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
actionBar?.hide()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
AppModeStore.load(this)
loadEntries(this.baseContext)
incomingEntry.value = Kp2a.entryFromIntent(intent)
setContent {
SysTheme {
@@ -39,19 +59,38 @@ class MainActivity : FragmentActivity() {
if (!navController.popBackStack()) finish()
}
LaunchedEffect(Unit) {
when (AppModeStore.mode.value) {
null -> navController.navigate("mode")
AppMode.STANDALONE ->
if (!hasCredentials(context)) navController.navigate("init")
AppMode.KP2A -> {}
}
}
LaunchedEffect(incomingEntry.value) {
val entry = incomingEntry.value ?: return@LaunchedEffect
incomingEntry.value = null
if (AppModeStore.isKp2a) cacheEntry(context, entry)
else transientEntry.value = entry
navController.navigate("view/${entry.uid}")
}
// The app draws edge-to-edge (targetSdk >= 35): keep every screen clear of the
// status and navigation bars, and paint the bars with the theme background.
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
.systemBarsPadding()
) {
NavHost(navController = navController, startDestination = "launcher") {
composable("exit") { finish() }
composable("mode") { ModeSelectScreen(navController) }
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") ?: "")
}
val entry = findEntry(e.arguments?.getString("entryId"))
if (entry == null) return@composable navController.navigate("launcher")
ViewEntryScreen(navController, entry)
}
@@ -60,3 +99,9 @@ class MainActivity : FragmentActivity() {
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Kp2a.entryFromIntent(intent)?.let { incomingEntry.value = it }
}
}
@@ -1,5 +1,6 @@
package net.helcel.fidelity.activity.fragment
import android.content.Context
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -13,8 +14,11 @@ 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.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.Checkbox
import androidx.compose.material.CheckboxDefaults
@@ -31,6 +35,7 @@ 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.material.icons.filled.FolderOpen
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -47,21 +52,28 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.google.zxing.FormatException
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.helcel.fidelity.R
import net.helcel.fidelity.activity.ToastHelper
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.ensureUnlocked
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onCameraScan
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onFileScan
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onSaveKp2a
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onSaveStandalone
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.AppModeStore
import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
import net.helcel.fidelity.tools.FidelityEntry
import net.helcel.fidelity.tools.FidelityRepository
import net.helcel.fidelity.tools.FidelityRepository.activeEntry
import net.helcel.fidelity.tools.FidelityRepository.addEntry
import net.helcel.fidelity.tools.KeepassDatabase
import net.helcel.fidelity.tools.KeepassDatabase.addEntry
import net.helcel.fidelity.tools.FidelityRepository.cacheEntry
import net.helcel.fidelity.tools.Kp2a
import kotlin.time.Duration.Companion.milliseconds
@Preview
@@ -78,10 +90,26 @@ fun CreateEntryScreen(navController: NavHostController?) {
var isLoading by remember { mutableStateOf(false) }
val ctx = LocalContext.current
val scope = rememberCoroutineScope()
val kp2a = AppModeStore.isKp2a
// Standalone only: where a new card is created (root unless changed). Existing cards
// (uid set) stay where they are.
var group by remember { mutableStateOf(KeepassDatabase.getRoot()) }
val showGroup = !kp2a && entry.uid == null
fun pickGroup() {
isLoading = true
scope.launch {
if (ensureUnlocked(ctx, navController!!)) {
group = group ?: KeepassDatabase.getRoot()
showDialog = true
}
isLoading = false
}
}
LaunchedEffect(entry) {
isValidBarcode = false
delay(500)
delay(500.milliseconds)
if (entry.code.isEmpty()) return@LaunchedEffect
try {
val bmp = generateBarcode(entry.code, entry.format, 600)
@@ -102,34 +130,29 @@ fun CreateEntryScreen(navController: NavHostController?) {
}
if (showDialog) {
TreeSelectorDialog(
onDismiss = {
TreeSelectorDialog(groupsOnly = true) {
showDialog = false
if(it!=null){
entry = entry.copy(uid = it.nodeId?.id.toString())
if(it is Entry){
entry = entry.copy(title = it.title)
if (it is Group) group = it
}
}
}
)
}
val formats = stringArrayResource(R.array.format_array)
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
.imePadding(),
) {
Column(
modifier = Modifier
.padding(16.dp, 32.dp),
.verticalScroll(rememberScrollState())
.padding(16.dp, 32.dp)
.padding(bottom = 120.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
)
{
OutlinedTextField(
value = entry.title,
enabled = entry.uid!=null,
onValueChange = {
entry = entry.copy(title = it)
errorTitle = ""
@@ -139,8 +162,7 @@ fun CreateEntryScreen(navController: NavHostController?) {
modifier = Modifier.fillMaxWidth(),
singleLine = true,
colors = TextFieldDefaults.textFieldColors(
textColor = if(entry.uid!=null)MaterialTheme.colors.onBackground
else MaterialTheme.colors.secondary
textColor = MaterialTheme.colors.onBackground
),
)
if (errorTitle.isNotEmpty()) {
@@ -158,8 +180,9 @@ fun CreateEntryScreen(navController: NavHostController?) {
),
label = { Text("Code") },
isError = errorCode.isNotEmpty(),
modifier = Modifier.fillMaxWidth(),
singleLine = true
maxLines = 5,
singleLine = false,
modifier = Modifier.fillMaxWidth()
)
if (errorCode.isNotEmpty()) {
Text(errorCode, color = MaterialTheme.colors.error)
@@ -199,6 +222,23 @@ fun CreateEntryScreen(navController: NavHostController?) {
Icon(Icons.Default.FileOpen, contentDescription = null)
}
}
if (showGroup) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text("Group: ", color = MaterialTheme.colors.onBackground)
Text(
group?.title ?: "(database root)",
color = MaterialTheme.colors.onBackground,
modifier = Modifier.weight(1f),
maxLines = 1,
)
Button(onClick = { pickGroup() }) {
Icon(Icons.Default.FolderOpen, contentDescription = "Choose group")
}
}
}
if (barcodeBitmap != null) {
Image(
bitmap = barcodeBitmap!!.asImageBitmap(),
@@ -227,41 +267,22 @@ fun CreateEntryScreen(navController: NavHostController?) {
errorCode = c
errorFormat = f
},
isValidBarcode
isValidBarcode,
) {
if (FidelityRepository.getRoot() == null) {
if (kp2a) {
onSaveKp2a(ctx, navController!!, entry)
} else {
isLoading = true
scope.launch {
onRefresh(ctx, navController!!)
onSaveStandalone(ctx, navController!!, entry, group)
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()),
enabled = isValidBarcode && entry.title.isNotEmpty(),
) {
Text(if(entry.uid==null)"Select Entry" else "Save", style = MaterialTheme.typography.h6)
Text("Save", style = MaterialTheme.typography.h6)
}
}
@@ -344,7 +365,7 @@ private fun onSubmitIfValid(
var tErr = ""
var cErr = ""
var fErr = ""
if (entry.uid!=null && entry.title.isBlank()) tErr = "Title cannot be empty"
if (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"
@@ -356,9 +377,47 @@ private fun onSubmitIfValid(
}
object CreateEntryEventHandler {
/** Standalone: the database must be open before groups can be browsed or cards written. */
suspend fun ensureUnlocked(context: Context, navController: NavHostController): Boolean =
KeepassDatabase.getRoot() != null || onRefresh(context, navController)
/** Standalone: updates the existing entry, or creates one in [group] (the root when null). */
suspend fun onSaveStandalone(
context: Context,
navController: NavHostController,
entry: FidelityEntry,
group: Group?,
) {
if (!ensureUnlocked(context, navController)) return
val root = KeepassDatabase.getRoot() ?: return
val target =
if (entry.uid != null) entry
else entry.copy(uid = (group ?: root).nodeId.id.toString())
addEntry(context, target)
if (onSave(context, navController)) onSubmit(navController)
}
/**
* KP2A creates the entry in its own task and never reports back, so the card is cached
* right away; a later fetch replaces the local uid with KP2A's.
*/
fun onSaveKp2a(context: Context, navController: NavHostController, entry: FidelityEntry) {
val kpEntry = entry.copy(uid = Kp2a.localUid(entry.title))
if (Kp2a.launchAdd(context, kpEntry)) {
cacheEntry(context, kpEntry)
onSubmit(navController)
}
}
fun onSubmit(navController: NavHostController){
navController.popBackStack()
activeEntry.value = activeEntry.value.copy(null,"","","",false)
activeEntry.value = activeEntry.value.copy(
uid = null,
title = "",
code = "",
format = "",
protected = false
)
}
fun onFileScan(navController: NavHostController){
@@ -0,0 +1,152 @@
package net.helcel.fidelity.activity.fragment
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
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.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.HideSource
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
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.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
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.onRemove
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onView
import net.helcel.fidelity.tools.AppModeStore
import net.helcel.fidelity.tools.FidelityEntry
/** One card in the launcher grid: tap to view, long-press for the actions menu. */
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun FidelityRow(
navController: NavHostController,
e: FidelityEntry
) {
var expanded by remember { mutableStateOf(false) }
val context = LocalContext.current
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 }
) {
// KP2A entries can only be edited in KP2A itself; offer to drop the cached copy.
if (AppModeStore.isKp2a)
DropdownMenuItem(onClick = {
expanded = false
onRemove(context, e)
}) {
Icon(
Icons.Default.Delete,
contentDescription = "remove",
)
Spacer(modifier= Modifier.width(8.dp))
Text("Remove")
}
else
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")
}
}
}
}
@@ -1,12 +1,16 @@
package net.helcel.fidelity.activity.fragment
import android.app.Activity
import android.content.Context
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -17,25 +21,24 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TextFieldDefaults
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.Close
import androidx.compose.material.icons.filled.HideSource
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material.icons.filled.SwapHoriz
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -44,29 +47,29 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
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.ToastHelper
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.isSearchVisible
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.activity.fragment.LauncherEventHandlers.searchQuery
import net.helcel.fidelity.tools.AppModeStore
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.cacheEntry
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
import net.helcel.fidelity.tools.KeepassDatabase
import net.helcel.fidelity.tools.FidelityRepository.loadEntries
import net.helcel.fidelity.tools.FidelityRepository.removeEntry
import net.helcel.fidelity.tools.Kp2a
@Preview
@OptIn(ExperimentalMaterial3Api::class)
@@ -79,9 +82,32 @@ fun LauncherScreen(
var showHidden by remember { mutableStateOf(false) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
val sortedEntries = remember(entries) {
val focusRequester = remember { FocusRequester() }
val kp2a = AppModeStore.isKp2a
val kp2aQueryLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
val entry = Kp2a.entryFromIntent(result.data)
if (entry == null) {
ToastHelper.show(context, "Entry has no fidelity code")
return@rememberLauncherForActivityResult
}
cacheEntry(context, entry)
onView(navController, entry)
}
BackHandler(enabled = isSearchVisible) {
onQuery()
}
val sortedEntries = remember(entries, showHidden, searchQuery) {
derivedStateOf {
entries.filter{showHidden || !it.hidden}.sortedWith(
entries.filter {
(showHidden || !it.hidden) &&
(searchQuery.isEmpty() || it.title.contains(searchQuery, ignoreCase = true))
}.sortedWith(
compareByDescending<FidelityEntry> { it.pinned }
.thenBy { it.hidden }
.thenByDescending { it.lastUse }
@@ -98,13 +124,83 @@ fun LauncherScreen(
onRefresh = {
isRefreshingState = true
scope.launch {
onRefresh(context, navController)
// KP2A owns the database: nothing to sync, just reload the cache.
if (kp2a) loadEntries(context)
else onRefresh(context, navController)
isRefreshingState = false
}
},
isRefreshing = isRefreshingState,
modifier = Modifier.fillMaxSize()
) {
Column(modifier = Modifier.fillMaxSize()) {
// Top bar: hidden-cards toggle on the left, storage mode on the right.
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
val hiddenTint =
if (showHidden) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary
TextButton(onClick = { showHidden = !showHidden }) {
Icon(
Icons.Default.HideSource,
contentDescription = "Show Hidden",
modifier = Modifier.size(16.dp),
tint = hiddenTint
)
Spacer(modifier = Modifier.width(4.dp))
Text(
if (showHidden) "Showing hidden" else "Show hidden",
style = MaterialTheme.typography.caption,
color = hiddenTint
)
}
TextButton(onClick = { navController.navigate("mode") }) {
Icon(
Icons.Default.SwapHoriz,
contentDescription = "Switch mode",
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colors.secondary
)
Spacer(modifier = Modifier.width(4.dp))
Text(
if (kp2a) "Keepass2Android" else "Standalone",
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.secondary
)
}
}
if (isSearchVisible) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
colors = TextFieldDefaults.textFieldColors(
textColor = MaterialTheme.colors.onBackground
),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.focusRequester(focusRequester),
label = { Text("Search") },
singleLine = true,
trailingIcon = {
Icon(
Icons.Default.Close,
contentDescription = "Clear",
modifier = Modifier.clickable {
searchQuery = ""
onQuery()
}
)
}
)
}
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier
@@ -118,6 +214,7 @@ fun LauncherScreen(
FidelityRow(navController, entry)
}
}
}
FloatingActionButton(
onClick = { onQuery() },
modifier = Modifier
@@ -137,17 +234,19 @@ fun LauncherScreen(
) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
if (kp2a)
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,
onClick = { Kp2a.launchQuery(context, kp2aQueryLauncher) },
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 16.dp, bottom = 88.dp),
backgroundColor = MaterialTheme.colors.secondary,
) {
Icon(Icons.Default.HideSource,
tint= if(showHidden) MaterialTheme.colors.background else MaterialTheme.colors.onSecondary,
contentDescription = "Show Hidden")
Icon(
Icons.Default.Key,
tint = MaterialTheme.colors.onSecondary,
contentDescription = "Fetch from Keepass2Android"
)
}
}
@@ -166,156 +265,35 @@ fun LauncherScreen(
}
@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 {
var isSearchVisible by mutableStateOf(false)
var searchQuery by mutableStateOf("")
fun onAdd(navController: NavHostController) {
navController.navigate("edit")
}
fun onQuery() {
//TODO
isSearchVisible = !isSearchVisible
if (!isSearchVisible) searchQuery = ""
}
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())
/** Standalone: writes the database back. Missing credentials send the user to setup. */
suspend fun onSave(context: Context, navController: NavHostController): Boolean {
if (!KeepassDatabase.ensureCredentials(context)) {
navController.navigate("init")
return false
}
return KeepassDatabase.save(context)
}
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())
/** Standalone: (re)opens the database and imports its cards. Missing credentials send the user to setup. */
suspend fun onRefresh(context: Context, navController: NavHostController): Boolean {
if (!KeepassDatabase.ensureCredentials(context)) {
navController.navigate("init")
return false
}
return KeepassDatabase.unlock(context)
}
fun onView(navController: NavHostController, entry: FidelityEntry) {
@@ -338,6 +316,10 @@ object LauncherEventHandlers {
entries[index] = entry.copy(hidden = !entry.hidden)
}
fun onRemove(context: Context, entry: FidelityEntry) {
removeEntry(context, entry)
}
fun onEdit(navController: NavHostController, entry: FidelityEntry){
activeEntry.value = entry
navController.navigate("edit")
@@ -0,0 +1,154 @@
package net.helcel.fidelity.activity.fragment
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.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.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Extension
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
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.launch
import net.helcel.fidelity.tools.AppMode
import net.helcel.fidelity.tools.AppModeStore
import net.helcel.fidelity.tools.FidelityRepository.loadEntries
import net.helcel.fidelity.tools.KeePassStore.hasCredentials
import net.helcel.fidelity.tools.Kp2a
@Preview
@Composable
fun ModeSelectScreen(navController: NavHostController?) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val current = AppModeStore.mode.value
val kp2aInstalled = remember { Kp2a.isAvailable(context) }
BackHandler {
// Nothing to go back to before the first choice has been made.
if (current == null) navController!!.navigate("exit")
else navController!!.popBackStack()
}
fun select(mode: AppMode) {
if (mode != current) {
AppModeStore.set(context, mode)
loadEntries(context)
}
navController!!.popBackStack("launcher", inclusive = false)
scope.launch {
if (mode == AppMode.STANDALONE && !hasCredentials(context))
navController.navigate("init")
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center
) {
Text(
"Storage Mode",
style = MaterialTheme.typography.h5,
color = MaterialTheme.colors.onBackground
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"Where should your fidelity cards be stored?",
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackground
)
Spacer(modifier = Modifier.height(16.dp))
ModeCard(
icon = Icons.Default.FolderOpen,
title = "Standalone",
description = "Open a KeePass database (.kdbx) directly. " +
"Entries are read and written by this app.",
selected = current == AppMode.STANDALONE,
) { select(AppMode.STANDALONE) }
Spacer(modifier = Modifier.height(12.dp))
ModeCard(
icon = Icons.Default.Extension,
title = "Keepass2Android plugin",
description = "Use the Keepass2Android app as the database. " +
"Entries are queried from and created in Keepass2Android.",
selected = current == AppMode.KP2A,
) { select(AppMode.KP2A) }
if (!kp2aInstalled) {
Spacer(modifier = Modifier.height(8.dp))
Text(
"Keepass2Android does not seem to be installed.",
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.error
)
}
}
}
}
@Composable
private fun ModeCard(
icon: ImageVector,
title: String,
description: String,
selected: Boolean,
onClick: () -> Unit,
) {
val container = if (selected) MaterialTheme.colors.primary else MaterialTheme.colors.surface
val content = if (selected) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onSurface
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(
containerColor = container,
contentColor = content
),
border = if (selected) null else CardDefaults.outlinedCardBorder(),
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(icon, contentDescription = null, modifier = Modifier.size(32.dp), tint = content)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(title, style = MaterialTheme.typography.h6, color = content)
Spacer(modifier = Modifier.height(4.dp))
Text(description, style = MaterialTheme.typography.body2, color = content)
}
}
}
}
@@ -19,7 +19,9 @@ 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.CreditCard
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Folder
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -32,11 +34,15 @@ 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
import net.helcel.fidelity.tools.KeepassDatabase
/**
* Browses the loaded database. With [groupsOnly] entries are hidden, so the result is the group
* a new card is created in; otherwise an existing entry can be picked to attach the card to.
*/
@Preview
@Composable
fun TreeSelectorDialog(onDismiss: (Node?) -> Unit = {}) {
fun TreeSelectorDialog(groupsOnly: Boolean = false, onDismiss: (Node?) -> Unit = {}) {
Dialog(
onDismissRequest = {onDismiss(null)},
content = {
@@ -46,8 +52,8 @@ fun TreeSelectorDialog(onDismiss: (Node?) -> Unit = {}) {
RoundedCornerShape(8.dp)
)
) {
var currentRoot by remember { mutableStateOf(FidelityRepository.getRoot()) }
var selection by remember { mutableStateOf<Node?>(FidelityRepository.getRoot()) }
var currentRoot by remember { mutableStateOf(KeepassDatabase.getRoot()) }
var selection by remember { mutableStateOf<Node?>(KeepassDatabase.getRoot()) }
Column(
modifier = Modifier.fillMaxWidth().padding(8.dp)
@@ -76,32 +82,29 @@ fun TreeSelectorDialog(onDismiss: (Node?) -> Unit = {}) {
LazyColumn(modifier = Modifier.fillMaxHeight(0.75f)) {
items(currentRoot?.getChildGroups() ?: emptyList()) { entry ->
val isSel = (entry.nodeId == selection?.nodeId)
val hasChildren = entry.getChildGroups().isNotEmpty() ||
(!groupsOnly && entry.getChildEntries().isNotEmpty())
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
}
if (hasChildren) currentRoot = entry
}
.padding(8.dp)
) {
if (entry.getChildEntries().isNotEmpty() || entry.getChildGroups()
.isNotEmpty()
) {
Icon(
imageVector = Icons.Default.Folder,
contentDescription = "group",
tint = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground
)
if (hasChildren)
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),
@@ -109,7 +112,7 @@ fun TreeSelectorDialog(onDismiss: (Node?) -> Unit = {}) {
)
}
}
items(currentRoot?.getChildEntries() ?: emptyList()) { entry ->
items(if (groupsOnly) emptyList() else currentRoot?.getChildEntries() ?: emptyList()) { entry ->
val isSel = (entry.nodeId == selection?.nodeId)
Row(
modifier = Modifier
@@ -120,6 +123,11 @@ fun TreeSelectorDialog(onDismiss: (Node?) -> Unit = {}) {
}
.padding(8.dp)
) {
Icon(
imageVector = Icons.Default.CreditCard,
contentDescription = "entry",
tint = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground
)
Text(
entry.title,
modifier = Modifier.padding(start = 8.dp),
@@ -28,6 +28,7 @@ import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -52,8 +53,10 @@ 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.KeepassDatabase
import net.helcel.fidelity.tools.KeepassDatabase.genCredentials
import net.helcel.fidelity.tools.KeepassDatabase.importDB
import net.helcel.fidelity.tools.KeepassDatabase.start
import net.helcel.fidelity.tools.KeePassStore.loadCredentials
import net.helcel.fidelity.tools.KeePassStore.packCredentials
import net.helcel.fidelity.tools.KeePassStore.saveCredentials
@@ -116,8 +119,8 @@ fun InitialScreen(
LaunchedEffect(Unit) {
scope.launch(Dispatchers.Main) {
when(val res = loadCredentials(context)) {
CredentialResult.AuthFailed -> null
CredentialResult.NoData -> null
CredentialResult.AuthFailed -> ToastHelper.show(context, "Unable to Load Credentials")
CredentialResult.NoData -> ToastHelper.show(context, "Unable to Load Credentials")
is CredentialResult.Success -> {
if (res.db != null) dbFile = res.db
if (res.key != null) keyFile = res.key
@@ -219,13 +222,14 @@ fun InitialScreen(
onClick = {
loading = true
scope.launch {
if(onOpen(context, dbFile!!, password, keyFile)){
val res = onOpen(context, dbFile!!, password, keyFile)
if(res != null){
ToastHelper.show(context, "Successful... Importing")
importDB(context)
navController!!.popBackStack()
navController.navigate("init")
navController.navigate("launcher")
}else{
ToastHelper.show(context, "Auth failed...")
navController!!.popBackStack()
navController.navigate("exit")
ToastHelper.show(context, "Failed... Retry")
}
}
},
@@ -233,6 +237,13 @@ fun InitialScreen(
) {
Text("Continue")
}
TextButton(
enabled = !loading,
onClick = { navController!!.navigate("mode") },
modifier = Modifier.fillMaxWidth()
) {
Text("Use Keepass2Android instead", color = MaterialTheme.colors.secondary)
}
}
Box(contentAlignment = Alignment.BottomCenter, modifier = Modifier
.fillMaxSize()
@@ -249,26 +260,33 @@ fun InitialScreen(
}
object SetupEventHandlers {
suspend fun onOpen(context: Context, db: Uri, p: String, key: Uri?): Boolean {
/** Opens the file with the given credentials, then stores them for later sessions. */
suspend fun onOpen(context: Context, db: Uri, p: String, key: Uri?): CredentialResult.Success? {
try {
val packCred = packCredentials(db, p, key)
withContext(Dispatchers.IO) {
start(context, db, genCredentials(context, packCred)
)
val opened = withContext(Dispatchers.IO) {
start(context, db, genCredentials(context, packCred))
}
if (!opened) {
ToastHelper.show(context, "Unable to open the database: check the password and key file")
return null
}
val res = withContext(Dispatchers.Main) {
saveCredentials(context, packCred)
}
return when (res) {
CredentialResult.AuthFailed, CredentialResult.NoData -> false
is CredentialResult.Success -> true
CredentialResult.AuthFailed, CredentialResult.NoData -> null
is CredentialResult.Success -> {
KeepassDatabase.credentials = res
res
}
}
} catch (e: Exception) {
ToastHelper.show(context, e.message.toString())
println("Err${e.toString()}")
println(e.message)
return false
return null
}
}
}
@@ -20,6 +20,7 @@ import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -62,11 +63,19 @@ fun ViewEntryScreen(
activity?.window?.attributes = activity.window?.attributes?.apply {
screenBrightness = if (isFull) 1f else BRIGHTNESS_OVERRIDE_NONE
}
}
// Generate once per entry (not on every recomposition) and surface zxing's reason,
// e.g. "Contents do not pass checksum" for a mistyped EAN.
LaunchedEffect(entry) {
try {
bitmap = generateBarcode(entry.code, entry.format, 1024)
} catch (_: Exception) {
} catch (e: Exception) {
bitmap = null
Toast.makeText(context, "Invalid barcode format", Toast.LENGTH_SHORT).show()
Toast.makeText(
context,
"Invalid barcode: " + (e.message ?: "unsupported format"),
Toast.LENGTH_LONG
).show()
}
}
BackHandler {
@@ -0,0 +1,89 @@
package net.helcel.fidelity.pluginSDK
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
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 const val PREF_HOSTS = "KP2A.PluginAccess.hosts"
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?>
) {
getPrefsForHost(ctx, hostPackage).edit {
putString(PREF_KEY_TOKEN, accessToken)
putString(PREF_KEY_SCOPE, stringArrayToString(scopes))
}
val hostPrefs = ctx.getSharedPreferences(PREF_HOSTS, Context.MODE_PRIVATE)
if (!hostPrefs.contains(hostPackage))
hostPrefs.edit { putString(hostPackage, "") }
}
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)
prefs.edit { clear() }
val hostPrefs = ctx.getSharedPreferences(PREF_HOSTS, Context.MODE_PRIVATE)
if (hostPrefs.contains(hostPackage))
hostPrefs.edit { remove(hostPackage) }
}
}
@@ -0,0 +1,9 @@
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"
}
@@ -0,0 +1,49 @@
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", "true")
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
}
}
@@ -0,0 +1,51 @@
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,
)
)
}
@@ -0,0 +1,32 @@
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 SCOPE_QUERY_CREDENTIALS = "keepass2android.SCOPE_QUERY_CREDENTIALS"
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 EXTRA_ENTRY_ID = "keepass2android.EXTRA_ENTRY_ID"
const val ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
"keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
}
@@ -0,0 +1,39 @@
package net.helcel.fidelity.tools
import android.content.Context
import androidx.compose.runtime.mutableStateOf
import androidx.core.content.edit
const val FIDELITY_PREFS = "fidelity_prefs"
enum class AppMode {
/** Opens a KDBX file directly through the bundled KeePassDX engine. */
STANDALONE,
/** Delegates storage to the Keepass2Android app through its plugin interface. */
KP2A,
}
object AppModeStore {
private const val KEY_MODE = "app_mode"
/** Null until the user has picked a mode on the selection screen. */
val mode = mutableStateOf<AppMode?>(null)
val isKp2a: Boolean
get() = mode.value == AppMode.KP2A
fun load(context: Context): AppMode? {
val prefs = context.getSharedPreferences(FIDELITY_PREFS, Context.MODE_PRIVATE)
mode.value = prefs.getString(KEY_MODE, null)?.let { name ->
AppMode.entries.firstOrNull { it.name == name }
}
return mode.value
}
fun set(context: Context, newMode: AppMode) {
context.getSharedPreferences(FIDELITY_PREFS, Context.MODE_PRIVATE)
.edit { putString(KEY_MODE, newMode.name) }
mode.value = newMode
}
}
@@ -13,7 +13,6 @@ import com.google.zxing.common.HybridBinarizer
import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString
import java.util.concurrent.Executors
@OptIn(ExperimentalGetImage::class)
object BarcodeScanner {
@@ -56,6 +55,4 @@ object BarcodeScanner {
fun bitmapUseCase(bitmap: Bitmap, cb: (String?, String?) -> Unit) {
processImage(bitmap, cb)
}
}
@@ -2,9 +2,12 @@ package net.helcel.fidelity.tools
import android.content.Context
import android.net.Uri
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import android.widget.Toast
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import javax.crypto.Cipher
@@ -16,6 +19,7 @@ import com.kunzisoft.keepass.utils.parseUri
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.suspendCancellableCoroutine
import net.helcel.fidelity.activity.ToastHelper
import java.security.KeyStore
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
@@ -36,6 +40,17 @@ sealed class CredentialResult {
private const val KEY_ALIAS = "keepass_bio_key"
// Android 11+ can unlock a keystore key with the device PIN/pattern/password as well, which
// keeps standalone mode usable on devices without (enrolled) biometrics. Below that, a
// CryptoObject can only be released by a biometric.
private val deviceCredentialSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
private val allowedAuthenticators: Int
get() = if (deviceCredentialSupported)
BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
else
BiometricManager.Authenticators.BIOMETRIC_STRONG
fun getOrCreateBiometricKey(): SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
keyStore.getKey(KEY_ALIAS, null)?.let { return it as SecretKey }
@@ -47,6 +62,12 @@ fun getOrCreateBiometricKey(): SecretKey {
).apply {
setBlockModes(KeyProperties.BLOCK_MODE_GCM)
setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
if (deviceCredentialSupported)
setUserAuthenticationParameters(
0,
KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
)
else
setUserAuthenticationRequired(true)
setInvalidatedByBiometricEnrollment(true)
}.build()
@@ -108,23 +129,44 @@ object KeePassStore {
suspend fun showBiometricPrompt(activity: FragmentActivity, enc: Boolean): Cipher? {
val prefs = activity.securePrefs.data.first()
return suspendCancellableCoroutine { cont ->
val status = BiometricManager.from(activity).canAuthenticate(allowedAuthenticators)
if (status != BiometricManager.BIOMETRIC_SUCCESS) {
ToastHelper.show(
activity,
if (deviceCredentialSupported) "Set up a screen lock or fingerprint first"
else "Enroll a fingerprint first",
Toast.LENGTH_LONG
)
cont.resume(null) { _, _, _ -> }
return@suspendCancellableCoroutine
}
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) {} }
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
cont.resume(result.cryptoObject?.cipher) { _, _, _ -> }
}
override fun onAuthenticationError(code: Int, msg: CharSequence) {
cont.resume(null) { _, _, _ -> }
}
// onAuthenticationFailed is a retryable miss: the prompt stays open, so the
// coroutine must stay suspended until it either succeeds or errors out.
}
)
val iv = if(enc) null else prefs[KeePassKeys.IV]?.let { Base64.decode(it, Base64.DEFAULT) }
if (!enc && iv == null) { cont.resume(null) {} }
if (!enc && iv == null) {
cont.resume(null) { _, _, _ -> }
return@suspendCancellableCoroutine
}
val cipher = getCipherForDecryption(getOrCreateBiometricKey(), iv)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock KeePass")
.setSubtitle("Authenticate to access your KeePass database")
.setNegativeButtonText("Cancel")
.setAllowedAuthenticators(allowedAuthenticators)
// A negative button is mandatory without, and forbidden with, device credential.
.apply { if (!deviceCredentialSupported) setNegativeButtonText("Cancel") }
.build()
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
@@ -0,0 +1,29 @@
package net.helcel.fidelity.tools
import kotlinx.serialization.Serializable
/** Names of the custom KeePass fields a card is stored in. */
object FidelityKeepassFields {
const val FIDELITYFORMAT = "FidelityFormat"
const val FIDELITYCODE = "FidelityCode"
}
/**
* A loyalty card as shown by the app.
*
* [uid] is the KeePass entry UUID in standalone mode, or the id Keepass2Android reports
* (falling back to a title-derived one) in plugin mode. The last three fields are
* per-device preferences that never reach the database.
*/
@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,
)
@@ -0,0 +1,82 @@
package net.helcel.fidelity.tools
import android.content.Context
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.core.content.edit
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
/**
* The on-device list of cards shown by the launcher, persisted in shared preferences.
*
* In standalone mode it mirrors the cards found in the KDBX file (see
* [KeepassDatabase.importDB]); in Keepass2Android mode it is a cache of the entries the
* user has fetched so far.
*/
object FidelityRepository {
val entries = mutableStateListOf<FidelityEntry>()
/** Card being edited on the create screen. */
val activeEntry = mutableStateOf(FidelityEntry())
/** Entry handed over by Keepass2Android for viewing without being cached. */
val transientEntry = mutableStateOf<FidelityEntry?>(null)
// Each mode keeps its own cache: standalone uids are KDBX UUIDs, KP2A ones are not.
private fun entriesKey() = if (AppModeStore.isKp2a) "entries_kp2a" else "entries"
fun saveEntries(context: Context) {
val prefs = context.getSharedPreferences(FIDELITY_PREFS, Context.MODE_PRIVATE)
// In KP2A mode "protected" means the code must never leave the KP2A database.
val persisted = if (AppModeStore.isKp2a) entries.filter { !it.protected } else entries
prefs.edit { putString(entriesKey(), Json.encodeToString(
ListSerializer(FidelityEntry.serializer()),
persisted
)) }
}
fun loadEntries(context: Context) {
val prefs = context.getSharedPreferences(FIDELITY_PREFS, Context.MODE_PRIVATE)
entries.clear()
try {
val json = prefs.getString(entriesKey(), null) ?: return
val list = Json.decodeFromString(
ListSerializer(FidelityEntry.serializer()),
json
)
entries.addAll(list)
}catch(_: Exception){
prefs.edit{ putString(entriesKey(),Json.encodeToString(
ListSerializer(FidelityEntry.serializer()),emptyList()))
}
}
}
/**
* Upserts an entry coming from Keepass2Android into the local cache, keeping the
* per-device flags (pin/hide/last use) of the entry it replaces. Matching also
* falls back to title+code because an optimistically cached entry gets a local uid
* until KP2A returns it with its own.
*/
fun cacheEntry(context: Context, entry: FidelityEntry) {
val idx = entries.indexOfFirst {
it.uid == entry.uid || (it.title == entry.title && it.code == entry.code)
}
if (idx >= 0) {
val old = entries[idx]
entries[idx] = entry.copy(pinned = old.pinned, hidden = old.hidden, lastUse = old.lastUse)
} else {
entries.add(entry)
}
saveEntries(context)
}
fun removeEntry(context: Context, entry: FidelityEntry) {
entries.removeIf { it.uid == entry.uid }
saveEntries(context)
}
fun findEntry(uid: String?): FidelityEntry? =
entries.find { it.uid == uid } ?: transientEntry.value?.takeIf { it.uid == uid }
}
@@ -2,12 +2,6 @@ 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
@@ -17,39 +11,75 @@ 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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.helcel.fidelity.activity.ToastHelper
import net.helcel.fidelity.tools.FidelityRepository.entries
import net.helcel.fidelity.tools.FidelityRepository.saveEntries
import net.helcel.fidelity.tools.KeePassStore.loadCredentials
import java.io.ByteArrayInputStream
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 {
/**
* Standalone mode: the KDBX file opened through the bundled KeePassDX engine.
*
* [unlock] and [save] are the session-level operations the screens use; the rest is the
* raw file handling underneath them.
*/
object KeepassDatabase {
private var db: Database = Database()
private var binaryDir: File? = null
val entries = mutableStateListOf<FidelityEntry>()
val activeEntry = mutableStateOf(FidelityEntry())
/** Credentials released by the user for this process, so authentication happens once. */
var credentials: CredentialResult.Success? = null
fun getRoot(): Group? {
return db.rootGroup
}
/** Makes sure [credentials] are available, asking the user to authenticate if needed. */
suspend fun ensureCredentials(context: Context): Boolean {
if (credentials != null) return true
return when (val res = loadCredentials(context)) {
is CredentialResult.Success -> {
credentials = res
true
}
CredentialResult.AuthFailed, CredentialResult.NoData -> {
ToastHelper.show(context, "Unable to Load Credentials")
false
}
}
}
/** Opens the file with [credentials] and imports its cards into [FidelityRepository]. */
suspend fun unlock(context: Context): Boolean {
val cred = credentials ?: return false
val opened = withContext(Dispatchers.IO) {
start(context, cred.db, genCredentials(context, cred))
}
if (!opened) {
ToastHelper.show(context, "Unable to open the database")
return false
}
importDB(context)
return true
}
/** Writes the database back to its file. */
suspend fun save(context: Context): Boolean {
val cred = credentials ?: return false
val saved = try {
withContext(Dispatchers.IO) { end(context, cred.db, genCredentials(context, cred)) }
} catch (e: Exception) {
println(e)
false
}
if (!saved) ToastHelper.show(context, "Unable to save the database")
return saved
}
fun start(ctx: Context, uri: Uri?, c: MasterCredential): Boolean {
if (binaryDir == null) binaryDir = ctx.getBinaryDir()
if (uri == null) return false
@@ -59,7 +89,7 @@ object FidelityRepository {
db.loadData(
bitStream, c,
{ hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) },
false, binaryDir!!,
readOnly=false, allowUserVerification = false,binaryDir!!,
{ BinaryData.canMemoryBeAllocatedInRAM(ctx, it) },
false, null
)
@@ -84,12 +114,13 @@ object FidelityRepository {
hardwareKey: HardwareKey? = null
): MasterCredential {
return MasterCredential(
cred.password,
cred.password.toCharArray(),
cred.key?.let { ctx.contentResolver.openInputStream(cred.key)?.readBytes() },
hardwareKey
)
}
/** Replaces the card list with the cards found in the database, keeping per-device flags. */
fun importDB(context: Context) {
val seenID= arrayListOf<String>()
fun importDBRec(group: Group) {
@@ -103,8 +134,8 @@ object FidelityRepository {
val newEntry = FidelityEntry(
uid=it.nodeId.id.toString(),
title=it.title,
code=code.protectedValue.stringValue,
format=format.protectedValue.stringValue,
code=code.protectedValue.toString(),
format=format.protectedValue.toString(),
protected=code.protectedValue.isProtected,
)
val idx = entries.indexOfFirst { e -> e.uid == newEntry.uid }
@@ -119,8 +150,6 @@ object FidelityRepository {
} else {
entries.add(newEntry)
}
}
group.getChildGroups().forEach { importDBRec(it) }
}
@@ -133,36 +162,19 @@ object FidelityRepository {
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()))
}
}
}
/**
* Writes the card into the loaded database. [FidelityEntry.uid] is either the id of an
* existing entry, which is updated in place, or the id of the group to create it in.
*/
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 {
val id = NodeIdUUID(UUID.fromString(entry.uid))
val existing = db.getEntryById(id)
val dbEntry = existing ?: db.createEntry() ?: return
dbEntry.apply {
title = entry.title
// Keepass2Android lists entries for this app by URL, so cards written here stay
// reachable from KP2A mode as well.
if (url.isBlank()) url = Kp2a.appUrl(ctx)
putExtraField(
Field(
FidelityKeepassFields.FIDELITYCODE,
@@ -172,14 +184,14 @@ object FidelityRepository {
putExtraField(
Field(
FidelityKeepassFields.FIDELITYFORMAT,
ProtectedString(string= entry.format)
ProtectedString(true, entry.format.toCharArray())
)
)
if(dbParent!=null) title = entry.title
dbParent?.addChildEntry(dbEntry)
}
if (existing != null) db.updateEntry(dbEntry)
else db.addEntryTo(dbEntry, db.getGroupById(id) ?: db.rootGroup ?: return)
entries.removeIf {it.uid == entry.uid}
entries.add(entry.copy(uid=dbEntry?.nodeId?.id.toString()))
entries.add(entry.copy(uid=dbEntry.nodeId.id.toString()))
saveEntries(ctx)
}
}
@@ -0,0 +1,109 @@
package net.helcel.fidelity.tools
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
import net.helcel.fidelity.activity.ToastHelper
import net.helcel.fidelity.pluginSDK.KeepassDef
import net.helcel.fidelity.pluginSDK.Kp2aControl
import net.helcel.fidelity.pluginSDK.Strings
import org.json.JSONArray
import org.json.JSONException
import java.util.UUID
/**
* Bridge between [FidelityEntry] and the Keepass2Android plugin interface.
*
* Entries live in the KP2A database; this app only keeps a local cache of the
* non-protected ones (see [FidelityRepository.cacheEntry]).
*/
object Kp2a {
private const val CODE_FIELD = FidelityKeepassFields.FIDELITYCODE
private const val FORMAT_FIELD = FidelityKeepassFields.FIDELITYFORMAT
// Legacy flag written by pre-1.3 versions: "true" means "do not cache locally".
private const val PROTECT_CODE_FIELD = "FidelityProtectedCode"
private const val NOT_INSTALLED = "Keepass2Android Not Installed"
/** URL that marks a database entry as belonging to this app. */
fun appUrl(context: Context): String = "androidapp://" + context.packageName
fun isAvailable(context: Context): Boolean =
Kp2aControl.getQueryEntryForOwnPackageIntent()
.resolveActivity(context.packageManager) != null
/** Asks KP2A to pick one of the entries whose URL points at this app. */
fun launchQuery(context: Context, launcher: ActivityResultLauncher<Intent>): Boolean {
return try {
launcher.launch(Kp2aControl.getQueryEntryForOwnPackageIntent())
true
} catch (_: ActivityNotFoundException) {
ToastHelper.show(context, NOT_INSTALLED)
false
}
}
/**
* Opens KP2A on a pre-filled "new entry" form. KP2A runs in its own task, so no
* activity result comes back: the caller must cache the entry optimistically.
*/
fun launchAdd(context: Context, entry: FidelityEntry): Boolean {
val fields = HashMap<String, String>()
fields[KeepassDef.TitleField] = entry.title
fields[KeepassDef.UrlField] = appUrl(context)
fields[CODE_FIELD] = entry.code
fields[FORMAT_FIELD] = entry.format
fields[PROTECT_CODE_FIELD] = entry.protected.toString()
val protectedFields = if (entry.protected) arrayListOf(CODE_FIELD) else null
return try {
context.startActivity(Kp2aControl.getAddEntryIntent(fields, protectedFields))
true
} catch (_: ActivityNotFoundException) {
ToastHelper.show(context, NOT_INSTALLED)
false
}
}
/** Stable id for entries KP2A did not tag with its own UUID. */
fun localUid(title: String): String =
UUID.nameUUIDFromBytes("kp2a:$title".toByteArray(Charsets.UTF_8)).toString()
/**
* Extracts a fidelity entry from an intent produced by KP2A (query result, or the
* launch intent when the user opens this app from an entry). Returns null when the
* intent carries no fidelity data.
*/
fun entryFromIntent(intent: Intent?): FidelityEntry? {
if (intent?.hasExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA) != true) return null
val fields = Kp2aControl.getEntryFieldsFromIntent(intent)
val code = fields[CODE_FIELD] ?: return null
val format = fields[FORMAT_FIELD] ?: return null
val title = fields[KeepassDef.TitleField] ?: ""
val protected = fields[PROTECT_CODE_FIELD]?.toBooleanStrictOrNull()
?: protectedFieldsFromIntent(intent).contains(CODE_FIELD)
return FidelityEntry(
uid = intent.getStringExtra(Strings.EXTRA_ENTRY_ID) ?: localUid(title),
title = title,
code = code,
format = format,
protected = protected,
)
}
// KP2A sends the protected field names either as a string list or a JSON array.
private fun protectedFieldsFromIntent(intent: Intent): List<String> {
intent.getStringArrayListExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST)?.let { return it }
val json = intent.getStringExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST) ?: return emptyList()
return try {
val a = JSONArray(json)
List(a.length()) { a.optString(it) }
} catch (_: JSONException) {
emptyList()
}
}
}
@@ -2,4 +2,5 @@
<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"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
+7 -9
View File
@@ -1,16 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
// ext.kotlin_version = '1.8.20'
// ext.android_core_version = '1.10.1'
// ext.android_appcompat_version = '1.6.1'
// ext.android_material_version = '1.9.0'
ext.android_test_version = '1.5.2'
ext.joda_time_version = '2.14.1'
ext.commons_io_version = '2.21.0'
ext.android_test_version = '1.7.0'
}
plugins {
id 'com.android.application' version '8.13.1' apply false
id 'com.android.library' version '8.13.1' apply false
id 'org.jetbrains.kotlin.android' version '2.2.21' apply false
id 'com.autonomousapps.dependency-analysis' version '3.4.1' apply true
id 'com.android.application' version '9.4.1' apply false
id 'com.android.library' version '9.4.1' apply false
id 'org.jetbrains.kotlin.android' version '2.4.20' apply false
id 'com.autonomousapps.dependency-analysis' version '3.13.0' apply true
}
+4
View File
@@ -22,3 +22,7 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.onlyEnableUnitTestForTheTestedBuildType=false
android.uniquePackageNames=false
android.r8.strictFullModeForKeepRules=false
android.dependency.useConstraints=false
Binary file not shown.
+3 -1
View File
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+1 -1
View File
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
Vendored
+10 -21
View File
@@ -23,8 +23,8 @@
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,7 +65,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@@ -73,21 +73,10 @@ goto fail
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
+1 -1
View File
@@ -1 +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>
<p><i>Keepass-Fidelity</i> adds an interface to view&save barcodes (QR included) with a Keepass Database, either by opening a KDBX file directly (standalone) or through the Keepass2Android plugin interface.</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 loaded from the database</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 database</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>
+1 -1
View File
@@ -1 +1 @@
Fidelity (Membership/Loyalty) Card plugin for Keepass2Android
Fidelity (Membership/Loyalty) Card app with Keepass Database Storage
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
for file in external/KeePassDX/{crypto,database}/build.gradle; do
if [ -f "$file" ]; then
sed -i "/id 'kotlin-android'/d" "$file"
sed -i "/apply plugin: 'kotlin-android'/d" "$file"
sed -i '/kotlinOptions {/,/}/d' "$file"
fi
done