250 Commits

Author SHA1 Message Date
Renovate Bot
f7babb188a Update dependency androidx.compose:compose-bom to v2025.11.00 2025-11-07 02:01:42 +00:00
bot
94ead96c5f Merge pull request 'Update dependency androidx.navigation:navigation-compose to v2.9.6' (#174) from renovate/androidx.navigation-navigation-compose-2.x into main 2025-11-06 03:03:35 +01:00
Renovate Bot
f3aac29466 Update dependency androidx.navigation:navigation-compose to v2.9.6 2025-11-06 02:03:14 +00:00
bot
b37cfb9315 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.4.1' (#173) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2025-10-31 03:01:49 +01:00
Renovate Bot
4a8b8a1f9c Update plugin com.autonomousapps.dependency-analysis to v3.4.1 2025-10-31 02:01:41 +00:00
bot
826d9c8a4c Merge pull request 'Update dependency gradle to v9.2.0' (#172) from renovate/gradle-9.x into main 2025-10-30 03:02:15 +01:00
Renovate Bot
33394d895b Update dependency gradle to v9.2.0 2025-10-30 02:02:12 +00:00
bot
deb8b238bd Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.4.0' (#171) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2025-10-29 03:02:30 +01:00
Renovate Bot
07b4b9d375 Update plugin com.autonomousapps.dependency-analysis to v3.4.0 2025-10-29 02:02:22 +00:00
bot
3987ac3a93 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.3.0' (#170) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2025-10-26 03:03:22 +01:00
Renovate Bot
419a2e2442 Update plugin com.autonomousapps.dependency-analysis to v3.3.0 2025-10-26 02:03:16 +00:00
bot
d30fe7ed7e Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.serialization to v2.2.21' (#169) from renovate/org.jetbrains.kotlin.plugin.serialization-2.x into main 2025-10-25 04:11:24 +02:00
Renovate Bot
9c45359e6e Update plugin org.jetbrains.kotlin.plugin.serialization to v2.2.21 2025-10-25 02:11:22 +00:00
bot
83670603a0 Merge pull request 'Update plugin org.jetbrains.kotlin.plugin.compose to v2.2.21' (#168) from renovate/org.jetbrains.kotlin.plugin.compose-2.x into main 2025-10-25 04:11:08 +02:00
Renovate Bot
a4bf5e7c39 Update plugin org.jetbrains.kotlin.plugin.compose to v2.2.21 2025-10-25 02:10:58 +00:00
bot
d179407451 Merge pull request 'Update plugin org.jetbrains.kotlin.android to v2.2.21' (#167) from renovate/org.jetbrains.kotlin.android-2.x into main 2025-10-24 04:06:44 +02:00
bot
5c2b13dc38 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.9.4' (#166) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2025-10-24 04:06:41 +02:00
Renovate Bot
2cf38db3fe Update plugin org.jetbrains.kotlin.android to v2.2.21 2025-10-24 02:06:39 +00:00
Renovate Bot
63606f8565 Update dependency androidx.compose.ui:ui-tooling to v1.9.4 2025-10-24 02:06:37 +00:00
bot
9759aab12b Merge pull request 'Update dependency androidx.compose.material:material to v1.9.4' (#165) from renovate/androidx.compose.material-material-1.x into main 2025-10-23 04:01:31 +02:00
bot
31175fb738 Merge pull request 'Update dependency androidx.compose:compose-bom to v2025.10.01' (#164) from renovate/androidx.compose-compose-bom-2025.x into main 2025-10-23 04:01:29 +02:00
Renovate Bot
e6d24eef24 Update dependency androidx.compose.material:material to v1.9.4 2025-10-23 02:01:28 +00:00
Renovate Bot
188d5c770a Update dependency androidx.compose:compose-bom to v2025.10.01 2025-10-23 02:01:26 +00:00
bot
c3fadc7224 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.2.0' (#163) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2025-10-22 04:02:32 +02:00
Renovate Bot
1e0e5e8aca Update plugin com.autonomousapps.dependency-analysis to v3.2.0 2025-10-22 02:02:25 +00:00
bot
cda5fcd90f Merge pull request 'Update dependency org.joda:joda-convert to v3' (#162) from renovate/org.joda-joda-convert-3.x into main 2025-10-13 04:01:30 +02:00
Renovate Bot
09ec6500bd Update dependency org.joda:joda-convert to v3 2025-10-13 02:01:24 +00:00
bot
acebf2c908 Merge pull request 'Update dependency joda-time:joda-time to v2.14.0' (#154) from renovate/joda-time-joda-time-2.x into main 2025-10-12 04:03:15 +02:00
Renovate Bot
e3b3ac9c7a Update dependency joda-time:joda-time to v2.14.0 2025-10-12 02:03:13 +00:00
bot
36d4c1b826 Merge pull request 'Update plugin com.autonomousapps.dependency-analysis to v3.1.0' (#161) from renovate/com.autonomousapps.dependency-analysis-3.x into main 2025-10-12 04:03:05 +02:00
Renovate Bot
e0da4230b6 Update plugin com.autonomousapps.dependency-analysis to v3.1.0 2025-10-12 02:03:00 +00:00
bot
ce45d61e33 Merge pull request 'Update dependency androidx.compose:compose-bom to v2025.10.00' (#160) from renovate/androidx.compose-compose-bom-2025.x into main 2025-10-11 04:02:53 +02:00
Renovate Bot
7a1bea8906 Update dependency androidx.compose:compose-bom to v2025.10.00 2025-10-11 02:02:51 +00:00
bot
ee7205ea40 Merge pull request 'Update dependency androidx.compose.ui:ui-tooling to v1.9.3' (#159) from renovate/androidx.compose.ui-ui-tooling-1.x into main 2025-10-11 04:02:41 +02:00
Renovate Bot
4d7d98b7f1 Update dependency androidx.compose.ui:ui-tooling to v1.9.3 2025-10-11 02:02:39 +00:00
bot
cef9be401f Merge pull request 'Update dependency androidx.compose.material:material to v1.9.3' (#158) from renovate/androidx.compose.material-material-1.x into main 2025-10-10 04:01:31 +02:00
bot
13c25015a2 Merge pull request 'Update dependency androidx.camera:camera-view to v1.5.1' (#157) from renovate/androidx.camera-camera-view-1.x into main 2025-10-10 04:01:30 +02:00
Renovate Bot
db3cb482c7 Update dependency androidx.compose.material:material to v1.9.3 2025-10-10 02:01:29 +00:00
Renovate Bot
2014843301 Update dependency androidx.camera:camera-view to v1.5.1 2025-10-10 02:01:26 +00:00
bot
967167d0da Merge pull request 'Update dependency androidx.camera:camera-lifecycle to v1.5.1' (#156) from renovate/androidx.camera-camera-lifecycle-1.x into main 2025-10-09 04:01:37 +02:00
bot
66b304ad61 Merge pull request 'Update dependency androidx.camera:camera-camera2 to v1.5.1' (#155) from renovate/androidx.camera-camera-camera2-1.x into main 2025-10-09 04:01:33 +02:00
Renovate Bot
b83802d258 Update dependency androidx.camera:camera-lifecycle to v1.5.1 2025-10-09 02:01:32 +00:00
Renovate Bot
e1aac8994b Update dependency androidx.camera:camera-camera2 to v1.5.1 2025-10-09 02:01:30 +00:00
bot
f6adc5fddd Merge pull request 'Update dependency org.joda:joda-convert to v2.2.4' (#153) from renovate/org.joda-joda-convert-2.x into main 2025-10-08 04:01:35 +02:00
Renovate Bot
142f4db1ac Update dependency org.joda:joda-convert to v2.2.4 2025-10-08 02:01:30 +00:00
soraefir
78eff7783d Fix attempt 2025-10-07 21:25:35 +02:00
soraefir
8cbae70e97 Fix build 2025-10-07 21:10:23 +02:00
soraefir
068720ebad Fix build 2025-10-07 20:31:42 +02:00
soraefir
fd7412b1ee Debug tooling 2025-10-07 01:02:45 +02:00
soraefir
8a202ba617 Release fallback 2025-10-07 00:55:06 +02:00
soraefir
bc7ad64f4c Update submodule version 2025-10-07 00:47:04 +02:00
soraefir
e5abad07c4 Fix submodule build 2025-10-07 00:44:25 +02:00
soraefir
644f8c685d submodules 2025-10-07 00:42:50 +02:00
soraefir
8b5c1cd942 Migrated 2025-10-07 00:37:19 +02:00
Renovate Bot
7e1443deca Update dependency gradle to v9.1.0 2025-09-19 02:02:07 +00:00
Renovate Bot
55bf2b8d03 Update plugin com.autonomousapps.dependency-analysis to v3.0.4 2025-09-18 02:01:34 +00:00
Renovate Bot
e79d3124d4 Update plugin com.autonomousapps.dependency-analysis to v3.0.3 2025-09-16 02:01:06 +00:00
Renovate Bot
3866c314c5 Update dependency androidx.camera:camera-view to v1.5.0 2025-09-14 02:02:01 +00:00
Renovate Bot
fd3ad5302d Update dependency androidx.camera:camera-lifecycle to v1.5.0 2025-09-13 02:02:27 +00:00
Renovate Bot
a64eb719be Update dependency androidx.camera:camera-camera2 to v1.5.0 2025-09-13 02:02:13 +00:00
Renovate Bot
e22a17d371 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.2.20 2025-09-12 04:08:49 +02:00
Renovate Bot
c6897668c8 Update plugin org.jetbrains.kotlin.android to v2.2.20 2025-09-12 02:00:57 +00:00
Renovate Bot
2dac8329ec Update plugin com.autonomousapps.dependency-analysis to v3.0.2 2025-09-11 04:10:06 +02:00
Renovate Bot
65171ecbb6 Update dependency com.google.code.gson:gson to v2.13.2 2025-09-11 02:09:54 +00:00
Renovate Bot
fb0ee7e6e3 Update plugin com.autonomousapps.dependency-analysis to v3 2025-09-10 02:00:58 +00:00
Renovate Bot
8f14ce4c35 Update actions/setup-java action to v5 2025-09-09 04:01:09 +02:00
Renovate Bot
052e8fc27a Update plugin com.android.library to v8.13.0 2025-09-09 02:01:03 +00:00
Renovate Bot
8fd70fb37f Update plugin com.android.application to v8.13.0 2025-09-08 04:08:40 +02:00
Renovate Bot
f2a5ca1804 Update dependency com.google.android.material:material to v1.13.0 2025-09-08 02:08:35 +00:00
Renovate Bot
2f07ea6d97 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.2.10 2025-09-07 02:10:53 +00:00
Renovate Bot
0c4bcf198d Update plugin org.jetbrains.kotlin.android to v2.2.10 2025-09-07 02:10:35 +00:00
Renovate Bot
113017de3b Update actions/checkout action to v5 2025-08-12 02:01:40 +00:00
Renovate Bot
c53df20c4d Update dependency gradle to v9 2025-08-02 04:03:36 +02:00
Renovate Bot
f193b972af Update plugin com.android.library to v8.12.0 2025-08-02 02:03:14 +00:00
Renovate Bot
707cd7bd4e Update plugin com.android.application to v8.12.0 2025-08-01 02:01:51 +00:00
Renovate Bot
4aae580c2b Update plugin com.android.library to v8.11.1 2025-07-12 02:02:37 +00:00
Renovate Bot
343b95de8c Update plugin com.android.application to v8.11.1 2025-07-11 02:07:33 +00:00
Renovate Bot
5105597620 Update plugin org.jetbrains.kotlin.android to v2.2.0 2025-07-05 02:04:15 +00:00
Renovate Bot
07cfec550b Update dependency gradle to v8.14.3 2025-07-05 02:04:02 +00:00
Renovate Bot
d31a4c4c69 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.2.0 2025-06-29 02:08:41 +00:00
Renovate Bot
974f8a6738 Update plugin com.android.library to v8.11.0 2025-06-29 02:02:44 +00:00
Renovate Bot
934215723b Update plugin com.autonomousapps.dependency-analysis to v2.19.0 2025-06-28 02:02:52 +00:00
Renovate Bot
546fbb55d6 Update plugin com.android.application to v8.11.0 2025-06-25 02:07:16 +00:00
Renovate Bot
e1d05ba16f Update dependency gradle to v8.14.2 2025-06-06 02:02:40 +00:00
Renovate Bot
0ec542ef25 Update plugin com.android.library to v8.10.1 2025-06-01 02:02:15 +00:00
Renovate Bot
b9203a4659 Update plugin com.autonomousapps.dependency-analysis to v2.18.0 2025-05-31 02:03:20 +00:00
Renovate Bot
c6ca8ef754 Update plugin com.android.application to v8.10.1 2025-05-29 02:01:28 +00:00
Renovate Bot
2bc3372db1 Update dependency gradle to v8.14.1 2025-05-23 02:02:41 +00:00
Renovate Bot
76cbdb7832 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.1.21 2025-05-17 02:08:38 +00:00
Renovate Bot
87ea1df4cd Update plugin org.jetbrains.kotlin.android to v2.1.21 2025-05-14 02:06:23 +00:00
Renovate Bot
ceffc79929 Update plugin com.android.library to v8.10.0 2025-05-10 02:04:59 +00:00
Renovate Bot
0914ebe475 Update plugin com.android.application to v8.10.0 2025-05-07 02:02:27 +00:00
Renovate Bot
f57bb2b935 Update plugin com.autonomousapps.dependency-analysis to v2.17.0 2025-04-28 02:01:20 +00:00
Renovate Bot
d896124765 Update plugin com.android.library to v8.9.2 2025-04-27 02:03:32 +00:00
Renovate Bot
a3e06eea84 Update dependency gradle to v8.14 2025-04-27 02:03:15 +00:00
Renovate Bot
01d206fb2b Update plugin com.android.application to v8.9.2 2025-04-26 02:03:24 +00:00
Renovate Bot
d6824843f0 Update dependency com.google.code.gson:gson to v2.13.1 2025-04-24 02:02:17 +00:00
Renovate Bot
7635266a78 Update plugin com.autonomousapps.dependency-analysis to v2.16.0 2025-04-12 02:02:24 +00:00
Renovate Bot
3b27d27c02 Update dependency com.google.code.gson:gson to v2.13.0 2025-04-12 02:02:13 +00:00
Renovate Bot
571f7d60cd Update plugin com.autonomousapps.dependency-analysis to v2.14.0 2025-04-05 02:03:18 +00:00
Renovate Bot
ce23372932 Update plugin com.android.library to v8.9.1 2025-03-31 02:01:36 +00:00
Renovate Bot
1024b8f4aa Update dependency androidx.camera:camera-view to v1.4.2 2025-03-30 02:03:18 +00:00
Renovate Bot
73e2e20398 Update plugin com.android.application to v8.9.1 2025-03-30 02:03:08 +00:00
Renovate Bot
a95c6951f2 Update dependency androidx.camera:camera-lifecycle to v1.4.2 2025-03-29 02:03:22 +00:00
Renovate Bot
0966aa5054 Update dependency androidx.camera:camera-camera2 to v1.4.2 2025-03-27 02:02:29 +00:00
Renovate Bot
8ca461ee0a Update plugin com.autonomousapps.dependency-analysis to v2.13.0 2025-03-23 02:02:32 +00:00
Renovate Bot
6c7b1e2675 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.1.20 2025-03-22 02:07:21 +00:00
Renovate Bot
2359fdca81 Update plugin org.jetbrains.kotlin.android to v2.1.20 2025-03-21 02:06:26 +00:00
Renovate Bot
998ed5abc0 Update plugin com.autonomousapps.dependency-analysis to v2.12.0 2025-03-15 02:02:51 +00:00
Renovate Bot
e4452160f6 Update plugin com.android.library to v8.9.0 2025-03-09 02:02:18 +00:00
Renovate Bot
8e67f2d885 Update plugin com.android.application to v8.9.0 2025-03-08 02:03:19 +00:00
Renovate Bot
e3943b33ff Update dependency gradle to v8.13 2025-03-02 02:03:37 +00:00
Renovate Bot
e26055afe7 Update plugin com.android.application to v8.8.2 2025-03-01 02:02:59 +00:00
Renovate Bot
587607f7dc Update dependency com.android.tools:desugar_jdk_libs_nio to v2.1.5 2025-02-26 02:02:34 +00:00
Renovate Bot
262dc08881 Update plugin com.autonomousapps.dependency-analysis to v2.10.1 2025-02-22 02:02:24 +00:00
Renovate Bot
cea9c27d57 Update plugin com.android.library to v8.8.1 2025-02-16 02:03:07 +00:00
Renovate Bot
17a6526b29 Update plugin com.autonomousapps.dependency-analysis to v2.8.2 2025-02-15 02:04:23 +00:00
Renovate Bot
a931335a2b Update plugin com.android.application to v8.8.1 2025-02-14 02:02:36 +00:00
Renovate Bot
aafd4f76d6 Update plugin com.autonomousapps.dependency-analysis to v2.8.1 2025-02-08 02:03:27 +00:00
Renovate Bot
5d464826dc Update dependency com.google.code.gson:gson to v2.12.1 2025-02-02 02:02:30 +00:00
Renovate Bot
6eddd15d81 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.1.10 2025-02-01 02:06:06 +00:00
Renovate Bot
6fee471dec Update plugin org.jetbrains.kotlin.android to v2.1.10 2025-01-28 02:05:47 +00:00
Renovate Bot
38727a239d Update dependency gradle to v8.12.1 2025-01-25 02:04:09 +00:00
Renovate Bot
83e2cf733e Update plugin com.autonomousapps.dependency-analysis to v2.7.0 2025-01-12 01:02:04 +00:00
Renovate Bot
3214d772b2 Update plugin com.android.library to v8.8.0 2025-01-11 01:02:20 +00:00
Renovate Bot
40c3f39c49 Update plugin com.android.application to v8.8.0 2025-01-10 01:01:23 +00:00
Renovate Bot
6215ffa7b6 Update dependency com.android.tools:desugar_jdk_libs_nio to v2.1.4 2024-12-21 01:03:13 +00:00
Renovate Bot
bf073da67b Update dependency gradle to v8.12 2024-12-21 01:03:03 +00:00
Renovate Bot
49fe5ca037 Update dependency androidx.camera:camera-view to v1.4.1 2024-12-15 01:02:20 +00:00
Renovate Bot
62f854db27 Update plugin com.autonomousapps.dependency-analysis to v2.6.1 2024-12-15 01:02:08 +00:00
Renovate Bot
aea6fa6c69 Update dependency androidx.camera:camera-lifecycle to v1.4.1 2024-12-14 07:46:37 +00:00
Renovate Bot
029a1fcde7 Update dependency androidx.camera:camera-camera2 to v1.4.1 2024-12-12 01:01:07 +00:00
Renovate Bot
92c99bec22 Update plugin com.android.library to v8.7.3 2024-12-08 01:02:45 +00:00
Renovate Bot
663c1236a4 Update plugin com.autonomousapps.dependency-analysis to v2.6.0 2024-12-07 01:05:26 +00:00
Renovate Bot
a9582ffb05 Update plugin com.android.application to v8.7.3 2024-12-03 03:27:24 +00:00
Renovate Bot
b11fb89bd9 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.1.0 2024-11-30 01:05:21 +00:00
Renovate Bot
1e6bebe853 Update plugin org.jetbrains.kotlin.android to v2.1.0 2024-11-28 01:05:00 +00:00
Renovate Bot
96904bce79 Update plugin com.autonomousapps.dependency-analysis to v2.5.0 2024-11-23 01:05:21 +00:00
Renovate Bot
08675a5fc3 Update dependency gradle to v8.11.1 2024-11-21 01:02:54 +00:00
Renovate Bot
019046474c Update dependency gradle to v8.11 2024-11-12 01:03:36 +00:00
Renovate Bot
0e63b6a50d Update plugin com.autonomousapps.dependency-analysis to v2.4.2 2024-11-09 01:05:30 +00:00
Renovate Bot
608ff610d8 Update dependency com.android.tools:desugar_jdk_libs_nio to v2.1.3 2024-11-08 01:02:13 +00:00
a009ce0c15 Merge pull request 'Update dependency androidx.camera:camera-view to v1.4.0' (#64) from renovate/androidx.camera-camera-view-1.x into main
Reviewed-on: #64
2024-11-07 07:56:07 +01:00
Renovate Bot
1ba95c54a2 Update dependency androidx.camera:camera-view to v1.4.0 2024-11-04 01:01:06 +00:00
Renovate Bot
51987f54e1 Update dependency androidx.camera:camera-lifecycle to v1.4.0 2024-11-03 01:05:52 +00:00
Renovate Bot
efb3a436c4 Update plugin com.android.library to v8.7.2 2024-11-03 01:05:40 +00:00
Renovate Bot
8a22d3b66e Update dependency androidx.camera:camera-camera2 to v1.4.0 2024-11-02 01:06:00 +00:00
Renovate Bot
c404d498d5 Update plugin com.android.application to v8.7.2 2024-11-01 01:01:30 +00:00
Renovate Bot
10d35956b3 Update plugin com.autonomousapps.dependency-analysis to v2.3.0 2024-10-25 00:01:26 +00:00
Renovate Bot
1de639dc46 Update plugin com.android.library to v8.7.1 2024-10-20 00:04:05 +00:00
Renovate Bot
a297988d33 Update plugin com.autonomousapps.dependency-analysis to v2.2.0 2024-10-19 00:04:49 +00:00
Renovate Bot
e788d064a5 Update plugin com.android.application to v8.7.1 2024-10-15 00:01:19 +00:00
Renovate Bot
d6692b5b7c Update plugin org.jetbrains.kotlin.plugin.serialization to v2.0.21 2024-10-12 00:08:35 +00:00
Renovate Bot
5ac4ba1d43 Update plugin org.jetbrains.kotlin.android to v2.0.21 2024-10-11 00:05:14 +00:00
Renovate Bot
50573a37c4 Update plugin com.android.application to v8.7.0 2024-10-05 00:06:34 +00:00
Renovate Bot
2528b7df5d Update plugin com.autonomousapps.dependency-analysis to v2.1.4 2024-10-05 00:06:20 +00:00
Renovate Bot
e6159f6f42 Update plugin com.autonomousapps.dependency-analysis to v2.1.1 2024-09-28 00:06:00 +00:00
Renovate Bot
f2982be549 Update dependency gradle to v8.10.2 2024-09-24 00:05:38 +00:00
Renovate Bot
0c5f7a658f Update plugin com.android.library to v8.6.1 2024-09-21 00:06:47 +00:00
Renovate Bot
9b90057f85 Update plugin com.android.application to v8.6.1 2024-09-18 00:03:41 +00:00
Renovate Bot
a9192314de Update plugin com.autonomousapps.dependency-analysis to v2.0.2 2024-09-14 00:05:24 +00:00
Renovate Bot
aa08418109 Update dependency gradle to v8.10.1 2024-09-10 00:04:35 +00:00
Renovate Bot
5b239ace83 Update dependency com.android.tools:desugar_jdk_libs_nio to v2.1.2 2024-09-05 00:02:36 +00:00
Renovate Bot
555cd8ada2 Update plugin com.android.library to v8.6.0 2024-09-01 00:03:21 +00:00
Renovate Bot
b188313eb9 Update plugin com.autonomousapps.dependency-analysis to v2 2024-09-01 00:03:09 +00:00
Renovate Bot
74ea62e8cd Update plugin com.android.application to v8.6.0 2024-08-31 00:06:45 +00:00
Renovate Bot
a59d79aa0e Update dependency com.android.tools:desugar_jdk_libs_nio to v2.1.1 2024-08-29 00:05:45 +00:00
Renovate Bot
e8021f37dd Update plugin org.jetbrains.kotlin.plugin.serialization to v2.0.20 2024-08-24 00:06:43 +00:00
Renovate Bot
4e179d8698 Update plugin org.jetbrains.kotlin.android to v2.0.20 2024-08-23 00:05:23 +00:00
Renovate Bot
a91f8545b0 Update dependency gradle to v8.10 2024-08-15 00:03:43 +00:00
Renovate Bot
94642047fb Update plugin org.jetbrains.kotlin.android to v2.0.10 2024-08-11 00:06:35 +00:00
Renovate Bot
e99f615fcd Update plugin com.android.application to v8.5.2 2024-08-11 00:06:26 +00:00
Renovate Bot
3ba61e87f9 Update plugin org.jetbrains.kotlin.plugin.serialization to v2.0.10 2024-08-10 00:07:09 +00:00
Renovate Bot
b798200883 Update plugin com.android.library to v8.5.2 2024-08-10 00:06:58 +00:00
Renovate Bot
2998362518 Update plugin com.autonomousapps.dependency-analysis to v1.33.0 2024-07-28 00:04:38 +00:00
Renovate Bot
73e3add4a8 Update plugin com.android.library to v8.5.1 2024-07-14 00:05:29 +00:00
Renovate Bot
5b43db3ebd Update plugin com.android.application to v8.5.1 2024-07-13 00:06:16 +00:00
Renovate Bot
f9535fe2da Update dependency gradle to v8.9 2024-07-12 00:04:09 +00:00
soraefir
aa20ec5a06 Version bump 2024-06-28 22:35:59 +02:00
soraefir
917a01b2ed Updates and fixes 2024-06-28 22:21:02 +02:00
Renovate Bot
e7f55c2be2 Update plugin com.android.library to v8.5.0 2024-06-17 00:02:36 +00:00
Renovate Bot
eb765e09e7 Update dependency androidx.camera:camera-view to v1.3.4 2024-06-16 00:03:56 +00:00
Renovate Bot
1cf4a6bc36 Update plugin com.android.application to v8.5.0 2024-06-16 00:03:49 +00:00
Renovate Bot
7f0212fc5d Update dependency androidx.camera:camera-lifecycle to v1.3.4 2024-06-15 00:05:06 +00:00
Renovate Bot
413d8bd7bf Update dependency androidx.camera:camera-camera2 to v1.3.4 2024-06-13 00:02:43 +00:00
Renovate Bot
931adbb4dd Update dependency gradle to v8.8 2024-06-01 00:06:27 +00:00
Renovate Bot
e6b2dfe37a Update plugin org.jetbrains.kotlin.android to v2 2024-05-26 00:07:56 +00:00
Renovate Bot
f73f9b5acf Update plugin org.jetbrains.kotlin.plugin.serialization to v2 2024-05-25 13:50:32 +00:00
Renovate Bot
ba2d0ac024 Update plugin com.android.library to v8.4.1 2024-05-25 13:47:04 +00:00
Renovate Bot
f33b4672b0 Update plugin com.autonomousapps.dependency-analysis to v1.32.0 2024-05-25 00:05:43 +00:00
Renovate Bot
b6b69587fa Update plugin com.android.application to v8.4.1 2024-05-21 00:02:49 +00:00
06a006c0a2 Merge pull request 'Update dependency com.google.code.gson:gson to v2.11.0' (#18) from renovate/com.google.code.gson-gson-2.x into main
Reviewed-on: #18
2024-05-20 10:34:47 +02:00
Renovate Bot
8ae38f4250 Update dependency com.google.code.gson:gson to v2.11.0 2024-05-20 00:02:26 +00:00
Renovate Bot
3e1252cc0a Update plugin com.android.library to v8.4.0 2024-05-12 00:11:36 +00:00
Renovate Bot
e4357a66e0 Update plugin org.jetbrains.kotlin.plugin.serialization to v1.9.24 2024-05-11 00:11:39 +00:00
Renovate Bot
21f2c0d69f Update plugin org.jetbrains.kotlin.android to v1.9.24 2024-05-08 00:09:57 +00:00
Renovate Bot
340789989c Update plugin com.android.application to v8.4.0 2024-05-04 00:04:58 +00:00
Renovate Bot
2428f4e50b Update dependency com.google.android.material:material to v1.12.0 2024-05-03 00:02:26 +00:00
f4c9eddd22 Merge pull request 'Update dependency androidx.camera:camera-view to v1.3.3' (#12) from renovate/androidx.camera-camera-view-1.x into main
Reviewed-on: #12
2024-04-21 02:36:39 +02:00
Renovate Bot
6a5e971619 Update dependency androidx.camera:camera-view to v1.3.3 2024-04-21 00:04:40 +00:00
Renovate Bot
5829f18908 Update dependency androidx.camera:camera-lifecycle to v1.3.3 2024-04-20 00:04:38 +00:00
Renovate Bot
426d94ba81 Update dependency androidx.camera:camera-camera2 to v1.3.3 2024-04-17 19:25:16 +00:00
a58d208d49 Merge pull request 'Update plugin com.android.application to v8.3.2' (#7) from renovate/com.android.application-8.x into main
Reviewed-on: #7
2024-04-13 01:08:03 +02:00
Renovate Bot
069edaf6a2 Update plugin com.android.application to v8.3.2 2024-04-12 23:07:08 +00:00
d0aa2fbeb9 Merge pull request 'Update plugin com.android.library to v8.3.2' (#8) from renovate/com.android.library-8.x into main
Reviewed-on: #8
2024-04-13 01:05:24 +02:00
17c75f27bc Merge pull request 'Update gradle/wrapper-validation-action action to v3' (#9) from renovate/gradle-wrapper-validation-action-3.x into main
Reviewed-on: #9
2024-04-13 01:05:09 +02:00
Renovate Bot
1738664f83 Update gradle/wrapper-validation-action action to v3 2024-04-12 23:02:34 +00:00
Renovate Bot
a5b55fe214 Update plugin com.android.library to v8.3.2 2024-04-11 00:01:50 +00:00
0cd90413d1 Update app/src/main/AndroidManifest.xml 2024-04-06 19:26:33 +02:00
soraefir
653ee1ccc1 Fix camera crash & autosave 2024-04-06 19:24:59 +02:00
Renovate Bot
9b6a69e227 Update plugin com.autonomousapps.dependency-analysis to v1.31.0 2024-04-05 00:02:16 +00:00
soraefir
e40305b680 Updated icon and permissions 2024-03-29 15:49:10 +01:00
soraefir
668e9d653f Scaled icon down 2024-03-29 13:47:22 +01:00
bcfcb85121 Update README.md 2024-03-29 13:29:40 +01:00
49c650c8a9 Update README.md 2024-03-29 13:29:19 +01:00
soraefir
b5f52c7e13 Release 1.2a prep 2024-03-29 13:28:30 +01:00
soraefir
d81922d2c9 File Scanner & Metadata 2024-03-29 13:27:27 +01:00
84b2c2c455 Update README.md 2024-03-25 07:13:58 +01:00
841c3dea24 Update README.md 2024-03-24 15:45:28 +01:00
soraefir
6596f347a1 Updated icon 2024-03-24 15:41:26 +01:00
soraefir
a2dd009533 Fixed versioning 2024-03-24 13:29:13 +01:00
soraefir
4104104b16 minor release 2024-03-24 13:17:09 +01:00
soraefir
0d838b6209 Removed missing encoder 2024-03-24 13:11:15 +01:00
soraefir
1d41671fb6 prep 1.1 2024-03-24 13:01:46 +01:00
soraefir
84ebdcc9a8 Support more formats & cleanup 2024-03-24 13:00:22 +01:00
soraefir
8181da421e Deleted GoogleML usage 2024-03-24 12:40:33 +01:00
soraefir
af97381bdb Fix Readme 2024-03-24 11:37:28 +01:00
soraefir
ba94beab37 Fix Readme 2024-03-24 11:36:35 +01:00
19b0f91852 Update README.md 2024-03-24 11:32:34 +01:00
soraefir
3b0bf2097e Github images 2024-03-24 11:16:26 +01:00
455c95021b LICENSE 2024-03-24 10:45:37 +01:00
soraefir
78ad7f983f CI Fix 2024-03-24 10:34:02 +01:00
soraefir
0a0b55294f Fix CI 2024-03-24 10:28:30 +01:00
soraefir
040b9c3af4 Fix CI 2024-03-24 10:26:31 +01:00
soraefir
796c76f36c CI Signing 2024-03-24 10:23:30 +01:00
soraefir
517f0240e3 CI release 2024-03-23 15:12:15 +01:00
soraefir
b02920ca41 CI Autorelease 2024-03-23 15:03:09 +01:00
soraefir
b289648260 CI Artifact 2024-03-23 14:25:04 +01:00
soraefir
b6de7ca409 CI fix tag 2024-03-23 14:13:37 +01:00
soraefir
12fb04974e CI on tag 2024-03-23 14:09:54 +01:00
soraefir
cdd4d6db1b Fix CI 2024-03-23 13:59:01 +01:00
soraefir
a403bdea61 CI testing 2024-03-23 13:50:35 +01:00
657489e255 Merge pull request 'Update plugin com.android.library to v8.3.1' (#4) from renovate/com.android.library-8.x into main
Reviewed-on: #4
2024-03-23 13:32:22 +01:00
95f609bc53 Merge pull request 'Update dependency gradle to v8.7' (#5) from renovate/gradle-8.x into main
Reviewed-on: #5
2024-03-23 13:32:06 +01:00
soraefir
f14929cdf6 Prepare R1.0 2024-03-23 13:21:24 +01:00
Renovate Bot
ab5c32f2db Update dependency gradle to v8.7 2024-03-23 01:05:44 +00:00
Renovate Bot
cd8a84531e Update plugin com.android.library to v8.3.1 2024-03-23 01:05:33 +00:00
Renovate Bot
baee52c69b Update plugin com.android.application to v8.3.1 2024-03-23 01:05:27 +00:00
soraefir
5edff2f5d3 Cleaning 2024-03-22 07:27:27 +01:00
soraefir
f691b20952 Added clearing list 2024-03-22 07:27:27 +01:00
80 changed files with 2735 additions and 2112 deletions

BIN
.github/images/apk.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
.github/images/izzy.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

65
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
#file: noinspection SpellCheckingInspection
name: CI-Android APK
env:
main_project_module: app
playstore_name: KeepassFidelity
on:
push:
branches: [ main ]
tags:
- '**'
pull_request:
branches: [ main ]
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
with:
submodules: true
- name: set up secrets
run: |
echo "${{ secrets.RELEASE_KEYSTORE }}" > keystore.asc
echo "${{ secrets.RELEASE_KEY}}" > key.asc
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch keystore.asc > app/keystore.properties
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch key.asc > app/key.jks
- uses: gradle/wrapper-validation-action@v3
- name: create and checkout branch
if: github.event_name == 'pull_request'
env:
BRANCH: ${{ github.head_ref }}
run: git checkout -B "$BRANCH"
- name: set up JDK
uses: actions/setup-java@v5
with:
java-version: 17
distribution: "temurin"
cache: 'gradle'
- name: Build APK
run: ./gradlew assemble
# - name: Upload APK
# uses: actions/upload-artifact@v4
# with:
# name: app.apk
# path: app/build/outputs/apk/release/app-release.apk
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
app/build/outputs/apk/release/app-release.apk

4
.gitignore vendored
View File

@@ -7,7 +7,11 @@ local.properties/
.DS_Store
build/
app/build/
app/debug/
app/release/
captures/
.externalNativeBuild
.cxx
local.properties
keystore.properties
key.jks

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "external/KeePassDX"]
path = external/KeePassDX
url = https://github.com/Kunzisoft/KeePassDX.git

24
LICENSE Normal file
View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

View File

@@ -0,0 +1,73 @@
<!--suppress ALL -->
<div align="center">
<h1>Keepass Fidelity</h1>
<img width="100px" src="./metadata/en-US/images/icon.png" alt="Logo">
<p>A minimalist fidelity/loyalty card plugin</p>
<img src="https://forthebadge.com/images/badges/built-for-android.svg" alt="Built for Android">
<img src="https://forthebadge.com/images/badges/built-with-love.svg" alt="Built with love">
<br>
<a href="https://github.com/choelzl/keepass-fidelity/actions/workflows/build.yml">
<img src="https://github.com/choelzl/keepass-fidelity/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status">
</a>
</div>
## 🌄 Screenshots
<div align="center">
<table>
<tr>
<td style="width: 33%; height: 100px;"><img src="./metadata/en-US/images/phoneScreenshots/launcher.jpg" alt="Launcher" style="width: 100%; height: 100%;"></td>
<td style="width: 33%; height: 100px;"><img src="./metadata/en-US/images/phoneScreenshots/view.jpg" alt="View" style="width: 100%; height: 100%;"></td>
<td style="width: 33%; height: 100px;"><img src="./metadata/en-US/images/phoneScreenshots/edit.jpg" alt="Edit" style="width: 100%; height: 100%;"></td>
</tr>
</table>
</div>
## ⭐ Features
- Search entries in [Keepass2Android](https://github.com/PhilippC/keepass2android/)
- Scan & Create entries
- Recently used history for fast access
- Protect entries from caching
- Minimalist design and features
- Supported Formats: CODE_39, CODE_93, CODE_128, EAN_8, EAN_13, UPC_A, UPC_E, CODE_QR, PDF_417, AZTEC, CODABAR, DATA_MATRIX, ITF
## 📳 Installation
<div style="display: flex; justify-content: center; align-items: center; flex-direction: row;">
<a href="https://apt.izzysoft.de/fdroid/index/apk/net.helcel.fidelity">
<img width="200" height="80" alt="Izzy Download" src=".github/images/izzy.png">
</a>
<a href="https://github.com/choelzl/keepass-fidelity/releases/latest">
<img width="200" height="84" alt="APK Download" src=".github/images/apk.png">
</a>
</div>
## ⚙️ Permissions
- `CAMERA`: necessary for importing barcodes from camera
- `READ_MEDIA_VISUAL_USER_SELECTED`: necessary for the importing barcode from images
## 📝 Contribute
Keepass-Fidelity is a user-driven project. We welcome any contribution, big or small.
- **🖥️ Development:** Fix bugs, implement features, or research issues. Open a PR for review.
- **🍥 Design:** Improve interfaces, including accessibility and usability.
- **📂 Issue Reporting:** Report bugs and edge cases with relevant info.
- **🌍 Localization:** Translate if it doesn't support your language.
## ✏️ Acknowledgements
Thanks to all contributors, the developers of our dependencies, and our users.
## 📝 License
```
Copyright (C) 2024 Helcel
Licensed under the Unlicense
For more information, please refer to <https://unlicense.org>
```

View File

@@ -1,54 +1,128 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.23'
id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.21'
id 'org.jetbrains.kotlin.plugin.compose' version '2.2.21'
}
android {
namespace 'net.helcel.fidelity'
compileSdk 34
compileSdk 36
defaultConfig {
applicationId 'net.helcel.fidelity'
versionName "1.0d"
buildConfigField("String", "APP_NAME", "\"Keepass Fidelity\"")
manifestPlaceholders["APP_NAME"] = "Keepass Fidelity"
minSdk 28
targetSdk 34
versionCode 1
versionName "1.0"
targetSdk 36
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfigs {
create("release") {
try {
def keystorePropertiesFile = rootProject.file("app/keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
} catch (FileNotFoundException e) {
println("File not found: ${e.message}")
}
}
}
buildTypes {
debug {
debuggable true
}
release {
minifyEnabled true
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
signedRelease {
initWith(buildTypes.release)
matchingFallbacks = ['release']
signingConfig = signingConfigs.getByName("release")
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
encoding 'utf-8'
}
buildFeatures {
viewBinding true
compose true
buildConfig true
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
composeOptions {
kotlinCompilerExtensionVersion = "2.2.20"
}
kotlin {
jvmToolchain(21)
}
lint {
disable 'UsingMaterialAndMaterial3Libraries'
disable 'PreviewAnnotationInFunctionWithParameters'
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material3:material3:1.4.0'
implementation 'androidx.compose.material:material:1.9.4'
implementation 'androidx.compose.material:material-icons-extended:1.7.8'
implementation 'androidx.navigation:navigation-compose:2.9.6'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
implementation 'androidx.camera:camera-camera2:1.3.2'
implementation 'androidx.camera:camera-lifecycle:1.3.2'
implementation 'androidx.camera:camera-view:1.3.2'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.google.zxing:core:3.5.3'
implementation 'com.google.mlkit:barcode-scanning:17.2.0'
}
implementation "androidx.biometric:biometric:1.2.0-alpha05"
implementation "androidx.security:security-crypto:1.1.0"
implementation "androidx.datastore:datastore-preferences:1.1.7"
implementation "androidx.security:security-crypto:1.1.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5'
implementation 'androidx.camera:camera-lifecycle:1.5.1'
implementation 'androidx.camera:camera-view:1.5.1'
runtimeOnly 'androidx.camera:camera-camera2:1.5.1'
implementation 'com.google.android.material:material:1.13.0'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0'
implementation 'com.google.zxing:core:3.5.3'
implementation project(":database")
implementation project(":crypto")
implementation platform('androidx.compose:compose-bom:2025.11.00')
implementation 'androidx.compose.ui:ui-tooling:1.9.4'
implementation 'androidx.compose.ui:ui-tooling-preview'
//Submodule
//noinspection NewerVersionAvailable
implementation 'joda-time:joda-time:2.14.0'
implementation 'org.joda:joda-convert:3.0.1'
}

7
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,7 @@
# Gson uses generic type information stored in a class file when working with
# fields. Proguard removes such information by default, keep it.
-keepattributes Signature
-keep class org.joda.convert.** { *; }
# Optional. For using GSON @Expose annotation
-keepattributes AnnotationDefault,RuntimeVisibleAnnotations

View File

@@ -1,49 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<application
android:icon="@drawable/logo"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher_round"
android:label="${APP_NAME}"
android:supportsRtl="true">
<activity
android:name=".activity.MainActivity"
android:exported="true"
android:theme="@style/Theme.Fidelity">
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".pluginSDK.PluginAccessReceiver"
android:exported="true">
<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>
<receiver
android:name=".pluginSDK.PluginActionBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="keepass2android.ACTION_OPEN_ENTRY" />
<action android:name="keepass2android.ACTION_CLOSE_ENTRY_VIEW" />
<action android:name="keepass2android.ACTION_ENTRY_ACTION_SELECTED" />
<action android:name="keepass2android.ACTION_LOCK_DATABASE" />
<action android:name="keepass2android.ACTION_UNLOCK_DATABASE" />
<action android:name="keepass2android.ACTION_CLOSE_DATABASE" />
<action android:name="keepass2android.ACTION_OPEN_DATABASE" />
</intent-filter>
</receiver>
</application>
</manifest>

BIN
app/src/main/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,65 @@
package net.helcel.fidelity.activity
import android.content.Context
import android.os.Build
import android.widget.Toast
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.Colors
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.preference.PreferenceManager
import net.helcel.fidelity.R
object ToastHelper{
fun show(context: Context, message: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(context, message, duration).show()
}
}
@Composable
fun SysTheme(
content: @Composable () -> Unit
) {
val context = LocalContext.current
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val themeKey = prefs.getString(stringResource(R.string.key_theme), stringResource(R.string.system))
val darkTheme = when (themeKey) {
stringResource(R.string.system) -> isSystemInDarkTheme()
stringResource(R.string.light) -> false
stringResource(R.string.dark) -> true
else -> isSystemInDarkTheme()
}
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if(darkTheme) dynamicDarkColorScheme(LocalContext.current ) else dynamicLightColorScheme(LocalContext.current )
} else {
if(darkTheme) darkColorScheme() else lightColorScheme()
}
val m2colors = Colors(
primary = colorScheme.primary,
primaryVariant = colorScheme.primaryContainer,
secondary = colorScheme.secondary,
background = colorScheme.background,
surface = colorScheme.surface,
onPrimary = colorScheme.onPrimary,
onSecondary = colorScheme.onSecondary,
onBackground = colorScheme.onBackground,
onSurface = colorScheme.onSurface,
secondaryVariant = colorScheme.secondary,
error = colorScheme.error,
onError = colorScheme.onError,
isLight = !darkTheme,
)
MaterialTheme(
colors = m2colors,
content = content
)
}

View File

@@ -1,48 +1,62 @@
package net.helcel.fidelity.activity
import android.content.Context
import android.content.SharedPreferences
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import net.helcel.fidelity.R
import net.helcel.fidelity.activity.fragment.Launcher
import net.helcel.fidelity.databinding.ActMainBinding
import net.helcel.fidelity.tools.CacheManager
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActMainBinding
private lateinit var sharedPreferences: SharedPreferences
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.FragmentActivity
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import net.helcel.fidelity.activity.fragment.CreateEntryScreen
import net.helcel.fidelity.activity.fragment.FileScanner
import net.helcel.fidelity.activity.fragment.InitialScreen
import net.helcel.fidelity.activity.fragment.LauncherScreen
import net.helcel.fidelity.activity.fragment.ScannerScreen
import net.helcel.fidelity.activity.fragment.ViewEntryScreen
import net.helcel.fidelity.tools.FidelityRepository.entries
import net.helcel.fidelity.tools.FidelityRepository.loadEntries
import net.helcel.fidelity.tools.KeePassStore.hasCredentials
class MainActivity : FragmentActivity() {
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedPreferences =
this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE)
CacheManager.loadFidelity(sharedPreferences)
actionBar?.hide()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
loadEntries(this.baseContext)
binding = ActMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setContent {
SysTheme {
val navController = rememberNavController()
val context = LocalContext.current
onBackPressedDispatcher.addCallback(this) {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStackImmediate()
} else {
finish()
BackHandler {
if (!navController.popBackStack()) finish()
}
LaunchedEffect(Unit) {
if(!hasCredentials(context)) navController.navigate("init")
}
NavHost(navController = navController, startDestination = "launcher") {
composable("exit") { finish() }
composable("launcher") { LauncherScreen(navController) }
composable("init"){ InitialScreen (navController)}
composable("scanCam") { ScannerScreen(navController) }
composable("scanFile") { FileScanner(navController) }
composable("edit"){ CreateEntryScreen(navController) }
composable("view/{entryId}") { e ->
val entry = entries.find {
it.uid == (e.arguments?.getString("entryId") ?: "")
}
if (entry == null) return@composable navController.navigate("launcher")
ViewEntryScreen(navController,entry)
}
}
}
}
if (savedInstanceState == null)
loadLauncher()
}
private fun loadLauncher() {
supportFragmentManager.beginTransaction()
.add(R.id.container, Launcher())
.commit()
}
}

View File

@@ -1,50 +0,0 @@
package net.helcel.fidelity.activity.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import net.helcel.fidelity.databinding.ListItemFidelityBinding
class FidelityListAdapter(
private val triples: ArrayList<Triple<String?, String?, String?>>,
private val onItemClicked: (Triple<String?, String?, String?>) -> Unit
) :
RecyclerView.Adapter<FidelityListAdapter.FidelityViewHolder>() {
private lateinit var binding: ListItemFidelityBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FidelityViewHolder {
binding = ListItemFidelityBinding.inflate(LayoutInflater.from(parent.context))
binding.root.setLayoutParams(
LinearLayout.LayoutParams(
MATCH_PARENT, WRAP_CONTENT
)
)
return FidelityViewHolder(binding.root)
}
override fun onBindViewHolder(holder: FidelityViewHolder, position: Int) {
val triple = triples[position]
holder.bind(triple)
}
override fun getItemCount(): Int = triples.size
inner class FidelityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(triple: Triple<String?, String?, String?>) {
val text = "${triple.first}"
binding.textView.text = text
binding.card.setOnClickListener { onItemClicked(triple) }
}
}
}

View File

@@ -1,143 +1,370 @@
package net.helcel.fidelity.activity.fragment
import android.content.ActivityNotFoundException
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Button
import androidx.compose.material.Checkbox
import androidx.compose.material.CheckboxDefaults
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ExposedDropdownMenuBox
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Camera
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.google.zxing.FormatException
import com.kunzisoft.keepass.database.element.Entry
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.helcel.fidelity.R
import net.helcel.fidelity.databinding.FragCreateEntryBinding
import net.helcel.fidelity.pluginSDK.Kp2aControl
import net.helcel.fidelity.activity.ToastHelper
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onCameraScan
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onFileScan
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onSubmit
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onRefresh
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onSave
import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
import net.helcel.fidelity.tools.CacheManager
import net.helcel.fidelity.tools.ErrorToaster
import net.helcel.fidelity.tools.KeepassWrapper
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
private const val DEBOUNCE_DELAY = 500L
class CreateEntry : Fragment() {
@Preview
@Composable
fun CreateEntryScreen(navController: NavHostController?) {
var entry by remember { activeEntry }
var errorTitle by remember { mutableStateOf("") }
var errorCode by remember { mutableStateOf("") }
var errorFormat by remember { mutableStateOf("") }
private val handler = Handler(Looper.getMainLooper())
private lateinit var binding: FragCreateEntryBinding
var barcodeBitmap by remember { mutableStateOf<Bitmap?>(null) }
var isValidBarcode by remember { mutableStateOf(false) }
var showDialog by remember { mutableStateOf(false) }
var isLoading by remember { mutableStateOf(false) }
val ctx = LocalContext.current
val scope = rememberCoroutineScope()
private val resultLauncherAdd = KeepassWrapper.resultLauncherAdd(this) {
val r = KeepassWrapper.entryExtract(it)
if (!KeepassWrapper.isProtected(it)) {
CacheManager.addFidelity(r)
LaunchedEffect(entry) {
isValidBarcode = false
delay(500)
if (entry.code.isEmpty()) return@LaunchedEffect
try {
val bmp = generateBarcode(entry.code, entry.format, 600)
barcodeBitmap = bmp
isValidBarcode = true
errorCode = ""
} catch (_: FormatException) {
barcodeBitmap = null
errorCode = "Invalid Format"
} catch (e: IllegalArgumentException) {
barcodeBitmap = null
errorCode = if (e.message == "com.google.zxing.FormatException") "Invalid Format"
else e.message ?: "Invalid Argument"
} catch (e: Exception) {
barcodeBitmap = null
ToastHelper.show(ctx, e.message ?: e.toString())
}
startViewEntry(r.first, r.second, r.third)
}
private var isValid: Boolean = false
if (showDialog) {
TreeSelectorDialog(
onDismiss = {
showDialog = false
if(it!=null){
entry = entry.copy(uid = it.nodeId?.id.toString())
if(it is Entry){
entry = entry.copy(title = it.title)
}
}
}
)
}
val formats = stringArrayResource(R.array.format_array)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragCreateEntryBinding.inflate(layoutInflater)
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
) {
Column(
modifier = Modifier
.padding(16.dp, 32.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
)
{
OutlinedTextField(
value = entry.title,
enabled = entry.uid!=null,
onValueChange = {
entry = entry.copy(title = it)
errorTitle = ""
},
label = { Text(text = "Title") },
isError = errorTitle.isNotEmpty(),
modifier = Modifier.fillMaxWidth(),
singleLine = true,
colors = TextFieldDefaults.textFieldColors(
textColor = if(entry.uid!=null)MaterialTheme.colors.onBackground
else MaterialTheme.colors.secondary
),
)
if (errorTitle.isNotEmpty()) {
Text(errorTitle, color = MaterialTheme.colors.error)
}
val formats = resources.getStringArray(R.array.format_array)
val arrayAdapter = ArrayAdapter(requireContext(), R.layout.list_item_dropdown, formats)
binding.editTextFormat.setAdapter(arrayAdapter)
OutlinedTextField(
value = entry.code,
onValueChange = {
entry = entry.copy(code = it)
errorCode = ""
},
colors = TextFieldDefaults.textFieldColors(
textColor = MaterialTheme.colors.onBackground
),
label = { Text("Code") },
isError = errorCode.isNotEmpty(),
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
if (errorCode.isNotEmpty()) {
Text(errorCode, color = MaterialTheme.colors.error)
}
val res = KeepassWrapper.bundleExtract(arguments)
binding.editTextCode.setText(res.second)
binding.editTextFormat.setText(res.third, false)
FormatDropdown(
formats,
entry.format,
errorFormat.ifEmpty { null },
) {
entry = entry.copy(format = it)
errorFormat = ""
}
if (errorFormat.isNotEmpty()) {
Text(errorFormat, color = MaterialTheme.colors.error)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Checkbox(
checked = entry.protected,
onCheckedChange = {
entry = entry.copy(protected = it)
},
colors = CheckboxDefaults.colors()
)
Text("Protected", color = MaterialTheme.colors.onBackground)
Spacer(modifier = Modifier.weight(1f))
Button(onClick = { onCameraScan(navController!!) }) {
Icon(Icons.Default.Camera, contentDescription = null)
}
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = { onFileScan(navController!!) }) {
Icon(Icons.Default.FileOpen, contentDescription = null)
}
}
if (barcodeBitmap != null) {
Image(
bitmap = barcodeBitmap!!.asImageBitmap(),
contentDescription = "Barcode preview",
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
)
Spacer(modifier = Modifier.height(8.dp))
}
val changeListener = {
isValid = false
handler.removeCallbacksAndMessages(null)
handler.postDelayed({
updatePreview()
}, DEBOUNCE_DELAY)
}
binding.editTextCode.addTextChangedListener { changeListener() }
binding.editTextFormat.addTextChangedListener { changeListener() }
binding.editTextFormat.addTextChangedListener { binding.editTextFormat.error = null }
binding.btnSave.setOnClickListener {
if (!isValid() || !isValid) {
ErrorToaster.formIncomplete(requireActivity())
} else {
val kpentry = KeepassWrapper.entryCreate(
this,
binding.editTextTitle.text.toString(),
binding.editTextCode.text.toString(),
binding.editTextFormat.text.toString(),
binding.checkboxProtected.isChecked,
)
try {
resultLauncherAdd.launch(
Kp2aControl.getAddEntryIntent(
kpentry.first,
kpentry.second
)
)
} catch (e: ActivityNotFoundException) {
ErrorToaster.noKP2AFound(requireActivity())
}
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(48.dp),
verticalAlignment = Alignment.CenterVertically
) {
Button(
onClick = {
onSubmitIfValid(
entry,
setErrors = { t, c, f ->
errorTitle = t
errorCode = c
errorFormat = f
},
isValidBarcode
) {
if (FidelityRepository.getRoot() == null) {
isLoading = true
scope.launch {
onRefresh(ctx, navController!!)
isLoading = false
if(entry.uid!=null){
addEntry(ctx,entry)
isLoading = true
onSave(ctx,navController)
isLoading = false
onSubmit(navController)
}else {
showDialog = true
}
}
} else {
if(entry.uid!=null){
addEntry(ctx,entry)
isLoading = true
scope.launch {
onSave(ctx, navController!!)
isLoading = false
onSubmit(navController)
}
}else {
showDialog = true
}
}
}
},
enabled = isValidBarcode.and(entry.uid==null || entry.title.isNotEmpty()),
) {
Text(if(entry.uid==null)"Select Entry" else "Save", style = MaterialTheme.typography.h6)
}
}
updatePreview()
return binding.root
}
private fun updatePreview() {
try {
val barcodeBitmap = generateBarcode(
binding.editTextCode.text.toString(),
binding.editTextFormat.text.toString(),
600
)
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
isValid = true
} catch (e: FormatException) {
binding.imageViewPreview.setImageBitmap(null)
binding.editTextCode.error = "Invalid format"
} catch (e: IllegalArgumentException) {
binding.imageViewPreview.setImageBitmap(null)
binding.editTextCode.error = e.message
} catch (e: Exception) {
binding.imageViewPreview.setImageBitmap(null)
println(e.javaClass)
println(e.message)
e.printStackTrace()
if (isLoading) {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background.copy(alpha = 0.75f))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { }
),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
}
}
private fun isValid(): Boolean {
var valid = true
if (binding.editTextTitle.text!!.isEmpty()) {
valid = false
binding.editTextTitle.error = "Title cannot be empty"
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun FormatDropdown(
formats: Array<String>,
format: String,
errorFormat: String?,
onFormatChange: (String) -> Unit,
) {
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded }
) {
OutlinedTextField(
value = format,
onValueChange = {},
readOnly = true, // important for dropdown
label = { Text("Format", color=MaterialTheme.colors.onBackground) },
trailingIcon = {
Icon(
Icons.Default.ArrowDropDown,
contentDescription = "Expand",
)
},
colors = TextFieldDefaults.textFieldColors(
textColor = MaterialTheme.colors.onBackground
),
isError = errorFormat != null,
modifier = Modifier.fillMaxWidth()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
formats.forEach { option ->
DropdownMenuItem(
onClick = {
onFormatChange(option)
expanded = false
}
) {
Text(option)
}
}
}
if (binding.editTextCode.text!!.isEmpty()) {
valid = false
binding.editTextCode.error = "Code cannot be empty"
}
if (binding.editTextFormat.text!!.isEmpty()) {
valid = false
binding.editTextFormat.error = "Format cannot be empty"
}
return valid
}
}
private fun onSubmitIfValid(
entry: FidelityEntry,
setErrors: (String, String, String) -> Unit,
isValidBarcode: Boolean,
onValid: (FidelityEntry) -> Unit
) {
var tErr = ""
var cErr = ""
var fErr = ""
if (entry.uid!=null && entry.title.isBlank()) tErr = "Title cannot be empty"
if (entry.code.isBlank()) cErr = "Code cannot be empty"
if (entry.format.isBlank()) fErr = "Format cannot be empty"
setErrors(tErr, cErr, fErr)
if (tErr.isEmpty() && cErr.isEmpty() && fErr.isEmpty() && isValidBarcode) {
onValid(entry.copy())
}
}
object CreateEntryEventHandler {
fun onSubmit(navController: NavHostController){
navController.popBackStack()
activeEntry.value = activeEntry.value.copy(null,"","","",false)
}
private fun startViewEntry(title: String?, code: String?, fmt: String?) {
val viewEntryFragment = ViewEntry()
viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt)
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.container, viewEntryFragment).commit()
fun onFileScan(navController: NavHostController){
navController.navigate("scanFile")
}
fun onCameraScan(navController: NavHostController){
navController.navigate("scanCam")
}
}

View File

@@ -1,127 +1,345 @@
package net.helcel.fidelity.activity.fragment
import android.content.ActivityNotFoundException
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import net.helcel.fidelity.R
import net.helcel.fidelity.activity.adapter.FidelityListAdapter
import net.helcel.fidelity.databinding.FragLauncherBinding
import net.helcel.fidelity.pluginSDK.Kp2aControl
import net.helcel.fidelity.tools.CacheManager
import net.helcel.fidelity.tools.ErrorToaster
import net.helcel.fidelity.tools.KeepassWrapper
import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.HideSource
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onAdd
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onEdit
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onHide
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onPin
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onQuery
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onRefresh
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onView
import net.helcel.fidelity.tools.CredentialResult
import net.helcel.fidelity.tools.FidelityEntry
import net.helcel.fidelity.tools.FidelityRepository.activeEntry
import net.helcel.fidelity.tools.FidelityRepository.end
import net.helcel.fidelity.tools.FidelityRepository.entries
import net.helcel.fidelity.tools.FidelityRepository.genCredentials
import net.helcel.fidelity.tools.FidelityRepository.importDB
import net.helcel.fidelity.tools.FidelityRepository.start
import net.helcel.fidelity.tools.KeePassStore.loadCredentials
class Launcher : Fragment() {
private lateinit var binding: FragLauncherBinding
private lateinit var fidelityListAdapter: FidelityListAdapter
private val resultLauncherQuery = KeepassWrapper.resultLauncherQuery(this) {
val r = KeepassWrapper.entryExtract(it)
if (!KeepassWrapper.isProtected(it)) {
CacheManager.addFidelity(r)
}
startViewEntry(r.first, r.second, r.third)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragLauncherBinding.inflate(layoutInflater)
binding.btnQuery.setOnClickListener { startGetFromKeepass() }
binding.btnAdd.setOnClickListener {
if (binding.menuAdd.visibility == View.GONE)
showMenuAdd()
else
hideMenuAdd()
}
hideMenuAdd()
binding.btnScan.setOnClickListener {
startScanner()
hideMenuAdd()
}
binding.btnManual.setOnClickListener {
startCreateEntry()
hideMenuAdd()
}
binding.fidelityList.layoutManager =
LinearLayoutManager(requireContext())
fidelityListAdapter = FidelityListAdapter(CacheManager.getFidelity()) {
startViewEntry(it.first, it.second, it.third)
}
binding.fidelityList.adapter = fidelityListAdapter
recyclerSlideHelper().attachToRecyclerView(binding.fidelityList)
return binding.root
}
private fun hideMenuAdd() {
binding.btnAdd.setImageResource(R.drawable.cross)
binding.menuAdd.visibility = View.GONE
}
private fun showMenuAdd() {
binding.btnAdd.setImageResource(R.drawable.minus)
binding.menuAdd.visibility = View.VISIBLE
}
private fun startGetFromKeepass() {
try {
this.resultLauncherQuery.launch(Kp2aControl.queryEntryIntentForOwnPackage)
} catch (e: ActivityNotFoundException) {
ErrorToaster.noKP2AFound(requireActivity())
@Preview
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LauncherScreen(
navController: NavHostController?,
) {
if(navController==null) return
var isRefreshingState by remember { mutableStateOf(false) }
var showHidden by remember { mutableStateOf(false) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
val sortedEntries = remember(entries) {
derivedStateOf {
entries.filter{showHidden || !it.hidden}.sortedWith(
compareByDescending<FidelityEntry> { it.pinned }
.thenBy { it.hidden }
.thenByDescending { it.lastUse }
)
}
}
private fun startFragment(fragment: Fragment) {
requireActivity().supportFragmentManager.beginTransaction()
.addToBackStack("Launcher")
.replace(R.id.container, fragment).commit()
}
private fun startScanner() {
startFragment(Scanner())
}
Box(modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)) {
private fun startCreateEntry() {
startFragment(CreateEntry())
}
private fun startViewEntry(title: String?, code: String?, fmt: String?) {
val viewEntryFragment = ViewEntry()
viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt)
startFragment(viewEntryFragment)
}
private fun recyclerSlideHelper(): ItemTouchHelper {
return ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
0, ItemTouchHelper.LEFT
PullToRefreshBox(
onRefresh = {
isRefreshingState = true
scope.launch {
onRefresh(context, navController)
isRefreshingState = false
}
},
isRefreshing = isRefreshingState,
modifier = Modifier.fillMaxSize()
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val pos = viewHolder.adapterPosition
CacheManager.rmFidelity(pos)
fidelityListAdapter.notifyItemRemoved(pos)
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier
.fillMaxSize()
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(sortedEntries.value) { entry ->
FidelityRow(navController, entry)
}
}
})
FloatingActionButton(
onClick = { onQuery() },
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 16.dp),
) {
Icon(
Icons.Default.Search,
contentDescription = "Query",
modifier = Modifier.size(32.dp)
)
}
FloatingActionButton(
onClick = { onAdd(navController) }, modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp)
) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
FloatingActionButton(
onClick = {
showHidden=!showHidden
}, modifier = Modifier
.align(Alignment.BottomStart)
.padding(16.dp).size(24.dp),
backgroundColor = if(showHidden) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary,
) {
Icon(Icons.Default.HideSource,
tint= if(showHidden) MaterialTheme.colors.background else MaterialTheme.colors.onSecondary,
contentDescription = "Show Hidden")
}
}
if (isRefreshingState)
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background.copy(alpha = 0.75f))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { }
)
)
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun FidelityRow(
navController: NavHostController,
e: FidelityEntry
) {
var expanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.fillMaxWidth()) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(2.dp)
.combinedClickable(
onClick = { onView(navController, e) },
onLongClick = { expanded = true },
),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colors.primary,
contentColor = MaterialTheme.colors.background
),
) {
Box(modifier = Modifier.fillMaxSize().padding(2.dp)) {
Row(modifier = Modifier.padding(14.dp)) {
Text(
text = e.title,
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.onPrimary
)
}
Row(modifier = Modifier.align(Alignment.TopEnd)) {
if (e.hidden)
Icon(
Icons.Default.HideSource, contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colors.onPrimary
)
if (e.hidden && e.pinned)
Spacer(modifier = Modifier.width(8.dp))
if (e.pinned)
Icon(
Icons.Default.PushPin, contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colors.onPrimary
)
}
}
}
DropdownMenu(
modifier = Modifier,
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(onClick = {
expanded = false
onEdit(navController, e)
}) {
Icon(
Icons.Default.Edit,
contentDescription = "edit",
)
Spacer(modifier= Modifier.width(8.dp))
Text("Edit")
}
DropdownMenuItem(onClick = {
expanded = false
onPin(e)
}) {
Icon(
Icons.Default.PushPin,
contentDescription = "pin",
)
Spacer(modifier= Modifier.width(8.dp))
if(e.pinned) Text("Unpin")
else Text("Pin")
}
DropdownMenuItem(onClick = {
expanded = false
onHide(e)
}) {
Icon(
Icons.Default.HideSource,
contentDescription = "hide",
)
Spacer(modifier= Modifier.width(8.dp))
if(e.hidden) Text("Unhide")
else Text("Hide")
}
}
}
}
object LauncherEventHandlers {
fun onAdd(navController: NavHostController) {
navController.navigate("edit")
}
fun onQuery() {
//TODO
}
var CRED: CredentialResult.Success? = null
suspend fun onSave(context: Context, navController: NavHostController){
try {
if (CRED == null) {
val res = loadCredentials(context)
when (res) {
CredentialResult.AuthFailed, CredentialResult.NoData -> null
is CredentialResult.Success -> CRED = res
}
}
CRED!!
val cred = withContext(Dispatchers.IO) {
genCredentials(context, CRED!!)
}
if (withContext(Dispatchers.IO) {
end(context, CRED!!.db, cred)
})
throw Exception("Error in saving")
} catch (e: Exception) {
println(e.toString())
navController.navigate("init")
}
}
suspend fun onRefresh(context: Context, navController: NavHostController) {
try {
if (CRED == null) {
val res = loadCredentials(context)
when (res) {
CredentialResult.AuthFailed, CredentialResult.NoData -> null
is CredentialResult.Success -> CRED = res
}
}
CRED!!
val cred = withContext(Dispatchers.IO) {
genCredentials(context, CRED!!)
}
if (withContext(Dispatchers.IO) {
start(context, CRED!!.db, cred)
})
importDB(context)
} catch (e: Exception) {
println(e.toString())
navController.navigate("init")
}
}
fun onView(navController: NavHostController, entry: FidelityEntry) {
navController.navigate("view/${entry.uid}")
val index = entries.indexOfFirst { it.uid == entry.uid }
if (index != -1)
entries[index] = entry.copy(lastUse = System.currentTimeMillis().toInt())
}
fun onPin(entry: FidelityEntry){
val index = entries.indexOfFirst { it.uid == entry.uid }
if (index != -1)
entries[index] = entry.copy(pinned = !entry.pinned)
}
fun onHide(entry: FidelityEntry){
val index = entries.indexOfFirst { it.uid == entry.uid }
if (index != -1)
entries[index] = entry.copy(hidden = !entry.hidden)
}
fun onEdit(navController: NavHostController, entry: FidelityEntry){
activeEntry.value = entry
navController.navigate("edit")
}
}

View File

@@ -1,116 +1,224 @@
@file:Suppress("PreviewAnnotationInFunctionWithParameters",
"PreviewAnnotationInFunctionWithParameters"
)
package net.helcel.fidelity.activity.fragment
import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
import android.os.Bundle
import android.graphics.BitmapFactory
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import net.helcel.fidelity.R
import net.helcel.fidelity.databinding.FragScannerBinding
import net.helcel.fidelity.tools.BarcodeScanner.getAnalysisUseCase
import net.helcel.fidelity.tools.KeepassWrapper
import androidx.camera.view.PreviewView
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FlashOn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.helcel.fidelity.activity.fragment.ScannerEventHandler.onResult
import net.helcel.fidelity.tools.BarcodeScanner
import net.helcel.fidelity.tools.BarcodeScanner.analysisUseCase
import net.helcel.fidelity.tools.FidelityRepository.activeEntry
private const val CAMERA_PERMISSION_REQUEST_CODE = 1
@androidx.compose.ui.tooling.preview.Preview
@Composable
fun ScannerScreen(
navController: NavController
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val scope = rememberCoroutineScope()
class Scanner : Fragment() {
val cameraProviderFuture = remember {
ProcessCameraProvider.getInstance(context)
}
var camera: Camera? by remember { mutableStateOf(null) }
var torchOn by remember { mutableStateOf(false) }
private lateinit var binding: FragScannerBinding
val done = remember { mutableStateOf(false) }
val previewView = remember { PreviewView(context) }
private var code: String = ""
private var fmt: String = ""
private var valid: Boolean = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragScannerBinding.inflate(layoutInflater)
binding.bottomText.setOnClickListener {
startCreateEntry()
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { granted ->
if (granted) {
val cameraProvider = cameraProviderFuture.get()
val previewUseCase = Preview.Builder().build().also {
it.surfaceProvider = previewView.surfaceProvider
}
val analysisUseCase = analysisUseCase { detectedCode, detectedFormat ->
if (detectedCode.isNullOrEmpty() || detectedFormat.isNullOrEmpty()) return@analysisUseCase
if(done.value) return@analysisUseCase
scope.launch(Dispatchers.Main) {
activeEntry.value =
activeEntry.value.copy(code = detectedCode, format = detectedFormat)
done.value = true
onResult(navController)
}
return@analysisUseCase
}
try {
cameraProvider.unbindAll()
camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
previewUseCase,
analysisUseCase
)
} catch (e: Exception) {
Log.e("ScannerScreen", "Camera bind failed: ${e.message}")
}
} else {
Toast.makeText(context, "Camera permission denied", Toast.LENGTH_SHORT).show()
scope.launch(Dispatchers.Main){
onResult(navController)
}
}
}
when (hasCameraPermission()) {
true -> bindCameraUseCases()
else -> requestPermission()
}
return binding.root
)
LaunchedEffect(Unit) {
permissionLauncher.launch(Manifest.permission.CAMERA)
}
private fun startCreateEntry() {
val createEntryFragment = CreateEntry()
createEntryFragment.arguments =
KeepassWrapper.bundleCreate(null, this.code, this.fmt)
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.container, createEntryFragment)
.commit()
}
private fun hasCameraPermission() =
ActivityCompat.checkSelfPermission(
requireContext(),
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
private fun requestPermission() {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(Manifest.permission.CAMERA),
CAMERA_PERMISSION_REQUEST_CODE
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
factory = { previewView },
modifier = Modifier.fillMaxSize()
)
ActivityCompat.OnRequestPermissionsResultCallback { c, p, i ->
require(c == CAMERA_PERMISSION_REQUEST_CODE)
require(p.contains(Manifest.permission.CAMERA))
val el = i[p.indexOf(Manifest.permission.CAMERA)]
if (el != PackageManager.PERMISSION_GRANTED) {
startCreateEntry()
}
ScannerOverlay(
modifier = Modifier.fillMaxSize()
)
Button(onClick = {
torchOn = !torchOn
camera?.cameraControl?.enableTorch(torchOn)
}, modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
) {
Icon(Icons.Default.FlashOn, contentDescription = null)
}
if(!done.value)
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.BottomCenter) // same spot as buttons
.padding(bottom =80.dp),
)
}
}
private fun bindCameraUseCases() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val previewUseCase = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.cameraView.surfaceProvider)
}
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
val analysisUseCase = getAnalysisUseCase { code, format ->
if (code != null && format != null) {
this.code = code
this.fmt = format
this.valid = true
@Composable
fun ScannerOverlay(
modifier: Modifier = Modifier
) {
Canvas(modifier = modifier.fillMaxSize()) {
val widthF = size.width
val heightF = size.height
drawRect(
color = Color(0x80000000), // semi-transparent black
size = size
)
val squareSize = 0.75f * minOf(widthF, heightF)
val left = (widthF - squareSize) / 2
val top = (heightF - squareSize) / 2
drawRect(
color = Color.Transparent,
topLeft = Offset(left, top),
size = Size(squareSize, squareSize),
blendMode = BlendMode.Clear
)
}
}
@Composable
fun FileScanner(navController: NavHostController) {
val context = LocalContext.current
rememberCoroutineScope()
val pickImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia()
) { uri ->
if (uri == null) {
Toast.makeText(context, "No file selected", Toast.LENGTH_SHORT).show()
onResult(navController)
return@rememberLauncherForActivityResult
}
try {
val inputStream = context.contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
BarcodeScanner.bitmapUseCase(bitmap) { code, format ->
if (!code.isNullOrEmpty() && !format.isNullOrEmpty()) {
activeEntry.value = activeEntry.value.copy(code=code, format=format)
onResult(navController)
} else {
this.valid = false
Toast.makeText(context, "No barcode found", Toast.LENGTH_SHORT).show()
onResult(navController)
}
}
try {
cameraProvider.bindToLifecycle(
this,
cameraSelector,
previewUseCase,
analysisUseCase
)
} catch (illegalStateException: IllegalStateException) {
Log.e(ContentValues.TAG, illegalStateException.message.orEmpty())
} catch (illegalArgumentException: IllegalArgumentException) {
Log.e(ContentValues.TAG, illegalArgumentException.message.orEmpty())
}
}, ContextCompat.getMainExecutor(requireContext()))
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(context, "Failed to load image", Toast.LENGTH_SHORT).show()
onResult(navController)
}
}
LaunchedEffect(Unit) {
pickImageLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
}
BackHandler {
onResult(navController)
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
object ScannerEventHandler {
fun onResult(navController: NavController) {
navController.popBackStack()
}
}

View File

@@ -0,0 +1,144 @@
package net.helcel.fidelity.activity.fragment
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import net.helcel.fidelity.tools.FidelityRepository
@Preview
@Composable
fun TreeSelectorDialog(onDismiss: (Node?) -> Unit = {}) {
Dialog(
onDismissRequest = {onDismiss(null)},
content = {
Column(
modifier = Modifier.fillMaxWidth().background(
MaterialTheme.colors.background,
RoundedCornerShape(8.dp)
)
) {
var currentRoot by remember { mutableStateOf(FidelityRepository.getRoot()) }
var selection by remember { mutableStateOf<Node?>(FidelityRepository.getRoot()) }
Column(
modifier = Modifier.fillMaxWidth().padding(8.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Button(
onClick = {
selection = currentRoot
currentRoot = currentRoot?.parent
},
enabled = currentRoot?.parent != null
) {
Icon(Icons.AutoMirrored.Filled.Undo, contentDescription = "up")
}
Spacer(modifier = Modifier.width(8.dp))
Text(
currentRoot?.title ?: "?",
color = MaterialTheme.colors.onBackground,
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.width(8.dp))
}
Spacer(modifier = Modifier.height(8.dp))
LazyColumn(modifier = Modifier.fillMaxHeight(0.75f)) {
items(currentRoot?.getChildGroups() ?: emptyList()) { entry ->
val isSel = (entry.nodeId == selection?.nodeId)
Row(
modifier = Modifier
.fillMaxWidth()
.background(color = if (isSel) MaterialTheme.colors.primary else MaterialTheme.colors.background)
.clickable {
if (entry.getChildEntries().isNotEmpty()) {
currentRoot = entry
selection = entry
} else if (entry.getChildGroups().isNotEmpty()) {
currentRoot = entry
selection = entry
} else {
selection = entry
}
}
.padding(8.dp)
) {
if (entry.getChildEntries().isNotEmpty() || entry.getChildGroups()
.isNotEmpty()
) {
Icon(
imageVector = Icons.Default.ExpandMore,
contentDescription = null,
tint = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground
)
}
Text(
entry.title,
modifier = Modifier.padding(start = 8.dp),
color = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground
)
}
}
items(currentRoot?.getChildEntries() ?: emptyList()) { entry ->
val isSel = (entry.nodeId == selection?.nodeId)
Row(
modifier = Modifier
.fillMaxWidth()
.background(color = if (isSel) MaterialTheme.colors.primary else MaterialTheme.colors.background)
.clickable {
selection = entry
}
.padding(8.dp)
) {
Text(
entry.title,
modifier = Modifier.padding(start = 8.dp),
color = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Button(
modifier = Modifier.align(Alignment.CenterHorizontally),
enabled = selection != null,
onClick = {
onDismiss(selection)
}) {
Text("Select " + if (selection is Group) "Group" else "Entry")
}
}
}
}
)
}

View File

@@ -0,0 +1,274 @@
package net.helcel.fidelity.activity.fragment
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts.OpenDocument
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.Checkbox
import androidx.compose.material.CheckboxDefaults
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.helcel.fidelity.activity.ToastHelper
import net.helcel.fidelity.activity.fragment.SetupEventHandlers.onOpen
import net.helcel.fidelity.tools.CredentialResult
import net.helcel.fidelity.tools.FidelityRepository.genCredentials
import net.helcel.fidelity.tools.FidelityRepository.start
import net.helcel.fidelity.tools.KeePassStore.loadCredentials
import net.helcel.fidelity.tools.KeePassStore.packCredentials
import net.helcel.fidelity.tools.KeePassStore.saveCredentials
class GetPersistentContent : OpenDocument() {
@SuppressLint("InlinedApi")
override fun createIntent(context: Context, input: Array<String>): Intent {
return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_DEFAULT)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
}
}
@Preview
@Composable
fun InitialScreen(
navController: NavHostController?
) {
var loading by remember { mutableStateOf(false) }
var dbFile by remember { mutableStateOf<Uri?>(null) }
var password by remember { mutableStateOf("") }
var keyFile by remember { mutableStateOf<Uri?>(null) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
val dbFilePickerLauncher = rememberLauncherForActivityResult(
contract = GetPersistentContent(),
) {
if(it!=null) {
dbFile = it
scope.launch(Dispatchers.IO) {
context.contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
}
}
val keyFilePickerLauncher = rememberLauncherForActivityResult(
contract = GetPersistentContent()
) {
if(it!=null) {
keyFile = it
scope.launch(Dispatchers.IO) {
context.contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
}
}
BackHandler {
navController!!.navigate("exit")
}
LaunchedEffect(Unit) {
scope.launch(Dispatchers.Main) {
when(val res = loadCredentials(context)) {
CredentialResult.AuthFailed -> null
CredentialResult.NoData -> null
is CredentialResult.Success -> {
if (res.db != null) dbFile = res.db
if (res.key != null) keyFile = res.key
if (res.password != "" && password == "") password = res.password
}
}
}
}
Box(modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.background(MaterialTheme.colors.background),
verticalArrangement = Arrangement.Center
) {
Text(
"Keypass Database Setup",
style = MaterialTheme.typography.h5,
color = MaterialTheme.colors.onBackground
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text("KDBX Database:", color = MaterialTheme.colors.onBackground)
Spacer(modifier = Modifier.width(8.dp))
Checkbox(
enabled = !loading,
modifier = Modifier
.background(
MaterialTheme.colors.primary,
RoundedCornerShape(8.dp)
)
.size(32.dp),
checked = dbFile != null,
onCheckedChange = { dbFilePickerLauncher.launch(arrayOf("*/*")) },
colors = CheckboxDefaults.colors(
uncheckedColor = MaterialTheme.colors.primary,
checkedColor = MaterialTheme.colors.primary,
checkmarkColor = MaterialTheme.colors.onPrimary
),
)
}
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
enabled = !loading,
value = password,
onValueChange = { password = it },
label = { Text("Password") },
singleLine = true,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Unspecified,
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
colors = TextFieldDefaults.textFieldColors(
textColor = MaterialTheme.colors.onBackground
),
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text("KDBX Key File:", color = MaterialTheme.colors.onBackground)
Spacer(modifier = Modifier.width(8.dp))
Checkbox(
enabled = !loading,
modifier = Modifier
.background(
MaterialTheme.colors.primary,
RoundedCornerShape(8.dp)
)
.size(32.dp),
checked = keyFile != null,
onCheckedChange = { keyFilePickerLauncher.launch(arrayOf("*/*")) },
colors = CheckboxDefaults.colors(
uncheckedColor = MaterialTheme.colors.primary,
checkedColor = MaterialTheme.colors.primary,
checkmarkColor = MaterialTheme.colors.onPrimary
),
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
enabled = !loading && password.isNotBlank() && dbFile != null ,
onClick = {
loading = true
scope.launch {
if(onOpen(context, dbFile!!, password, keyFile)){
navController!!.popBackStack()
navController.navigate("init")
}else{
ToastHelper.show(context, "Auth failed...")
navController!!.popBackStack()
navController.navigate("exit")
}
}
},
modifier = Modifier.fillMaxWidth()
) {
Text("Continue")
}
}
Box(contentAlignment = Alignment.BottomCenter, modifier = Modifier
.fillMaxSize()
.padding(32.dp)){
if(loading )
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.BottomCenter) // same spot as buttons
.padding(bottom = 80.dp),
)
}
}
}
object SetupEventHandlers {
suspend fun onOpen(context: Context, db: Uri, p: String, key: Uri?): Boolean {
try {
val packCred = packCredentials(db, p, key)
withContext(Dispatchers.IO) {
start(context, db, genCredentials(context, packCred)
)
}
val res = withContext(Dispatchers.Main) {
saveCredentials(context, packCred)
}
return when (res) {
CredentialResult.AuthFailed, CredentialResult.NoData -> false
is CredentialResult.Success -> true
}
} catch (e: Exception) {
ToastHelper.show(context, e.message.toString())
println("Err${e.toString()}")
println(e.message)
return false
}
}
}

View File

@@ -1,75 +1,125 @@
package net.helcel.fidelity.activity.fragment
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.google.zxing.FormatException
import net.helcel.fidelity.databinding.FragViewEntryBinding
import android.app.Activity
import android.graphics.Bitmap
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
import net.helcel.fidelity.tools.ErrorToaster
import net.helcel.fidelity.tools.KeepassWrapper
import net.helcel.fidelity.tools.FidelityEntry
import kotlin.let
import kotlin.math.min
class ViewEntry : Fragment() {
@Preview
@Composable
fun PreviewEntryScreen(){
ViewEntryScreen(null, FidelityEntry("Title","AAA","QR"))
}
private lateinit var binding: FragViewEntryBinding
@Composable
fun ViewEntryScreen(
navController: NavHostController?,
entry: FidelityEntry
) {
val context = LocalContext.current
val activity = context as? Activity
var isFull by remember { mutableStateOf(false) }
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
private var title: String? = null
private var code: String? = null
private var fmt: String? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragViewEntryBinding.inflate(layoutInflater)
val res = KeepassWrapper.bundleExtract(arguments)
title = res.first
code = res.second
fmt = res.third
adjustLayout()
updatePreview()
return binding.root
}
private fun updatePreview() {
binding.title.text = title
SideEffect {
activity?.window?.attributes = activity.window?.attributes?.apply {
screenBrightness = if (isFull) 1f else BRIGHTNESS_OVERRIDE_NONE
}
try {
val barcodeBitmap = generateBarcode(
code!!, fmt!!, 1024
bitmap = generateBarcode(entry.code, entry.format, 1024)
} catch (_: Exception) {
bitmap = null
Toast.makeText(context, "Invalid barcode format", Toast.LENGTH_SHORT).show()
}
}
BackHandler {
isFull=false
navController!!.popBackStack()
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
.clickable(
onClick = { isFull = !isFull },
indication = null, // remove ripple effect
interactionSource = remember { MutableInteractionSource() }
),
contentAlignment = Alignment.TopCenter
) {
if (!isFull) {
Text(
text = entry.title,
color = Color.White,
style = MaterialTheme.typography.h4,
modifier = Modifier.padding(32.dp)
)
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
} catch (e: FormatException) {
ErrorToaster.invalidFormat(requireActivity())
binding.imageViewPreview.setImageBitmap(null)
} catch (e: IllegalArgumentException) {
binding.imageViewPreview.setImageBitmap(null)
ErrorToaster.invalidFormat(requireActivity())
} catch (e: Exception) {
binding.imageViewPreview.setImageBitmap(null)
println(e.javaClass)
println(e.message)
e.printStackTrace()
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
adjustLayout()
}
BoxWithConstraints(
modifier = Modifier
.fillMaxSize().padding(8.dp),
contentAlignment = Alignment.Center
) {
bitmap?.let {
private fun adjustLayout() {
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
binding.title.visibility = View.GONE
} else {
binding.title.visibility = View.VISIBLE
val modifier = Modifier
.fillMaxSize()
.width(maxWidth)
.height(maxHeight)
.padding(16.dp)
.aspectRatio(it.width.toFloat()/it.height.toFloat())
.rotate(if (isFull) 90f else 0f)
.scale(if(isFull) min(it.width.dp/maxHeight,it.height.dp/maxWidth) else 1f)
Image(
bitmap = it.asImageBitmap(),
contentDescription = "Barcode",
modifier = modifier,
contentScale = ContentScale.Fit,
)
} ?: CircularProgressIndicator(color = Color.White)
}
}
}

View File

@@ -1,179 +0,0 @@
package net.helcel.fidelity.pluginSDK
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.text.TextUtils
import android.util.Log
import org.json.JSONArray
import org.json.JSONException
object AccessManager {
private const val _tag = "Kp2aPluginSDK"
private const val PREF_KEY_SCOPE = "scope"
private const val PREF_KEY_TOKEN = "token"
private fun stringArrayToString(values: ArrayList<String?>): String? {
val a = JSONArray()
for (i in values.indices) {
a.put(values[i])
}
return if (values.isNotEmpty()) {
a.toString()
} else {
null
}
}
private fun stringToStringArray(s: String?): ArrayList<String> {
val strings = ArrayList<String>()
if (!TextUtils.isEmpty(s)) {
try {
val a = JSONArray(s)
for (i in 0 until a.length()) {
val url = a.optString(i)
strings.add(url)
}
} catch (e: JSONException) {
e.printStackTrace()
}
}
return strings
}
fun storeAccessToken(
ctx: Context,
hostPackage: String,
accessToken: String,
scopes: ArrayList<String?>
) {
val prefs = getPrefsForHost(ctx, hostPackage)
val edit = prefs.edit()
edit.putString(PREF_KEY_TOKEN, accessToken)
val scopesString = stringArrayToString(scopes)
edit.putString(PREF_KEY_SCOPE, scopesString)
edit.apply()
Log.d(
_tag,
"stored access token " + accessToken.substring(
0,
4
) + "... for " + scopes.size + " scopes (" + scopesString + ")."
)
val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
if (!hostPrefs.contains(hostPackage)) {
hostPrefs.edit().putString(hostPackage, "").apply()
}
}
fun preparePopup(popupMenu: Any) {
try {
val fields = popupMenu.javaClass.declaredFields
for (field in fields) {
if ("mPopup" == field.name) {
field.isAccessible = true
val menuPopupHelper = field[popupMenu]
val classPopupHelper = Class.forName(
menuPopupHelper
.javaClass.name
)
val setForceIcons = classPopupHelper.getMethod(
"setForceShowIcon", Boolean::class.javaPrimitiveType
)
setForceIcons.invoke(menuPopupHelper, true)
break
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun getPrefsForHost(
ctx: Context,
hostPackage: String
): SharedPreferences {
val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE)
return prefs
}
fun tryGetAccessToken(ctx: Context, hostPackage: String, scopes: ArrayList<String?>): String? {
if (TextUtils.isEmpty(hostPackage)) {
Log.d(_tag, "hostPackage is empty!")
return null
}
Log.d(_tag, "trying to find prefs for $hostPackage")
val prefs = getPrefsForHost(ctx, hostPackage)
val scopesString = prefs.getString(PREF_KEY_SCOPE, "")
Log.d(_tag, "available scopes: $scopesString")
val currentScope = stringToStringArray(scopesString)
if (isSubset(scopes, currentScope)) {
return prefs.getString(PREF_KEY_TOKEN, null)
} else {
Log.d(_tag, "looks like scope changed. Access token invalid.")
return null
}
}
private fun isSubset(
requiredScopes: ArrayList<String?>,
availableScopes: ArrayList<String>
): Boolean {
for (r in requiredScopes) {
if (availableScopes.indexOf(r) < 0) {
Log.d(_tag, "Scope " + r + " not available. " + availableScopes.size)
return false
}
}
return true
}
fun removeAccessToken(
ctx: Context, hostPackage: String,
accessToken: String
) {
val prefs = getPrefsForHost(ctx, hostPackage)
Log.d(_tag, "removing AccessToken.")
if (prefs.getString(PREF_KEY_TOKEN, "") == accessToken) {
val edit = prefs.edit()
edit.clear()
edit.apply()
}
val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
if (hostPrefs.contains(hostPackage)) {
hostPrefs.edit().remove(hostPackage).apply()
}
}
fun getAllHostPackages(ctx: Context): Set<String> {
val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
val result: MutableSet<String> = HashSet()
for (host in prefs.all.keys) {
try {
val info = ctx.packageManager.getPackageInfo(host, PackageManager.GET_META_DATA)
//if we get here, the package is still there
result.add(host)
} catch (e: PackageManager.NameNotFoundException) {
//host gone. ignore.
}
}
return result
}
/**
* Returns a valid access token or throws PluginAccessException
*/
fun getAccessToken(
context: Context, hostPackage: String,
scopes: ArrayList<String?>
): String {
val accessToken = tryGetAccessToken(context, hostPackage, scopes)
?: throw PluginAccessException(hostPackage, scopes)
return accessToken
}
}

View File

@@ -1,45 +0,0 @@
package net.helcel.fidelity.pluginSDK
object KeepassDefs {
/// <summary>
/// Default identifier string for the title field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
var TitleField: String = "Title"
/// <summary>
/// Default identifier string for the user name field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
private var UserNameField: String = "UserName"
/// <summary>
/// Default identifier string for the password field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
private var PasswordField: String = "Password"
/// <summary>
/// Default identifier string for the URL field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
var UrlField: String = "URL"
/// <summary>
/// Default identifier string for the notes field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
private var NotesField: String = "Notes"
fun IsStandardField(strFieldName: String?): Boolean {
if (strFieldName == null) return false
if (strFieldName == TitleField) return true
if (strFieldName == UserNameField) return true
if (strFieldName == PasswordField) return true
if (strFieldName == UrlField) return true
if (strFieldName == NotesField) return true
return false
}
}

View File

@@ -1,107 +0,0 @@
package net.helcel.fidelity.pluginSDK
import android.content.Intent
import android.text.TextUtils
import org.json.JSONException
import org.json.JSONObject
object Kp2aControl {
/**
* Creates and returns an intent to launch Keepass2Android for adding an entry with the given fields.
* @param fields Key/Value pairs of the field values. See KeepassDefs for standard keys.
* @param protectedFields List of keys of the protected fields.
* @return Intent to start Keepass2Android.
* @throws JSONException
*/
fun getAddEntryIntent(
fields: HashMap<String?, String?>?,
protectedFields: ArrayList<String?>?
): Intent {
return getAddEntryIntent(JSONObject((fields as Map<*, *>?)!!).toString(), protectedFields)
}
private fun getAddEntryIntent(
outputData: String?,
protectedFields: ArrayList<String?>?
): Intent {
val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK)
startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT)
startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startKp2aIntent.putExtra("KP2A_APPTASK", "CreateEntryThenCloseTask")
startKp2aIntent.putExtra("ShowUserNotifications", "false") //KP2A expects a StringExtra
startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData)
if (protectedFields != null) startKp2aIntent.putStringArrayListExtra(
Strings.EXTRA_PROTECTED_FIELDS_LIST,
protectedFields
)
return startKp2aIntent
}
/**
* Creates an intent to open a Password Entry matching searchText
* @param searchText queryString
* @param showUserNotifications if true, the notifications (copy to clipboard, keyboard) are displayed
* @param closeAfterOpen if true, the entry is opened and KP2A is immediately closed
* @return Intent to start KP2A with
*/
fun getOpenEntryIntent(
searchText: String?,
showUserNotifications: Boolean,
closeAfterOpen: Boolean
): Intent {
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", "SearchUrlTask")
startKp2aIntent.putExtra("ShowUserNotifications", showUserNotifications.toString())
startKp2aIntent.putExtra("CloseAfterCreate", closeAfterOpen.toString())
startKp2aIntent.putExtra("UrlToSearch", searchText)
return startKp2aIntent
}
/**
* Creates an intent to query a password entry from KP2A. The credentials are returned as Activity result.
* @param searchText Text to search for. Should be a URL or "androidapp://com.my.package."
* @return an Intent to start KP2A with
*/
fun getQueryEntryIntent(searchText: String?): Intent {
val i = Intent(Strings.ACTION_QUERY_CREDENTIALS)
if (!TextUtils.isEmpty(searchText)) i.putExtra(Strings.EXTRA_QUERY_STRING, searchText)
return i
}
val queryEntryIntentForOwnPackage: Intent
/**
* Creates an intent to query a password entry from KP2A, matching to the current app's package .
* The credentials are returned as Activity result.
* This requires SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE.
* @return an Intent to start KP2A with
*/
get() = Intent(Strings.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE)
/**
* Converts the entry fields returned in an intent from a query to a hashmap.
* @param intent data received in onActivityResult after getQueryEntryIntent(ForOwnPackage)
* @return HashMap with keys = field names (see KeepassDefs for standard keys) and values = values
*/
fun getEntryFieldsFromIntent(intent: Intent): HashMap<String, String> {
val res = HashMap<String, String>()
try {
val json = JSONObject(intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)!!)
val iter = json.keys()
while (iter.hasNext()) {
val key = iter.next()
val value = json[key].toString()
res[key] = value
}
} catch (e: JSONException) {
e.printStackTrace()
} catch (e: NullPointerException) {
e.printStackTrace()
}
return res
}
}

View File

@@ -1,82 +0,0 @@
package net.helcel.fidelity.pluginSDK
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
/**
* Broadcast flow between Host and Plugin
* ======================================
*
* The host is responsible for deciding when to initiate the session. It
* should initiate the session as soon as plugins are required or when a plugin
* has been updated through the OS.
* It will then send a broadcast to request the currently required scope.
* The plugin then sends a broadcast to the app which scope is required. If an
* access token is already available, it's sent along with the requset.
*
* If a previous permission has been revoked (or the app settings cleared or the
* permissions have been extended or the token is invalid for any other reason)
* the host will answer with a Revoked-Permission broadcast (i.e. the plugin is
* unconnected.)
*
* Unconnected plugins must be permitted by the user (requiring user action).
* When the user grants access, the plugin will receive an access token for
* the host. This access token is valid for the requested scope. If the scope
* changes (e.g after an update of the plugin), the access token becomes invalid.
*
*/
abstract class PluginAccessBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
val action = intent.action
Log.d(_tag, "received broadcast with action=$action")
if (action == null) 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)
Log.d(_tag, "requesting access for " + scopes.size + " tokens.")
ctx.sendBroadcast(rpi)
}
/**
*
* @return the list of required scopes for this plugin.
*/
abstract val scopes: ArrayList<String?>
companion object {
private const val _tag = "Kp2aPluginSDK"
}
}

View File

@@ -1,14 +0,0 @@
package net.helcel.fidelity.pluginSDK
class PluginAccessException : Exception {
constructor(what: String?) : super(what)
constructor(hostPackage: String?, scopes: ArrayList<String?>)
companion object {
/**
*
*/
private const val serialVersionUID = 1L
}
}

View File

@@ -1,16 +0,0 @@
package net.helcel.fidelity.pluginSDK
import kotlin.collections.ArrayList
class PluginAccessReceiver : PluginAccessBroadcastReceiver() {
override val scopes: ArrayList<String?> = ArrayList()
init {
this.scopes.add(Strings.SCOPE_DATABASE_ACTIONS)
this.scopes.add(Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE)
}
}

View File

@@ -1,224 +0,0 @@
package net.helcel.fidelity.pluginSDK
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
class PluginActionBroadcastReceiver : BroadcastReceiver() {
open class PluginActionBase
(var context: Context, protected var _intent: Intent) {
val hostPackage: String?
get() = _intent.getStringExtra(Strings.EXTRA_SENDER)
}
open class PluginEntryActionBase(context: Context, intent: Intent) :
PluginActionBase(context, intent) {
protected val entryFieldsFromIntent: HashMap<String, String>
get() {
val res = HashMap<String, String>()
try {
val json = JSONObject(_intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)!!)
val iter = json.keys()
while (iter.hasNext()) {
val key = iter.next()
val value = json[key].toString()
res[key] = value
}
} catch (e: JSONException) {
e.printStackTrace()
}
return res
}
protected val protectedFieldsListFromIntent: Array<String?>?
get() {
try {
val json =
JSONArray(_intent.getStringExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST))
val res = arrayOfNulls<String>(json.length())
for (i in 0 until json.length()) res[i] = json.getString(i)
return res
} catch (e: JSONException) {
e.printStackTrace()
return null
}
}
open val entryId: String?
get() = _intent.getStringExtra(Strings.EXTRA_ENTRY_ID)
@Throws(PluginAccessException::class)
fun setEntryField(fieldId: String?, fieldValue: String?, isProtected: Boolean) {
val i = Intent(Strings.ACTION_SET_ENTRY_FIELD)
val scope = ArrayList<String?>()
scope.add(Strings.SCOPE_CURRENT_ENTRY)
i.putExtra(
Strings.EXTRA_ACCESS_TOKEN, AccessManager.getAccessToken(
context, hostPackage!!, scope
)
)
i.setPackage(hostPackage)
i.putExtra(Strings.EXTRA_SENDER, context.packageName)
i.putExtra(Strings.EXTRA_FIELD_VALUE, fieldValue)
i.putExtra(Strings.EXTRA_ENTRY_ID, entryId)
i.putExtra(Strings.EXTRA_FIELD_ID, fieldId)
i.putExtra(Strings.EXTRA_FIELD_PROTECTED, isProtected)
context.sendBroadcast(i)
}
}
private inner class ActionSelectedAction(ctx: Context, intent: Intent) :
PluginEntryActionBase(ctx, intent) {
val actionData: Bundle?
/**
*
* @return the Bundle associated with the action. This bundle can be set in OpenEntry.add(Entry)FieldAction
*/
get() = _intent.getBundleExtra(Strings.EXTRA_ACTION_DATA)
private val fieldId: String?
/**
*
* @return the field id which was selected. null if an entry action (in the options menu) was selected.
*/
get() = _intent.getStringExtra(Strings.EXTRA_FIELD_ID)
val isEntryAction: Boolean
/**
*
* @return true if an entry action, i.e. an option from the options menu, was selected. False if an option
* in a popup menu for a certain field was selected.
*/
get() = fieldId == null
val entryFields: HashMap<String, String>
/**
*
* @return a hashmap containing the entry fields in key/value form
*/
get() = entryFieldsFromIntent
val protectedFieldsList: Array<String?>?
/**
*
* @return an array with the keys of all protected fields in the entry
*/
get() = protectedFieldsListFromIntent
}
private inner class CloseEntryViewAction(context: Context, intent: Intent) :
PluginEntryActionBase(context, intent) {
override val entryId: String?
get() = _intent.getStringExtra(Strings.EXTRA_ENTRY_ID)
}
private open inner class OpenEntryAction(context: Context, intent: Intent) :
PluginEntryActionBase(context, intent) {
val entryFields: HashMap<String, String>
get() = entryFieldsFromIntent
val protectedFieldsList: Array<String?>?
/**
*
* @return an array with the keys of all protected fields in the entry
*/
get() = protectedFieldsListFromIntent
@Throws(PluginAccessException::class)
fun addEntryAction(
actionDisplayText: String?,
actionIconResourceId: Int,
actionData: Bundle?
) {
addEntryFieldAction(null, null, actionDisplayText, actionIconResourceId, actionData)
}
@Throws(PluginAccessException::class)
fun addEntryFieldAction(
actionId: String?,
fieldId: String?,
actionDisplayText: String?,
actionIconResourceId: Int,
actionData: Bundle?
) {
val i = Intent(Strings.ACTION_ADD_ENTRY_ACTION)
val scope = ArrayList<String?>()
scope.add(Strings.SCOPE_CURRENT_ENTRY)
i.putExtra(
Strings.EXTRA_ACCESS_TOKEN, AccessManager.getAccessToken(
context, hostPackage!!, scope
)
)
i.setPackage(hostPackage)
i.putExtra(Strings.EXTRA_SENDER, context.packageName)
i.putExtra(Strings.EXTRA_ACTION_DATA, actionData)
i.putExtra(Strings.EXTRA_ACTION_DISPLAY_TEXT, actionDisplayText)
i.putExtra(Strings.EXTRA_ACTION_ICON_RES_ID, actionIconResourceId)
i.putExtra(Strings.EXTRA_ENTRY_ID, entryId)
i.putExtra(Strings.EXTRA_FIELD_ID, fieldId)
i.putExtra(Strings.EXTRA_ACTION_ID, actionId)
context.sendBroadcast(i)
}
}
private inner class DatabaseAction(context: Context, intent: Intent) :
PluginActionBase(context, intent) {
val fileDisplayName: String?
get() = _intent.getStringExtra(Strings.EXTRA_DATABASE_FILE_DISPLAYNAME)
val filePath: String?
get() = _intent.getStringExtra(Strings.EXTRA_DATABASE_FILEPATH)
val action: String?
get() = _intent.action
}
//EntryOutputModified is very similar to OpenEntry because it receives the same
//data (+ the field id which was modified)
private inner class EntryOutputModifiedAction(context: Context, intent: Intent) :
OpenEntryAction(context, intent) {
val modifiedFieldId: String?
get() = _intent.getStringExtra(Strings.EXTRA_FIELD_ID)
}
override fun onReceive(ctx: Context, intent: Intent) {
val action = intent.action
Log.d(
"KP2A.pluginsdk",
"received broadcast in PluginActionBroadcastReceiver with action=$action"
)
if (action == null) return
if (action == Strings.ACTION_OPEN_ENTRY) {
openEntry(OpenEntryAction(ctx, intent))
} else if (action == Strings.ACTION_CLOSE_ENTRY_VIEW) {
closeEntryView(CloseEntryViewAction(ctx, intent))
} else if (action == Strings.ACTION_ENTRY_ACTION_SELECTED) {
actionSelected(ActionSelectedAction(ctx, intent))
} else if (action == Strings.ACTION_ENTRY_OUTPUT_MODIFIED) {
entryOutputModified(EntryOutputModifiedAction(ctx, intent))
} else if (action == Strings.ACTION_LOCK_DATABASE || action == Strings.ACTION_UNLOCK_DATABASE || action == Strings.ACTION_OPEN_DATABASE || action == Strings.ACTION_CLOSE_DATABASE) {
dbAction(DatabaseAction(ctx, intent))
} else {
//TODO handle unexpected action
}
}
private fun closeEntryView(closeEntryView: CloseEntryViewAction?) {}
private fun actionSelected(actionSelected: ActionSelectedAction?) {}
private fun openEntry(oe: OpenEntryAction?) {}
private fun entryOutputModified(eom: EntryOutputModifiedAction?) {}
private fun dbAction(db: DatabaseAction?) {}
}

View File

@@ -1,195 +0,0 @@
package net.helcel.fidelity.pluginSDK
object Strings {
/**
* Plugin is notified about actions like open/close/update a database.
*/
const val SCOPE_DATABASE_ACTIONS = "keepass2android.SCOPE_DATABASE_ACTIONS"
/**
* Plugin is notified when an entry is opened.
*/
const val SCOPE_CURRENT_ENTRY = "keepass2android.SCOPE_CURRENT_ENTRY"
/**
* Plugin may query credentials for its own package
*/
const val SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
"keepass2android.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
/**
* Plugin may query credentials for a deliberate package
*/
const val SCOPE_QUERY_CREDENTIALS = "keepass2android.SCOPE_QUERY_CREDENTIALS"
/**
* Extra key to transfer a (json serialized) list of scopes
*/
const val EXTRA_SCOPES = "keepass2android.EXTRA_SCOPES"
const val EXTRA_PLUGIN_PACKAGE = "keepass2android.EXTRA_PLUGIN_PACKAGE"
/**
* Extra key for sending the package name of the sender of a broadcast.
* Should be set in every broadcast.
*/
const val EXTRA_SENDER = "keepass2android.EXTRA_SENDER"
/**
* Extra key for sending a request token. The request token is passed from
* KP2A to the plugin. It's used in the authorization process.
*/
const val EXTRA_REQUEST_TOKEN = "keepass2android.EXTRA_REQUEST_TOKEN"
/**
* Action to start KP2A with an AppTask
*/
const val ACTION_START_WITH_TASK = "keepass2android.ACTION_START_WITH_TASK"
/**
* Action sent from KP2A to the plugin to indicate that the plugin should request
* access (sending it's scopes)
*/
const val ACTION_TRIGGER_REQUEST_ACCESS = "keepass2android.ACTION_TRIGGER_REQUEST_ACCESS"
/**
* Action sent from the plugin to KP2A including the scopes.
*/
const val ACTION_REQUEST_ACCESS = "keepass2android.ACTION_REQUEST_ACCESS"
/**
* Action sent from the KP2A to the plugin when the user grants access.
* Will contain an access token.
*/
const val ACTION_RECEIVE_ACCESS = "keepass2android.ACTION_RECEIVE_ACCESS"
/**
* Action sent from KP2A to the plugin to indicate that access is not or no longer valid.
*/
const val ACTION_REVOKE_ACCESS = "keepass2android.ACTION_REVOKE_ACCESS"
/**
* Action for startActivity(). Opens an activity in the Plugin Host to edit the plugin settings (i.e. enable it)
*/
const val ACTION_EDIT_PLUGIN_SETTINGS = "keepass2android.ACTION_EDIT_PLUGIN_SETTINGS"
/**
* Action sent from KP2A to the plugin to indicate that an entry was opened.
* The Intent contains the full entry data.
*/
const val ACTION_OPEN_ENTRY = "keepass2android.ACTION_OPEN_ENTRY"
/**
* Action sent from KP2A to the plugin to indicate that an entry output field was modified/added.
* The Intent contains the full new entry data.
*/
const val ACTION_ENTRY_OUTPUT_MODIFIED = "keepass2android.ACTION_ENTRY_OUTPUT_MODIFIED"
/**
* Action sent from KP2A to the plugin to indicate that an entry activity was closed.
*/
const val ACTION_CLOSE_ENTRY_VIEW = "keepass2android.ACTION_CLOSE_ENTRY_VIEW"
/**
* Extra key for a string containing the GUID of the entry.
*/
const val EXTRA_ENTRY_ID = "keepass2android.EXTRA_ENTRY_DATA"
/**
* Json serialized data of the PwEntry (C# class) representing the opened entry.
* currently not implemented.
*/
//const val EXTRA_ENTRY_DATA = "keepass2android.EXTRA_ENTRY_DATA";
/**
* Json serialized list of fields, transformed using the database context (i.e. placeholders are replaced already)
*/
const val EXTRA_ENTRY_OUTPUT_DATA = "keepass2android.EXTRA_ENTRY_OUTPUT_DATA"
/**
* Json serialized lisf of field keys, specifying which field of the EXTRA_ENTRY_OUTPUT_DATA is protected.
*/
const val EXTRA_PROTECTED_FIELDS_LIST = "keepass2android.EXTRA_PROTECTED_FIELDS_LIST"
/**
* Extra key for passing the access token (both ways)
*/
const val EXTRA_ACCESS_TOKEN = "keepass2android.EXTRA_ACCESS_TOKEN"
/**
* Action for an intent from the plugin to KP2A to add menu options regarding the currently open entry.
* Requires SCOPE_CURRENT_ENTRY.
*/
const val ACTION_ADD_ENTRY_ACTION = "keepass2android.ACTION_ADD_ENTRY_ACTION"
const val EXTRA_ACTION_DISPLAY_TEXT = "keepass2android.EXTRA_ACTION_DISPLAY_TEXT"
const val EXTRA_ACTION_ICON_RES_ID = "keepass2android.EXTRA_ACTION_ICON_RES_ID"
const val EXTRA_FIELD_ID = "keepass2android.EXTRA_FIELD_ID"
/**
* Used to pass an id for the action. Each actionId may occur only once per field, otherwise the previous
* action with same id is replaced by the new action.
*/
const val EXTRA_ACTION_ID = "keepass2android.EXTRA_ACTION_ID"
/** Extra for ACTION_ADD_ENTRY_ACTION and ACTION_ENTRY_ACTION_SELECTED to pass data specifying the action parameters.*/
const val EXTRA_ACTION_DATA = "keepass2android.EXTRA_ACTION_DATA"
/**
* Action for an intent from KP2A to the plugin when an action added with ACTION_ADD_ENTRY_ACTION was selected by the user.
*
*/
const val ACTION_ENTRY_ACTION_SELECTED = "keepass2android.ACTION_ENTRY_ACTION_SELECTED"
/**
* Extra key for the string which is used to query the credentials. This should be either a URL for
* a web login (google.com or a full URI) or something in the form "androidapp://com.my.package"
*/
const val EXTRA_QUERY_STRING = "keepass2android.EXTRA_QUERY_STRING"
/**
* Action when plugin wants to query credentials for its own package
*/
const val ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
"keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
/**
* Action when plugin wants to query credentials for a deliberate package
* The query string is passed as intent data
*/
const val ACTION_QUERY_CREDENTIALS = "keepass2android.ACTION_QUERY_CREDENTIALS"
/**
* Action for an intent from the plugin to KP2A to set (i.e. add or update) a field in the entry.
* May be used to update existing or add new fields at any time while the entry is opened.
*/
const val ACTION_SET_ENTRY_FIELD = "keepass2android.ACTION_SET_ENTRY_FIELD"
/** Actions for an intent from KP2A to the plugin to inform that a database was opened, closed, quicklocked or quickunlocked.*/
const val ACTION_OPEN_DATABASE = "keepass2android.ACTION_OPEN_DATABASE"
const val ACTION_CLOSE_DATABASE = "keepass2android.ACTION_CLOSE_DATABASE"
const val ACTION_LOCK_DATABASE = "keepass2android.ACTION_LOCK_DATABASE"
const val ACTION_UNLOCK_DATABASE = "keepass2android.ACTION_UNLOCK_DATABASE"
/** Extra for ACTION_OPEN_DATABASE and ACTION_CLOSE_DATABASE containing a filepath which is used
* by KP2A internally to identify the file. Use only where necessary, might contain credentials
* for accessing the file (on remote storage).*/
const val EXTRA_DATABASE_FILEPATH = "keepass2android.EXTRA_DATABASE_FILEPATH"
/** Extra for ACTION_OPEN_DATABASE and ACTION_CLOSE_DATABASE containing a filepath which can be
* displayed to the user.*/
const val EXTRA_DATABASE_FILE_DISPLAYNAME = "keepass2android.EXTRA_DATABASE_FILE_DISPLAYNAME"
const val EXTRA_FIELD_VALUE = "keepass2android.EXTRA_FIELD_VALUE"
const val EXTRA_FIELD_PROTECTED = "keepass2android.EXTRA_FIELD_PROTECTED"
const val PREFIX_STRING = "STRING_"
const val PREFIX_BINARY = "BINARY_"
}

View File

@@ -1,77 +0,0 @@
package net.helcel.fidelity.tools
import android.content.ContentValues
import android.util.Log
import androidx.annotation.OptIn
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString
import java.util.concurrent.Executors
@OptIn(ExperimentalGetImage::class)
object BarcodeScanner {
private fun processImageProxy(
barcodeScanner: BarcodeScanner,
imageProxy: ImageProxy,
cb: (String?, String?) -> Unit
) {
imageProxy.image?.let { image ->
val inputImage =
InputImage.fromMediaImage(
image,
imageProxy.imageInfo.rotationDegrees
)
barcodeScanner.process(inputImage)
.addOnSuccessListener { barcodeList ->
println(barcodeList.map { e -> e.displayValue })
println(barcodeList.map { e -> e.format })
val barcode =
barcodeList.getOrNull(0)
if (barcode != null)
cb(barcode.displayValue, formatToString(barcode.format))
}
.addOnFailureListener {
Log.e(ContentValues.TAG, it.message.orEmpty())
}.addOnCompleteListener {
imageProxy.image?.close()
imageProxy.close()
}
}
}
fun getAnalysisUseCase(cb: (String?, String?) -> Unit): ImageAnalysis {
val options = BarcodeScannerOptions.Builder().setBarcodeFormats(
Barcode.FORMAT_CODE_128,
Barcode.FORMAT_CODE_39,
Barcode.FORMAT_CODE_93,
Barcode.FORMAT_EAN_8,
Barcode.FORMAT_EAN_13,
Barcode.FORMAT_QR_CODE,
Barcode.FORMAT_UPC_A,
Barcode.FORMAT_UPC_E,
Barcode.FORMAT_PDF417
).build()
val scanner = BarcodeScanning.getClient(options)
val analysisUseCase = ImageAnalysis.Builder()
.build()
analysisUseCase.setAnalyzer(
Executors.newSingleThreadExecutor()
) { imageProxy ->
processImageProxy(scanner, imageProxy, cb)
}
return analysisUseCase
}
}

View File

@@ -1,6 +1,5 @@
package net.helcel.fidelity.tools
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.zxing.BarcodeFormat
object BarcodeFormatConverter {
@@ -16,23 +15,40 @@ object BarcodeFormatConverter {
"UPC_A" -> BarcodeFormat.UPC_A
"UPC_E" -> BarcodeFormat.UPC_E
"PDF_417" -> BarcodeFormat.PDF_417
"AZTEC" -> BarcodeFormat.AZTEC
"CODABAR" -> BarcodeFormat.CODABAR
"MAXICODE" -> BarcodeFormat.MAXICODE
"DATA_MATRIX" -> BarcodeFormat.DATA_MATRIX
"ITF" -> BarcodeFormat.ITF
"RSS_14" -> BarcodeFormat.RSS_14
"RSS_EXPANDED" -> BarcodeFormat.RSS_EXPANDED
"UPC_EAN" -> BarcodeFormat.UPC_EAN_EXTENSION
else -> throw Exception("Unsupported Format: $f")
}
}
fun formatToString(f: Int): String {
fun formatToString(f: BarcodeFormat): String {
return when (f) {
Barcode.FORMAT_CODE_128 -> "CODE_128"
Barcode.FORMAT_CODE_39 -> "CODE_39"
Barcode.FORMAT_CODE_93 -> "CODE_93"
Barcode.FORMAT_EAN_8 -> "EAN_8"
Barcode.FORMAT_EAN_13 -> "EAN_13"
Barcode.FORMAT_QR_CODE -> "CODE_QR"
Barcode.FORMAT_UPC_A -> "UPC_A"
Barcode.FORMAT_UPC_E -> "UPC_E"
Barcode.FORMAT_PDF417 -> "PDF_417"
else -> throw Exception("Unsupported Format: $f")
BarcodeFormat.CODE_39 -> "CODE_39"
BarcodeFormat.CODE_93 -> "CODE_93"
BarcodeFormat.CODE_128 -> "CODE_128"
BarcodeFormat.EAN_8 -> "EAN_8"
BarcodeFormat.EAN_13 -> "EAN_13"
BarcodeFormat.QR_CODE -> "CODE_QR"
BarcodeFormat.UPC_A -> "UPC_A"
BarcodeFormat.UPC_E -> "UPC_E"
BarcodeFormat.PDF_417 -> "PDF_417"
BarcodeFormat.AZTEC -> "AZTEC"
BarcodeFormat.CODABAR -> "CODABAR"
BarcodeFormat.MAXICODE -> "MAXICODE"
BarcodeFormat.DATA_MATRIX -> "DATA_MATRIX"
BarcodeFormat.ITF -> "ITF"
BarcodeFormat.RSS_14 -> "RSS_14"
BarcodeFormat.RSS_EXPANDED -> "RSS_EXPANDED"
BarcodeFormat.UPC_EAN_EXTENSION -> "UPC_EAN"
//else -> throw Exception("Unsupported Format: $f")
}
}
}
}

View File

@@ -6,6 +6,8 @@ import com.google.zxing.MultiFormatWriter
import com.google.zxing.WriterException
import com.google.zxing.common.BitMatrix
import net.helcel.fidelity.tools.BarcodeFormatConverter.stringToFormat
import androidx.core.graphics.set
import androidx.core.graphics.createBitmap
object BarcodeGenerator {
@@ -19,25 +21,23 @@ object BarcodeGenerator {
android.graphics.Color.WHITE
}
fun generateBarcode(content: String, f: String, width: Int): Bitmap? {
if (content.isEmpty() || f.isEmpty()) {
fun generateBarcode(content: String?, f: String?, w: Int): Bitmap? {
if (content.isNullOrEmpty() || f.isNullOrEmpty()) {
return null
}
try {
val format = stringToFormat(f)
val writer = MultiFormatWriter()
val height = (formatToRatio(format) * width).toInt()
val height = (w * formatToRatio(format)).toInt()
val width = (w * 1.0f).toInt()
val bitMatrix: BitMatrix = writer.encode(content, format, width, height)
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val bitmap = createBitmap(width, height)
for (x in 0 until width) {
for (y in 0 until height) {
bitmap.setPixel(
x,
y,
getPixelColor(bitMatrix, x, y)
)
bitmap[x, y] = getPixelColor(bitMatrix, x, y)
}
}
return bitmap

View File

@@ -0,0 +1,61 @@
package net.helcel.fidelity.tools
import android.graphics.Bitmap
import androidx.annotation.OptIn
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import com.google.zxing.BinaryBitmap
import com.google.zxing.MultiFormatReader
import com.google.zxing.NotFoundException
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.ReaderException
import com.google.zxing.common.HybridBinarizer
import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString
import java.util.concurrent.Executors
@OptIn(ExperimentalGetImage::class)
object BarcodeScanner {
private fun processImage(
bitmap: Bitmap,
cb: (String?, String?) -> Unit
) {
val binaryBitmap = createBinaryBitmap(bitmap)
val reader = MultiFormatReader()
try {
val result = reader.decode(binaryBitmap)
cb(result.text, formatToString(result.barcodeFormat))
} catch (_: NotFoundException) {
cb(null, null)
} catch (_: ReaderException) {
cb(null, null)
}
}
private fun createBinaryBitmap(bitmap: Bitmap): BinaryBitmap {
val pixels = IntArray(bitmap.width * bitmap.height)
bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
val source =
RGBLuminanceSource(bitmap.width, bitmap.height, pixels)
return BinaryBitmap(HybridBinarizer(source))
}
fun analysisUseCase(cb: (String?, String?) -> Unit): ImageAnalysis {
val analysisUseCase = ImageAnalysis.Builder().build()
analysisUseCase.setAnalyzer(
Executors.newSingleThreadExecutor()
) { imageProxy ->
val bitmap = imageProxy.toBitmap()
imageProxy.close()
bitmapUseCase(bitmap, cb)
}
return analysisUseCase
}
fun bitmapUseCase(bitmap: Bitmap, cb: (String?, String?) -> Unit) {
processImage(bitmap, cb)
}
}

View File

@@ -0,0 +1,142 @@
package net.helcel.fidelity.tools
import android.content.Context
import android.net.Uri
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import javax.crypto.Cipher
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.parseUri
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.suspendCancellableCoroutine
import java.security.KeyStore
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
val Context.securePrefs by preferencesDataStore("keepass_prefs")
object KeePassKeys {
val DB_FILE_PATH = stringPreferencesKey("db_file_path")
val PASSWORD = stringPreferencesKey("password_enc")
val KEY_FILE_PATH = stringPreferencesKey("key_file_path")
val IV = stringPreferencesKey("iv")
}
sealed class CredentialResult {
data class Success(val db: Uri?, val password: String, val key: Uri?) : CredentialResult()
object NoData : CredentialResult()
object AuthFailed : CredentialResult()
}
private const val KEY_ALIAS = "keepass_bio_key"
fun getOrCreateBiometricKey(): SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
keyStore.getKey(KEY_ALIAS, null)?.let { return it as SecretKey }
val keyGenerator =
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).apply {
setBlockModes(KeyProperties.BLOCK_MODE_GCM)
setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
setUserAuthenticationRequired(true)
setInvalidatedByBiometricEnrollment(true)
}.build()
keyGenerator.init(spec)
return keyGenerator.generateKey()
}
fun getCipherForDecryption(key: SecretKey, iv: ByteArray?): Cipher {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
if(iv==null) cipher.init(Cipher.ENCRYPT_MODE, key)
else cipher.init(Cipher.DECRYPT_MODE, key, javax.crypto.spec.GCMParameterSpec(128, iv))
return cipher
}
object KeePassStore {
suspend fun saveCredentials(
context: Context, cred: CredentialResult.Success
): CredentialResult {
val cipher = showBiometricPrompt(context as FragmentActivity, true)
?: return CredentialResult.AuthFailed
val encPasswordB = cipher.doFinal(cred.password.toByteArray(Charsets.UTF_8))
context.securePrefs.edit { prefs ->
prefs[KeePassKeys.DB_FILE_PATH] = cred.db.toString()
prefs[KeePassKeys.PASSWORD] = Base64.encodeToString(encPasswordB, Base64.DEFAULT)
prefs[KeePassKeys.IV] = Base64.encodeToString(cipher.iv, Base64.DEFAULT)
cred.key?.let { prefs[KeePassKeys.KEY_FILE_PATH] = it.toString() }
}
return cred
}
suspend fun hasCredentials(context: Context): Boolean {
val prefs = context.securePrefs.data.first()
return prefs[KeePassKeys.DB_FILE_PATH] != null &&
prefs[KeePassKeys.PASSWORD] != null
}
fun packCredentials(dbFilePath:Uri?, password: String, keyFilePath: Uri?): CredentialResult.Success {
return CredentialResult.Success(dbFilePath, password, keyFilePath)
}
suspend fun loadCredentials(context: Context): CredentialResult {
val prefs = context.securePrefs.data.first { true }
val dbFilePath = prefs[KeePassKeys.DB_FILE_PATH] ?: return CredentialResult.NoData
val encryptedBase64 = prefs[KeePassKeys.PASSWORD] ?: return CredentialResult.NoData
val keyFilePath = prefs[KeePassKeys.KEY_FILE_PATH]
val cipher = showBiometricPrompt(context as FragmentActivity, false)
?: return CredentialResult.AuthFailed
val decrypted = cipher.doFinal(Base64.decode(encryptedBase64, Base64.DEFAULT))
return packCredentials(
dbFilePath.parseUri(),
String(decrypted, Charsets.UTF_8),
keyFilePath?.parseUri()
)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun showBiometricPrompt(activity: FragmentActivity, enc: Boolean): Cipher? {
val prefs = activity.securePrefs.data.first()
return suspendCancellableCoroutine { cont ->
val executor = ContextCompat.getMainExecutor(activity)
val biometricPrompt = BiometricPrompt(
activity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { cont.resume(result.cryptoObject?.cipher) {} }
override fun onAuthenticationError(code: Int, msg: CharSequence) { cont.resume(null) {} }
override fun onAuthenticationFailed() { cont.resume(null) {} }
}
)
val iv = if(enc) null else prefs[KeePassKeys.IV]?.let { Base64.decode(it, Base64.DEFAULT) }
if (!enc && iv == null) { cont.resume(null) {} }
val cipher = getCipherForDecryption(getOrCreateBiometricKey(), iv)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock KeePass")
.setSubtitle("Authenticate to access your KeePass database")
.setNegativeButtonText("Cancel")
.build()
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}
}
fun retrieveResponseFromChallenge(
hardwareKey: HardwareKey,
seed: ByteArray?,
): ByteArray {
val response: ByteArray = "".toByteArray()
return response
}

View File

@@ -1,50 +0,0 @@
package net.helcel.fidelity.tools
import android.content.SharedPreferences
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
object CacheManager {
const val PREF_NAME = "FIDELITY"
private const val ENTRY_KEY = "FIDELITY"
private var data: ArrayList<Triple<String?, String?, String?>> = ArrayList()
private var pref: SharedPreferences? = null
fun addFidelity(item: Triple<String?, String?, String?>) {
val exists = data.find { it.first == item.first }
if (exists != null)
data.remove(exists)
data.add(0, item)
saveFidelity()
}
fun rmFidelity(idx: Int) {
data.removeAt(idx)
saveFidelity()
}
private fun saveFidelity() {
val editor = pref?.edit()
val gson = Gson()
val json = gson.toJson(data)
editor?.putString(ENTRY_KEY, json)
editor?.apply()
}
fun loadFidelity(pref: SharedPreferences) {
this.pref = pref
val gson = Gson()
val json = pref.getString(ENTRY_KEY, null)
val type = object : TypeToken<List<Triple<String, String, Int>>>() {}.type
data = gson.fromJson(json, type) ?: ArrayList()
}
fun getFidelity(): ArrayList<Triple<String?, String?, String?>> {
return data
}
}

View File

@@ -1,22 +0,0 @@
package net.helcel.fidelity.tools
import android.app.Activity
import android.widget.Toast
object ErrorToaster {
private fun helper(activity: Activity, message: String, length: Int) {
Toast.makeText(activity, message, length).show()
}
fun noKP2AFound(activity: Activity) {
helper(activity, "KeePass2Android Not Installed", Toast.LENGTH_LONG)
}
fun formIncomplete(activity: Activity) {
helper(activity, "Form Incomplete", Toast.LENGTH_SHORT)
}
fun invalidFormat(activity: Activity) {
helper(activity, "Invalid Format", Toast.LENGTH_SHORT)
}
}

View File

@@ -0,0 +1,185 @@
package net.helcel.fidelity.tools
import android.content.Context
import android.net.Uri
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import kotlinx.serialization.Serializable
import java.io.ByteArrayInputStream
import kotlinx.serialization.json.Json
import androidx.core.content.edit
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.MasterCredential
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.getBinaryDir
import kotlinx.serialization.builtins.ListSerializer
import java.io.File
import java.util.UUID
object FidelityKeepassFields {
const val FIDELITYFORMAT = "FidelityFormat"
const val FIDELITYCODE = "FidelityCode"
}
@Serializable
data class FidelityEntry(
val uid: String? = null,
val title: String = "",
val code: String = "",
val format: String = "",
val protected: Boolean = false,
val hidden: Boolean = false,
val pinned: Boolean = false,
val lastUse: Int = 0,
)
object FidelityRepository {
private var db: Database = Database()
private var binaryDir: File? = null
val entries = mutableStateListOf<FidelityEntry>()
val activeEntry = mutableStateOf(FidelityEntry())
fun getRoot(): Group? {
return db.rootGroup
}
fun start(ctx: Context, uri: Uri?, c: MasterCredential): Boolean {
if (binaryDir == null) binaryDir = ctx.getBinaryDir()
if (uri == null) return false
try {
val bitStream =
ByteArrayInputStream(ctx.contentResolver.openInputStream(uri)?.readBytes())
db.loadData(
bitStream, c,
{ hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) },
false, binaryDir!!,
{ BinaryData.canMemoryBeAllocatedInRAM(ctx, it) },
false, null
)
return true
} catch (e: Exception) {
println(e)
return false
}
}
fun end(ctx: Context, uri: Uri?, c: MasterCredential): Boolean {
if (uri == null) return false
db.saveData(
File(binaryDir, db.binaryCache.hashCode().toString()),{ ctx.contentResolver.openOutputStream(uri) },
false, c, { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) })
return true
}
fun genCredentials(
ctx: Context,
cred: CredentialResult.Success,
hardwareKey: HardwareKey? = null
): MasterCredential {
return MasterCredential(
cred.password,
cred.key?.let { ctx.contentResolver.openInputStream(cred.key)?.readBytes() },
hardwareKey
)
}
fun importDB(context: Context) {
val seenID= arrayListOf<String>()
fun importDBRec(group: Group) {
group.getChildEntries().forEach {
val fields = it.getExtraFields()
val code = fields.firstOrNull { e -> e.name == FidelityKeepassFields.FIDELITYCODE }
val format =
fields.firstOrNull { e -> e.name == FidelityKeepassFields.FIDELITYFORMAT }
if (code == null || format == null) return@forEach
val newEntry = FidelityEntry(
uid=it.nodeId.id.toString(),
title=it.title,
code=code.protectedValue.stringValue,
format=format.protectedValue.stringValue,
protected=code.protectedValue.isProtected,
)
val idx = entries.indexOfFirst { e -> e.uid == newEntry.uid }
seenID.add(newEntry.uid!!)
if (idx >= 0) {
val oldEntry = entries[idx]
entries[idx] = newEntry.copy(
pinned = oldEntry.pinned,
hidden = oldEntry.hidden,
lastUse = oldEntry.lastUse
)
} else {
entries.add(newEntry)
}
}
group.getChildGroups().forEach { importDBRec(it) }
}
if (db.rootGroup != null)
importDBRec(db.rootGroup!!)
entries.removeAll { !seenID.contains(it.uid)}
val distinct = entries.distinctBy { it.uid }
entries.clear()
entries.addAll(distinct)
saveEntries(context)
}
fun saveEntries(context: Context) {
val prefs = context.getSharedPreferences("fidelity_prefs", Context.MODE_PRIVATE)
prefs.edit { putString("entries", Json.encodeToString(
ListSerializer(FidelityEntry.serializer()),
entries
)) }
}
fun loadEntries(context: Context) {
val prefs = context.getSharedPreferences("fidelity_prefs", Context.MODE_PRIVATE)
try {
val json = prefs.getString("entries", null) ?: return
val list = Json.decodeFromString(
ListSerializer(FidelityEntry.serializer()),
json
)
entries.clear()
entries.addAll(list)
}catch(_: Exception){
prefs.edit{ putString("entries",Json.encodeToString(
ListSerializer(FidelityEntry.serializer()),emptyList()))
}
}
}
fun addEntry(ctx: Context, entry: FidelityEntry) {
val dbEntry = db.getEntryById(NodeIdUUID(UUID.fromString(entry.uid))) ?: db.createEntry()
val dbParent = db.getGroupById(NodeIdUUID(UUID.fromString(entry.uid)))
dbEntry?.apply {
putExtraField(
Field(
FidelityKeepassFields.FIDELITYCODE,
ProtectedString(entry.protected, entry.code)
)
)
putExtraField(
Field(
FidelityKeepassFields.FIDELITYFORMAT,
ProtectedString(string= entry.format)
)
)
if(dbParent!=null) title = entry.title
dbParent?.addChildEntry(dbEntry)
}
entries.removeIf {it.uid == entry.uid}
entries.add(entry.copy(uid=dbEntry?.nodeId?.id.toString()))
saveEntries(ctx)
}
}

View File

@@ -1,101 +0,0 @@
package net.helcel.fidelity.tools
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import net.helcel.fidelity.pluginSDK.KeepassDefs
import net.helcel.fidelity.pluginSDK.Kp2aControl
object KeepassWrapper {
private const val CODE_FIELD: String = "FidelityCode"
private const val FORMAT_FIELD: String = "FidelityFormat"
private const val PROTECT_CODE_FIELD: String = "FidelityProtectedCode"
fun entryCreate(
fragment: Fragment,
title: String,
code: String,
format: String,
protectCode: Boolean,
): Pair<HashMap<String?, String?>, ArrayList<String?>> {
val fields = HashMap<String?, String?>()
val protected = ArrayList<String?>()
fields[KeepassDefs.TitleField] = title
fields[KeepassDefs.UrlField] =
"androidapp://" + fragment.requireActivity().packageName
fields[CODE_FIELD] = code
fields[FORMAT_FIELD] = format
fields[PROTECT_CODE_FIELD] = protectCode.toString()
protected.add(CODE_FIELD)
return Pair(fields, protected)
}
fun resultLauncherAdd(
fragment: Fragment,
callback: (HashMap<String, String>) -> Unit
): ActivityResultLauncher<Intent> {
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
val credentials = Kp2aControl.getEntryFieldsFromIntent(
data!!
)
println(credentials)
callback(credentials)
}
}
}
fun resultLauncherQuery(
fragment: Fragment,
callback: (HashMap<String, String>) -> Unit
): ActivityResultLauncher<Intent> {
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
val credentials = Kp2aControl.getEntryFieldsFromIntent(
data!!
)
println(credentials)
callback(credentials)
}
}
}
fun entryExtract(map: HashMap<String, String>): Triple<String?, String?, String?> {
return Triple(
map[KeepassDefs.TitleField],
map[CODE_FIELD],
map[FORMAT_FIELD]
)
}
fun bundleCreate(title: String?, code: String?, fmt: String?): Bundle {
val data = Bundle()
data.putString("title", title)
data.putString("code", code)
data.putString("fmt", fmt)
return data
}
fun bundleExtract(data: Bundle?): Triple<String?, String?, String?> {
return Triple(
data?.getString("title"),
data?.getString("code"),
data?.getString("fmt")
)
}
fun isProtected(map: HashMap<String, String>): Boolean {
return map[PROTECT_CODE_FIELD].toBoolean()
}
}

View File

@@ -1,21 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="52"
android:viewportHeight="52">
<group
android:translateX="-10"
android:translateY="-10">
<path
android:fillColor="#EA5A47"
android:pathData="M60.7,26.2c0,-7.2 -5.9,-13.1 -13.1,-13.1c-5,0 -9.3,2.8 -11.5,6.9c-2.2,-4.1 -6.6,-6.9 -11.5,-6.9c-7.2,0 -13.1,5.9 -13.1,13.1c0,3.1 1.1,6 2.9,8.2l0,0l21.8,27l21.8,-27l0,0C59.6,32.2 60.7,29.4 60.7,26.2z" />
<path
android:fillColor="#00000000"
android:pathData="M60.7,26.2c0,-7.2 -5.9,-13.1 -13.1,-13.1c-5,0 -9.3,2.8 -11.5,6.9c-2.2,-4.1 -6.6,-6.9 -11.5,-6.9c-7.2,0 -13.1,5.9 -13.1,13.1c0,3.1 1.1,6 2.9,8.2l0,0l21.8,27l21.8,-27l0,0C59.6,32.2 60.7,29.4 60.7,26.2z"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
</vector>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group android:scaleX="1.2833333"
android:scaleY="1.2833333"
android:translateX="-16.612345"
android:translateY="-16.612345">
<group
android:translateX="34"
android:translateY="26">
<group
android:scaleX="0.8"
android:scaleY="1.0"
android:translateX="0"
android:translateY="0">
<path
android:fillColor="@color/blue"
android:pathData="M59.959,52.794H12.041c-0.552,0 -1,-0.448 -1,-1v-29.547c0,-0.552 0.448,-1 1,-1h47.918c0.552,0 1,0.448 1,1v29.547C60.959,52.347 60.511,52.794 59.959,52.794z"
android:strokeWidth="2"
android:strokeColor="#000000" />
</group>
<group
android:scaleX="0.4"
android:scaleY="0.5"
android:translateX="27"
android:translateY="15.75">
<path
android:fillColor="@color/red"
android:pathData="M46.5,56l-10,-11l-10,11l0,-45l20,0z"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="@color/red2"
android:fillAlpha="1.0"
android:pathData="M41.5,11l0,39l5,6l0,-45z"
android:strokeColor="#00000000" />
</group>
<group
android:scaleX="0.75"
android:scaleY="0.75"
android:translateX="6"
android:translateY="10">
<path
android:fillColor="#00000000"
android:pathData="M9,21V52 M12,21V52 M20,21V50 M28,21V50 M15,50V21H17V50H15 M23,50V21H25V50H23 M31,50V21H32V50H31 M35,21V52 M38,21V52"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
</group>
</group>
</vector>

View File

@@ -1,31 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="52"
android:viewportHeight="52">
<group
android:translateX="-10"
android:translateY="-10">
<path
android:fillColor="#00000000"
android:pathData="M30.735,34.656l-16.432,16.026l0,7.24l7.565,0l0,-4.637l5.125,0l0,-5.857l5.098,0l2.404,-2.404l0,-4.358l2.015,0"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M48.52,23.998m-3.952,0a3.952,3.952 0,1 1,7.904 0a3.952,3.952 0,1 1,-7.904 0"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M34.226,31.178c-1.43,-4.238 -0.347,-9.221 3.18,-12.695c4.845,-4.772 12.465,-4.889 17.022,-0.263s4.322,12.244 -0.522,17.016c-3.917,3.858 -9.648,4.674 -14.108,2.4"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
</vector>

View File

@@ -1,31 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="52"
android:viewportHeight="52">
<group
android:translateX="-10"
android:translateY="-10">
<path
android:pathData="M53,32.25l1.875,0l0,26.875l-38,0l0,-26.875l1.875,0z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M21.375,28.915c0,-8.379 6.415,-16.274 14.318,-16.523c7.97,-0.251 15.41,7.285 14.742,16.523"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M25.548,28.915c0,-6.335 4.576,-12.305 10.212,-12.493c5.684,-0.19 10.991,5.508 10.514,12.493"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="128dp"
android:height="128dp"
android:gravity="center"
android:drawable="@drawable/card" />
<item
android:width="64dp"
android:height="64dp"
android:drawable="@drawable/barcode"
android:gravity="center"
android:right="32dp" />
<item
android:width="52dp"
android:height="52dp"
android:drawable="@drawable/bookmark"
android:gravity="center"
android:left="72dp"
android:bottom="20dp" />
</layer-list>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group
android:translateX="34"
android:translateY="26">
<group
android:scaleX="0.8"
android:scaleY="1.0"
android:translateX="0"
android:translateY="0">
<path
android:fillColor="@color/blue"
android:pathData="M59.959,52.794H12.041c-0.552,0 -1,-0.448 -1,-1v-29.547c0,-0.552 0.448,-1 1,-1h47.918c0.552,0 1,0.448 1,1v29.547C60.959,52.347 60.511,52.794 59.959,52.794z"
android:strokeWidth="2"
android:strokeColor="#000000" />
</group>
<group
android:scaleX="0.4"
android:scaleY="0.5"
android:translateX="27"
android:translateY="15.75">
<path
android:fillColor="@color/red"
android:pathData="M46.5,56l-10,-11l-10,11l0,-45l20,0z"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="@color/red2"
android:fillAlpha="1.0"
android:pathData="M41.5,11l0,39l5,6l0,-45z"
android:strokeColor="#00000000" />
</group>
<group
android:scaleX="0.75"
android:scaleY="0.75"
android:translateX="6"
android:translateY="10">
<path
android:fillColor="#00000000"
android:pathData="M9,21V52 M12,21V52 M20,21V50 M28,21V50 M15,50V21H17V50H15 M23,50V21H25V50H23 M31,50V21H32V50H31 M35,21V52 M38,21V52"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
</group>
</vector>

View File

@@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="58"
android:viewportHeight="58">
<group android:translateX="-10" android:translateY="-8">
<path
android:pathData="m57.008,20.304v-3.356l-27.338,-0.002c-0.198,0 -0.359,-0.165 -0.359,-0.368l-0.069,-1.517c-0.116,-1.788 -1.34,-3.003 -2.997,-3.003h-11.287c-1.657,0 -3,1.343 -3,3v40.943"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="m17.027,55.568c-0.59,1.954 -2.972,4.139 -4.646,4.394l44.665,0.011c1.657,0 2.323,-0.439 3,-3s7,-31.657 7,-31.657c0,-0.552 -0.448,-1 -1,-1H24.965c-0.552,0 -1,0.448 -1,1 0,0 -6.348,28.299 -6.938,30.253Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@@ -0,0 +1,302 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="58"
android:viewportHeight="58">
<group android:translateX="-8" android:translateY="-8">
<path
android:pathData="M20,20h4v4h-4z"
android:fillColor="#000"/>
<path
android:pathData="M20,48h4v4h-4z"
android:fillColor="#000"/>
<path
android:pathData="M48,20h4v4h-4z"
android:fillColor="#000"/>
<path
android:pathData="M18,40m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M16,38m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M20,38m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M34,46m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M40,38m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M40,28m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M32,16m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M46,32m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M52,32m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M52,44m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M54,48m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M56,56m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M32,56m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M44,56m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M46,54m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M44,52m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M16,32m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M40,54m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#000"/>
<path
android:pathData="M12,12h48v48h-48z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"/>
<path
android:pathData="M16,16h12v12h-12z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"/>
<path
android:pathData="M20,20h4v4h-4z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"/>
<path
android:pathData="M16,44h12v12h-12z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"/>
<path
android:pathData="M20,48h4v4h-4z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"/>
<path
android:pathData="M44,16h12v12h-12z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"/>
<path
android:pathData="M48,20h4v4h-4z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"/>
<path
android:pathData="M18,36V34H26"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M20,34V32"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M24,34V40"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M24,38H26"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M38,32V30"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M56,34H54"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M42,42H44V40"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M28,32H30"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M34,32H40"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M38,16V20H36V28"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M36,26H32V28"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M36,20H32"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M36,22H34V18"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M28,36H36"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M30,36V40H28"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M34,36V38"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M32,44V42H38V48H42V46H50V56"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M36,40V44H42M46,40H42V48H44"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M48,34V38H50V42H48V46"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M50,38V36H52"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M52,50H48V52"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M32,52H34V54H36V50"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M56,32V38H54"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M44,36V34"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M56,42V44"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M54,52H56"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M40,22V24"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical"
tools:context=".activity.MainActivity">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MergeRootFrame" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,106 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity.fragment.CreateEntry">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/nameInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/codeInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/code"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/checkboxProtected"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextCode"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkboxProtected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:button="@drawable/lock_checkbox"
android:scaleX="0.40"
android:scaleY="0.40"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/codeInputLayout"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/formatInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/format"
android:labelFor="@id/edit_text_format">
<AutoCompleteTextView
android:id="@+id/edit_text_format"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/imageViewPreview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:contentDescription="@string/barcode_preview"
android:scaleType="fitCenter" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnSave"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_margin="24dp"
android:contentDescription="@string/save"
app:fabCustomSize="46dp"
app:maxImageSize="32dp"
app:srcCompat="@drawable/save" />
</RelativeLayout>

View File

@@ -1,83 +0,0 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity.fragment.Launcher">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/fidelityList"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnQuery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_gravity="center"
android:layout_margin="24dp"
android:contentDescription="@string/query"
app:fabCustomSize="46dp"
app:maxImageSize="32dp"
app:srcCompat="@drawable/search" />
<LinearLayout
android:id="@+id/expandedMenuContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_margin="16dp"
android:background="@android:color/transparent"
android:orientation="vertical"
tools:ignore="RelativeOverlap">
<LinearLayout
android:id="@+id/menuAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnScan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:contentDescription="@string/scan"
app:fabCustomSize="46dp"
app:maxImageSize="32dp"
app:srcCompat="@drawable/camera" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnManual"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:contentDescription="@string/manual"
app:fabCustomSize="46dp"
app:maxImageSize="32dp"
app:srcCompat="@drawable/edit" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="8dp"
android:contentDescription="@string/expand"
app:fabCustomSize="46dp"
app:maxImageSize="32dp" />
</LinearLayout>
</RelativeLayout>

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity.fragment.Scanner">
<androidx.camera.view.PreviewView
android:id="@+id/cameraView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/bottomText"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_alignParentBottom="true"
android:background="#ffffff"
android:textSize="24sp" />
</RelativeLayout>

View File

@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".activity.fragment.ViewEntry">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:hint="@string/title"
android:textAlignment="center"
android:textSize="42sp"
app:layout_constraintBottom_toTopOf="@id/imageViewPreview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<ImageView
android:id="@+id/imageViewPreview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:contentDescription="@string/barcode_preview"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/textViewFeelings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="15dp"
android:text=""
android:textSize="18sp"
android:textStyle="bold" />

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
app:cardMaxElevation="4dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="darkgray">#FF0C1D2E</color>
<color name="gray">#425F7C</color>
<color name="lightgray">#FF93A9BE</color>
<color name="white">#FFF0F3F7</color>
<color name="blue">#7DB9F5</color>
<color name="blue2">#3193F5</color>
<color name="red">#F57D7D</color>
<color name="red2">#F53131</color>
</resources>

View File

@@ -1,3 +0,0 @@
<resources>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0C1D2E</color>
</resources>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="kp2aplugin_title" tools:keep="@string/kp2aplugin_title">Fidelity</string>
<string name="kp2aplugin_shortdesc">Stores and Displays fidelity and other cards</string>
<string name="kp2aplugin_author" tools:keep="@string/kp2aplugin_author">[soraefir](soraefir)</string>
<string name="app_name">Keepass Fidelity</string>
<resources>
<string name="key_theme">App theme</string>
<string name="system">System</string>
<string name="light">Light</string>
<string name="dark">Dark</string>
<string name="key_stats">Statistics</string>
<string name="barcode_preview">barcode preview</string>
<string name="expand">Expand</string>
@@ -15,15 +15,20 @@
<string name="code">Code</string>
<string name="format">Format</string>
<string name="save">Save</string>
<string name="open">Open</string>
<string-array name="format_array">
<item>CODE_128</item>
<item>CODE_39</item>
<item>CODE_93</item>
<item>CODE_128</item>
<item>EAN_8</item>
<item>EAN_13</item>
<item>CODE_QR</item>
<item>UPC_A</item>
<item>UPC_E</item>
<item>PDF_417</item>
<item>AZTEC</item>
<item>CODABAR</item>
<item>DATA_MATRIX</item>
<item>ITF</item>
</string-array>
</resources>

View File

@@ -1,3 +0,0 @@
<resources>
</resources>

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Fidelity" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">?attr/colorAccent</item>
</style>
</resources>

View File

@@ -1,7 +1,16 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
// ext.kotlin_version = '1.8.20'
// ext.android_core_version = '1.10.1'
// ext.android_appcompat_version = '1.6.1'
// ext.android_material_version = '1.9.0'
ext.android_test_version = '1.5.2'
}
plugins {
id 'com.android.application' version '8.3.0' apply false
id 'com.android.library' version '8.3.0' apply false
id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
id 'com.android.application' version '8.13.0' apply false
id 'com.android.library' version '8.13.0' apply false
id 'org.jetbrains.kotlin.android' version '2.2.21' apply false
id 'com.autonomousapps.dependency-analysis' version '3.4.1' apply true
}

1
external/KeePassDX vendored Submodule

Submodule external/KeePassDX added at 1b98bd740c

View File

@@ -15,7 +15,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
android.enableJetifier=true
android.enableJetifier=false
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

15
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -112,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -170,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -203,15 +203,14 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

5
gradlew.bat vendored
View File

@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -68,11 +70,10 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -0,0 +1 @@
<p><i>Keepass-Fidelity</i> adds an interface to view/save barcodes (QR included) to Keepass through the plugin interface of the Keepass2Android app.</p><p><br></p><ul><li><b>Launcher:</b> view and launch recent entries (a per entry flag can disable this behaviour)</li><li><b>View:</b> view entries from the history or queried from Keepass2Android</li><li><b>Create:</b> add entries from the camera, an image of by filling out a form. The entry is then created in the Keepass2Android app</li><li><b>Data:</b> the app uses the following data Title (entry name), barcode type (QR, UPC, ...), barcode content (number/text content) and a "secure" flag (enable/disable caching the entry).</li></ul>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1 @@
<ul><li><b>CAMERA:</b> necessary for importing barcodes from camera</li><li><b>READ_MEDIA_VISUAL_USER_SELECTED:</b> necessary for the importing barcode from images</li></ul>

View File

@@ -0,0 +1 @@
Fidelity (Membership/Loyalty) Card plugin for Keepass2Android

View File

@@ -14,5 +14,11 @@ dependencyResolutionManagement {
maven { url 'https://jitpack.io' }
}
}
rootProject.name = "BeenDroid"
include(":database")
project(":database").projectDir = file("external/KeePassDX/database")
include(":crypto")
project(":crypto").projectDir = file("external/KeePassDX/crypto")
rootProject.name = "Fidelity"
include ':app'