Compare commits

..

205 Commits

Author SHA1 Message Date
J-Jamet
9fc5e6751b Merge branch 'release/2.5beta29' 2020-03-25 12:06:08 +01:00
J-Jamet
4c1630312b Add UUID color 2020-03-25 11:46:43 +01:00
J-Jamet
d397c5c996 Add UUID ref for entry 2020-03-25 11:22:19 +01:00
J-Jamet
f6c41b5a60 Replace the strong nodes 2020-03-24 20:49:33 +01:00
J-Jamet
06eb5c01c3 Update versions 2020-03-24 20:44:50 +01:00
J-Jamet
7df49f91e8 Merge branch 'release/2.5beta29' of github.com:Kunzisoft/KeePassDX into release/2.5beta29 2020-03-24 20:10:27 +01:00
J-Jamet
96dcbb0ce7 Merge branch 'develop' into release/2.5beta29 2020-03-24 20:09:47 +01:00
J-Jamet
5f828fb986 Fix setting 2020-03-24 20:09:30 +01:00
J-Jamet
533d663938 Merge branch 'translations' into develop 2020-03-24 19:44:38 +01:00
J-Jamet
ae788503a9 Fix html_about licence 2020-03-24 19:44:15 +01:00
anonymous
cf0acd9c73 Translated using Weblate (Italian)
Currently translated at 78.4% (334 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:42:45 +01:00
Filippo De Bortoli
0857f2f1cf Translated using Weblate (Italian)
Currently translated at 78.4% (334 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:42:43 +01:00
Ema Panz
c05d412bdb Translated using Weblate (Italian)
Currently translated at 71.1% (303 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:18:14 +01:00
anonymous
c8e0ce717d Translated using Weblate (Italian)
Currently translated at 71.1% (303 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-03-24 11:18:14 +01:00
J-Jamet
cc3485b201 Update Readme 2020-03-23 21:10:28 +01:00
J-Jamet
81ea7080c2 Show UUID as KeePass UUID 2020-03-21 20:25:37 +01:00
J-Jamet
76ff6f5ae0 Fix reference loop 2020-03-21 19:01:05 +01:00
J-Jamet
0c0d0b7a6f Encapsulate readonly for launch method 2020-03-21 17:23:17 +01:00
J-Jamet
ec8cf1f6b7 Move autofill setting up 2020-03-21 17:16:49 +01:00
J-Jamet
da44310d1b Fix focus validation button 2020-03-21 17:03:36 +01:00
J-Jamet
4bd9c84bb0 Add dialog to validate discard entry changes 2020-03-21 16:52:50 +01:00
J-Jamet
3b1269a770 Fix bind listeners 2020-03-21 16:12:07 +01:00
J-Jamet
7c986ccee8 Fix card_view_margin issue 2020-03-21 16:08:23 +01:00
J-Jamet
903bad8f36 Fix launch of open db service exception 2020-03-21 14:36:33 +01:00
J-Jamet
4b9577437c Hide add button when nodes are selected 2020-03-21 12:27:18 +01:00
J-Jamet
c316011fbc Show toast on invalid key exception 2020-03-21 12:16:45 +01:00
J-Jamet
3fb1f18c22 Education screen for OTP and entry edit menu 2020-03-19 19:47:51 +01:00
J-Jamet
53935058f5 Add OTP icon 2020-03-19 19:16:38 +01:00
J-Jamet
a3860c9581 Generate password icon as dice 2020-03-19 18:34:14 +01:00
J-Jamet
dc20899d26 Update CHANGELOG 2020-03-19 17:48:56 +01:00
J-Jamet
62ac3ddb75 Merge branch 'feature/Autofill_Improvement' into develop 2020-03-19 17:23:24 +01:00
J-Jamet
b792a61bf9 Improve hint "on" and "off" recognition 2020-03-19 15:18:06 +01:00
J-Jamet
aae9f9e1cb Warning when cancel autofill 2020-03-19 14:59:33 +01:00
J-Jamet
d098bf5e6a Upgrade Autofill algorithm 2020-03-19 14:35:22 +01:00
J-Jamet
b0e8a3ecd9 Add expiration datetime 2020-03-18 12:03:12 +01:00
J-Jamet
4efa684022 Merge branch 'feature/Edit_Expired_Date' into develop 2020-03-18 11:53:46 +01:00
J-Jamet
f2c8082990 Fix date picker kitkat issue 2020-03-18 11:53:21 +01:00
J-Jamet
1abba80045 Fix dialog theme 2020-03-18 11:45:52 +01:00
J-Jamet
68564a2b75 Merge branch 'develop' into feature/Edit_Expired_Date 2020-03-18 09:46:28 +01:00
J-Jamet
385b701b38 Fix cardview margin 2020-03-18 09:42:36 +01:00
Éfrit
11c9a1d707 Translated using Weblate (French)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-03-17 00:42:55 +01:00
J-Jamet
c5aef6b561 New entry edit tool menu style 2020-03-15 12:21:26 +01:00
J-Jamet
dfcf73cfd0 Merge branch 'develop' into feature/Edit_Expired_Date 2020-03-14 21:15:58 +01:00
J-Jamet
7fc9389700 Always show menu 2020-03-14 21:15:37 +01:00
J-Jamet
d1af7349bc Edit button as validation 2020-03-14 20:51:47 +01:00
J-Jamet
0fd955197d Expires selection 2020-03-14 20:16:36 +01:00
Destiny Li
cbf33507d1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-14 17:37:29 +01:00
WaldiS
bc60a5d97e Translated using Weblate (Polish)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-03-14 17:37:27 +01:00
zeritti
57596b2991 Translated using Weblate (Czech)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-03-14 17:37:23 +01:00
solokot
43b3602a52 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-14 17:37:22 +01:00
J-Jamet
c09ec961b8 Merge branch 'feature/Entry_Edit_BottomBar' into develop 2020-03-14 11:00:47 +01:00
J-Jamet
d140b453b2 Merge branch 'feature/Entry_Edit_BottomBar' into develop 2020-03-14 10:36:52 +01:00
J-Jamet
9eb42636ec Change read icons 2020-03-13 21:58:32 +01:00
J-Jamet
ee2d663fce New icons for entry edit tools 2020-03-13 21:33:38 +01:00
J-Jamet
8e83615a22 Add Edit Toolbar menu on top 2020-03-13 17:06:09 +01:00
J-Jamet
0ff129c5ca Edit Entry with Card View 2020-03-13 11:06:56 +01:00
WaldiS
e49858439f Translated using Weblate (Polish)
Currently translated at 99.5% (424 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-03-12 20:33:30 +01:00
zeritti
3df07f7f47 Translated using Weblate (Czech)
Currently translated at 99.5% (424 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-03-12 20:33:30 +01:00
Aurel F
ff9e179593 Translated using Weblate (Romanian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-03-12 20:33:28 +01:00
J-Jamet
7539fee04b Add BottomBar in EntryEdit 2020-03-12 19:15:03 +01:00
J-Jamet
71a339a58f Suppress small warning 2020-03-12 17:15:17 +01:00
J-Jamet
9ef2ea016b Suppress deprecation for Keyboard 2020-03-12 17:14:46 +01:00
J-Jamet
de4936a16d Set nullable getSystemService 2020-03-12 17:05:53 +01:00
J-Jamet
574d2b8904 Wait 3 seconds before the lock after the screen turns off #59 2020-03-12 16:56:46 +01:00
J-Jamet
1f3f7634e7 Allow empty title in entries #423 2020-03-12 14:41:02 +01:00
J-Jamet
3c0725baff Fix remember key file option 2020-03-12 14:24:33 +01:00
J-Jamet
b0e1411012 Update CHANGELOG 2020-03-12 14:20:48 +01:00
J-Jamet
39daf4714d Merge branch 'feature/Delete_Registered_Keyfile_New_Credentials' into develop 2020-03-12 14:17:35 +01:00
J-Jamet
4706afa823 Fix credentials options 2020-03-12 14:16:30 +01:00
J-Jamet
25977d389d Add DAO command to delete registered keyfile 2020-03-12 13:18:36 +01:00
J-Jamet
f760110569 default_database_path_key in strings.xml 2020-03-12 09:28:46 +01:00
J-Jamet
133e78fe97 Update CHANGELOG 2020-03-12 09:20:29 +01:00
J-Jamet
d92e0c8620 Fix magikeyboard lock 2020-03-11 19:17:51 +01:00
J-Jamet
62fdb69d6b Fix small element 2020-03-11 18:56:45 +01:00
J-Jamet
def57c9fb2 Merge branch 'feature/Lock_Database' issue 483 2020-03-11 18:42:34 +01:00
J-Jamet
21c9c898c3 Add lock timer in service, notification remains lock to capture the broadcast 2020-03-11 18:30:11 +01:00
J-Jamet
1f03c922c2 Encapsulate lock broadcast 2020-03-11 16:46:44 +01:00
J-Jamet
3f6ae6bdac Fix node update #487 2020-03-11 12:42:18 +01:00
jan madsen
60615ee1eb Translated using Weblate (Danish)
Currently translated at 98.5% (420 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-03-10 20:23:08 +01:00
J-Jamet
92b0d1bfa9 Upgrade to version 2.5beta29 2020-03-10 19:02:37 +01:00
J-Jamet
237988dc1f Merge tag '2.5beta28' into develop
2.5beta28
2020-03-10 19:00:08 +01:00
J-Jamet
a846ec29ca Merge branch 'release/2.5beta28' 2020-03-10 18:59:59 +01:00
J-Jamet
4533e96bff Upgrade CHANGELOG 2020-03-10 18:48:57 +01:00
J-Jamet
0a401c3ac9 Fix strings 2020-03-10 18:41:35 +01:00
J-Jamet
468abaf077 Fix strings 2020-03-10 18:29:19 +01:00
J-Jamet
4ccf2f641c Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into develop 2020-03-10 18:14:14 +01:00
Dominik Baláž
34eb2785cf Translated using Weblate (Slovak)
Currently translated at 21.5% (92 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2020-03-10 17:21:26 +01:00
anonymous
09dbfe323e Translated using Weblate (Slovak)
Currently translated at 21.5% (92 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2020-03-10 17:21:26 +01:00
Destiny Li
1f06c5b425 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-10 11:33:21 +01:00
Destiny Li
b98e089f7a Translated using Weblate (Chinese (Traditional))
Currently translated at 48.3% (206 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2020-03-10 11:33:20 +01:00
Aurel F
a0ad06ed0a Translated using Weblate (Romanian)
Currently translated at 40.8% (174 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-03-10 11:33:18 +01:00
solokot
ec63365429 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-10 11:33:10 +01:00
J-Jamet
2cb85e4346 Update items after appearance change #452 2020-03-10 08:58:22 +01:00
J-Jamet
0d7c479c51 Merge branch 'feature/Migration_SDK_29' into develop 2020-03-10 08:40:53 +01:00
J-Jamet
5a6c21e662 Try to fix read only bug #480 2020-03-10 08:38:49 +01:00
J-Jamet
d6cadac98f Fix small warning 2020-03-08 13:22:11 +01:00
J-Jamet
dac2fc2c37 Use PreferenceManager from AndroidX 2020-03-08 13:10:16 +01:00
J-Jamet
0fb45cef0d Encapsulate preferences 2020-03-08 13:04:25 +01:00
anonymous
5ebdbd4003 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-08 12:08:21 +01:00
Destiny Li
b30f1023cb Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-03-08 12:08:21 +01:00
J-Jamet
e5f65a4d1e Fix % issue in string 2020-03-08 11:48:47 +01:00
J-Jamet
ab42a65aa4 Fix null issues 2020-03-08 11:41:13 +01:00
J-Jamet
e351456bfe First pass migration SDK 29 2020-03-08 11:03:21 +01:00
J-Jamet
452e68b08f Fix C cast warning 2020-03-08 10:52:29 +01:00
J-Jamet
d65beed7a1 Fix small warnings 2020-03-08 10:49:06 +01:00
J-Jamet
f5a5a0e8cb Add generated JSON for database 2020-03-08 10:41:52 +01:00
solokot
98380a0906 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-08 10:20:37 +01:00
J-Jamet
22fe7508f3 Fix doForEachChild warning 2020-03-08 10:16:31 +01:00
J-Jamet
c8e241fc76 Fix Room incremental warning 2020-03-08 09:50:32 +01:00
J-Jamet
2c943e00d0 Fix writeEnum warning 2020-03-08 09:50:07 +01:00
J-Jamet
7ddb83b72d Fix DateFormatter warning 2020-03-08 09:36:45 +01:00
Kunzisoft
50bac01699 Translated using Weblate (French)
Currently translated at 99.7% (425 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-03-08 09:33:23 +01:00
Aurel F
e0a92dfadd Translated using Weblate (Romanian)
Currently translated at 12.2% (52 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ro/
2020-03-08 09:33:22 +01:00
solokot
b50c951091 Translated using Weblate (Russian)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-03-08 09:33:17 +01:00
Kunzisoft
2f589a95a9 Translated using Weblate (English)
Currently translated at 100.0% (426 of 426 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-03-08 09:33:15 +01:00
J-Jamet
53eac86a95 Update AndroidX dependencies and fix small warning 2020-03-08 09:17:18 +01:00
J-Jamet
9412f8955e Update kotlin to 1.3.61 2020-03-08 09:07:14 +01:00
J-Jamet
71c98d82b1 Update gradle to 5.6.4 2020-03-08 09:00:56 +01:00
J-Jamet
42c1a925b4 Upgrade app to 2.54beta28 2020-03-08 08:59:41 +01:00
J-Jamet
1e84534ffd Merge tag '2.5beta27' into develop
2.5beta27
2020-03-07 14:41:37 +01:00
J-Jamet
b087733e37 Merge branch 'release/2.5beta27' 2020-03-07 14:41:29 +01:00
J-Jamet
675efe29ac Fix unused string 2020-03-07 14:31:08 +01:00
J-Jamet
9833af8225 Update french translations 2020-03-07 14:13:10 +01:00
J-Jamet
790c624571 Fix strong html 2020-03-07 14:06:50 +01:00
J-Jamet
67a70a8453 Merge branch 'translations' into develop 2020-03-07 13:57:47 +01:00
J-Jamet
64bb05e2dd Update CHANGELOG 2020-03-07 13:29:09 +01:00
J-Jamet
c111db6e73 Rename preference keys 2020-03-07 13:25:53 +01:00
J-Jamet
24c5915bd3 Setting to request a search when opening a database #220 2020-03-07 13:18:45 +01:00
J-Jamet
04c3717618 Remove screenshot from bug report 2020-03-07 12:41:57 +01:00
J-Jamet
78275a0984 Fix search empty list 2020-03-07 12:37:46 +01:00
J-Jamet
bd908ed10d Update CHANGELOG 2020-03-07 12:01:03 +01:00
J-Jamet
5473deec95 Show URL before username when title is empty #463 2020-03-07 11:58:47 +01:00
Ettore Atalan
653258afd2 Translated using Weblate (German)
Currently translated at 99.7% (418 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-03-07 03:33:06 +01:00
J-Jamet
00a2210eea Fix button menu color 2020-03-06 17:36:02 +01:00
J-Jamet
ec5688a013 Remove autofill setting for incompatible devices 2020-03-06 17:29:48 +01:00
J-Jamet
fc0c7b5708 Update CHANGELOG 2020-03-06 15:54:02 +01:00
J-Jamet
464f7ac486 Sorted list and comparator improvement 2020-03-06 15:46:08 +01:00
solokot
fb2457146c Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-29 19:33:06 +01:00
solokot
d90870838e Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-28 19:15:38 +01:00
Andrew
8c45f23291 Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-28 19:15:37 +01:00
Kunzisoft
315a3efa52 Added translation using Weblate (Romanian) 2020-02-27 14:01:57 +01:00
J-Jamet
28abb5ae6f Delete password from starting event #461 2020-02-27 13:26:41 +01:00
J-Jamet
bb78d89682 Try to fix aliased text #414 2020-02-27 13:17:01 +01:00
J-Jamet
fd36e19168 Update CHANGELOG 2020-02-27 12:57:31 +01:00
J-Jamet
58db516e44 Fix refresh appearance settings #452 #451 2020-02-27 12:54:47 +01:00
J-Jamet
2b875e94dc Add GZip string to attachment view if database is compressed 2020-02-27 12:38:16 +01:00
J-Jamet
0ba2447f55 Change about page 2020-02-27 10:16:43 +01:00
solokot
8e684d0d3a Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-25 23:32:46 +01:00
MrEnderfall
ce3c1d4685 Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-25 23:32:46 +01:00
J-Jamet
2cd77d47eb Move up remember keyfile locations setting 2020-02-23 13:43:09 +01:00
J-Jamet
ac9366e351 Update CHANGELOGS 2020-02-23 13:39:57 +01:00
J-Jamet
af356586f8 Merge branch 'feature/Check_Broken_Link' into develop 2020-02-23 13:33:56 +01:00
J-Jamet
7650db81a4 Hide broken database links 2020-02-23 13:15:09 +01:00
J-Jamet
8925c86afd Fix remember key file after press back 2020-02-23 12:49:40 +01:00
J-Jamet
8f1836009e Fix database and keyfile locations settings 2020-02-23 12:39:39 +01:00
J-Jamet
5fcf3f9b95 Fix erasing file 2020-02-23 10:47:28 +01:00
Kunzisoft
1c466d9e40 Translated using Weblate (French)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-02-22 17:33:01 +01:00
WaldiS
b97c2d9cbc Translated using Weblate (Polish)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-02-22 17:33:01 +01:00
zeritti
3d970e4967 Translated using Weblate (Czech)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-02-22 17:33:01 +01:00
J-Jamet
765c8f53dd Remove permission variable and rename preference 2020-02-20 19:53:49 +01:00
J-Jamet
4e81caeadf Remove unused external storage permission for Sdk > 18 2020-02-20 19:41:48 +01:00
J-Jamet
08b10e4d58 Update string 2020-02-20 19:35:52 +01:00
J-Jamet
f8594f72e8 Fix small elements 2020-02-20 19:31:44 +01:00
J-Jamet
24ebee07cd Fix small elements 2020-02-20 19:26:03 +01:00
J-Jamet
a06ebb0991 Force read only when file is not writable 2020-02-20 19:20:00 +01:00
J-Jamet
cd5d4498e7 Fix small element 2020-02-20 16:11:50 +01:00
J-Jamet
0bd62780c6 Check if uri is broken 2020-02-20 14:13:11 +01:00
J-Jamet
896f9327d6 Fix error messages #431 2020-02-20 11:58:00 +01:00
J-Jamet
cc83a99efe Try to fix #465 2020-02-20 09:06:11 +01:00
J Smith
b6da20fef7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2020-02-19 23:53:44 +01:00
Kunzisoft
2912678559 Translated using Weblate (French)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-02-19 23:53:38 +01:00
WaldiS
409e870ef0 Translated using Weblate (Polish)
Currently translated at 99.5% (417 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-02-19 23:53:38 +01:00
Kunzisoft
3d84074a0a Translated using Weblate (English)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-02-19 23:53:37 +01:00
Oğuz Ersen
a717fdfed4 Translated using Weblate (Turkish)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-02-19 23:53:36 +01:00
J-Jamet
be1f68015b Merge branch 'master' into develop 2020-02-17 19:05:23 +01:00
WaldiS
2c29dcf1f6 Translated using Weblate (Polish)
Currently translated at 99.0% (415 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2020-02-17 00:50:57 +01:00
Filip Miletic
2b1173177f Translated using Weblate (Croatian)
Currently translated at 79.4% (333 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-02-17 00:50:55 +01:00
jan madsen
3c0f7dc79c Translated using Weblate (Danish)
Currently translated at 98.8% (414 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2020-02-17 00:50:42 +01:00
marzzzello
39d813bf3a Translated using Weblate (German)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-02-17 00:50:40 +01:00
Filip Miletic
5e2bc0d05b Translated using Weblate (Croatian)
Currently translated at 51.3% (215 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-02-14 04:18:25 +01:00
iLocIT!
ec751159ae Translated using Weblate (German)
Currently translated at 89.5% (375 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2020-02-14 04:18:02 +01:00
Filip Miletic
dda8b95f83 Translated using Weblate (Croatian)
Currently translated at 11.0% (46 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2020-02-13 19:31:33 +01:00
Filip Miletic
286012fe2a Added translation using Weblate (Croatian) 2020-02-13 11:04:02 +01:00
Jérémy JAMET
ab27299789 Update description for other KeePass app 2020-02-12 17:09:14 +01:00
Éfrit
dfcdd5aa88 Translated using Weblate (French)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-02-11 21:50:45 +01:00
Allan Nordhøy
a05ea52689 Translated using Weblate (Italian)
Currently translated at 72.8% (305 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2020-02-11 21:50:45 +01:00
zeritti
de12b5de5b Translated using Weblate (Czech)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2020-02-11 21:50:44 +01:00
solokot
57b03eaca4 Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-11 21:50:43 +01:00
Retrial
36bd00b760 Translated using Weblate (Greek)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2020-02-11 21:50:42 +01:00
Oğuz Ersen
4578a9974a Translated using Weblate (Turkish)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-02-11 21:50:33 +01:00
J-Jamet
d0371f58c6 Capture exception to avoid crash after clear clipboard 2020-02-08 17:39:08 +01:00
solokot
9f80457351 Translated using Weblate (Russian)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-08 16:39:00 +01:00
J-Jamet
56daf6f676 Remove JVMOverload in ClipboardHelper 2020-02-08 12:09:12 +01:00
J-Jamet
7f7b8d423b Merge branch 'translations' into develop 2020-02-08 11:34:35 +01:00
J-Jamet
031afc80cb Fix contribution strings 2020-02-08 11:32:56 +01:00
J-Jamet
b77e28b04d Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2020-02-08 11:27:36 +01:00
Allan Nordhøy
2f8c3fdcfe Translated using Weblate (Norwegian Bokmål)
Currently translated at 81.4% (341 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2020-02-08 11:27:15 +01:00
solokot
f501a87099 Translated using Weblate (Russian)
Currently translated at 99.8% (418 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2020-02-08 11:27:14 +01:00
Kunzisoft
d0ec5f26dd Translated using Weblate (English)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2020-02-08 11:27:13 +01:00
Mesut Akcan
97776e9329 Translated using Weblate (Turkish)
Currently translated at 98.3% (412 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2020-02-08 11:27:11 +01:00
Kunzisoft
a19356c49e Translated using Weblate (French)
Currently translated at 100.0% (419 of 419 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2020-02-08 11:21:45 +01:00
J-Jamet
4f16918cf0 Remove unused strings 2020-02-08 11:19:45 +01:00
J-Jamet
1af9761144 Merge branch 'master' into develop 2020-02-07 22:14:20 +01:00
J-Jamet
d74e814c79 Fix fastlane data 2020-02-07 22:05:58 +01:00
J-Jamet
16d09bca6c Merge tag '2.5.0.0beta26' into develop
2.5.0.0beta26
2020-02-07 21:45:51 +01:00
169 changed files with 5101 additions and 2743 deletions

View File

@@ -20,9 +20,6 @@ Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
** Keepass Database **
- Created with: [e.g Windows KeePass 2.42]
- Version: [e.g. 2]

View File

@@ -1,4 +1,25 @@
KeepassDX (2.5.0.0beta26)
KeePassDX(2.5beta29)
* Upgrade autofill algorithm
* Delete registered KeyFile after save new credentials
* Fix title and username entry view refresh after an update
* Fix database lock request (open notification always active)
* Allow empty title in entries
* Add expiration datetime
KeePassDX(2.5beta28)
* Fix read only database
* Upgrade to Android SDK 29
KeePassDX (2.5beta27)
* New setting to hide broken links
* Show URL when title is empty
* Setting to open search field at database opening
* Fix settings for database locations
* Fix error message when database file not writable
* Fix appearance refresh settings
* Sort optimization
KeePassDX (2.5.0.0beta26)
* Download attachments
* Change file size string format
* Prevent screenshot for all screen
@@ -11,7 +32,7 @@ KeepassDX (2.5.0.0beta26)
* Fix dates
* Fix UUID message for Database v1
KeepassDX (2.5.0.0beta25)
KeePassDX (2.5.0.0beta25)
* Setting for Recycle Bin
* Fix Recycle bin issues
* Fix TOTP
@@ -19,7 +40,7 @@ KeepassDX (2.5.0.0beta25)
* Fix update group
* Fix OOM
KeepassDX (2.5.0.0beta24)
KeePassDX (2.5.0.0beta24)
* Add OTP (HOTP / TOTP)
* Add settings (Color, Security, Master Key)
* Show history of each entry
@@ -29,7 +50,7 @@ KeepassDX (2.5.0.0beta24)
* Open/Save database as service / Add persistent notification
* Fix settings / edit group / small bugs
KeepassDX (2.5.0.0beta23)
KeePassDX (2.5.0.0beta23)
* New, more secure database creation workflow
* Recognize more database files
* Add alias for history files (WARNING: history is erased)
@@ -38,14 +59,14 @@ KeepassDX (2.5.0.0beta23)
* Fix OOM with KeyFile
* Fix small issues
KeepassDX (2.5.0.0beta22)
KeePassDX (2.5.0.0beta22)
* Rebuild code for actions
* Add UUID as entry view
* Fix bug with natural order
* Fix number of entries in databaseV1
* New entry views
KeepassDX (2.5.0.0beta21)
KeePassDX (2.5.0.0beta21)
* Fix nested groups no longer visible in V1 databases
* Improved data import algorithm for V1 databases
* Add natural database sort
@@ -53,10 +74,10 @@ KeepassDX (2.5.0.0beta21)
* Fix button disabled with only KeyFile
* Show the number of entries in a group
KeepassDX (2.5.0.0beta20)
KeePassDX (2.5.0.0beta20)
* Fix a major bug that displays an entry history
KeepassDX (2.5.0.0beta19)
KeePassDX (2.5.0.0beta19)
* Add lock button always visible
* New connection workflow
* Code refactored in Kotlin
@@ -67,7 +88,7 @@ KeepassDX (2.5.0.0beta19)
* Fix memory when load database
* Fix small bugs
KeepassDX (2.5.0.0beta18)
KeePassDX (2.5.0.0beta18)
* New recent databases views
* New information dialog
* Custom fields for the Magikeyboard
@@ -76,10 +97,10 @@ KeepassDX (2.5.0.0beta18)
* Fix memory when opening the database
* Memory management for attachments
KeepassDX (2.5.0.0beta17)
KeePassDX (2.5.0.0beta17)
* Fix font and search
KeepassDX (2.5.0.0beta16)
KeePassDX (2.5.0.0beta16)
* New search in a single fragment
* Search suggestions
* Added the display of usernames
@@ -87,20 +108,20 @@ KeepassDX (2.5.0.0beta16)
* Fix read-only mode
* Fix parcelable / toolbar / back
KeepassDX (2.5.0.0beta15)
KeePassDX (2.5.0.0beta15)
* Read only mode
* Best group recovery for the navigation fragment
* Fix copies in notifications
* Fix orientation
* Added translations
KeepassDX (2.5.0.0beta14)
KeePassDX (2.5.0.0beta14)
* Optimize all the memory with parcelables / fix search
KeepassDX (2.5.0.0beta13)
KeePassDX (2.5.0.0beta13)
* Fix memory issue with parcelable (crash in beta12 version)
KeepassDX (2.5.0.0beta12)
KeePassDX (2.5.0.0beta12)
* Added the Magikeyboard to fill the forms (settings still in development)
* Added move and copy for groups and entries
* New navigation in a single screen / new animations between activities
@@ -113,10 +134,10 @@ KeepassDX (2.5.0.0beta12)
* Fix the fingerprint recognition (WARNING : The keystore is reinit, you must delete the old keys)
* Fix small bugs
KeepassDX (2.5.0.0beta11)
KeePassDX (2.5.0.0beta11)
* Fix crash in beta10 version
KeepassDX (2.5.0.0beta10)
KeePassDX (2.5.0.0beta10)
* Dynamically change Algorithm and Key Derivation Function in settings
* Upgrade translations
* New red volcano theme, fix classic dark theme
@@ -124,7 +145,7 @@ KeepassDX (2.5.0.0beta10)
* Update fingerprint state with checkbox
* Fix bugs
KeepassDX (2.5.0.0beta9)
KeePassDX (2.5.0.0beta9)
* Education Screens to learn how to use the app
* New designs
* New custom font for character visibility
@@ -133,9 +154,9 @@ KeepassDX (2.5.0.0beta9)
* Change setting organisation
* Pro version
KeepassDX (2.5.0.0beta8)
KeePassDX (2.5.0.0beta8)
* Hide custom entries protected
* Best management of field references (https://keepass.info/help/base/fieldrefs.html)
* Best management of field references (https://KeePass.info/help/base/fieldrefs.html)
* Change database / default settings
* Add Autofill for search
* Add sorting by last access and by creation time
@@ -143,7 +164,7 @@ KeepassDX (2.5.0.0beta8)
* Refactor old code
* Fix bugs
KeepassDX (2.5.0.0beta7)
KeePassDX (2.5.0.0beta7)
* Rebuild Notifications
* Change links to https
* Add extended Ascii (ñæËÌÂÝÜ...)
@@ -152,10 +173,10 @@ KeepassDX (2.5.0.0beta7)
* Add setting to prevent the password copy
* Fix bugs
KeepassDX (2.5.0.0beta6)
KeePassDX (2.5.0.0beta6)
* Fix crash
KeepassDX (2.5.0.0beta5)
KeePassDX (2.5.0.0beta5)
* Autofill (Android O)
* Deletion for group
* New sorts with (Asc/Dsc, Groups before or after)
@@ -176,7 +197,7 @@ KeepassDX (2.5.0.0beta5)
* Fix many small bugs
* Add recycle bin setting (not yet accessible)
KeepassDX (2.5.0.0beta4)
KeePassDX (2.5.0.0beta4)
* Show only file name
* Setting for full path
* Add information for each database file
@@ -185,7 +206,7 @@ KeepassDX (2.5.0.0beta4)
* Delete view assignment for fingerprint opening
* Merge KeePassDroid 2.2.1
KeepassDX (2.5.0.0beta3)
KeePassDX (2.5.0.0beta3)
* New database workflow with new screens and folder selection
* Settings for default password generation
* Fingerprint dialog for explanations
@@ -196,17 +217,17 @@ KeepassDX (2.5.0.0beta3)
* Merge KeePassDroid 2.2.0.9
* Add corruption fix mode
KeepassDX (2.5.0.0beta2)
KeePassDX (2.5.0.0beta2)
* Remove libs for F-Droid
KeepassDX (2.5.0.0beta1)
* Fork KeepassDroid
KeePassDX (2.5.0.0beta1)
* Fork KeePassDroid
* Add Material Design
* Add Light and Night theme
* Min API is 14
* Solve bug for fingerprint
* Update French translation
* Change donation (see KeepassDroid to contribute on both projects)
* Change donation (see KeePassDroid to contribute on both projects)
KeePassDroid (2.2.1)
* Fix kdbx4 date corruption
@@ -467,7 +488,7 @@ KeePassDroid (1.9.10)
KeePassDroid (1.9.9)
* Go back to explicitly storing blank fields in the database
(works around bug in keepassx)
(works around bug in KeePassx)
* Add support for native code on MIPS architectures
* Adding Vibrate permission. On some devices notifications fail
without the vibrate permission.

View File

@@ -30,7 +30,7 @@ KeePassDX is a **free open source password manager for Android**, which helps yo
Yes, KeePassDX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
*Note : If you access the application from a store, visual features may not be available to incentivize the contribution to the work of open source projects. These optional visuals are accessible after a donation (and a small congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
*Note : If you access the application from a store, visual styles may not be available to encourage contribution to the work of open source projects. These optional styles are accessible after a contribution (and a congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
## Contributions
@@ -59,9 +59,11 @@ Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassD
## Other devices
- [KeePass XC](https://keepassxc.org/) (https://keepassxc.org/) works with **GNU/Linux**, **Mac** and **Windows**, is updated regularly and under the terms of the GNU General Public License. This is the recommended version for computers.
- [KeePass](https://keepass.info/) (https://keepass.info/) is the original and official project for desktop, with technical documentation for standardized database files. It is updated regularly with active maintenance (written in C#).
- [KeePass](https://keepass.info/) (https://keepass.info/) is the historical project, with good technical documentation for standardized database files but only running on **Windows**.
- [KeePassXC](https://keepassxc.org/) (https://keepassxc.org/) is an alternative integration to KeePass written in C++.
- [KeeWeb](https://keeweb.info/) (https://keeweb.info/) is a web version also compatible with KeePass files.
## License

View File

@@ -4,21 +4,28 @@ apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 28
buildToolsVersion '28.0.3'
compileSdkVersion 29
buildToolsVersion '29.0.3'
defaultConfig {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 28
versionCode = 26
versionName = "2.5.0.0beta26"
targetSdkVersion 29
versionCode = 29
versionName = "2.5beta29"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
kapt {
arguments {
arg("room.incremental", "true")
arg("room.schemaLocation", "$projectDir/schemas".toString())
}
}
}
externalNativeBuild {
@@ -79,7 +86,7 @@ android {
}
def spongycastleVersion = "1.58.0.0"
def room_version = "2.2.1"
def room_version = "2.2.5"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@@ -88,7 +95,9 @@ dependencies {
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.biometric:biometric:1.0.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.0.1'
// To upgrade with style
implementation 'com.google.android.material:material:1.0.0'
implementation "androidx.room:room-runtime:$room_version"

View File

@@ -0,0 +1,84 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "56438e5f7372ef3e36e33b782aed245d",
"entities": [
{
"tableName": "file_database_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "databaseAlias",
"columnName": "database_alias",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "keyFileUri",
"columnName": "keyfile_uri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "updated",
"columnName": "updated",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"database_uri"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "cipher_database",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "encryptedValue",
"columnName": "encrypted_value",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "specParameters",
"columnName": "specs_parameters",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"database_uri"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '56438e5f7372ef3e36e33b782aed245d')"
]
}
}

View File

@@ -8,10 +8,15 @@
android:normalScreens="true"
android:largeScreens="true"
android:anyDensity="true" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.USE_BIOMETRIC" />
<uses-permission
android:name="android.permission.VIBRATE"/>
<uses-permission
android:maxSdkVersion="18"
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:label="@string/app_name"
@@ -128,7 +133,7 @@
<activity
android:name="com.kunzisoft.keepass.activities.AboutActivity"
android:launchMode="singleTask"
android:label="@string/menu_about" />
android:label="@string/about" />
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
android:configChanges="keyboardHidden" />

View File

@@ -21,15 +21,15 @@ package com.kunzisoft.keepass.activities
import android.content.pm.PackageManager.NameNotFoundException
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.MenuItem
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.core.text.HtmlCompat
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import org.joda.time.DateTime
class AboutActivity : StylishActivity() {
@@ -40,7 +40,7 @@ class AboutActivity : StylishActivity() {
setContentView(R.layout.activity_about)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.title = getString(R.string.menu_about)
toolbar.title = getString(R.string.about)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
@@ -64,9 +64,17 @@ class AboutActivity : StylishActivity() {
val buildTextView = findViewById<TextView>(R.id.activity_about_build)
buildTextView.text = build
findViewById<TextView>(R.id.activity_about_licence_text).apply {
movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
HtmlCompat.FROM_HTML_MODE_LEGACY)
}
val disclaimerText = findViewById<TextView>(R.id.disclaimer)
disclaimerText.text = getString(R.string.disclaimer_formal, DateTime().year)
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution),
HtmlCompat.FROM_HTML_MODE_LEGACY)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {

View File

@@ -34,6 +34,7 @@ import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
@@ -60,11 +61,13 @@ import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.createDocument
import com.kunzisoft.keepass.utils.onCreateDocumentResult
import com.kunzisoft.keepass.view.EntryContentsView
import com.kunzisoft.keepass.view.showActionError
import java.util.*
import kotlin.collections.HashMap
class EntryActivity : LockingActivity() {
private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var titleIconView: ImageView? = null
private var historyView: View? = null
@@ -114,6 +117,7 @@ class EntryActivity : LockingActivity() {
invalidateOptionsMenu()
// Get views
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
titleIconView = findViewById(R.id.entry_icon)
historyView = findViewById(R.id.history_container)
@@ -137,7 +141,7 @@ class EntryActivity : LockingActivity() {
finish()
}
}
// TODO Visual error for entry history
coordinatorLayout?.showActionError(result)
}
}
@@ -146,9 +150,11 @@ class EntryActivity : LockingActivity() {
// Get Entry from UUID
try {
val keyEntry: NodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
mEntry = mDatabase?.getEntryById(keyEntry)
mEntryLastVersion = mEntry
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
if (keyEntry != null) {
mEntry = mDatabase?.getEntryById(keyEntry)
mEntryLastVersion = mEntry
}
} catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
@@ -535,12 +541,10 @@ class EntryActivity : LockingActivity() {
override fun finish() {
// Transit data in previous Activity after an update
/*
TODO Slowdown when add entry as result
Intent intent = new Intent();
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
onFinish(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
*/
Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
}
super.finish()
}

View File

@@ -19,6 +19,8 @@
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Intent
import android.os.Bundle
import android.os.Handler
@@ -26,16 +28,20 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ScrollView
import android.widget.DatePicker
import android.widget.TimePicker
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.ActionMenuView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.snackbar.Snackbar
import androidx.core.widget.NestedScrollView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryEditActivityEducation
@@ -49,13 +55,16 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.EntryEditContentsView
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionError
import org.joda.time.DateTime
import java.util.*
class EntryEditActivity : LockingActivity(),
IconPickerDialogFragment.IconPickerListener,
GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener {
SetOTPDialogFragment.CreateOtpListener,
DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener {
private var mDatabase: Database? = null
@@ -68,8 +77,9 @@ class EntryEditActivity : LockingActivity(),
// Views
private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: ScrollView? = null
private var scrollView: NestedScrollView? = null
private var entryEditContentsView: EntryEditContentsView? = null
private var entryEditAddToolBar: ActionMenuView? = null
private var saveView: View? = null
// Education
@@ -92,6 +102,16 @@ class EntryEditActivity : LockingActivity(),
entryEditContentsView = findViewById(R.id.entry_edit_contents)
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
entryEditContentsView?.onDateClickListener = View.OnClickListener {
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
val dateTime = DateTime(expiresDate)
val defaultYear = dateTime.year
val defaultMonth = dateTime.monthOfYear-1
val defaultDay = dateTime.dayOfMonth
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
.show(supportFragmentManager, "DatePickerFragment")
}
}
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
@@ -165,17 +185,46 @@ class EntryEditActivity : LockingActivity(),
// Add listener to the icon
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
// Generate password button
entryEditContentsView?.setOnPasswordGeneratorClickListener { openPasswordGenerator() }
// Bottom Bar
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
entryEditAddToolBar?.apply {
menuInflater.inflate(R.menu.entry_edit, menu)
menu.findItem(R.id.menu_add_field).apply {
val allowCustomField = mNewEntry?.allowCustomFields() == true
isEnabled = allowCustomField
isVisible = allowCustomField
}
menu.findItem(R.id.menu_add_otp).apply {
val allowOTP = mDatabase?.allowOTP == true
isEnabled = allowOTP
isVisible = allowOTP
}
setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.menu_generate_password -> {
openPasswordGenerator()
true
}
R.id.menu_add_field -> {
addNewCustomField()
true
}
R.id.menu_add_otp -> {
setupOTP()
true
}
else -> true
}
}
}
// Save button
saveView = findViewById(R.id.entry_edit_save)
saveView = findViewById(R.id.entry_edit_validate)
saveView?.setOnClickListener { saveEntry() }
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) {
addNewCustomField()
}
// Verify the education views
entryEditActivityEducation = EntryEditActivityEducation(this)
@@ -188,15 +237,7 @@ class EntryEditActivity : LockingActivity(),
finish()
}
}
// Show error
if (!result.isSuccess) {
result.message?.let { resultMessage ->
Snackbar.make(coordinatorLayout!!,
resultMessage,
Snackbar.LENGTH_LONG).asError().show()
}
}
coordinatorLayout?.showActionError(result)
}
}
@@ -213,6 +254,9 @@ class EntryEditActivity : LockingActivity(),
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
url = newEntry.url
password = newEntry.password
expires = newEntry.expires
if (expires)
expiresDate = newEntry.expiryTime
notes = newEntry.notes
for (entry in newEntry.customFields.entries) {
post {
@@ -234,7 +278,11 @@ class EntryEditActivity : LockingActivity(),
username = entryView.username
url = entryView.url
password = entryView.password
notes = entryView.notes
expires = entryView.expires
if (entryView.expires) {
expiryTime = entryView.expiresDate
}
notes = entryView. notes
entryView.customFields.forEach { customField ->
putExtraField(customField.name, customField.protectedValue)
}
@@ -265,6 +313,13 @@ class EntryEditActivity : LockingActivity(),
entryEditContentsView?.addEmptyCustomField()
}
private fun setupOTP() {
// Retrieve the current otpElement if exists
// and open the dialog to set up the OTP
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
.show(supportFragmentManager, "addOTPDialog")
}
/**
* Saves the new entry or update an existing entry in the database
*/
@@ -313,8 +368,6 @@ class EntryEditActivity : LockingActivity(),
// Save database not needed here
menu.findItem(R.id.menu_save_database)?.isVisible = false
MenuUtil.contributionMenuInflater(inflater, menu)
if (mDatabase?.allowOTP == true)
inflater.inflate(R.menu.entry_otp, menu)
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
@@ -324,12 +377,10 @@ class EntryEditActivity : LockingActivity(),
}
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
val passwordView = entryEditContentsView?.generatePasswordView
val addNewFieldView = entryEditContentsView?.addNewFieldButton
val generatePasswordEducationPerformed = passwordView != null
val passwordGeneratorView: View? = entryEditAddToolBar?.findViewById(R.id.menu_generate_password)
val generatePasswordEducationPerformed = passwordGeneratorView != null
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
passwordView,
passwordGeneratorView,
{
openPasswordGenerator()
},
@@ -338,14 +389,28 @@ class EntryEditActivity : LockingActivity(),
}
)
if (!generatePasswordEducationPerformed) {
// entryNewFieldEducationPerformed
mNewEntry != null && mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
val addNewFieldEducationPerformed = mNewEntry != null
&& mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView,
{
addNewCustomField()
})
},
{
performedNextEducation(entryEditActivityEducation)
}
)
if (!addNewFieldEducationPerformed) {
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
setupOtpView,
{
setupOTP()
})
}
}
}
@@ -362,14 +427,9 @@ class EntryEditActivity : LockingActivity(),
MenuUtil.onContributionItemSelected(this)
return true
}
R.id.menu_add_otp -> {
// Retrieve the current otpElement if exists
// and open the dialog to set up the OTP
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
.show(supportFragmentManager, "addOTPDialog")
return true
android.R.id.home -> {
onBackPressed()
}
android.R.id.home -> finish()
}
return super.onOptionsItemSelected(item)
@@ -389,6 +449,39 @@ class EntryEditActivity : LockingActivity(),
}
}
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
// To fix android 4.4 issue
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
if (datePicker?.isShown == true) {
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
// Save the date
entryEditContentsView?.expiresDate =
DateInstant(DateTime(expiresDate)
.withYear(year)
.withMonthOfYear(month + 1)
.withDayOfMonth(day)
.toDate())
// Launch the time picker
val dateTime = DateTime(expiresDate)
val defaultHour = dateTime.hourOfDay
val defaultMinute = dateTime.minuteOfHour
TimePickerFragment.getInstance(defaultHour, defaultMinute)
.show(supportFragmentManager, "TimePickerFragment")
}
}
}
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
// Save the date
entryEditContentsView?.expiresDate =
DateInstant(DateTime(expiresDate)
.withHourOfDay(hours)
.withMinuteOfHour(minutes)
.toDate())
}
}
override fun onSaveInstanceState(outState: Bundle) {
mNewEntry?.let {
populateEntryWithViews(it)
@@ -412,6 +505,15 @@ class EntryEditActivity : LockingActivity(),
// Do nothing here
}
override fun onBackPressed() {
AlertDialog.Builder(this)
.setMessage(R.string.discard_changes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok) { _, _ ->
super@EntryEditActivity.onBackPressed()
}.create().show()
}
override fun finish() {
// Assign entry callback as a result in all case
try {

View File

@@ -28,7 +28,6 @@ import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.preference.PreferenceManager
import android.util.Log
import android.view.Menu
import android.view.MenuItem
@@ -52,6 +51,7 @@ import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.asError
import kotlinx.android.synthetic.main.activity_file_selection.*
@@ -141,8 +141,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
if (!(savedInstanceState != null
&& savedInstanceState.containsKey(EXTRA_STAY)
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val databasePath = prefs.getString(PasswordActivity.KEY_DEFAULT_DATABASE_PATH, "")
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
UriUtil.parse(databasePath)?.let { databaseFileUri ->
launchPasswordActivityWithPath(databaseFileUri)
@@ -162,9 +161,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
onActionFinish = { actionTask, _ ->
when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> {
// TODO Check
// mAdapterDatabaseHistory?.notifyDataSetChanged()
// updateFileListVisibility()
GroupActivity.launch(this@FileDatabaseSelectActivity)
}
}
@@ -274,12 +270,28 @@ class FileDatabaseSelectActivity : StylishActivity(),
updateExternalStorageWarning()
// Construct adapter with listeners
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
databaseFileHistoryList?.let {
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(it)
updateFileListVisibility()
mAdapterDatabaseHistory?.notifyDataSetChanged()
if (PreferencesUtil.showRecentFiles(this)) {
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
databaseFileHistoryList?.let { historyList ->
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(this@FileDatabaseSelectActivity)
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(
// Show only uri accessible
historyList.filter {
if (hideBrokenLocations) {
UriUtil.parse(it.databaseUri)?.let { historyUri ->
UriUtil.isUriAccessible(contentResolver, historyUri)
} ?: false
} else
true
})
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
}
}
} else {
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
}
// Register progress task

View File

@@ -76,6 +76,7 @@ import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.AddNodeButtonView
import com.kunzisoft.keepass.view.ToolbarAction
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionError
class GroupActivity : LockingActivity(),
GroupEditDialogFragment.EditGroupListener,
@@ -101,6 +102,7 @@ class GroupActivity : LockingActivity(),
private var mListNodesFragment: ListNodesFragment? = null
private var mCurrentGroupIsASearch: Boolean = false
private var mRequestStartupSearch = true
// Nodes
private var mRootGroup: Group? = null
@@ -141,6 +143,8 @@ class GroupActivity : LockingActivity(),
// Retrieve elements after an orientation change
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(REQUEST_STARTUP_SEARCH_KEY))
mRequestStartupSearch = savedInstanceState.getBoolean(REQUEST_STARTUP_SEARCH_KEY)
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY))
mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY)
}
@@ -251,15 +255,7 @@ class GroupActivity : LockingActivity(),
}
}
if (!result.isSuccess) {
coordinatorLayout?.let { coordinatorLayout ->
result.exception?.errorId?.let { errorId ->
Snackbar.make(coordinatorLayout, errorId, Snackbar.LENGTH_LONG).asError().show()
} ?: result.message?.let { message ->
Snackbar.make(coordinatorLayout, message, Snackbar.LENGTH_LONG).asError().show()
}
}
}
coordinatorLayout?.showActionError(result)
finishNodeAction()
@@ -339,6 +335,7 @@ class GroupActivity : LockingActivity(),
mOldGroupToUpdate?.let {
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it)
}
outState.putBoolean(REQUEST_STARTUP_SEARCH_KEY, mRequestStartupSearch)
super.onSaveInstanceState(outState)
}
@@ -349,7 +346,8 @@ class GroupActivity : LockingActivity(),
// If it's a search
if (Intent.ACTION_SEARCH == intent.action) {
return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' })
val searchString = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
return mDatabase?.search(searchString)
}
// else a real group
else {
@@ -441,15 +439,14 @@ class GroupActivity : LockingActivity(),
enableAddGroup(addGroupEnabled)
enableAddEntry(addEntryEnabled)
if (isEnable)
showButton()
showButton()
}
}
private fun refreshNumberOfChildren() {
numberChildrenView?.apply {
if (PreferencesUtil.showNumberEntries(context)) {
text = mCurrentGroup?.getChildEntries(*Group.ChildFilter.getDefaults(context))?.size?.toString() ?: ""
text = mCurrentGroup?.getNumberOfChildEntries(*Group.ChildFilter.getDefaults(context))?.toString() ?: ""
visibility = View.VISIBLE
} else {
visibility = View.GONE
@@ -506,6 +503,7 @@ class GroupActivity : LockingActivity(),
private fun finishNodeAction() {
actionNodeMode?.finish()
actionNodeMode = null
addNodeButtonView?.showButton()
}
override fun onNodeSelected(nodes: List<Node>): Boolean {
@@ -517,6 +515,7 @@ class GroupActivity : LockingActivity(),
} else {
actionNodeMode?.invalidate()
}
addNodeButtonView?.hideButton()
} else {
finishNodeAction()
}
@@ -666,13 +665,15 @@ class GroupActivity : LockingActivity(),
}
// Get the SearchView and set the searchable configuration
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
menu.findItem(R.id.menu_search)?.let {
val searchView = it.actionView as SearchView?
searchView?.apply {
setSearchableInfo(searchManager.getSearchableInfo(
ComponentName(this@GroupActivity, GroupActivity::class.java)))
(searchManager?.getSearchableInfo(
ComponentName(this@GroupActivity, GroupActivity::class.java)))?.let { searchableInfo ->
setSearchableInfo(searchableInfo)
}
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
suggestionsAdapter = mSearchSuggestionAdapter
setOnSuggestionListener(object : SearchView.OnSuggestionListener {
@@ -690,6 +691,13 @@ class GroupActivity : LockingActivity(),
}
})
}
// Expand the search view if defined in settings
if (mRequestStartupSearch
&& PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) {
// To request search only one time
mRequestStartupSearch = false
it.expandActionView()
}
}
super.onCreateOptionsMenu(menu)
@@ -855,8 +863,8 @@ class GroupActivity : LockingActivity(),
.iconPicked(bundle)
}
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) {
mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
}
override fun startActivity(intent: Intent) {
@@ -900,8 +908,8 @@ class GroupActivity : LockingActivity(),
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
// Not directly get the entry from intent data but from database
mListNodesFragment?.rebuildList()
// Directly used the onActivityResult in fragment
mListNodesFragment?.onActivityResult(requestCode, resultCode, data)
}
private fun removeSearchInIntent(intent: Intent) {
@@ -942,24 +950,33 @@ class GroupActivity : LockingActivity(),
private val TAG = GroupActivity::class.java.name
private const val REQUEST_STARTUP_SEARCH_KEY = "REQUEST_STARTUP_SEARCH_KEY"
private const val GROUP_ID_KEY = "GROUP_ID_KEY"
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
private fun buildAndLaunchIntent(context: Context, group: Group?, readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
val checkTime = if (context is Activity)
TimeoutHelper.checkTimeAndLockIfTimeout(context)
else
TimeoutHelper.checkTime(context)
if (checkTime) {
val intent = Intent(context, GroupActivity::class.java)
if (group != null) {
intent.putExtra(GROUP_ID_KEY, group.nodeId)
}
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
intentBuildLauncher.invoke(intent)
private fun buildIntent(context: Context, group: Group?, readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(context, GroupActivity::class.java)
if (group != null) {
intent.putExtra(GROUP_ID_KEY, group.nodeId)
}
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
intentBuildLauncher.invoke(intent)
}
private fun checkTimeAndBuildIntent(activity: Activity, group: Group?, readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
buildIntent(activity, group, readOnly, intentBuildLauncher)
}
}
private fun checkTimeAndBuildIntent(context: Context, group: Group?, readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
if (TimeoutHelper.checkTime(context)) {
buildIntent(context, group, readOnly, intentBuildLauncher)
}
}
@@ -970,9 +987,9 @@ class GroupActivity : LockingActivity(),
*/
@JvmOverloads
fun launch(context: Context, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
TimeoutHelper.recordTime(context)
buildAndLaunchIntent(context, null, readOnly) { intent ->
fun launch(context: Context,
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
context.startActivity(intent)
}
}
@@ -984,9 +1001,9 @@ class GroupActivity : LockingActivity(),
*/
// TODO implement pre search to directly open the direct group
fun launchForKeyboardSelection(context: Context, readOnly: Boolean) {
TimeoutHelper.recordTime(context)
buildAndLaunchIntent(context, null, readOnly) { intent ->
fun launchForKeyboardSelection(context: Context,
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
EntrySelectionHelper.startActivityForEntrySelection(context, intent)
}
}
@@ -999,9 +1016,10 @@ class GroupActivity : LockingActivity(),
// TODO implement pre search to directly open the direct group
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure, readOnly: Boolean) {
TimeoutHelper.recordTime(activity)
buildAndLaunchIntent(activity, null, readOnly) { intent ->
fun launchForAutofillResult(activity: Activity,
assistStructure: AssistStructure,
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
checkTimeAndBuildIntent(activity, null, readOnly) { intent ->
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure)
}
}

View File

@@ -21,9 +21,7 @@ package com.kunzisoft.keepass.activities
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.util.Log
@@ -54,7 +52,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
private var nodeClickListener: NodeClickListener? = null
private var onScrollListener: OnScrollListener? = null
private var listView: RecyclerView? = null
private var mNodesRecyclerView: RecyclerView? = null
var mainGroup: Group? = null
private set
private var mAdapter: NodeAdapter? = null
@@ -69,8 +67,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
private var notFoundView: View? = null
private var isASearchResult: Boolean = false
// Preferences for sorting
private var prefs: SharedPreferences? = null
private var readOnly: Boolean = false
get() {
@@ -155,7 +151,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
})
}
}
prefs = PreferenceManager.getDefaultSharedPreferences(context)
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -169,11 +164,17 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
// To apply theme
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_list_nodes, container, false)
listView = rootView.findViewById(R.id.nodes_list)
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list)
notFoundView = rootView.findViewById(R.id.not_found_container)
mNodesRecyclerView?.apply {
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
layoutManager = LinearLayoutManager(context)
adapter = mAdapter
}
onScrollListener?.let { onScrollListener ->
listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
onScrollListener.onScrolled(dy)
@@ -181,8 +182,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
})
}
rebuildList()
return rootView
}
@@ -194,14 +193,14 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
// Refresh data
mAdapter?.notifyDataSetChanged()
rebuildList()
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
// To show the " no search entry found "
listView?.visibility = View.GONE
mNodesRecyclerView?.visibility = View.GONE
notFoundView?.visibility = View.VISIBLE
} else {
listView?.visibility = View.VISIBLE
mNodesRecyclerView?.visibility = View.VISIBLE
notFoundView?.visibility = View.GONE
}
}
@@ -209,30 +208,27 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
fun rebuildList() {
// Add elements to the list
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
}
listView?.apply {
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
layoutManager = LinearLayoutManager(context)
adapter = mAdapter
mAdapter?.apply {
rebuildList(mainGroup)
// To visually change the elements
if (PreferencesUtil.APPEARANCE_CHANGED) {
notifyDataSetChanged()
PreferencesUtil.APPEARANCE_CHANGED = false
}
}
}
}
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
// Toggle setting
prefs?.edit()?.apply {
putString(getString(R.string.sort_node_key), sortNodeEnum.name)
putBoolean(getString(R.string.sort_ascending_key), ascending)
putBoolean(getString(R.string.sort_group_before_key), groupsBefore)
putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom)
apply()
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
// Save setting
context?.let {
PreferencesUtil.saveNodeSort(it, sortNodeEnum, sortNodeParameters)
}
// Tell the adapter to refresh it's list
mAdapter?.notifyChangeSort(sortNodeEnum, ascending, groupsBefore)
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
}
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
rebuildList()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -373,15 +369,11 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode ->
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
mAdapter?.addNode(newNode)
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
//mAdapter.updateLastNodeRegister(newNode);
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
}
}
addNode(changedNode)
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
mAdapter?.notifyDataSetChanged()
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
}
}

View File

@@ -23,26 +23,22 @@ import android.app.Activity
import android.app.assist.AssistStructure
import android.app.backup.BackupManager
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.*
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.widget.*
import android.widget.Button
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager
import androidx.core.app.ActivityCompat
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
@@ -74,11 +70,10 @@ import com.kunzisoft.keepass.view.asError
import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException
class PasswordActivity : StylishActivity() {
open class PasswordActivity : StylishActivity() {
// Views
private var toolbar: Toolbar? = null
private var containerView: View? = null
private var filenameView: TextView? = null
private var passwordView: EditText? = null
@@ -88,18 +83,26 @@ class PasswordActivity : StylishActivity() {
private var checkboxKeyFileView: CompoundButton? = null
private var checkboxDefaultDatabaseView: CompoundButton? = null
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
private var infoContainerView: ViewGroup? = null
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private var mDatabaseFileUri: Uri? = null
private var mDatabaseKeyFileUri: Uri? = null
private var prefs: SharedPreferences? = null
private var mRememberKeyFile: Boolean = false
private var mOpenFileHelper: OpenFileHelper? = null
private var mPermissionAsked = false
private var readOnly: Boolean = false
private var mForceReadOnly: Boolean = false
set(value) {
infoContainerView?.visibility = if (value) {
readOnly = true
View.VISIBLE
} else {
View.GONE
}
field = value
}
private var mProgressDialogThread: ProgressDialogThread? = null
@@ -108,10 +111,6 @@ class PasswordActivity : StylishActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = PreferenceManager.getDefaultSharedPreferences(this)
mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this)
setContentView(R.layout.activity_password)
toolbar = findViewById(R.id.toolbar)
@@ -121,7 +120,7 @@ class PasswordActivity : StylishActivity() {
supportActionBar?.setDisplayShowHomeEnabled(true)
containerView = findViewById(R.id.container)
confirmButtonView = findViewById(R.id.pass_ok)
confirmButtonView = findViewById(R.id.activity_password_open_button)
filenameView = findViewById(R.id.filename)
passwordView = findViewById(R.id.password)
keyFileView = findViewById(R.id.pass_keyfile)
@@ -129,8 +128,8 @@ class PasswordActivity : StylishActivity() {
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
advancedUnlockInfoView = findViewById(R.id.biometric_info)
infoContainerView = findViewById(R.id.activity_password_info_container)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
val browseView = findViewById<View>(R.id.open_database_button)
@@ -164,6 +163,12 @@ class PasswordActivity : StylishActivity() {
enableOrNotTheConfirmationButton()
}
// If is a view intent
getUriFromIntent(intent)
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
}
mProgressDialogThread = ProgressDialogThread(this).apply {
onActionFinish = { actionTask, result ->
when (actionTask) {
@@ -176,10 +181,9 @@ class PasswordActivity : StylishActivity() {
}
}
// Remove the password in view in all cases
removePassword()
if (result.isSuccess) {
mDatabaseKeyFileUri = null
clearCredentialsViews(true)
launchGroupActivity()
} else {
var resultError = ""
@@ -235,6 +239,24 @@ class PasswordActivity : StylishActivity() {
}
}
private fun getUriFromIntent(intent: Intent?) {
// If is a view intent
val action = intent?.action
if (action != null
&& action == VIEW_INTENT) {
mDatabaseFileUri = intent.data
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
} else {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
getUriFromIntent(intent)
}
private fun launchGroupActivity() {
EntrySelectionHelper.doEntrySelectionAction(intent,
{
@@ -263,13 +285,15 @@ class PasswordActivity : StylishActivity() {
}
override fun onResume() {
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
if (Database.getInstance().loaded)
launchGroupActivity()
// If the database isn't accessible make sure to clear the password field, if it
// was saved in the instance state
if (Database.getInstance().loaded) {
setEmptyViews()
clearCredentialsViews()
}
// For check shutdown
@@ -281,45 +305,31 @@ class PasswordActivity : StylishActivity() {
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
mDatabaseKeyFileUri?.let {
outState.putString(KEY_KEYFILE, it.toString())
}
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
super.onSaveInstanceState(outState)
}
private fun initUriFromIntent() {
val databaseUri: Uri?
val keyFileUri: Uri?
// If is a view intent
val action = intent.action
if (action != null
&& action == VIEW_INTENT) {
databaseUri = intent.data
keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
} else {
databaseUri = intent.getParcelableExtra(KEY_FILENAME)
keyFileUri = intent.getParcelableExtra(KEY_KEYFILE)
}
mForceReadOnly = !UriUtil.isUriWritable(mDatabaseFileUri)
// Post init uri with KeyFile if needed
if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) {
if (mRememberKeyFile && (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
// Retrieve KeyFile in a thread
databaseUri?.let { databaseUriNotNull ->
mDatabaseFileUri?.let { databaseUri ->
FileDatabaseHistoryAction.getInstance(applicationContext)
.getKeyFileUriByDatabaseUri(databaseUriNotNull) {
.getKeyFileUriByDatabaseUri(databaseUri) {
onPostInitUri(databaseUri, it)
}
}
} else {
onPostInitUri(databaseUri, keyFileUri)
onPostInitUri(mDatabaseFileUri, mDatabaseKeyFileUri)
}
}
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
mDatabaseFileUri = databaseFileUri
mDatabaseKeyFileUri = keyFileUri
// Define title
databaseFileUri?.let {
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
@@ -328,26 +338,18 @@ class PasswordActivity : StylishActivity() {
}
// Define Key File text
val keyUriString = keyFileUri?.toString() ?: ""
if (keyUriString.isNotEmpty() && mRememberKeyFile) { // Bug KeepassDX #18
populateKeyFileTextView(keyUriString)
if (mRememberKeyFile) {
populateKeyFileTextView(keyFileUri?.toString())
}
// Define listeners for default database checkbox and validate button
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
var newDefaultFileName: Uri? = null
var newDefaultFileUri: Uri? = null
if (isChecked) {
newDefaultFileName = databaseFileUri ?: newDefaultFileName
newDefaultFileUri = databaseFileUri ?: newDefaultFileUri
}
prefs?.edit()?.apply {
newDefaultFileName?.let {
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
} ?: kotlin.run {
remove(KEY_DEFAULT_DATABASE_PATH)
}
apply()
}
PreferencesUtil.saveDefaultDatabasePath(this, newDefaultFileUri)
val backupManager = BackupManager(this@PasswordActivity)
backupManager.dataChanged()
@@ -355,7 +357,7 @@ class PasswordActivity : StylishActivity() {
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
// Retrieve settings for default database
val defaultFilename = prefs?.getString(KEY_DEFAULT_DATABASE_PATH, "")
val defaultFilename = PreferencesUtil.getDefaultDatabasePath(this)
if (databaseFileUri != null
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
@@ -365,6 +367,8 @@ class PasswordActivity : StylishActivity() {
// If Activity is launch with a password and want to open directly
val intent = intent
val password = intent.getStringExtra(KEY_PASSWORD)
// Consume the intent extra password
intent.removeExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) {
populatePasswordTextView(password)
@@ -430,10 +434,9 @@ class PasswordActivity : StylishActivity() {
}
}
private fun setEmptyViews() {
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
populatePasswordTextView(null)
// Bug KeepassDX #18
if (!mRememberKeyFile) {
if (clearKeyFile) {
populateKeyFileTextView(null)
}
}
@@ -499,18 +502,13 @@ class PasswordActivity : StylishActivity() {
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
}
private fun removePassword() {
passwordView?.setText("")
checkboxPasswordView?.isChecked = false
}
private fun loadDatabase(databaseFileUri: Uri?,
password: String?,
keyFileUri: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
removePassword()
clearCredentialsViews()
}
databaseFileUri?.let { databaseUri ->
@@ -551,7 +549,12 @@ class PasswordActivity : StylishActivity() {
val inflater = menuInflater
// Read menu
inflater.inflate(R.menu.open_file, menu)
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
if (mForceReadOnly) {
menu.removeItem(R.id.menu_open_file_read_mode_key)
} else {
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
}
MenuUtil.defaultMenuInflater(inflater, menu)
@@ -562,45 +565,14 @@ class PasswordActivity : StylishActivity() {
super.onCreateOptionsMenu(menu)
launchEducation(menu) {
launchCheckPermission()
}
launchEducation(menu)
return true
}
// Check permission
private fun launchCheckPermission() {
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
val permissions = arrayOf(writePermission)
if (Build.VERSION.SDK_INT >= 23
&& !readOnly
&& !mPermissionAsked) {
mPermissionAsked = true
// Check self permission to show or not the dialog
if (toolbar != null
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
WRITE_EXTERNAL_STORAGE_REQUEST -> {
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
}
}
}
}
// To fix multiple view education
private var performedEductionInProgress = false
private fun launchEducation(menu: Menu, onEducationFinished: ()-> Unit) {
private fun launchEducation(menu: Menu, onEducationFinished: (()-> Unit)? = null) {
if (!performedEductionInProgress) {
performedEductionInProgress = true
// Show education views
@@ -610,7 +582,7 @@ class PasswordActivity : StylishActivity() {
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
menu: Menu,
onEducationFinished: ()-> Unit) {
onEducationFinished: (()-> Unit)? = null) {
val educationToolbar = toolbar
val unlockEducationPerformed = educationToolbar != null
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
@@ -650,7 +622,7 @@ class PasswordActivity : StylishActivity() {
})
if (!biometricEducationPerformed) {
onEducationFinished.invoke()
onEducationFinished?.invoke()
}
}
}
@@ -699,6 +671,7 @@ class PasswordActivity : StylishActivity() {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
mDatabaseKeyFileUri = uri
populateKeyFileTextView(uri.toString())
}
}
@@ -707,7 +680,7 @@ class PasswordActivity : StylishActivity() {
// this block if not a key file response
when (resultCode) {
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
setEmptyViews()
clearCredentialsViews()
Database.getInstance().closeAndClear(applicationContext.filesDir)
}
}
@@ -718,8 +691,6 @@ class PasswordActivity : StylishActivity() {
private val TAG = PasswordActivity::class.java.name
const val KEY_DEFAULT_DATABASE_PATH = "KEY_DEFAULT_DATABASE_PATH"
private const val KEY_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW"
@@ -727,10 +698,6 @@ class PasswordActivity : StylishActivity() {
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, PasswordActivity::class.java)

View File

@@ -0,0 +1,61 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.DatePickerDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.DialogFragment
class DatePickerFragment : DialogFragment() {
private var mDefaultYear: Int = 2000
private var mDefaultMonth: Int = 1
private var mDefaultDay: Int = 1
private var mListener: DatePickerDialog.OnDateSetListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as DatePickerDialog.OnDateSetListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Create a new instance of DatePickerDialog and return it
return context?.let {
arguments?.apply {
if (containsKey(DEFAULT_YEAR_BUNDLE_KEY))
mDefaultYear = getInt(DEFAULT_YEAR_BUNDLE_KEY)
if (containsKey(DEFAULT_MONTH_BUNDLE_KEY))
mDefaultMonth = getInt(DEFAULT_MONTH_BUNDLE_KEY)
if (containsKey(DEFAULT_DAY_BUNDLE_KEY))
mDefaultDay = getInt(DEFAULT_DAY_BUNDLE_KEY)
}
DatePickerDialog(it, mListener, mDefaultYear, mDefaultMonth, mDefaultDay)
} ?: super.onCreateDialog(savedInstanceState)
}
companion object {
private const val DEFAULT_YEAR_BUNDLE_KEY = "DEFAULT_YEAR_BUNDLE_KEY"
private const val DEFAULT_MONTH_BUNDLE_KEY = "DEFAULT_MONTH_BUNDLE_KEY"
private const val DEFAULT_DAY_BUNDLE_KEY = "DEFAULT_DAY_BUNDLE_KEY"
fun getInstance(defaultYear: Int,
defaultMonth: Int,
defaultDay: Int): DatePickerFragment {
return DatePickerFragment().apply {
arguments = Bundle().apply {
putInt(DEFAULT_YEAR_BUNDLE_KEY, defaultYear)
putInt(DEFAULT_MONTH_BUNDLE_KEY, defaultMonth)
putInt(DEFAULT_DAY_BUNDLE_KEY, defaultDay)
}
}
}
}
}

View File

@@ -112,7 +112,7 @@ class IconPickerDialogFragment : DialogFragment() {
// Retrieve the textColor to tint the icon
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
ta?.recycle()
ta.recycle()
}
}

View File

@@ -82,7 +82,9 @@ class SortDialogFragment : DialogFragment() {
builder.setView(rootView)
// Add action buttons
.setPositiveButton(android.R.string.ok
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum,
SortNodeEnum.SortNodeParameters(mAscending, mGroupsBefore, mRecycleBinBottom))
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
@@ -150,10 +152,7 @@ class SortDialogFragment : DialogFragment() {
}
interface SortSelectionListener {
fun onSortSelected(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean,
recycleBinBottom: Boolean)
fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters)
}
companion object {

View File

@@ -0,0 +1,57 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.DatePickerDialog
import android.app.Dialog
import android.app.TimePickerDialog
import android.content.Context
import android.os.Bundle
import android.text.format.DateFormat
import androidx.fragment.app.DialogFragment
class TimePickerFragment : DialogFragment() {
private var defaultHour: Int = 0
private var defaultMinute: Int = 0
private var mListener: TimePickerDialog.OnTimeSetListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as TimePickerDialog.OnTimeSetListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Create a new instance of DatePickerDialog and return it
return context?.let {
arguments?.apply {
if (containsKey(DEFAULT_HOUR_BUNDLE_KEY))
defaultHour = getInt(DEFAULT_HOUR_BUNDLE_KEY)
if (containsKey(DEFAULT_MINUTE_BUNDLE_KEY))
defaultMinute = getInt(DEFAULT_MINUTE_BUNDLE_KEY)
}
TimePickerDialog(it, mListener, defaultHour, defaultMinute, DateFormat.is24HourFormat(activity))
} ?: super.onCreateDialog(savedInstanceState)
}
companion object {
private const val DEFAULT_HOUR_BUNDLE_KEY = "DEFAULT_HOUR_BUNDLE_KEY"
private const val DEFAULT_MINUTE_BUNDLE_KEY = "DEFAULT_MINUTE_BUNDLE_KEY"
fun getInstance(defaultHour: Int,
defaultMinute: Int): TimePickerFragment {
return TimePickerFragment().apply {
arguments = Bundle().apply {
putInt(DEFAULT_HOUR_BUNDLE_KEY, defaultHour)
putInt(DEFAULT_MINUTE_BUNDLE_KEY, defaultMinute)
}
}
}
}
}

View File

@@ -20,11 +20,7 @@
package com.kunzisoft.keepass.activities.lock
import android.app.Activity
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.util.Log
import android.view.View
@@ -34,12 +30,9 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.*
abstract class LockingActivity : StylishActivity() {
@@ -81,12 +74,10 @@ abstract class LockingActivity : StylishActivity() {
}
if (mTimeoutEnable) {
mLockReceiver = LockReceiver()
val intentFilter = IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(LOCK_ACTION)
mLockReceiver = LockReceiver {
lockAndExit()
}
registerReceiver(mLockReceiver, intentFilter)
registerLockReceiver(mLockReceiver)
}
mExitLock = false
@@ -151,29 +142,12 @@ abstract class LockingActivity : StylishActivity() {
}
override fun onDestroy() {
unregisterLockReceiver(mLockReceiver)
super.onDestroy()
if (mLockReceiver != null)
unregisterReceiver(mLockReceiver)
}
inner class LockReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// If allowed, lock and exit
if (!TimeoutHelper.temporarilyDisableTimeout) {
intent.action?.let {
when (it) {
Intent.ACTION_SCREEN_OFF -> if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this@LockingActivity)) {
lockAndExit()
}
LOCK_ACTION -> lockAndExit()
}
}
}
}
}
protected fun lockAndExit() {
sendBroadcast(Intent(LOCK_ACTION))
lock()
}
@@ -208,20 +182,8 @@ abstract class LockingActivity : StylishActivity() {
}
fun Activity.lock() {
// Stop the Magikeyboard service
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
MagikIME.removeEntry(this)
closeDatabase()
// Stop the notification service
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
Log.i(Activity::class.java.name, "Shutdown " + localClassName +
" after inactivity or manual lock")
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
cancelAll()
}
// Clear data
Database.getInstance().closeAndClear(applicationContext.filesDir)
// Add onActivityForResult response
setResult(LockingActivity.RESULT_EXIT_LOCK)
finish()

View File

@@ -28,6 +28,7 @@ import android.widget.ProgressBar
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
@@ -38,6 +39,8 @@ class EntryAttachmentsAdapter(val context: Context) : RecyclerView.Adapter<Entry
var entryAttachmentsList: MutableList<EntryAttachment> = ArrayList()
var onItemClickListener: ((item: EntryAttachment, position: Int)->Unit)? = null
private val mDatabase = Database.getInstance()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
}
@@ -49,7 +52,8 @@ class EntryAttachmentsAdapter(val context: Context) : RecyclerView.Adapter<Entry
holder.binaryFileSize.text = Formatter.formatFileSize(context,
entryAttachment.binaryAttachment.length())
holder.binaryFileCompression.apply {
if (entryAttachment.binaryAttachment.isCompressed == true) {
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|| entryAttachment.binaryAttachment.isCompressed == true) {
text = CompressionAlgorithm.GZip.getName(context.resources)
visibility = View.VISIBLE
} else {

View File

@@ -20,6 +20,8 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.graphics.Color
import android.graphics.PorterDuff
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView
import android.util.TypedValue
@@ -82,15 +84,26 @@ class FileDatabaseHistoryAdapter(private val context: Context)
// File path
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
holder.filePreciseInfoContainer.visibility = if (fileDatabaseInfo.found()) {
// Modification
holder.fileModification.text = fileDatabaseInfo.getModificationString()
// Size
holder.fileSize.text = fileDatabaseInfo.getSizeString()
if (fileDatabaseInfo.dataAccessible()) {
holder.fileInformation.clearColorFilter()
} else {
holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
}
View.VISIBLE
} else
View.GONE
// Modification
if (fileDatabaseInfo.lastModificationAccessible()) {
holder.fileModification.text = fileDatabaseInfo.getModificationString()
holder.fileModification.visibility = View.VISIBLE
} else {
holder.fileModification.visibility = View.GONE
}
// Size
if (fileDatabaseInfo.sizeAccessible()) {
holder.fileSize.text = fileDatabaseInfo.getSizeString()
holder.fileSize.visibility = View.VISIBLE
} else {
holder.fileSize.visibility = View.GONE
}
// Click on information
val isExpanded = position == mExpandedPosition
@@ -142,6 +155,10 @@ class FileDatabaseHistoryAdapter(private val context: Context)
return listDatabaseFiles.size
}
fun clearDatabaseFileHistoryList() {
listDatabaseFiles.clear()
}
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
listDatabaseFiles.clear()
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
@@ -178,7 +195,6 @@ class FileDatabaseHistoryAdapter(private val context: Context)
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
var filePath: TextView = itemView.findViewById(R.id.file_path)
var filePreciseInfoContainer: ViewGroup = itemView.findViewById(R.id.file_precise_info_container)
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
var fileSize: TextView = itemView.findViewById(R.id.file_size)
}

View File

@@ -21,15 +21,12 @@ package com.kunzisoft.keepass.adapters
import android.content.Context
import android.graphics.Color
import android.graphics.Paint
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
@@ -39,34 +36,34 @@ import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.setTextSize
import com.kunzisoft.keepass.view.strikeOut
import java.util.*
class NodeAdapter
/**
* Create node list adapter with contextMenu or not
* @param context Context to use
*/
(private val context: Context)
class NodeAdapter (private val context: Context)
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
private var nodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
private val nodeSortedListCallback: NodeSortedListCallback
private val nodeSortedList: SortedList<Node>
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
private var prefTextSize: Float = 0F
private var subtextSize: Float = 0F
private var infoTextSize: Float = 0F
private var numberChildrenTextSize: Float = 0F
private var iconSize: Float = 0F
private var listSort: SortNodeEnum = SortNodeEnum.DB
private var ascendingSort: Boolean = true
private var groupsBeforeSort: Boolean = true
private var recycleBinBottomSort: Boolean = true
private var prefSizeMultiplier: Float = 0F
private var subtextDefaultDimension: Float = 0F
private var infoTextDefaultDimension: Float = 0F
private var numberChildrenTextDefaultDimension: Float = 0F
private var iconDefaultDimension: Float = 0F
private var showUserNames: Boolean = true
private var showNumberEntries: Boolean = true
private var entryFilters = arrayOf<Group.ChildFilter>()
@@ -87,23 +84,15 @@ class NodeAdapter
get() = nodeSortedList.size() <= 0
init {
this.infoTextDefaultDimension = context.resources.getDimension(R.dimen.list_medium_size_default)
this.subtextDefaultDimension = context.resources.getDimension(R.dimen.list_small_size_default)
this.numberChildrenTextDefaultDimension = context.resources.getDimension(R.dimen.list_tiny_size_default)
this.iconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_size_default)
assignPreferences()
this.nodeSortedList = SortedList(Node::class.java, object : SortedListAdapterCallback<Node>(this) {
override fun compare(item1: Node, item2: Node): Int {
return listSort.getNodeComparator(ascendingSort, groupsBeforeSort, recycleBinBottomSort).compare(item1, item2)
}
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
return oldItem.type == newItem.type
&& oldItem.title == newItem.title
&& oldItem.icon == newItem.icon
}
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
return item1 == item2
}
})
this.nodeSortedListCallback = NodeSortedListCallback()
this.nodeSortedList = SortedList(Node::class.java, nodeSortedListCallback)
// Database
this.mDatabase = Database.getInstance()
@@ -118,17 +107,18 @@ class NodeAdapter
taTextColor.recycle()
}
private fun assignPreferences() {
this.prefTextSize = PreferencesUtil.getListTextSize(context)
this.infoTextSize = context.resources.getDimension(R.dimen.list_medium_size_default) * prefTextSize
this.subtextSize = context.resources.getDimension(R.dimen.list_small_size_default) * prefTextSize
this.numberChildrenTextSize = context.resources.getDimension(R.dimen.list_tiny_size_default) * prefTextSize
this.iconSize = context.resources.getDimension(R.dimen.list_icon_size_default) * prefTextSize
fun assignPreferences() {
this.prefSizeMultiplier = PreferencesUtil.getListTextSize(context)
notifyChangeSort(
PreferencesUtil.getListSort(context),
SortNodeEnum.SortNodeParameters(
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context),
PreferencesUtil.getRecycleBinBottomSort(context)
)
)
this.listSort = PreferencesUtil.getListSort(context)
this.ascendingSort = PreferencesUtil.getAscendingSort(context)
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
this.recycleBinBottomSort = PreferencesUtil.getRecycleBinBottomSort(context)
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
@@ -142,15 +132,25 @@ class NodeAdapter
* Rebuild the list by clear and build children from the group
*/
fun rebuildList(group: Group) {
this.nodeSortedList.clear()
assignPreferences()
try {
this.nodeSortedList.addAll(group.getChildren(*entryFilters))
} catch (e: Exception) {
Log.e(TAG, "Can't add node elements to the list", e)
Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show()
nodeSortedList.replaceAll(group.getFilteredChildren(*entryFilters)
)
}
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
override fun compare(item1: Node, item2: Node): Int {
return nodeComparator!!.compare(item1, item2)
}
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
return oldItem.type == newItem.type
&& oldItem.title == newItem.title
&& oldItem.icon == newItem.icon
}
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
return item1 == item2
}
notifyDataSetChanged()
}
fun contains(node: Node): Boolean {
@@ -261,10 +261,9 @@ class NodeAdapter
/**
* Notify a change sort of the list
*/
fun notifyChangeSort(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean) {
this.listSort = sortNodeEnum
this.ascendingSort = ascending
this.groupsBeforeSort = groupsBefore
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
this.nodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
}
override fun getItemViewType(position: Int): Int {
@@ -291,15 +290,15 @@ class NodeAdapter
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
// Relative size of the icon
layoutParams?.apply {
height = iconSize.toInt()
width = iconSize.toInt()
height = (iconDefaultDimension * prefSizeMultiplier).toInt()
width = (iconDefaultDimension * prefSizeMultiplier).toInt()
}
}
// Assign text
holder.text.apply {
text = subNode.title
setTextSize(textSizeUnit, infoTextSize)
setTextSize(textSizeUnit, infoTextDefaultDimension, prefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires)
}
// Add subText with username
@@ -320,7 +319,7 @@ class NodeAdapter
if (showUserNames && username.isNotEmpty()) {
visibility = View.VISIBLE
text = username
setTextSize(textSizeUnit, subtextSize)
setTextSize(textSizeUnit, subtextDefaultDimension, prefSizeMultiplier)
}
}
@@ -332,9 +331,9 @@ class NodeAdapter
if (showNumberEntries) {
holder.numberChildren?.apply {
text = (subNode as Group)
.getChildEntries(*entryFilters)
.size.toString()
setTextSize(textSizeUnit, numberChildrenTextSize)
.getNumberOfChildEntries(*entryFilters)
.toString()
setTextSize(textSizeUnit, numberChildrenTextDefaultDimension, prefSizeMultiplier)
visibility = View.VISIBLE
}
} else {
@@ -352,7 +351,6 @@ class NodeAdapter
holder.container.isSelected = actionNodesList.contains(subNode)
}
override fun getItemCount(): Int {
return nodeSortedList.size()

View File

@@ -38,8 +38,8 @@ class SearchEntryCursorAdapter(private val context: Context,
private val database: Database)
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
private val cursorInflater: LayoutInflater = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private val cursorInflater: LayoutInflater? = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
private var displayUsername: Boolean = false
private val iconColor: Int
@@ -58,7 +58,7 @@ class SearchEntryCursorAdapter(private val context: Context,
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
val view = cursorInflater.inflate(R.layout.item_search_entry, parent, false)
val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false)
val viewHolder = ViewHolder()
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)

View File

@@ -112,6 +112,14 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
).execute()
}
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
ActionDatabaseAsyncTask(
{
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
}
).execute()
}
fun deleteAllKeyFiles() {
ActionDatabaseAsyncTask(
{

View File

@@ -38,6 +38,9 @@ interface FileDatabaseHistoryDao {
@Delete
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
@Query("UPDATE file_database_history SET keyfile_uri=null WHERE database_uri = :databaseUriString")
fun deleteKeyFileByDatabaseUri(databaseUriString: String)
@Query("UPDATE file_database_history SET keyfile_uri=null")
fun deleteAllKeyFiles()

View File

@@ -26,15 +26,14 @@ import android.content.Intent
import android.os.Build
import android.service.autofill.Dataset
import android.service.autofill.FillResponse
import androidx.annotation.RequiresApi
import android.util.Log
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.model.EntryInfo
import java.util.*
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -56,10 +55,10 @@ object AutofillHelper {
return String.format("%s (%s)", entryInfo.title, entryInfo.username)
if (entryInfo.title.isNotEmpty())
return entryInfo.title
if (entryInfo.username.isNotEmpty())
return entryInfo.username
if (entryInfo.url.isNotEmpty())
return entryInfo.url
if (entryInfo.username.isNotEmpty())
return entryInfo.username
return ""
}
@@ -71,12 +70,12 @@ object AutofillHelper {
val builder = Dataset.Builder(views)
builder.setId(entryInfo.id)
struct.password.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.password)) }
val ids = ArrayList(struct.username)
if (entryInfo.username.contains("@") || struct.username.isEmpty())
ids.addAll(struct.email)
ids.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.username)) }
struct.usernameId?.let { usernameId ->
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
}
struct.passwordId?.let { password ->
builder.setValue(password, AutofillValue.forText(entryInfo.password))
}
return try {
builder.build()

View File

@@ -31,7 +31,6 @@ import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -42,9 +41,11 @@ class AutofillLauncherActivity : AppCompatActivity() {
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
if (assistStructure != null) {
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
GroupActivity.launchForAutofillResult(this, assistStructure, PreferencesUtil.enableReadOnlyDatabase(this))
GroupActivity.launchForAutofillResult(this,
assistStructure)
else {
FileDatabaseSelectActivity.launchForAutofillResult(this, assistStructure)
FileDatabaseSelectActivity.launchForAutofillResult(this,
assistStructure)
}
} else {
setResult(Activity.RESULT_CANCELED)

View File

@@ -35,13 +35,13 @@ class KeeAutofillService : AutofillService() {
val fillContexts = request.fillContexts
val latestStructure = fillContexts[fillContexts.size - 1].structure
cancellationSignal.setOnCancelListener { Log.e(TAG, "Cancel autofill not implemented in this sample.") }
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
val responseBuilder = FillResponse.Builder()
// Check user's settings for authenticating Responses and Datasets.
val parseResult = StructureParser(latestStructure).parse()
parseResult?.allAutofillIds()?.let { autofillIds ->
if (listOf(*autofillIds).isNotEmpty()) {
if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this)

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure
import android.os.Build
import androidx.annotation.RequiresApi
import android.text.InputType
import android.util.Log
import android.view.View
import android.view.autofill.AutofillId
@@ -35,74 +34,140 @@ import java.util.*
internal class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null
private var usernameCandidate: AutofillId? = null
private var lockHint: Boolean = false
fun parse(): Result? {
result = Result()
result?.apply {
usernameCandidate = null
for (i in 0 until structure.windowNodeCount) {
mainLoop@ for (i in 0 until structure.windowNodeCount) {
val windowNode = structure.getWindowNodeAt(i)
/*
title.add(windowNode.title)
windowNode.rootViewNode.webDomain?.let {
webDomain.add(it)
}
parseViewNode(windowNode.rootViewNode)
*/
if (parseViewNode(windowNode.rootViewNode))
break@mainLoop
}
// If not explicit username field found, add the field just before password field.
if (username.isEmpty() && email.isEmpty()
&& password.isNotEmpty() && usernameCandidate != null)
username.add(usernameCandidate!!)
if (usernameId == null && passwordId != null && usernameCandidate != null)
usernameId = usernameCandidate
}
return result
// Return the result only if password field is retrieved
return if (result?.passwordId != null)
result
else
null
}
private fun parseViewNode(node: AssistStructure.ViewNode) {
val hints = node.autofillHints
val autofillId = node.autofillId
if (autofillId != null) {
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
if (node.autofillId != null) {
val hints = node.autofillHints
if (hints != null && hints.isNotEmpty()) {
when {
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_USERNAME == it } -> result?.username?.add(autofillId)
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_EMAIL_ADDRESS == it } -> result?.email?.add(autofillId)
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_PASSWORD == it } -> result?.password?.add(autofillId)
else -> Log.d(TAG, "unsupported hints")
if (parseNodeByAutofillHint(node))
return true
} else {
if (parseNodeByHtmlAttributes(node))
return true
}
}
// Recursive method to process each node
for (i in 0 until node.childCount) {
if (parseViewNode(node.getChildAt(i)))
return true
}
return false
}
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId
node.autofillHints?.forEach {
when {
it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_USERNAME
|| it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_EMAIL_ADDRESS
|| it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_PHONE -> {
result?.usernameId = autofillId
Log.d(TAG, "Autofill username hint")
}
} else if (node.autofillType == View.AUTOFILL_TYPE_TEXT) {
val inputType = node.inputType
when {
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 -> result?.email?.add(autofillId)
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD > 0 -> result?.password?.add(autofillId)
result?.password?.isEmpty() == true -> usernameCandidate = autofillId
it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_PASSWORD
|| it.toLowerCase(Locale.ENGLISH).contains("password") -> {
result?.passwordId = autofillId
Log.d(TAG, "Autofill password hint")
return true
}
it.toLowerCase(Locale.ENGLISH) == "off" -> {
Log.d(TAG, "Autofill OFF hint")
lockHint = true
return false
}
it.toLowerCase(Locale.ENGLISH) == "on" -> {
Log.d(TAG, "Autofill ON hint")
if (parseNodeByHtmlAttributes(node))
return true
}
else -> Log.d(TAG, "Autofill unsupported hint $it")
}
}
return false
}
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
if (lockHint)
return false
val autofillId = node.autofillId
val nodHtml = node.htmlInfo
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
"input" -> {
nodHtml.attributes?.forEach { pairAttribute ->
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
"type" -> {
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
"tel", "email" -> {
result?.usernameId = autofillId
Log.d(TAG, "Autofill username type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"text" -> {
usernameCandidate = autofillId
Log.d(TAG, "Autofill type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"password" -> {
result?.passwordId = autofillId
Log.d(TAG, "Autofill password type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
}
}
}
}
}
}
for (i in 0 until node.childCount)
parseViewNode(node.getChildAt(i))
return false
}
@RequiresApi(api = Build.VERSION_CODES.O)
internal class Result {
val title: MutableList<CharSequence>
val webDomain: MutableList<String>
val username: MutableList<AutofillId>
val email: MutableList<AutofillId>
val password: MutableList<AutofillId>
var usernameId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
init {
title = ArrayList()
webDomain = ArrayList()
username = ArrayList()
email = ArrayList()
password = ArrayList()
}
var passwordId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
fun allAutofillIds(): Array<AutofillId> {
val all = ArrayList<AutofillId>()
all.addAll(username)
all.addAll(email)
all.addAll(password)
usernameId?.let {
all.add(it)
}
passwordId?.let {
all.add(it)
}
return all.toTypedArray()
}
}

View File

@@ -313,8 +313,9 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
}
override fun onBiometricException(e: Exception) {
if (e.localizedMessage != null)
setAdvancedUnlockedMessageView(e.localizedMessage)
e.localizedMessage?.let {
setAdvancedUnlockedMessageView(it)
}
}
private fun showFingerPrintViews(show: Boolean) {

View File

@@ -95,7 +95,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
// really not much to do when no fingerprint support found
isKeyManagerInit = false
} else {
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
try {
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.utils.UriUtil
@@ -70,6 +71,9 @@ open class AssignPasswordInDatabaseRunnable (
// Erase the biometric
CipherDatabaseAction.getInstance(context)
.deleteByDatabaseUri(mDatabaseUri)
// Erase the register keyfile
FileDatabaseHistoryAction.getInstance(context)
.deleteKeyFileByDatabaseUri(mDatabaseUri)
if (!result.isSuccess) {
// Erase the current master key

View File

@@ -24,6 +24,7 @@ import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
class CreateDatabaseRunnable(context: Context,
private val mDatabase: Database,
@@ -57,8 +58,11 @@ class CreateDatabaseRunnable(context: Context,
if (result.isSuccess) {
// Add database to recent files
FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri, mKeyFile)
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFile else null)
}
} else {
Log.e("CreateDatabaseRunnable", "Unable to create the database")
}

View File

@@ -74,14 +74,10 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onFinishRun() {
if (result.isSuccess) {
// Save keyFile in app database
val rememberKeyFile = PreferencesUtil.rememberKeyFiles(context)
if (rememberKeyFile) {
var keyUri = mKey
if (!rememberKeyFile) {
keyUri = null
}
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context)
.addOrUpdateDatabaseUri(mUri, keyUri)
.addOrUpdateDatabaseUri(mUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mKey else null)
}
// Register the biometric
@@ -90,8 +86,11 @@ class LoadDatabaseRunnable(private val context: Context,
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called
}
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
// Start the opening notification
DatabaseOpenNotificationService.startIfAllowed(context)
DatabaseOpenNotificationService.start(context)
} else {
mDatabase.closeAndClear(cacheDirectory)
}

View File

@@ -27,6 +27,7 @@ import android.os.Build
import android.os.Bundle
import android.os.IBinder
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.element.*
@@ -35,6 +36,7 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
@@ -85,12 +87,17 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
TimeoutHelper.temporarilyDisableTimeout(activity)
TimeoutHelper.temporarilyDisableTimeout()
// Stop the opening notification
DatabaseOpenNotificationService.stop(activity)
startOrUpdateDialog(titleId, messageId, warningId)
}
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
TimeoutHelper.temporarilyDisableTimeout(activity)
TimeoutHelper.temporarilyDisableTimeout()
// Stop the opening notification
DatabaseOpenNotificationService.stop(activity)
startOrUpdateDialog(titleId, messageId, warningId)
}
@@ -98,7 +105,18 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
onActionFinish?.invoke(actionTask, result)
// Remove the progress task
ProgressTaskDialogFragment.stop(activity)
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity)
TimeoutHelper.releaseTemporarilyDisableTimeout()
val inTime = if (activity is LockingActivity) {
TimeoutHelper.checkTimeAndLockIfTimeout(activity)
} else {
TimeoutHelper.checkTime(activity)
}
// Start the opening notification if in time
// (databaseOpenService is open manually in Action Open Task)
if (actionTask != ACTION_DATABASE_LOAD_TASK && inTime) {
DatabaseOpenNotificationService.start(activity)
}
}
}
@@ -126,7 +144,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
if (serviceConnection == null) {
serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder).apply {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addActionTaskListener(actionTaskListener)
getService().checkAction()
}

View File

@@ -415,7 +415,7 @@ class Database {
val searchResult = search(query, SearchHelper.MAX_SEARCH_ENTRY)
if (searchResult != null) {
// Search in hide entries but not meta-stream
for (entry in searchResult.getChildEntries(*Group.ChildFilter.getDefaults(context))) {
for (entry in searchResult.getFilteredChildEntries(*Group.ChildFilter.getDefaults(context))) {
entry.entryKDB?.let {
cursorKDB?.addEntry(it)
}
@@ -493,10 +493,11 @@ class Database {
var outputStream: OutputStream? = null
try {
outputStream = contentResolver.openOutputStream(uri)
val pmo =
mDatabaseKDB?.let { DatabaseOutputKDB(it, outputStream) }
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, outputStream) }
pmo?.output()
outputStream?.let { definedOutputStream ->
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
databaseOutput?.output()
}
} catch (e: Exception) {
throw IOException(e)
} finally {
@@ -723,7 +724,7 @@ class Database {
fun canRecycle(entry: Entry): Boolean {
var canRecycle: Boolean? = null
entry.entryKDB?.let {
canRecycle = mDatabaseKDB?.canRecycle(it)
canRecycle = mDatabaseKDB?.canRecycle()
}
entry.entryKDBX?.let {
canRecycle = mDatabaseKDBX?.canRecycle(it)
@@ -734,7 +735,7 @@ class Database {
fun canRecycle(group: Group): Boolean {
var canRecycle: Boolean? = null
group.groupKDB?.let {
canRecycle = mDatabaseKDB?.canRecycle(it)
canRecycle = mDatabaseKDB?.canRecycle()
}
group.groupKDBX?.let {
canRecycle = mDatabaseKDBX?.canRecycle(it)

View File

@@ -46,7 +46,7 @@ class DateInstant : Parcelable {
}
constructor(string: String) {
jDate = dateFormat.parse(string)
jDate = dateFormat.parse(string) ?: jDate
}
constructor() {
@@ -121,7 +121,7 @@ class DateInstant : Parcelable {
}
}
private fun isSameDate(d1: Date?, d2: Date?): Boolean {
private fun isSameDate(d1: Date, d2: Date): Boolean {
val cal1 = Calendar.getInstance()
cal1.time = d1
cal1.set(Calendar.MILLISECOND, 0)
@@ -142,7 +142,7 @@ class DateInstant : Parcelable {
fun getDateTimeString(resources: Resources, date: Date): String {
return java.text.DateFormat.getDateTimeInstance(
java.text.DateFormat.MEDIUM,
java.text.DateFormat.MEDIUM,
java.text.DateFormat.SHORT,
ConfigurationCompat.getLocales(resources.configuration)[0])
.format(date)
}

View File

@@ -26,17 +26,25 @@ import java.util.UUID
class DeletedObject {
var uuid: UUID = DatabaseVersioned.UUID_ZERO
var deletionTime: Date? = null
get() = if (field == null) {
Date(System.currentTimeMillis())
} else field
private var mDeletionTime: Date? = null
fun getDeletionTime(): Date {
if (mDeletionTime == null) {
mDeletionTime = Date(System.currentTimeMillis())
}
return mDeletionTime!!
}
fun setDeletionTime(deletionTime: Date) {
this.mDeletionTime = deletionTime
}
constructor()
@JvmOverloads
constructor(uuid: UUID, deletionTime: Date = Date()) {
this.uuid = uuid
this.deletionTime = deletionTime
this.mDeletionTime = deletionTime
}
override fun equals(other: Any?): Boolean {

View File

@@ -160,6 +160,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
return contained ?: false
}
override fun nodeIndexInParentForNaturalOrder(): Int {
return entryKDB?.nodeIndexInParentForNaturalOrder()
?: entryKDBX?.nodeIndexInParentForNaturalOrder()
?: -1
}
override var creationTime: DateInstant
get() = entryKDB?.creationTime ?: entryKDBX?.creationTime ?: DateInstant()
set(value) {
@@ -241,13 +247,13 @@ class Entry : Node, EntryVersionedInterface<Group> {
"$PMS_TAN_ENTRY $username"
} else {
if (title.isEmpty())
if (username.isEmpty())
if (url.isEmpty())
nodeId.toString()
if (url.isEmpty())
if (username.isEmpty())
nodeId.toString()
else
url
username
else
username
url
else
title
}

View File

@@ -187,6 +187,12 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
return contained ?: false
}
override fun nodeIndexInParentForNaturalOrder(): Int {
return groupKDB?.nodeIndexInParentForNaturalOrder()
?: groupKDBX?.nodeIndexInParentForNaturalOrder()
?: -1
}
override var creationTime: DateInstant
get() = groupKDB?.creationTime ?: groupKDBX?.creationTime ?: DateInstant()
set(value) {
@@ -225,62 +231,58 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
override val isCurrentlyExpires: Boolean
get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false
override fun getChildGroups(): MutableList<Group> {
val children = ArrayList<Group>()
groupKDB?.getChildGroups()?.forEach {
children.add(Group(it))
}
groupKDBX?.getChildGroups()?.forEach {
children.add(Group(it))
}
return children
override fun getChildGroups(): List<Group> {
return groupKDB?.getChildGroups()?.map {
Group(it)
} ?:
groupKDBX?.getChildGroups()?.map {
Group(it)
} ?:
ArrayList()
}
override fun getChildEntries(): MutableList<Entry> {
// To cal function with vararg
return getChildEntries(*emptyArray<ChildFilter>())
override fun getChildEntries(): List<Entry> {
return groupKDB?.getChildEntries()?.map {
Entry(it)
} ?:
groupKDBX?.getChildEntries()?.map {
Entry(it)
} ?:
ArrayList()
}
fun getChildEntries(vararg filter: ChildFilter): MutableList<Entry> {
val children = ArrayList<Entry>()
fun getFilteredChildEntries(vararg filter: ChildFilter): List<Entry> {
val withoutMetaStream = filter.contains(ChildFilter.META_STREAM)
val showExpiredEntries = !filter.contains(ChildFilter.EXPIRED)
groupKDB?.getChildEntries()?.forEach {
val entryToAddAsChild = Entry(it)
if ((!withoutMetaStream || (withoutMetaStream && !entryToAddAsChild.isMetaStream))
&& (!entryToAddAsChild.isCurrentlyExpires or showExpiredEntries))
children.add(entryToAddAsChild)
}
groupKDBX?.getChildEntries()?.forEach {
val entryToAddAsChild = Entry(it)
if (!entryToAddAsChild.isCurrentlyExpires or showExpiredEntries)
children.add(entryToAddAsChild)
}
return groupKDB?.getChildEntries()?.filter {
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream))
&& (!it.isCurrentlyExpires or showExpiredEntries)
}?.map {
Entry(it)
} ?:
groupKDBX?.getChildEntries()?.filter {
!it.isCurrentlyExpires or showExpiredEntries
}?.map {
Entry(it)
} ?:
ArrayList()
}
return children
fun getNumberOfChildEntries(vararg filter: ChildFilter): Int {
return getFilteredChildEntries(*filter).size
}
/**
* Filter entries and return children
* @return List of direct children (one level below) as NodeVersioned
*/
fun getChildren(vararg filter: ChildFilter): List<Node> {
val children = ArrayList<Node>()
children.addAll(getChildGroups())
fun getChildren(): List<Node> {
return getChildGroups() + getChildEntries()
}
groupKDB?.let {
children.addAll(getChildEntries(*filter))
}
groupKDBX?.let {
// No MetasStream in V4
children.addAll(getChildEntries(*filter))
}
return children
fun getFilteredChildren(vararg filter: ChildFilter): List<Node> {
return getChildGroups() + getFilteredChildEntries(*filter)
}
override fun addChildGroup(group: Group) {

View File

@@ -20,149 +20,198 @@
package com.kunzisoft.keepass.database.element
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type
import java.util.*
enum class SortNodeEnum {
DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME;
fun getNodeComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean): Comparator<Node> {
fun <G: GroupVersionedInterface<G, *>> getNodeComparator(sortNodeParameters: SortNodeParameters)
: Comparator<NodeVersionedInterface<G>> {
return when (this) {
DB -> NodeNaturalComparator(ascending, groupsBefore, false) // Force false because natural order contains recycle bin
TITLE -> NodeTitleComparator(ascending, groupsBefore, recycleBinBottom)
USERNAME -> NodeUsernameComparator(ascending, groupsBefore, recycleBinBottom)
CREATION_TIME -> NodeCreationComparator(ascending, groupsBefore, recycleBinBottom)
LAST_MODIFY_TIME -> NodeLastModificationComparator(ascending, groupsBefore, recycleBinBottom)
LAST_ACCESS_TIME -> NodeLastAccessComparator(ascending, groupsBefore, recycleBinBottom)
DB -> NodeNaturalComparator(sortNodeParameters) // Force false because natural order contains recycle bin
TITLE -> NodeTitleComparator(sortNodeParameters)
USERNAME -> NodeUsernameComparator(sortNodeParameters)
CREATION_TIME -> NodeCreationComparator(sortNodeParameters)
LAST_MODIFY_TIME -> NodeLastModificationComparator(sortNodeParameters)
LAST_ACCESS_TIME -> NodeLastAccessComparator(sortNodeParameters)
}
}
abstract class NodeComparator(var ascending: Boolean, var groupsBefore: Boolean, var recycleBinBottom: Boolean) : Comparator<Node> {
data class SortNodeParameters(var ascending: Boolean = true,
var groupsBefore: Boolean = true,
var recycleBinBottom: Boolean = true)
abstract fun compareBySpecificOrder(object1: Node, object2: Node): Int
abstract class NodeComparator
<
G: GroupVersionedInterface<*, *>,
T: NodeVersionedInterface<G>
>(var sortNodeParameters: SortNodeParameters)
: Comparator<T> {
private fun specificOrderOrHashIfEquals(object1: Node, object2: Node): Int {
val database = Database.getInstance()
abstract fun compareBySpecificOrder(object1: T, object2: T): Int
private fun specificOrderOrHashIfEquals(object1: T, object2: T): Int {
val specificOrderComp = compareBySpecificOrder(object1, object2)
return if (specificOrderComp == 0) {
object1.hashCode() - object2.hashCode()
} else if (!ascending) -specificOrderComp else specificOrderComp // If descending, revert
return when {
specificOrderComp == 0 -> object1.hashCode() - object2.hashCode()
sortNodeParameters.ascending -> specificOrderComp
else -> -specificOrderComp
}
}
override fun compare(object1: Node, object2: Node): Int {
override fun compare(object1: T, object2: T): Int {
if (object1 == object2)
return 0
if (object1.type == Type.GROUP) {
return if (object2.type == Type.GROUP) {
// RecycleBin at end of groups
val database = Database.getInstance()
if (database.isRecycleBinEnabled && recycleBinBottom) {
if (database.recycleBin == object1)
return 1
if (database.recycleBin == object2)
return -1
when (object1.type) {
Type.GROUP -> {
when (object2.type) {
Type.GROUP -> {
// RecycleBin at end of groups
if (database.isRecycleBinEnabled && sortNodeParameters.recycleBinBottom) {
if (database.recycleBin == object1)
return 1
if (database.recycleBin == object2)
return -1
}
return specificOrderOrHashIfEquals(object1, object2)
}
Type.ENTRY -> {
return if (sortNodeParameters.groupsBefore)
-1
else
1
}
}
specificOrderOrHashIfEquals(object1, object2)
} else if (object2.type == Type.ENTRY) {
if (groupsBefore)
-1
else
1
} else {
-1
}
} else if (object1.type == Type.ENTRY) {
return if (object2.type == Type.ENTRY) {
specificOrderOrHashIfEquals(object1, object2)
} else if (object2.type == Type.GROUP) {
if (groupsBefore)
1
else
-1
} else {
-1
Type.ENTRY -> {
return when (object2.type) {
Type.GROUP -> {
if (sortNodeParameters.groupsBefore)
1
else
-1
}
Type.ENTRY -> {
specificOrderOrHashIfEquals(object1, object2)
}
}
}
}
// Type not known
return -1
}
}
/**
* Comparator of node by natural database placement
*/
class NodeNaturalComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeNaturalComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.nodePositionInParent.compareTo(object2.nodePositionInParent)
override fun compareBySpecificOrder(object1: T, object2: T): Int {
return object1.nodeIndexInParentForNaturalOrder()
.compareTo(object2.nodeIndexInParentForNaturalOrder())
}
}
/**
* Comparator of Node by Title
*/
class NodeTitleComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeTitleComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.title.compareTo(object2.title, ignoreCase = true)
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val titleCompare = object1.title.compareTo(object2.title, ignoreCase = true)
return if (titleCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
titleCompare
}
}
/**
* Comparator of Node by Username, Groups by title
*/
class NodeUsernameComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeUsernameComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
override fun compareBySpecificOrder(object1: T, object2: T): Int {
return if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
// To get username if it's a ref
return (object1 as Entry).getEntryInfo(Database.getInstance()).username
.compareTo((object2 as Entry).getEntryInfo(Database.getInstance()).username,
val usernameCompare = (object1 as Entry).getEntryInfo(database).username
.compareTo((object2 as Entry).getEntryInfo(database).username,
ignoreCase = true)
if (usernameCompare == 0)
NodeTitleComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
usernameCompare
} else {
NodeTitleComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
}
return NodeTitleComparator(ascending, groupsBefore, recycleBinBottom).compare(object1, object2)
}
}
/**
* Comparator of node by creation
*/
class NodeCreationComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeCreationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.creationTime.date
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val creationCompare = object1.creationTime.date
.compareTo(object2.creationTime.date)
return if (creationCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
creationCompare
}
}
/**
* Comparator of node by last modification
*/
class NodeLastModificationComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeLastModificationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.lastModificationTime.date
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastModificationCompare = object1.lastModificationTime.date
.compareTo(object2.lastModificationTime.date)
return if (lastModificationCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
lastModificationCompare
}
}
/**
* Comparator of node by last access
*/
class NodeLastAccessComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
class NodeLastAccessComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) {
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
return object1.lastAccessTime.date
override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastAccessCompare = object1.lastAccessTime.date
.compareTo(object2.lastAccessTime.date)
return if (lastAccessCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters)
.compare(object1, object2)
else
lastAccessCompare
}
}
}

View File

@@ -231,8 +231,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
* @param node Node to remove
* @return true if node can be recycle, false elsewhere
*/
fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
// TODO #394 Backup pw3
// TODO #394 Backup KDB
// fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
fun canRecycle(): Boolean {
return true
}

View File

@@ -36,4 +36,14 @@ abstract class EntryVersioned
constructor(parcel: Parcel) : super(parcel)
override fun nodeIndexInParentForNaturalOrder(): Int {
if (nodeIndexInParentForNaturalOrder == -1) {
val numberOfGroups = parent?.getChildGroups()?.size
val indexInEntries = parent?.getChildEntries()?.indexOf(this)
if (numberOfGroups != null && indexInEntries != null)
return numberOfGroups + indexInEntries
}
return nodeIndexInParentForNaturalOrder
}
}

View File

@@ -90,6 +90,7 @@ class FieldReferencesEngine {
if (result != null) {
val found = result.entry
found?.stopToManageFieldReferences()
val wanted = result.wanted
var data: String? = null
@@ -145,22 +146,15 @@ class FieldReferencesEngine {
searchParametersV4.setupNone()
searchParametersV4.searchString = ref.substring(4)
if (scan == 'T') {
searchParametersV4.searchInTitles = true
} else if (scan == 'U') {
searchParametersV4.searchInUserNames = true
} else if (scan == 'A') {
searchParametersV4.searchInUrls = true
} else if (scan == 'P') {
searchParametersV4.searchInPasswords = true
} else if (scan == 'N') {
searchParametersV4.searchInNotes = true
} else if (scan == 'I') {
searchParametersV4.searchInUUIDs = true
} else if (scan == 'O') {
searchParametersV4.searchInOther = true
} else {
return null
when (scan) {
'T' -> searchParametersV4.searchInTitles = true
'U' -> searchParametersV4.searchInUserNames = true
'A' -> searchParametersV4.searchInUrls = true
'P' -> searchParametersV4.searchInPasswords = true
'N' -> searchParametersV4.searchInNotes = true
'I' -> searchParametersV4.searchInUUIDs = true
'O' -> searchParametersV4.searchInOther = true
else -> return null
}
val list = ArrayList<EntryKDBX>()

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.element.group
import android.os.Parcel
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
import com.kunzisoft.keepass.database.element.node.NodeVersioned
import java.util.*
abstract class GroupVersioned
<
@@ -34,9 +35,10 @@ abstract class GroupVersioned
private var titleGroup = ""
@Transient
private val childGroups = ArrayList<Group>()
private val childGroups = LinkedList<Group>()
@Transient
private val childEntries = ArrayList<Entry>()
private val childEntries = LinkedList<Entry>()
private var positionIndexChildren = 0
constructor() : super()
@@ -52,9 +54,8 @@ abstract class GroupVersioned
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>) {
super.updateWith(source)
titleGroup = source.titleGroup
childGroups.clear()
removeChildren()
childGroups.addAll(source.childGroups)
childEntries.clear()
childEntries.addAll(source.childEntries)
}
@@ -62,23 +63,27 @@ abstract class GroupVersioned
get() = titleGroup
set(value) { titleGroup = value }
override fun getChildGroups(): MutableList<Group> {
override fun getChildGroups(): List<Group> {
return childGroups
}
override fun getChildEntries(): MutableList<Entry> {
override fun getChildEntries(): List<Entry> {
return childEntries
}
override fun addChildGroup(group: Group) {
if (childGroups.contains(group))
removeChildGroup(group)
positionIndexChildren++
group.nodeIndexInParentForNaturalOrder = positionIndexChildren
this.childGroups.add(group)
}
override fun addChildEntry(entry: Entry) {
if (childEntries.contains(entry))
removeChildEntry(entry)
positionIndexChildren++
entry.nodeIndexInParentForNaturalOrder = positionIndexChildren
this.childEntries.add(entry)
}
@@ -95,6 +100,13 @@ abstract class GroupVersioned
this.childEntries.clear()
}
override fun nodeIndexInParentForNaturalOrder(): Int {
return if (nodeIndexInParentForNaturalOrder == -1)
childGroups.indexOf(this)
else
nodeIndexInParentForNaturalOrder
}
override fun toString(): String {
return titleGroup
}

View File

@@ -24,9 +24,9 @@ import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>, Entry> : NodeVersionedInterface<Group> {
fun getChildGroups(): MutableList<Group>
fun getChildGroups(): List<Group>
fun getChildEntries(): MutableList<Entry>
fun getChildEntries(): List<Entry>
fun addChildGroup(group: Group)
@@ -40,6 +40,7 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
fun allowAddEntryIfIsRoot(): Boolean
@Suppress("UNCHECKED_CAST")
fun doForEachChildAndForIt(entryHandler: NodeHandler<Entry>,
groupHandler: NodeHandler<Group>) {
doForEachChild(entryHandler, groupHandler)

View File

@@ -25,17 +25,6 @@ interface Node: NodeVersionedInterface<Group> {
val nodeId: NodeId<*>?
val nodePositionInParent: Int
get() {
parent?.getChildren(Group.ChildFilter.META_STREAM)?.let { children ->
children.forEachIndexed { index, nodeVersioned ->
if (nodeVersioned.nodeId == this.nodeId)
return index
}
}
return -1
}
fun addParentFrom(node: Node) {
parent = node.parent
}

View File

@@ -21,8 +21,7 @@ package com.kunzisoft.keepass.database.element.node
import android.os.Parcel
import android.os.Parcelable
import java.util.UUID
import java.util.*
class NodeIdUUID : NodeId<UUID> {

View File

@@ -40,6 +40,8 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
val id: IdType
get() = nodeId.id
var nodeIndexInParentForNaturalOrder = -1
protected constructor()
protected constructor(parcel: Parcel) {

View File

@@ -47,5 +47,10 @@ interface NodeVersionedInterface<ParentGroup> : NodeTimeInterface, Parcelable {
fun isContainedIn(container: ParentGroup): Boolean
/**
* Groups are always before in natural order (DB order)
*/
fun nodeIndexInParentForNaturalOrder(): Int
fun touch(modified: Boolean, touchParents: Boolean)
}

View File

@@ -61,7 +61,9 @@ class BinaryAttachment : Parcelable {
val compressedByte = parcel.readByte().toInt()
isCompressed = if (compressedByte == 2) null else compressedByte != 0
isProtected = parcel.readByte().toInt() != 0
dataFile = File(parcel.readString())
parcel.readString()?.let {
dataFile = File(it)
}
}
@Throws(IOException::class)
@@ -74,10 +76,10 @@ class BinaryAttachment : Parcelable {
@Throws(IOException::class)
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
if (dataFile != null) {
dataFile?.let { concreteDataFile ->
// To compress, create a new binary with file
if (isCompressed != true) {
val fileBinaryCompress = File(dataFile!!.parent, dataFile!!.name + "_temp")
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
var outputStream: GZIPOutputStream? = null
var inputStream: InputStream? = null
try {
@@ -91,8 +93,8 @@ class BinaryAttachment : Parcelable {
outputStream?.close()
// Remove unGzip file
if (dataFile!!.delete()) {
if (fileBinaryCompress.renameTo(dataFile)) {
if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = true
}
@@ -104,9 +106,9 @@ class BinaryAttachment : Parcelable {
@Throws(IOException::class)
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
if (dataFile != null) {
dataFile?.let { concreteDataFile ->
if (isCompressed != false) {
val fileBinaryDecompress = File(dataFile!!.parent, dataFile!!.name + "_temp")
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
var outputStream: FileOutputStream? = null
var inputStream: GZIPInputStream? = null
try {
@@ -120,8 +122,8 @@ class BinaryAttachment : Parcelable {
outputStream?.close()
// Remove gzip file
if (dataFile!!.delete()) {
if (fileBinaryDecompress.renameTo(dataFile)) {
if (concreteDataFile.delete()) {
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = false
}

View File

@@ -127,11 +127,7 @@ object DatabaseKDBXXML {
const val ElemCustomData = "CustomData"
const val ElemStringDictExItem = "Item"
val dateFormatter: ThreadLocal<SimpleDateFormat> = object : ThreadLocal<SimpleDateFormat>() {
override fun initialValue(): SimpleDateFormat {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
return dateFormat
}
val DateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}

View File

@@ -660,7 +660,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
ctxDeletedObject?.uuid = readUuid(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
ctxDeletedObject?.deletionTime = readTime(xpp)
ctxDeletedObject?.setDeletionTime(readTime(xpp))
} else {
readUnknown(xpp)
}
@@ -829,7 +829,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else {
try {
utcDate = DatabaseKDBXXML.dateFormatter.get()?.parse(sDate)
utcDate = DatabaseKDBXXML.DateFormatter.parse(sDate)
} catch (e: ParseException) {
// Catch with null test below
}

View File

@@ -384,9 +384,9 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeObject(name: String, value: Date?) {
private fun writeObject(name: String, value: Date) {
if (header!!.version < DatabaseHeaderKDBX.FILE_VERSION_32_4) {
writeObject(name, DatabaseKDBXXML.dateFormatter.get().format(value))
writeObject(name, DatabaseKDBXXML.DateFormatter.format(value))
} else {
val dt = DateTime(value)
val seconds = DateKDBXUtil.convertDateToKDBX4Time(dt)
@@ -553,7 +553,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
writeObject(DatabaseKDBXXML.ElemDeletionTime, value.deletionTime)
writeObject(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime())
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
}

View File

@@ -22,8 +22,8 @@ package com.kunzisoft.keepass.education
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
import android.util.Log
import androidx.preference.PreferenceManager
import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R
@@ -94,7 +94,8 @@ open class Education(val activity: Activity) {
R.string.education_copy_username_key,
R.string.education_entry_edit_key,
R.string.education_password_generator_key,
R.string.education_entry_new_field_key)
R.string.education_entry_new_field_key,
R.string.education_setup_OTP_key)
/**
@@ -271,6 +272,18 @@ open class Education(val activity: Activity) {
context.resources.getBoolean(R.bool.education_entry_new_field_default))
}
/**
* Determines whether the explanatory view to setup OTP has already been displayed.
*
* @param context The context to open the SharedPreferences
* @return boolean value of education_setup_OTP_key key
*/
fun isEducationSetupOTPPerformed(context: Context): Boolean {
val prefs = getEducationSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.education_setup_OTP_key),
context.resources.getBoolean(R.bool.education_setup_OTP_default))
}
/**
* Defines if the reset education preference has been reclicked
*

View File

@@ -37,7 +37,7 @@ class EntryEditActivityEducation(activity: Activity)
activity.getString(R.string.education_generate_password_title),
activity.getString(R.string.education_generate_password_summary))
.textColorInt(Color.WHITE)
.tintTarget(false)
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) {
@@ -66,7 +66,7 @@ class EntryEditActivityEducation(activity: Activity)
activity.getString(R.string.education_entry_new_field_title),
activity.getString(R.string.education_entry_new_field_summary))
.textColorInt(Color.WHITE)
.tintTarget(false)
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) {
@@ -82,4 +82,33 @@ class EntryEditActivityEducation(activity: Activity)
},
R.string.education_entry_new_field_key)
}
/**
* Check and display learning views
* Displays the explanation to setup OTP
*/
fun checkAndPerformedSetUpOTPEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationSetupOTPPerformed(activity),
TapTarget.forView(educationView,
activity.getString(R.string.education_setup_OTP_title),
activity.getString(R.string.education_setup_OTP_summary))
.textColorInt(Color.WHITE)
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) {
super.onTargetClick(view)
onEducationViewClick?.invoke(view)
}
override fun onOuterCircleClick(view: TapTargetView?) {
super.onOuterCircleClick(view)
view?.dismiss(false)
onOuterViewClick?.invoke(view)
}
},
R.string.education_setup_OTP_key)
}
}

View File

@@ -24,14 +24,13 @@ import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
class KeyboardLauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
GroupActivity.launchForKeyboardSelection(this, PreferencesUtil.enableReadOnlyDatabase(this))
GroupActivity.launchForKeyboardSelection(this)
else {
// Pass extra to get entry
FileDatabaseSelectActivity.launchForKeyboardSelection(this)

View File

@@ -17,12 +17,12 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
@file:Suppress("DEPRECATION")
package com.kunzisoft.keepass.magikeyboard
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.inputmethodservice.InputMethodService
import android.inputmethodservice.Keyboard
import android.inputmethodservice.KeyboardView
@@ -42,8 +42,7 @@ import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.REMOVE_ENTRY_MAGIKEYBOARD_ACTION
import com.kunzisoft.keepass.utils.*
class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
@@ -55,29 +54,18 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
private var fieldsAdapter: FieldsAdapter? = null
private var playSoundDuringCLick: Boolean = false
private var lockBroadcastReceiver: BroadcastReceiver? = null
private var lockReceiver: LockReceiver? = null
override fun onCreate() {
super.onCreate()
// Remove the entry and lock the keyboard when the lock signal is receive
lockBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
REMOVE_ENTRY_MAGIKEYBOARD_ACTION, LOCK_ACTION -> {
lockReceiver = LockReceiver {
removeEntryInfo()
assignKeyboardView()
}
}
}
}
registerReceiver(lockBroadcastReceiver,
IntentFilter().apply {
addAction(LOCK_ACTION)
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
}
)
registerLockReceiver(lockReceiver, true)
}
override fun onCreateInputView(): View {
@@ -187,12 +175,12 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
private fun switchToPreviousKeyboard() {
var imeManager: InputMethodManager? = null
try {
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
switchToPreviousInputMethod()
} else {
window.window?.let { window ->
imeManager.switchToLastInputMethod(window.attributes.token)
imeManager?.switchToLastInputMethod(window.attributes.token)
}
}
} catch (e: Exception) {
@@ -214,8 +202,8 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
KEY_BACK_KEYBOARD -> switchToPreviousKeyboard()
KEY_CHANGE_KEYBOARD -> {
val imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imeManager.showInputMethodPicker()
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?)
?.showInputMethodPicker()
}
KEY_UNLOCK -> {
}
@@ -301,7 +289,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
override fun onDestroy() {
dismissCustomKeys()
unregisterReceiver(lockBroadcastReceiver)
unregisterLockReceiver(lockReceiver)
super.onDestroy()
}

View File

@@ -45,7 +45,7 @@ class EntryInfo : Parcelable {
password = parcel.readString() ?: password
url = parcel.readString() ?: url
notes = parcel.readString() ?: notes
parcel.readList(customFields, Field::class.java.classLoader)
parcel.readList(customFields as List<Field>, Field::class.java.classLoader)
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
}

View File

@@ -94,20 +94,21 @@ class AttachmentFileNotificationService: LockNotificationService() {
val nextNotificationId = (downloadFileUris.values.maxBy { it.notificationId }
?.notificationId ?: notificationId) + 1
val entryAttachment: EntryAttachment = intent.getParcelableExtra(ATTACHMENT_KEY)
val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
downloadFileUris[downloadFileUri] = attachmentNotification
try {
AttachmentFileAsyncTask(downloadFileUri,
attachmentNotification,
contentResolver).apply {
onUpdate = { uri, attachment, notificationIdAttach ->
newNotification(uri, attachment, notificationIdAttach)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(downloadFileUri, attachment)
intent.getParcelableExtra<EntryAttachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
downloadFileUris[downloadFileUri] = attachmentNotification
AttachmentFileAsyncTask(downloadFileUri,
attachmentNotification,
contentResolver).apply {
onUpdate = { uri, attachment, notificationIdAttach ->
newNotification(uri, attachment, notificationIdAttach)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(downloadFileUri, attachment)
}
}
}
}.execute()
}.execute()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to download $downloadFileUri", e)
}

View File

@@ -22,14 +22,11 @@ package com.kunzisoft.keepass.notifications
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.preference.PreferenceManager
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.exception.ClipboardException
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER
import com.kunzisoft.keepass.utils.LOCK_ACTION
import java.util.*
@@ -62,9 +59,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
//Get settings
notificationTimeoutMilliSecs = PreferenceManager.getDefaultSharedPreferences(this)
.getString(getString(R.string.clipboard_timeout_key),
getString(R.string.clipboard_timeout_default))?.toLong() ?: TimeoutHelper.DEFAULT_TIMEOUT
notificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
when {
intent == null -> Log.w(TAG, "null intent")
@@ -78,12 +73,14 @@ class ClipboardEntryNotificationService : LockNotificationService() {
}
else -> for (actionKey in ClipboardEntryNotificationField.allActionKeys) {
if (actionKey == intent.action) {
val fieldToCopy = intent.getParcelableExtra<ClipboardEntryNotificationField>(
ClipboardEntryNotificationField.getExtraKeyLinkToActionKey(actionKey))
val nextFields = constructListOfField(intent)
// Remove the current field from the next fields
nextFields.remove(fieldToCopy)
copyField(fieldToCopy, nextFields)
intent.getParcelableExtra<ClipboardEntryNotificationField>(
ClipboardEntryNotificationField.getExtraKeyLinkToActionKey(actionKey))?.let {
fieldToCopy ->
val nextFields = constructListOfField(intent)
// Remove the current field from the next fields
nextFields.remove(fieldToCopy)
copyField(fieldToCopy, nextFields)
}
}
}
}
@@ -91,10 +88,12 @@ class ClipboardEntryNotificationService : LockNotificationService() {
}
private fun constructListOfField(intent: Intent?): ArrayList<ClipboardEntryNotificationField> {
var fieldList = ArrayList<ClipboardEntryNotificationField>()
if (intent != null && intent.extras != null) {
if (intent.extras!!.containsKey(EXTRA_CLIPBOARD_FIELDS))
fieldList = intent.getParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS)
val fieldList = ArrayList<ClipboardEntryNotificationField>()
if (intent?.extras?.containsKey(EXTRA_CLIPBOARD_FIELDS) == true) {
intent.getParcelableArrayListExtra<ClipboardEntryNotificationField>(EXTRA_CLIPBOARD_FIELDS)?.let { retrieveFields ->
fieldList.clear()
fieldList.addAll(retrieveFields)
}
}
return fieldList
}
@@ -208,7 +207,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
private fun cleanClipboard() {
try {
clipboardHelper?.cleanClipboard()
} catch (e: ClipboardException) {
} catch (e: Exception) {
Log.e(TAG, "Clipboard can't be cleaned", e)
}
}

View File

@@ -26,8 +26,9 @@ import android.os.Build
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.closeDatabase
class DatabaseOpenNotificationService: LockNotificationService() {
@@ -36,8 +37,14 @@ class DatabaseOpenNotificationService: LockNotificationService() {
private fun stopNotificationAndSendLock() {
// Send lock action
sendBroadcast(Intent(LOCK_ACTION))
// Stop the service
stopSelf()
}
override fun actionOnLock() {
closeDatabase()
// Remove the lock timer (no more needed if it exists)
TimeoutHelper.cancelLockTimer(this)
// Service is stopped after receive the broadcast
super.actionOnLock()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -60,13 +67,16 @@ class DatabaseOpenNotificationService: LockNotificationService() {
val database = Database.getInstance()
if (database.loaded) {
notificationManager?.notify(notificationId, buildNewNotification().apply {
startForeground(notificationId, buildNewNotification().apply {
setSmallIcon(R.drawable.notification_ic_database_open)
setContentTitle(getString(R.string.database_opened))
setContentText(database.name + " (" + database.version + ")")
setAutoCancel(false)
setContentIntent(pendingDatabaseIntent)
// Unfortunately swipe is disabled in lollipop+
setDeleteIntent(pendingDeleteIntent)
addAction(R.drawable.ic_lock_white_24dp, getString(R.string.lock),
pendingDeleteIntent)
}.build())
} else {
stopSelf()
@@ -80,9 +90,11 @@ class DatabaseOpenNotificationService: LockNotificationService() {
companion object {
const val ACTION_CLOSE_DATABASE = "ACTION_CLOSE_DATABASE"
fun startIfAllowed(context: Context) {
if (PreferencesUtil.isPersistentNotificationEnable(context)) {
// Start the opening notification
fun start(context: Context) {
// Start the opening notification, keep it active to receive lock
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(Intent(context, DatabaseOpenNotificationService::class.java))
} else {
context.startService(Intent(context, DatabaseOpenNotificationService::class.java))
}
}

View File

@@ -32,6 +32,7 @@ import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseR
import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable
import com.kunzisoft.keepass.database.action.node.*
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
@@ -209,8 +210,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
&& intent.hasExtra(KEY_FILE_KEY)
) {
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY)
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
if (databaseUri == null)
return null
return CreateDatabaseRunnable(this,
Database.getInstance(),
databaseUri,
@@ -236,12 +241,15 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
) {
val database = Database.getInstance()
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY)
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
if (databaseUri == null)
return null
return LoadDatabaseRunnable(
this,
database,
@@ -275,9 +283,10 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
&& intent.hasExtra(KEY_FILE_KEY)
) {
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
AssignPasswordInDatabaseRunnable(this,
Database.getInstance(),
intent.getParcelableExtra(DATABASE_URI_KEY),
databaseUri,
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
intent.getStringExtra(MASTER_PASSWORD_KEY),
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
@@ -304,10 +313,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { parent ->
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
if (parentId == null
|| newGroup == null)
return null
database.getGroupById(parentId)?.let { parent ->
AddGroupRunnable(this,
database,
intent.getParcelableExtra(GROUP_KEY),
newGroup,
parent,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
@@ -323,8 +339,14 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
database.getGroupById(intent.getParcelableExtra(GROUP_ID_KEY))?.let { oldGroup ->
val newGroup: Group = intent.getParcelableExtra(GROUP_KEY)
val groupId: NodeId<*>? = intent.getParcelableExtra(GROUP_ID_KEY)
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
if (groupId == null
|| newGroup == null)
return null
database.getGroupById(groupId)?.let { oldGroup ->
UpdateGroupRunnable(this,
database,
oldGroup,
@@ -343,10 +365,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { parent ->
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
if (parentId == null
|| newEntry == null)
return null
database.getGroupById(parentId)?.let { parent ->
AddEntryRunnable(this,
database,
intent.getParcelableExtra(ENTRY_KEY),
newEntry,
parent,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
@@ -362,8 +391,14 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { oldEntry ->
val newEntry: Entry = intent.getParcelableExtra(ENTRY_KEY)
val entryId: NodeId<UUID>? = intent.getParcelableExtra(ENTRY_ID_KEY)
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
if (entryId == null
|| newEntry == null)
return null
database.getEntryById(entryId)?.let { oldEntry ->
UpdateEntryRunnable(this,
database,
oldEntry,
@@ -383,7 +418,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { newParent ->
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
database.getGroupById(parentId)?.let { newParent ->
CopyNodesRunnable(this,
database,
getListNodesFromBundle(database, intent.extras!!),
@@ -403,7 +440,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { newParent ->
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
database.getGroupById(parentId)?.let { newParent ->
MoveNodesRunnable(this,
database,
getListNodesFromBundle(database, intent.extras!!),
@@ -438,7 +477,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { mainEntry ->
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
database.getEntryById(entryId)?.let { mainEntry ->
RestoreEntryHistoryDatabaseRunnable(this,
database,
mainEntry,
@@ -456,7 +497,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
&& intent.hasExtra(SAVE_DATABASE_KEY)
) {
val database = Database.getInstance()
database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { mainEntry ->
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
database.getEntryById(entryId)?.let { mainEntry ->
DeleteEntryHistoryDatabaseRunnable(this,
database,
mainEntry,
@@ -472,10 +515,18 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
return if (intent.hasExtra(OLD_ELEMENT_KEY)
&& intent.hasExtra(NEW_ELEMENT_KEY)
&& intent.hasExtra(SAVE_DATABASE_KEY)) {
val oldElement: CompressionAlgorithm? = intent.getParcelableExtra(OLD_ELEMENT_KEY)
val newElement: CompressionAlgorithm? = intent.getParcelableExtra(NEW_ELEMENT_KEY)
if (oldElement == null
|| newElement == null)
return null
return UpdateCompressionBinariesDatabaseRunnable(this,
Database.getInstance(),
intent.getParcelableExtra(OLD_ELEMENT_KEY),
intent.getParcelableExtra(NEW_ELEMENT_KEY),
oldElement,
newElement,
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
).apply {
mAfterSaveDatabase = { result ->

View File

@@ -62,7 +62,9 @@ class KeyboardEntryNotificationService : LockNotificationService() {
else -> {
notificationManager?.cancel(notificationId)
if (intent.hasExtra(ENTRY_INFO_KEY)) {
newNotification(intent.getParcelableExtra(ENTRY_INFO_KEY))
intent.getParcelableExtra<EntryInfo>(ENTRY_INFO_KEY)?.let {
newNotification(it)
}
}
}
}

View File

@@ -19,31 +19,28 @@
*/
package com.kunzisoft.keepass.notifications
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.LockReceiver
import com.kunzisoft.keepass.utils.registerLockReceiver
import com.kunzisoft.keepass.utils.unregisterLockReceiver
abstract class LockNotificationService : NotificationService() {
private var lockBroadcastReceiver: BroadcastReceiver? = null
private var mLockReceiver: LockReceiver? = null
protected open fun actionOnLock() {
// Stop the service in all cases
stopSelf()
}
override fun onCreate() {
super.onCreate()
// Register a lock receiver to stop notification service when lock on keyboard is performed
lockBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// Stop the service in all cases
stopSelf()
}
mLockReceiver = LockReceiver {
actionOnLock()
}
registerReceiver(lockBroadcastReceiver,
IntentFilter().apply {
addAction(LOCK_ACTION)
}
)
registerLockReceiver(mLockReceiver)
}
protected fun stopTask(task: Thread?) {
@@ -59,7 +56,7 @@ abstract class LockNotificationService : NotificationService() {
override fun onDestroy() {
unregisterReceiver(lockBroadcastReceiver)
unregisterLockReceiver(mLockReceiver)
super.onDestroy()
}

View File

@@ -281,8 +281,10 @@ object OtpEntryFields {
// malformed
return false
}
otpElement.period = matcher.group(1).toIntOrNull() ?: TOTP_DEFAULT_PERIOD
otpElement.tokenType = OtpTokenType.getFromString(matcher.group(2))
otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
otpElement.tokenType = matcher.group(2)?.let {
OtpTokenType.getFromString(it)
} ?: OtpTokenType.RFC6238
}
} catch (exception: Exception) {
return false

View File

@@ -75,16 +75,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
activity?.let { activity ->
allowCopyPassword()
findPreference<Preference>(getString(R.string.keyfile_key))?.setOnPreferenceChangeListener { _, newValue ->
findPreference<Preference>(getString(R.string.remember_database_locations_key))?.setOnPreferenceChangeListener { _, newValue ->
if (!(newValue as Boolean)) {
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAllKeyFiles()
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAll()
}
true
}
findPreference<Preference>(getString(R.string.recentfile_key))?.setOnPreferenceChangeListener { _, newValue ->
findPreference<Preference>(getString(R.string.remember_keyfile_locations_key))?.setOnPreferenceChangeListener { _, newValue ->
if (!(newValue as Boolean)) {
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAll()
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAllKeyFiles()
}
true
}
@@ -95,8 +95,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
setPreferencesFromResource(R.xml.preferences_form_filling, rootKey)
activity?.let { activity ->
val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key))
val autofillManager = activity.getSystemService(AutofillManager::class.java)
if (autofillManager != null && autofillManager.hasEnabledAutofillServices())
autoFillEnablePreference?.isChecked = autofillManager.hasEnabledAutofillServices()
@@ -143,13 +143,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
}
} else {
autoFillEnablePreference?.setOnPreferenceClickListener { preference ->
(preference as SwitchPreference).isChecked = false
val fragmentManager = fragmentManager!!
UnavailableFeatureDialogFragment.getInstance(Build.VERSION_CODES.O)
.show(fragmentManager, "unavailableFeatureDialog")
false
}
findPreference<Preference>(getString(R.string.autofill_key))?.isVisible = false
}
}
@@ -254,13 +248,19 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric(
activity,
object : BiometricUnlockDatabaseHelper.BiometricUnlockErrorCallback {
override fun onInvalidKeyException(e: Exception) {}
override fun onBiometricException(e: Exception) {
fun showException(e: Exception) {
Toast.makeText(context,
getString(R.string.biometric_scanning_error, e.localizedMessage),
Toast.LENGTH_SHORT).show()
}
override fun onInvalidKeyException(e: Exception) {
showException(e)
}
override fun onBiometricException(e: Exception) {
showException(e)
}
})
}
CipherDatabaseAction.getInstance(context.applicationContext).deleteAll()
@@ -282,6 +282,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
private fun onCreateAppearancePreferences(rootKey: String?) {
setPreferencesFromResource(R.xml.preferences_appearance, rootKey)
// To change list items appearance
PreferencesUtil.APPEARANCE_CHANGED = true
activity?.let { activity ->
findPreference<ListPreference>(getString(R.string.setting_style_key))?.setOnPreferenceChangeListener { _, newValue ->
var styleEnabled = true
@@ -342,8 +345,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
activity?.let { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key))
if (autoFillEnablePreference != null) {
findPreference<SwitchPreference?>(getString(R.string.settings_autofill_enable_key))?.let {
autoFillEnablePreference ->
val autofillManager = activity.getSystemService(AutofillManager::class.java)
autoFillEnablePreference.isChecked = autofillManager != null
&& autofillManager.hasEnabledAutofillServices()

View File

@@ -20,7 +20,8 @@
package com.kunzisoft.keepass.settings
import android.content.Context
import android.preference.PreferenceManager
import android.net.Uri
import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.timeout.TimeoutHelper
@@ -28,16 +29,72 @@ import java.util.*
object PreferencesUtil {
fun rememberKeyFiles(context: Context): Boolean {
var APPEARANCE_CHANGED = false
fun saveDefaultDatabasePath(context: Context, defaultDatabaseUri: Uri?) {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyfile_key),
context.resources.getBoolean(R.bool.keyfile_default))
prefs?.edit()?.apply {
defaultDatabaseUri?.let {
putString(context.getString(R.string.default_database_path_key), it.toString())
} ?: kotlin.run {
remove(context.getString(R.string.default_database_path_key))
}
apply()
}
}
fun getDefaultDatabasePath(context: Context): String? {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.default_database_path_key), "")
}
fun saveNodeSort(context: Context,
sortNodeEnum: SortNodeEnum,
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs?.edit()?.apply {
putString(context.getString(R.string.sort_node_key), sortNodeEnum.name)
putBoolean(context.getString(R.string.sort_ascending_key), sortNodeParameters.ascending)
putBoolean(context.getString(R.string.sort_group_before_key), sortNodeParameters.groupsBefore)
putBoolean(context.getString(R.string.sort_recycle_bin_bottom_key), sortNodeParameters.recycleBinBottom)
apply()
}
}
fun rememberDatabaseLocations(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.remember_database_locations_key),
context.resources.getBoolean(R.bool.remember_database_locations_default))
}
fun showRecentFiles(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.show_recent_files_key),
context.resources.getBoolean(R.bool.show_recent_files_default))
}
fun hideBrokenLocations(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.hide_broken_locations_key),
context.resources.getBoolean(R.bool.hide_broken_locations_default))
}
fun rememberKeyFileLocations(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.remember_keyfile_locations_key),
context.resources.getBoolean(R.bool.remember_keyfile_locations_default))
}
fun omitBackup(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.omitbackup_key),
context.resources.getBoolean(R.bool.omitbackup_default))
return prefs.getBoolean(context.getString(R.string.omit_backup_search_key),
context.resources.getBoolean(R.bool.omit_backup_search_default))
}
fun automaticallyFocusSearch(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.auto_focus_search_key),
context.resources.getBoolean(R.bool.auto_focus_search_default))
}
fun showUsernamesListEntries(context: Context): Boolean {
@@ -123,6 +180,13 @@ object PreferencesUtil {
}
}
fun getClipboardTimeout(context: Context): Long {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.clipboard_timeout_key),
context.getString(R.string.clipboard_timeout_default))?.toLong()
?: TimeoutHelper.DEFAULT_TIMEOUT
}
fun isLockDatabaseWhenScreenShutOffEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.lock_database_screen_off_key),
@@ -141,12 +205,6 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.enable_auto_save_database_default))
}
fun isPersistentNotificationEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.persistent_notification_key),
context.resources.getBoolean(R.bool.persistent_notification_default))
}
fun isBiometricUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
@@ -194,8 +252,8 @@ object PreferencesUtil {
fun isPasswordMask(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.maskpass_key),
context.resources.getBoolean(R.bool.maskpass_default))
return prefs.getBoolean(context.getString(R.string.hide_password_key),
context.resources.getBoolean(R.bool.hide_password_default))
}
fun fieldFontIsInVisibility(context: Context): Boolean {

View File

@@ -27,6 +27,7 @@ import android.net.Uri
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
@@ -35,6 +36,7 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.view.showActionError
open class SettingsActivity
: LockingActivity(),
@@ -43,6 +45,7 @@ open class SettingsActivity
private var backupManager: BackupManager? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var toolbar: Toolbar? = null
companion object {
@@ -74,6 +77,8 @@ open class SettingsActivity
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_toolbar)
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
toolbar = findViewById(R.id.toolbar)
toolbar?.setTitle(R.string.settings)
setSupportActionBar(toolbar)
@@ -92,6 +97,8 @@ open class SettingsActivity
(supportFragmentManager
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
?.onProgressDialogThreadResult(actionTask, result)
coordinatorLayout?.showActionError(result)
}
}

View File

@@ -120,7 +120,7 @@ class HashedBlockInputStream(inputStream: InputStream) : InputStream() {
}
val computedHash = messageDigest.digest(buffer)
if (computedHash == null || computedHash.size != HASH_SIZE) {
if (computedHash.size != HASH_SIZE) {
throw IOException("Hash wrong size")
}

View File

@@ -23,6 +23,7 @@ import java.io.IOException
import java.io.OutputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import kotlin.math.min
class HashedBlockOutputStream : OutputStream {
@@ -61,11 +62,11 @@ class HashedBlockOutputStream : OutputStream {
override fun close() {
if (bufferPos != 0) {
// Write remaining buffered amount
WriteHashedBlock()
writeHashedBlock()
}
// Write terminating block
WriteHashedBlock()
writeHashedBlock()
flush()
baseStream!!.close()
@@ -82,12 +83,12 @@ class HashedBlockOutputStream : OutputStream {
var counter = count
while (counter > 0) {
if (bufferPos == buffer!!.size) {
WriteHashedBlock()
writeHashedBlock()
}
val copyLen = Math.min(buffer!!.size - bufferPos, counter)
val copyLen = min(buffer!!.size - bufferPos, counter)
System.arraycopy(b, currentOffset, buffer, bufferPos, copyLen)
System.arraycopy(b, currentOffset, buffer!!, bufferPos, copyLen)
currentOffset += copyLen
bufferPos += copyLen
@@ -97,21 +98,21 @@ class HashedBlockOutputStream : OutputStream {
}
@Throws(IOException::class)
private fun WriteHashedBlock() {
private fun writeHashedBlock() {
baseStream!!.writeUInt(bufferIndex)
bufferIndex++
if (bufferPos > 0) {
var md: MessageDigest? = null
val messageDigest: MessageDigest
try {
md = MessageDigest.getInstance("SHA-256")
messageDigest = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
throw IOException("SHA-256 not implemented here.")
}
val hash: ByteArray
md!!.update(buffer, 0, bufferPos)
hash = md.digest()
messageDigest.update(buffer!!, 0, bufferPos)
hash = messageDigest.digest()
/*
if ( bufferPos == buffer.length) {
hash = md.digest(buffer);

View File

@@ -25,7 +25,6 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.preference.PreferenceManager
import android.text.SpannableString
import android.text.method.LinkMovementMethod
import android.text.util.Linkify
@@ -33,6 +32,7 @@ import android.widget.TextView
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.exception.ClipboardException
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
class ClipboardHelper(private val context: Context) {
@@ -47,7 +47,6 @@ class ClipboardHelper(private val context: Context) {
return mClipboardManager
}
@JvmOverloads
fun timeoutCopyToClipboard(text: String, toastString: String = "") {
if (toastString.isNotEmpty())
Toast.makeText(context, toastString, Toast.LENGTH_LONG).show()
@@ -59,13 +58,9 @@ class ClipboardHelper(private val context: Context) {
return
}
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val sClipClear = prefs.getString(context.getString(R.string.clipboard_timeout_key),
context.getString(R.string.clipboard_timeout_default))
val clipClearTime = (sClipClear ?: "300000").toLong()
if (clipClearTime > 0) {
mTimer.schedule(ClearClipboardTask(context, text), clipClearTime)
val clipboardTimeout = PreferencesUtil.getClipboardTimeout(context)
if (clipboardTimeout > 0) {
mTimer.schedule(ClearClipboardTask(context, text), clipboardTimeout)
}
}
@@ -90,7 +85,7 @@ class ClipboardHelper(private val context: Context) {
@Throws(ClipboardException::class)
fun copyToClipboard(label: String, value: String) {
try {
getClipboardManager()?.primaryClip = ClipData.newPlainText(label, value)
getClipboardManager()?.setPrimaryClip(ClipData.newPlainText(label, value))
} catch (e: Exception) {
throw ClipboardException(e)
}
@@ -98,12 +93,15 @@ class ClipboardHelper(private val context: Context) {
}
@Throws(ClipboardException::class)
@JvmOverloads
fun cleanClipboard(label: String = "") {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
getClipboardManager()?.clearPrimaryClip()
} else {
copyToClipboard(label, "")
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
getClipboardManager()?.clearPrimaryClip()
} else {
copyToClipboard(label, "")
}
} catch (e: Exception) {
throw ClipboardException(e)
}
}

View File

@@ -46,53 +46,63 @@ object TimeoutHelper {
private set
private fun getLockPendingIntent(context: Context): PendingIntent {
return PendingIntent.getBroadcast(context,
return PendingIntent.getBroadcast(context.applicationContext,
REQUEST_ID,
Intent(LOCK_ACTION),
PendingIntent.FLAG_CANCEL_CURRENT)
}
/**
* Record the current time to check it later with checkTime
* Start the lock timer by creating an alarm,
* if the method is recalled with a previous lock timer pending, the previous one is deleted
*/
fun recordTime(context: Context) {
// Record timeout time in case timeout service is killed
PreferencesUtil.saveCurrentTime(context)
private fun startLockTimer(context: Context) {
if (Database.getInstance().loaded) {
val timeout = PreferencesUtil.getAppTimeout(context)
// No timeout don't start timeout service
if (timeout != NEVER) {
val triggerTime = System.currentTimeMillis() + timeout
val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
Log.d(TAG, "TimeoutHelper start")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
am.setExact(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
} else {
am.set(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
// No timeout don't start timeout service
(context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
val triggerTime = System.currentTimeMillis() + timeout
Log.d(TAG, "TimeoutHelper start")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
} else {
alarmManager.set(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
}
}
}
}
}
/**
* Cancel the lock timer currently pending, useful if lock was triggered by another way
*/
fun cancelLockTimer(context: Context) {
(context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
Log.d(TAG, "TimeoutHelper cancel")
alarmManager.cancel(getLockPendingIntent(context))
}
}
/**
* Record the current time, to check it later with checkTime and start a new lock timer
*/
fun recordTime(context: Context) {
// Record timeout time in case timeout service is killed
PreferencesUtil.saveCurrentTime(context)
startLockTimer(context)
}
/**
* Check the time previously record with recordTime and do the [timeoutAction] if timeout
* if temporarilyDisableTimeout() is called, the function as no effect until releaseTemporarilyDisableTimeoutAndCheckTime() is called
* return 'false' if timeout, 'true' if in time
* return 'false' and send broadcast lock action if timeout, 'true' if in time
*/
fun checkTime(context: Context, timeoutAction: (() -> Unit)? = null): Boolean {
// No effect if temporarily disable
if (temporarilyDisableTimeout)
return true
// Cancel the lock PendingIntent
if (Database.getInstance().loaded) {
val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
Log.d(TAG, "TimeoutHelper cancel")
am.cancel(getLockPendingIntent(context))
}
// Check whether the timeout has expired
val currentTime = System.currentTimeMillis()
@@ -115,6 +125,7 @@ object TimeoutHelper {
if (diff >= appTimeout) {
// We have timed out
timeoutAction?.invoke()
context.sendBroadcast(Intent(LOCK_ACTION))
return false
}
return true
@@ -142,27 +153,14 @@ object TimeoutHelper {
/**
* Temporarily disable timeout, checkTime() function always return true
*/
fun temporarilyDisableTimeout(context: Context) {
fun temporarilyDisableTimeout() {
temporarilyDisableTimeout = true
// Stop the opening notification
DatabaseOpenNotificationService.stop(context)
}
/**
* Release the temporarily disable timeout and directly call checkTime()
* Release the temporarily disable timeout
*/
fun releaseTemporarilyDisableTimeoutAndLockIfTimeout(context: Context): Boolean {
fun releaseTemporarilyDisableTimeout() {
temporarilyDisableTimeout = false
val inTime = if (context is LockingActivity) {
checkTimeAndLockIfTimeout(context)
} else {
checkTime(context)
}
if (inTime) {
// Start the opening notification
DatabaseOpenNotificationService.startIfAllowed(context)
}
return inTime
}
}

View File

@@ -19,10 +19,91 @@
*/
package com.kunzisoft.keepass.utils
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Handler
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
const val DATABASE_START_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_START_TASK_ACTION"
const val DATABASE_STOP_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_STOP_TASK_ACTION"
const val LOCK_ACTION = "com.kunzisoft.keepass.LOCK"
const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY_MAGIKEYBOARD"
class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
private val screenOffHandler = Handler()
private var screenOffRunnable: Runnable? = null
override fun onReceive(context: Context, intent: Intent) {
screenOffRunnable?.let { runnable ->
screenOffHandler.removeCallbacks(runnable)
}
// If allowed, lock and exit
if (!TimeoutHelper.temporarilyDisableTimeout) {
intent.action?.let {
when (it) {
Intent.ACTION_SCREEN_OFF -> {
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) {
screenOffRunnable = Runnable {
lockAction.invoke()
}
// Launch the effective action after a small time
screenOffHandler.postDelayed(screenOffRunnable!!,
context.getString(R.string.timeout_screen_off).toLong())
}
}
LOCK_ACTION,
REMOVE_ENTRY_MAGIKEYBOARD_ACTION -> lockAction.invoke()
else -> {}
}
}
}
}
}
fun Context.registerLockReceiver(lockReceiver: LockReceiver?,
registerRemoveEntryMagikeyboard: Boolean = false) {
lockReceiver?.let {
registerReceiver(it, IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_SCREEN_ON)
addAction(LOCK_ACTION)
if (registerRemoveEntryMagikeyboard)
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
})
}
}
fun Context.unregisterLockReceiver(lockReceiver: LockReceiver?) {
lockReceiver?.let {
unregisterReceiver(it)
}
}
fun Context.closeDatabase() {
// Stop the Magikeyboard service
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
MagikIME.removeEntry(this)
// Stop the notification service
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
Log.i(Context::class.java.name, "Shutdown after inactivity or manual lock")
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?)?.apply {
cancelAll()
}
// Clear data
Database.getInstance().closeAndClear(applicationContext.filesDir)
}

View File

@@ -22,8 +22,6 @@ package com.kunzisoft.keepass.utils
import android.content.Context
import android.net.Uri
import android.text.format.Formatter
import androidx.documentfile.provider.DocumentFile
import java.io.File
import java.io.Serializable
import java.text.DateFormat
import java.util.*
@@ -34,7 +32,7 @@ open class FileInfo : Serializable {
var fileUri: Uri?
var filePath: String? = null
var fileName: String? = ""
var lastModification = Date()
var lastModification = Date(0L)
var size: Long = 0L
constructor(context: Context, fileUri: Uri) {
@@ -51,22 +49,11 @@ open class FileInfo : Serializable {
fun init() {
this.filePath = fileUri?.path
if (EXTERNAL_STORAGE_AUTHORITY == fileUri?.authority) {
fileUri?.let { fileUri ->
DocumentFile.fromSingleUri(context, fileUri)?.let { file ->
size = file.length()
fileName = file.name
lastModification = Date(file.lastModified())
}
}
} else {
filePath?.let {
File(it).let { file ->
size = file.length()
fileName = file.name
lastModification = Date(file.lastModified())
}
}
UriUtil.getFileData(context, fileUri)?.let { file ->
size = file.length()
fileName = file.name
lastModification = Date(file.lastModified())
}
if (fileName == null || fileName!!.isEmpty()) {
@@ -74,10 +61,18 @@ open class FileInfo : Serializable {
}
}
fun found(): Boolean {
fun lastModificationAccessible(): Boolean {
return lastModification.after(Date(0L))
}
fun sizeAccessible(): Boolean {
return size != 0L
}
fun dataAccessible(): Boolean {
return UriUtil.isUriAccessible(context.contentResolver, fileUri)
}
fun getModificationString(): String {
return DateFormat.getDateTimeInstance()
.format(lastModification)
@@ -86,9 +81,4 @@ open class FileInfo : Serializable {
fun getSizeString(): String {
return Formatter.formatFileSize(context, size)
}
companion object {
private const val EXTERNAL_STORAGE_AUTHORITY = "com.android.externalstorage.documents"
}
}

View File

@@ -100,5 +100,5 @@ object ParcelableUtil {
inline fun <reified T : Enum<T>> Parcel.readEnum() =
readString()?.let { enumValueOf<T>(it) }
inline fun <T : Enum<T>> Parcel.writeEnum(value: T?) =
fun <T : Enum<T>> Parcel.writeEnum(value: T?) =
writeString(value?.name)

View File

@@ -19,8 +19,7 @@
*/
package com.kunzisoft.keepass.utils
import java.util.ArrayList
import java.util.Locale
import java.util.*
object StringUtil {
@@ -85,5 +84,17 @@ object StringUtil {
return currentText
}
}
fun UUID.toKeePassRefString(): String {
val tempString = toString().replace("-", "").toUpperCase(Locale.ENGLISH)
return StringBuffer(reverseString2(tempString.substring(12, 16)))
.append(reverseString2(tempString.substring(8, 12)))
.append(reverseString2(tempString.substring(0, 8)))
.append(reverseString2(tempString.substring(20, 32)))
.append(reverseString2(tempString.substring(16, 20))).toString()
}
private fun reverseString2(string: String): String {
return string.chunked(2).reversed().joinToString("")
}

View File

@@ -24,29 +24,82 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.InputStream
import java.util.*
object UriUtil {
@Throws(FileNotFoundException::class)
fun getUriInputStream(contentResolver: ContentResolver, uri: Uri?): InputStream? {
if (uri == null)
return null
val scheme = uri.scheme
return if (scheme == null || scheme.isEmpty() || scheme == "file") {
FileInputStream(uri.path!!)
} else if (scheme == "content") {
contentResolver.openInputStream(uri)
} else {
null
fun isUriAccessible(contentResolver: ContentResolver, fileUri: Uri?): Boolean {
if (fileUri == null)
return false
return try {
//https://developer.android.com/reference/android/content/res/AssetFileDescriptor
contentResolver.openInputStream(fileUri)?.close()
true
} catch (e: Exception) {
Log.e(UriUtil.javaClass.name, "Unable to access uri $fileUri : ${e.message}")
false
}
}
fun isUriWritable(fileUri: Uri?): Boolean {
if (fileUri == null)
return false
// TODO Uri writeable detection
return true
}
fun getFileData(context: Context, fileUri: Uri?): DocumentFile? {
if (fileUri == null)
return null
return when {
isFileScheme(fileUri) -> {
fileUri.path?.let {
File(it).let { file ->
return DocumentFile.fromFile(file)
}
}
}
isContentScheme(fileUri) -> DocumentFile.fromSingleUri(context, fileUri)
else -> null
}
}
@Throws(FileNotFoundException::class)
fun getUriInputStream(contentResolver: ContentResolver, fileUri: Uri?): InputStream? {
if (fileUri == null)
return null
return when {
isFileScheme(fileUri) -> fileUri.path?.let { FileInputStream(it) }
isContentScheme(fileUri) -> contentResolver.openInputStream(fileUri)
else -> null
}
}
private fun isFileScheme(fileUri: Uri): Boolean {
val scheme = fileUri.scheme
if (scheme == null || scheme.isEmpty() || scheme.toLowerCase(Locale.ENGLISH) == "file") {
return true
}
return false
}
private fun isContentScheme(fileUri: Uri): Boolean {
val scheme = fileUri.scheme
if (scheme != null && scheme.toLowerCase(Locale.ENGLISH) == "content") {
return true
}
return false
}
fun parse(stringUri: String?): Uri? {
return if (stringUri?.isNotEmpty() == true) {
Uri.parse(stringUri)

View File

@@ -69,8 +69,8 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
}
private fun inflate(context: Context) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.view_button_add_node, this)
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_button_add_node, this)
addEntryEnable = true
addGroupEnable = true
@@ -132,7 +132,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
}
fun showButton() {
if (addButtonView?.visibility != VISIBLE)
if (isEnable && addButtonView?.visibility != VISIBLE)
addButtonView?.show(onAddButtonVisibilityChangedListener)
}

View File

@@ -44,8 +44,8 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.view_advanced_unlock, this)
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_advanced_unlock, this)
unlockContainerView = findViewById(R.id.fingerprint_container)

View File

@@ -31,6 +31,7 @@ import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsAdapter
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
@@ -40,8 +41,8 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.utils.toKeePassRefString
import java.util.*
import androidx.recyclerview.widget.SimpleItemAnimator
class EntryContentsView @JvmOverloads constructor(context: Context,
@@ -91,6 +92,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val historyAdapter = EntryHistoryAdapter(context)
private val uuidView: TextView
private val uuidReferenceView: TextView
val isUserNamePresent: Boolean
get() = userNameContainerView.visibility == View.VISIBLE
@@ -99,8 +101,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
get() = passwordContainerView.visibility == View.VISIBLE
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.view_entry_contents, this)
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_entry_contents, this)
userNameContainerView = findViewById(R.id.entry_user_name_container)
userNameView = findViewById(R.id.entry_user_name)
@@ -146,6 +148,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
}
uuidView = findViewById(R.id.entry_UUID)
uuidReferenceView = findViewById(R.id.entry_UUID_reference)
val attrColorAccent = intArrayOf(R.attr.colorAccent)
val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent)
@@ -346,6 +349,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
fun assignUUID(uuid: UUID) {
uuidView.text = uuid.toString()
uuidReferenceView.text = uuid.toKeePassRefString()
}
/* -------------

View File

@@ -43,8 +43,8 @@ open class EntryCustomField @JvmOverloads constructor(context: Context,
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.item_entry_new_field, this)
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.item_entry_new_field, this)
labelView = findViewById(R.id.title)
valueView = findViewById(R.id.value)

View File

@@ -21,21 +21,21 @@ package com.kunzisoft.keepass.view
import android.content.Context
import android.graphics.Color
import com.google.android.material.textfield.TextInputLayout
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.*
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
import com.kunzisoft.keepass.model.Field
import org.joda.time.Duration
import org.joda.time.Instant
class EntryEditContentsView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
@@ -52,16 +52,26 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
private val entryPasswordLayoutView: TextInputLayout
private val entryPasswordView: EditText
private val entryConfirmationPasswordView: EditText
val generatePasswordView: View
private val entryCommentView: EditText
private val entryExpiresCheckBox: CompoundButton
private val entryExpiresTextView: TextView
private val entryNotesView: EditText
private val entryExtraFieldsContainer: ViewGroup
val addNewFieldButton: View
private var iconColor: Int = 0
private var expiresInstant: DateInstant = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
var onDateClickListener: OnClickListener? = null
set(value) {
field = value
if (entryExpiresCheckBox.isChecked)
entryExpiresTextView.setOnClickListener(value)
else
entryExpiresTextView.setOnClickListener(null)
}
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.view_entry_edit_contents, this)
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_entry_edit_contents, this)
entryTitleLayoutView = findViewById(R.id.entry_edit_container_title)
entryTitleView = findViewById(R.id.entry_edit_title)
@@ -71,10 +81,14 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
entryPasswordLayoutView = findViewById(R.id.entry_edit_container_password)
entryPasswordView = findViewById(R.id.entry_edit_password)
entryConfirmationPasswordView = findViewById(R.id.entry_edit_confirmation_password)
generatePasswordView = findViewById(R.id.entry_edit_generate_button)
entryCommentView = findViewById(R.id.entry_edit_notes)
entryExpiresCheckBox = findViewById(R.id.entry_edit_expires_checkbox)
entryExpiresTextView = findViewById(R.id.entry_edit_expires_text)
entryNotesView = findViewById(R.id.entry_edit_notes)
entryExtraFieldsContainer = findViewById(R.id.entry_edit_advanced_container)
addNewFieldButton = findViewById(R.id.entry_edit_add_new_field)
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
assignExpiresDateText()
}
// Retrieve the textColor to tint the icon
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
@@ -141,32 +155,46 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
}
}
fun setOnPasswordGeneratorClickListener(clickListener: () -> Unit) {
generatePasswordView.setOnClickListener { clickListener.invoke() }
private fun assignExpiresDateText() {
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
entryExpiresTextView.setOnClickListener(onDateClickListener)
expiresInstant.getDateTimeString(resources)
} else {
entryExpiresTextView.setOnClickListener(null)
resources.getString(R.string.never)
}
if (fontInVisibility)
entryExpiresTextView.applyFontVisibility()
}
var expires: Boolean
get() {
return entryExpiresCheckBox.isChecked
}
set(value) {
entryExpiresCheckBox.isChecked = value
assignExpiresDateText()
}
var expiresDate: DateInstant
get() {
return expiresInstant
}
set(value) {
expiresInstant = value
assignExpiresDateText()
}
var notes: String
get() {
return entryCommentView.text.toString()
return entryNotesView.text.toString()
}
set(value) {
entryCommentView.setText(value)
entryNotesView.setText(value)
if (fontInVisibility)
entryCommentView.applyFontVisibility()
entryNotesView.applyFontVisibility()
}
fun allowCustomField(allow: Boolean, action: () -> Unit) {
addNewFieldButton.apply {
if (allow) {
visibility = View.VISIBLE
setOnClickListener { action.invoke() }
} else {
visibility = View.GONE
setOnClickListener(null)
}
}
}
val customFields: MutableList<Field>
get() {
val customFieldsArray = ArrayList<Field>()
@@ -228,14 +256,6 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
fun isValid(): Boolean {
var isValid = true
// Require title
if (entryTitleView.text.toString().isEmpty()) {
entryTitleLayoutView.error = context.getString(R.string.error_title_required)
isValid = false
} else {
entryTitleLayoutView.error = null
}
// Validate password
if (entryPasswordView.text.toString() != entryConfirmationPasswordView.text.toString()) {
entryPasswordLayoutView.error = context.getString(R.string.error_pass_match)

View File

@@ -54,8 +54,8 @@ class EntryEditCustomField @JvmOverloads constructor(context: Context,
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.view_entry_new_field, this)
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_entry_new_field, this)
val deleteView = findViewById<View>(R.id.entry_new_field_delete)
deleteView.setOnClickListener { deleteViewFromParent() }
@@ -103,7 +103,7 @@ class EntryEditCustomField @JvmOverloads constructor(context: Context,
parent.removeView(this)
parent.invalidate()
} catch (e: ClassCastException) {
Log.e(javaClass.name, e.message)
Log.e(javaClass.name, "Unable to delete view", e)
}
}
}

View File

@@ -17,6 +17,8 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
@file:Suppress("DEPRECATION")
package com.kunzisoft.keepass.view
import android.content.Context

View File

@@ -29,8 +29,11 @@ import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.tasks.ActionRunnable
import java.util.*
/**
* Replace font by monospace, must be called after seText()
@@ -50,6 +53,11 @@ fun TextView.applyHiddenStyle(hide: Boolean) {
}
}
fun TextView.setTextSize(unit: Int, defaultSize: Float, multiplier: Float) {
if (multiplier > 0.0F && multiplier != 1.0F)
setTextSize(unit, defaultSize * multiplier)
}
fun TextView.strikeOut(strikeOut: Boolean) {
paintFlags = if (strikeOut)
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
@@ -99,4 +107,14 @@ fun Toolbar.expand(animate: Boolean = true) {
play(slideAnimator)
interpolator = AccelerateDecelerateInterpolator()
}.start()
}
fun CoordinatorLayout.showActionError(result: ActionRunnable.Result) {
if (!result.isSuccess) {
result.exception?.errorId?.let { errorId ->
Snackbar.make(this, errorId, Snackbar.LENGTH_LONG).asError().show()
} ?: result.message?.let { message ->
Snackbar.make(this, message, Snackbar.LENGTH_LONG).asError().show()
}
}
}

View File

@@ -384,7 +384,7 @@ typedef struct _master_key {
} master_key;
void *generate_key_material(void *arg) {
uint32_t generate_key_material(void *arg) {
#if defined(KPD_PROFILE)
struct timespec start, end;
#endif
@@ -435,7 +435,7 @@ void *generate_key_material(void *arg) {
pthread_mutex_unlock(&mk->lock2);
}
return (void *)flip;
return flip;
}
JNIEXPORT jbyteArray JNICALL Java_com_kunzisoft_keepass_crypto_finalkey_NativeFinalKey_nTransformMasterKey(JNIEnv *env, jobject this, jbyteArray seed, jbyteArray key, jlong rounds) {
@@ -474,12 +474,12 @@ JNIEXPORT jbyteArray JNICALL Java_com_kunzisoft_keepass_crypto_finalkey_NativeFi
(*env)->GetByteArrayRegion(env, key, 0, MASTER_KEY_SIZE, (jbyte *)mk.key1);
// step 2: encrypt the hash "rounds" (default: 6000) times
iret = pthread_create( &t1, NULL, generate_key_material, (void*)&mk );
iret = pthread_create( &t1, NULL, (void*)generate_key_material, (void*)&mk );
if( iret != 0 ) {
(*env)->ThrowNew(env, bad_arg, "TransformMasterKey: failed to launch thread 1"); // FIXME: get a better exception class for this...
return NULL;
}
iret = pthread_create( &t2, NULL, generate_key_material, (void*)&mk );
iret = pthread_create( &t2, NULL, (void*)generate_key_material, (void*)&mk );
if( iret != 0 ) {
(*env)->ThrowNew(env, bad_arg, "TransformMasterKey: failed to launch thread 2"); // FIXME: get a better exception class for this...
return NULL;

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:color="@color/white"
tools:targetApi="lollipop">
<item>
<shape>
<stroke android:color="?attr/colorAccent" android:width="1dp"/>
<solid android:color="@color/transparent"/>
</shape>
</item>
</ripple>

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<stroke android:color="@color/grey" android:width="1dp"/>
<solid android:color="@color/transparent"/>
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M16.5,6v11.5c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4V5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5v10.5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V6H10v9.5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5V5c0,-2.21 -1.79,-4 -4,-4S7,2.79 7,5v12.5c0,3.04 2.46,5.5 5.5,5.5s5.5,-2.46 5.5,-5.5V6h-1.5z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M11,17c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1zM11,3v4h2L13,5.08c3.39,0.49 6,3.39 6,6.92 0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-1.68 0.59,-3.22 1.58,-4.42L12,13l1.41,-1.41 -6.8,-6.8v0.02C4.42,6.45 3,9.05 3,12c0,4.97 4.02,9 9,9 4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9h-1zM18,12c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1 0.45,1 1,1 1,-0.45 1,-1zM6,12c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M9.77344,3.13281 C9.4515,3.1329,9.12601,3.22745,8.83789,3.42188 L1.73828,8.21289
C1.25807,8.53693,1.00007,9.06311,1,9.59961
C0.999958,9.92151,1.09268,10.247,1.28711,10.5352 L6.07813,17.6328
C6.5966,18.4011,7.63207,18.6025,8.40039,18.084 L8.59375,17.9531
C7.93444,17.5713,7.49368,16.9397,7.33398,16.2305
C7.25701,16.1464,7.21481,16.0308,7.23828,15.9102 L7.27148,15.7422
C7.271,15.7299,7.26758,15.7194,7.26758,15.707 L7.09961,15.8184
C6.92859,15.9337,6.69736,15.8897,6.58203,15.7188
C6.46665,15.5479,6.51062,15.3165,6.68164,15.2012 L6.86523,15.0801
L6.65039,15.0371 C6.44804,14.9977,6.31618,14.8059,6.35547,14.6035
C6.39476,14.4011,6.58863,14.2693,6.79102,14.3086 L7.00586,14.3516
L6.88477,14.1699 C6.76939,13.9991,6.81335,13.7677,6.98438,13.6523
C7.1554,13.537,7.38468,13.581,7.5,13.752 L7.62305,13.9316 L7.66406,13.7188
C7.69855,13.5416,7.8519,13.4274,8.02539,13.4258 L11.1816,6.77344
C11.4309,6.24809,11.8397,5.84958,12.3203,5.58984 L11.1602,3.87109
C10.8361,3.3909,10.3119,3.13267,9.77539,3.13281 L9.77344,3.13281 Z
M9.70703,5.15625 C9.90938,5.19561,10.0373,5.39136,9.99805,5.59375
L9.95898,5.80859 L10.1387,5.68555
C10.3097,5.57024,10.5409,5.61618,10.6563,5.78711
C10.7716,5.95797,10.7257,6.18741,10.5547,6.30273 L10.375,6.42578
L10.5879,6.46484 C10.7902,6.5042,10.922,6.70197,10.8828,6.9043
C10.8435,7.1067,10.6496,7.23855,10.4473,7.19922 L10.2344,7.15625
L10.3535,7.33594 C10.4689,7.5068,10.4249,7.73817,10.2539,7.85352
C10.0829,7.9688,9.85163,7.92289,9.73633,7.75195 L9.61523,7.57031
L9.57227,7.78516 C9.53298,7.98758,9.34104,8.11943,9.13867,8.08008
C8.93633,8.04072,8.80451,7.84684,8.84375,7.64453 L8.88477,7.43164
L8.70313,7.55273 C8.53228,7.66806,8.30282,7.62406,8.1875,7.45313
C8.07212,7.28226,8.11609,7.05088,8.28711,6.93555 L8.46875,6.8125
L8.25391,6.76953 C8.05154,6.73017,7.91971,6.53833,7.95898,6.33594
C7.99827,6.13352,8.19412,6.00167,8.39648,6.04102 L8.61133,6.08398
L8.48828,5.90234 C8.3729,5.73148,8.41882,5.50205,8.58984,5.38672
C8.76069,5.27139,8.99014,5.31344,9.10547,5.48438 L9.22656,5.66602
L9.26758,5.45313 C9.30687,5.25073,9.50467,5.11692,9.70703,5.15625 Z
M13.6602,6.24414 C13.0088,6.21529,12.3742,6.57309,12.0762,7.20117
L8.40625,14.9375 C8.00883,15.7749,8.36373,16.7686,9.20117,17.166
L16.9375,20.8379 C17.7749,21.2353,18.7686,20.8804,19.166,20.043 L22.8379,12.3066
C23.2353,11.4693,22.8804,10.4736,22.043,10.0762 L14.3066,6.40625
C14.0973,6.30689,13.8773,6.25374,13.6602,6.24414 Z M4.62109,8.49023
C4.66856,8.4801,4.71886,8.48028,4.76953,8.49023
C4.97188,8.52959,5.10374,8.72142,5.06445,8.92383 L5.02148,9.13867
L5.20313,9.01758 C5.37397,8.90227,5.60343,8.94625,5.71875,9.11719
C5.83413,9.28805,5.78999,9.51944,5.61914,9.63477 L5.43945,9.75586
L5.65039,9.79492 C5.85276,9.83428,5.9865,10.0321,5.94727,10.2344
C5.90798,10.4368,5.71213,10.5686,5.50977,10.5293 L5.29688,10.4883
L5.41797,10.668 C5.53335,10.8388,5.48938,11.0683,5.31836,11.1836
C5.14751,11.2989,4.91806,11.253,4.80273,11.082 L4.67969,10.9023 L4.63672,11.1172
C4.59743,11.3196,4.40355,11.4495,4.20117,11.4102
C3.99883,11.3708,3.867,11.1769,3.90625,10.9746 L3.94922,10.7617 L3.76953,10.8828
C3.59851,10.9981,3.36728,10.9541,3.25195,10.7832
C3.13658,10.6123,3.18054,10.3829,3.35156,10.2676 L3.53516,10.1426
L3.31836,10.1016 C3.11599,10.0622,2.98612,9.86842,3.02539,9.66602
C3.06468,9.46362,3.25857,9.33371,3.46094,9.37305 L3.67578,9.41406
L3.55078,9.23242 C3.4354,9.06156,3.48132,8.83214,3.65234,8.7168
C3.82337,8.60149,4.05462,8.64547,4.16992,8.81641 L4.29102,8.99805
L4.33203,8.7832 C4.36154,8.6314,4.47892,8.52021,4.62109,8.49023 Z
M19.2363,10.7852 C19.3814,10.7935,19.5126,10.8876,19.5645,11.0332
L19.6367,11.2363 L19.7305,11.041
C19.8188,10.8547,20.0403,10.7749,20.2266,10.8633
C20.4128,10.9517,20.4908,11.1712,20.4023,11.3574 L20.3086,11.5586
L20.5156,11.4844 C20.7098,11.4152,20.9211,11.5148,20.9902,11.709
C21.0594,11.9032,20.9597,12.1163,20.7656,12.1855 L20.5586,12.2578
L20.7578,12.3516 C20.944,12.44,21.022,12.6614,20.9336,12.8477
C20.8452,13.0339,20.6238,13.1118,20.4375,13.0234 L20.2402,12.9297
L20.3125,13.1367 C20.3817,13.3309,20.282,13.5421,20.0879,13.6113
C19.8937,13.6805,19.6825,13.5809,19.6133,13.3867 L19.5391,13.1777
L19.4453,13.377 C19.357,13.5632,19.1374,13.6411,18.9512,13.5527 L18.9492,13.5527
C18.763,13.4643,18.6832,13.2429,18.7715,13.0566 L18.8652,12.8613
L18.6621,12.9336 C18.4679,13.0027,18.2547,12.9012,18.1855,12.707
C18.1164,12.5128,18.218,12.3016,18.4121,12.2324 L18.6172,12.1582
L18.4199,12.0664 C18.2337,11.978,18.1557,11.7566,18.2441,11.5703
C18.3325,11.384,18.554,11.3042,18.7402,11.3926 L18.9355,11.4863 L18.8613,11.2832
C18.7922,11.089,18.8938,10.8759,19.0879,10.8066
C19.1364,10.7894,19.188,10.7824,19.2363,10.7852 Z M15.2695,12.1973
C15.4146,12.2056,15.5478,12.2997,15.5996,12.4453 L15.6699,12.6484
L15.7637,12.4531 C15.852,12.2669,16.0735,12.189,16.2598,12.2773
C16.446,12.3657,16.524,12.5872,16.4355,12.7734 L16.3438,12.9707 L16.5488,12.8965
C16.743,12.8273,16.9543,12.9289,17.0234,13.123
C17.0926,13.3172,16.991,13.5304,16.7969,13.5996 L16.5938,13.6699
L16.7891,13.7637 C16.9753,13.8521,17.0552,14.0735,16.9668,14.2598
C16.8784,14.446,16.6569,14.5239,16.4707,14.4355 L16.2734,14.3438
L16.3477,14.5488 C16.4168,14.743,16.3152,14.9542,16.1211,15.0234
C15.9269,15.0926,15.7176,14.993,15.6484,14.7988 L15.5742,14.5918
L15.4785,14.7891 C15.3902,14.9753,15.1687,15.0532,14.9824,14.9648
C14.7962,14.8765,14.7182,14.657,14.8066,14.4707 L14.8984,14.2734
L14.6953,14.3477 C14.5011,14.4168,14.2879,14.3152,14.2188,14.1211
C14.1496,13.9269,14.2493,13.7157,14.4434,13.6465 L14.6504,13.5723
L14.4531,13.4785 C14.2669,13.3901,14.1889,13.1687,14.2773,12.9824
C14.3657,12.7962,14.5872,12.7183,14.7734,12.8066 L14.9688,12.8984
L14.8945,12.6953 C14.8254,12.5011,14.9289,12.288,15.123,12.2188
C15.1716,12.2015,15.2212,12.1945,15.2695,12.1973 Z M11.3008,13.6113
C11.4458,13.6197,11.579,13.7138,11.6309,13.8594 L11.7051,14.0625
L11.7969,13.8672 C11.8852,13.6809,12.1087,13.6011,12.2949,13.6895
C12.4811,13.7778,12.5591,13.9993,12.4707,14.1855 L12.375,14.3848 L12.582,14.3105
C12.7762,14.2414,12.9894,14.341,13.0586,14.5352
C13.1277,14.7294,13.0242,14.9425,12.8301,15.0117 L12.627,15.084 L12.8242,15.1777
C13.0104,15.2661,13.0885,15.4876,13,15.6738
C12.9116,15.8601,12.6902,15.938,12.5039,15.8496 L12.3066,15.7559
L12.3789,15.9629 C12.4481,16.1571,12.3484,16.3683,12.1543,16.4375
C11.9601,16.5067,11.7489,16.4071,11.6797,16.2129 L11.6055,16.0039
L11.5137,16.2031 C11.4253,16.3894,11.2019,16.4693,11.0156,16.3809
L11.0156,16.3789 C10.8294,16.2905,10.7514,16.071,10.8398,15.8848
L10.9336,15.6875 L10.7305,15.7598
C10.5363,15.8289,10.3211,15.7273,10.252,15.5332
C10.1828,15.339,10.2844,15.1278,10.4785,15.0586 L10.6855,14.9863
L10.4863,14.8926 C10.3001,14.8042,10.2221,14.5828,10.3105,14.3965
C10.3989,14.2102,10.6184,14.1304,10.8047,14.2188 L11.0039,14.3125
L10.9297,14.1094 C10.8605,13.9152,10.9622,13.702,11.1563,13.6328
C11.2048,13.6156,11.2524,13.6086,11.3008,13.6113 Z" />
</vector>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<path
android:fillColor="#fffbfb"
android:pathData="M 18 3 L 18 6 L 15 6 L 15 8 L 18 8 L 18 11 L 20 11 L 20 8 L 23 8 L 23 6 L 20 6 L
20 3 L 18 3 z M 4 6 C 2.8920005 6 2.0000002 6.8920005 2 8 L 2 18 C 1.9999998
19.108 2.8920005 20 4 20 L 18 20 C 19.108 20 19.999999 19.108 20 18 L 20 13 L 18
13 L 18 16.533203 C 17.999999 17.345736 17.345736 18 16.533203 18 L 5.4667969 18
C 4.6542635 18 3.9999998 17.345736 4 16.533203 L 4 9.4667969 C 4.0000002
8.6542635 4.6542635 8 5.4667969 8 L 13 8 L 13 6 L 4 6 z M 5 10 L 5 12 L 17 12 L
17 10 L 5 10 z M 5 14 L 5 16 L 17 16 L 17 14 L 5 14 z" />
</group>
</vector>

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M9.19922,3.13867 L9.19922,4.73828 L9.19922,6.33984 L10.8008,6.33984
L10.8008,4.79297 C13.6944,5.15457,15.9841,7.44622,16.3457,10.3398
L14.8008,10.3398 L14.8008,11.9395 L16.3438,11.9395
C16.2972,12.3042,16.211,12.6543,16.1074,12.998 L17.7559,12.998
C17.9,12.399,18,11.7819,18,11.1387 C18,6.72041,14.4183,3.13867,10,3.13867
L9.19922,3.13867 Z M4.7207,5.17383 L4.59766,5.25
C4.30768,5.51569,4.03687,5.80202,3.78906,6.10742
C3.74408,6.16236,3.76404,6.13658,3.75391,6.14844
C2.62072,7.56491,2.00133,9.32361,2,11.1387
C2,11.4166,2.02145,11.6889,2.05078,11.959
C2.06909,12.128,2.09815,12.2932,2.12695,12.459
C2.15024,12.5993,2.17255,12.7392,2.20313,12.877
C2.21201,12.9175,2.21732,12.9597,2.22656,13 L2.23633,13
C2.42308,13.7827,2.72966,14.5165,3.12695,15.1914
C3.30082,14.5824,3.66062,14.0532,4.14453,13.666
C4.05066,13.4483,3.95825,13.2294,3.88867,13 L3.89063,13
C3.88529,12.9838,3.88033,12.9674,3.875,12.9512
C3.77847,12.6238,3.7054,12.2864,3.66211,11.9395 L5.19727,11.9395
L5.20703,11.9395 L5.20703,10.3398 L5.19727,10.3398 L3.66406,10.3398
C3.69055,10.1356,3.73432,9.93472,3.7793,9.73438
C3.81343,9.59208,3.85116,9.45203,3.89258,9.3125
C4.09265,8.63652,4.39089,7.99073,4.80859,7.41211 L9.10156,11.7051
C9.41495,12.0185,9.91902,12.0185,10.2324,11.7051
C10.5458,11.3917,10.5458,10.8876,10.2324,10.5742 L5.14258,5.48242
C5.02985,5.36975,4.72451,5.175,4.72266,5.17383 L4.7207,5.17383 Z M6,14
C4.892,14,4,14.892,4,16 L4,19 C4,20.108,4.892,21,6,21 L21,21
C22.108,21,23,20.108,23,19 L23,16 C23,14.892,22.108,14,21,14 L6,14 Z M7.5,15
C7.777,15,8,15.2787,8,15.625 L8,16.293 L8.47266,15.8203
C8.71749,15.5755,9.07172,15.5366,9.26758,15.7324
C9.46352,15.9283,9.42449,16.2825,9.17969,16.5273 L8.70703,17 L9.375,17
C9.72124,17,10,17.223,10,17.5 C10,17.777,9.72124,18,9.375,18 L8.70703,18
L9.17969,18.4727 C9.42449,18.7175,9.46367,19.0717,9.26758,19.2676
C9.07172,19.4634,8.71749,19.4245,8.47266,19.1797 L8,18.707 L8,19.375
C8,19.7212,7.777,20,7.5,20 C7.223,20,7,19.7212,7,19.375 L7,18.707
L6.52734,19.1797 C6.28251,19.4245,5.92828,19.4634,5.73242,19.2676
C5.53649,19.0717,5.57551,18.7175,5.82031,18.4727 L6.29297,18 L5.625,18
C5.27876,18,5,17.777,5,17.5 C5,17.223,5.27876,17,5.625,17 L6.29297,17
L5.82031,16.5273 C5.57551,16.2825,5.53633,15.9283,5.73242,15.7324
C5.92828,15.5366,6.28251,15.5755,6.52734,15.8203 L7,16.293 L7,15.625
C7,15.2787,7.223,15,7.5,15 Z M13.5,15 C13.777,15,14,15.2787,14,15.625 L14,16.293
L14.4727,15.8203 C14.7175,15.5755,15.0717,15.5366,15.2676,15.7324
C15.4635,15.9283,15.4245,16.2825,15.1797,16.5273 L14.707,17 L15.375,17
C15.7212,17,16,17.223,16,17.5 C16,17.777,15.7212,18,15.375,18 L14.707,18
L15.1797,18.4727 C15.4245,18.7175,15.4637,19.0717,15.2676,19.2676
C15.0717,19.4634,14.7175,19.4245,14.4727,19.1797 L14,18.707 L14,19.375
C14,19.7212,13.777,20,13.5,20 C13.223,20,13,19.7212,13,19.375 L13,18.707
L12.5273,19.1797 C12.2825,19.4245,11.9283,19.4634,11.7324,19.2676
C11.5365,19.0717,11.5755,18.7175,11.8203,18.4727 L12.293,18 L11.625,18
C11.2788,18,11,17.777,11,17.5 C11,17.223,11.2788,17,11.625,17 L12.293,17
L11.8203,16.5273 C11.5755,16.2825,11.5363,15.9283,11.7324,15.7324
C11.9283,15.5366,12.2825,15.5755,12.5273,15.8203 L13,16.293 L13,15.625
C13,15.2787,13.223,15,13.5,15 Z M19.5,15 C19.777,15,20,15.2787,20,15.625
L20,16.293 L20.4727,15.8203 C20.7175,15.5755,21.0717,15.5366,21.2676,15.7324
C21.4635,15.9283,21.4245,16.2825,21.1797,16.5273 L20.707,17 L21.375,17
C21.7212,17,22,17.223,22,17.5 C22,17.777,21.7212,18,21.375,18 L20.707,18
L21.1797,18.4727 C21.4245,18.7175,21.4637,19.0717,21.2676,19.2676
C21.0717,19.4634,20.7175,19.4245,20.4727,19.1797 L20,18.707 L20,19.375
C20,19.7212,19.777,20,19.5,20 C19.223,20,19,19.7212,19,19.375 L19,18.707
L18.5273,19.1797 C18.2825,19.4245,17.9283,19.4634,17.7324,19.2676
C17.5365,19.0717,17.5755,18.7175,17.8203,18.4727 L18.293,18 L17.625,18
C17.2788,18,17,17.777,17,17.5 C17,17.223,17.2788,17,17.625,17 L18.293,17
L17.8203,16.5273 C17.5755,16.2825,17.5363,15.9283,17.7324,15.7324
C17.9283,15.5366,18.2825,15.5755,18.5273,15.8203 L19,16.293 L19,15.625
C19,15.2787,19.223,15,19.5,15 Z" />
</vector>

View File

@@ -1,19 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="24"
android:viewportHeight="24"
android:width="24dp"
android:height="24dp">
<group
android:translateY="-8">
<group
android:scaleX="1.777778"
android:scaleY="1.777778"
android:translateX="-205.4844"
android:translateY="-31.99788">
<path
android:pathData="M125.00684 31.305444l0 -6.644528c0 -0.263013 -0.21159 -0.47461 -0.4746 -0.47461l-6.48633 0c-1.04808 0 -1.89843 0.850344 -1.89843 1.898435l0 6.328127c0 1.048093 0.85035 1.898437 1.89843 1.898437l6.48633 0c0.26301 0 0.4746 -0.2116 0.4746 -0.474611l0 -0.316409c0 -0.148311 -0.0692 -0.282785 -0.176 -0.369792 -0.083 -0.304543 -0.083 -1.172685 0 -1.477229 0.10684 -0.08504 0.176 -0.219501 0.176 -0.36782zm-6.32812 -4.469236c0 -0.06529 0.0534 -0.118652 0.11866 -0.118652l4.19238 0c0.0653 0 0.11866 0.05343 0.11866 0.118652l0 0.39551c0 0.06529 -0.0534 0.118653 -0.11866 0.118653l-4.19238 0c-0.0653 0 -0.11866 -0.05343 -0.11866 -0.118653l0 -0.39551zm0 1.265621c0 -0.06529 0.0534 -0.118653 0.11866 -0.118653l4.19238 0c0.0653 0 0.11866 0.05343 0.11866 0.118653l0 0.395509c0 0.06529 -0.0534 0.118653 -0.11866 0.118653l-4.19238 0c-0.0653 0 -0.11866 -0.05342 -0.11866 -0.118653l0 -0.395509zm5.0111 4.943847l-5.64391 0c-0.35003 0 -0.63282 -0.282781 -0.63282 -0.632808 0 -0.348045 0.28476 -0.632813 0.63282 -0.632813l5.64391 0c-0.0375 0.338162 -0.0375 0.927466 0 1.265621z"
android:fillColor="#ffffff" />
</group>
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<path
android:fillColor="#ffffff"
android:pathData="M 12 1 A 11.000003 11.000001 0 0 0 1 12 A 11.000003 11.000001 0 0 0 4 19.529297
L 4 18 L 4 16.119141 A 9.0000001 9.0000001 0 0 1 3 12 A 9.0000001 9.0000001 0 0
1 4.9746094 6.3886719 L 17.617188 19.03125 A 9.0000001 9.0000001 0 0 1 12 21 A
9.0000001 9.0000001 0 0 1 7.9121094 20 L 6 20 L 4.4609375 20 A 11.000003
11.000001 0 0 0 12 23 A 11.000003 11.000001 0 0 0 23 12 A 11.000003 11.000001 0
0 0 12 1 z M 12 3 A 9.0000001 9.0000001 0 0 1 21 12 A 9.0000001 9.0000001 0 0 1
19.025391 17.611328 L 6.3828125 4.96875 A 9.0000001 9.0000001 0 0 1 12 3 z M
16.566406 5.0078125 C 16.256202 5.0423192 15.932855 5.1941212 15.669922
5.4570312 L 14.46875 6.6601562 L 17.339844 9.53125 L 18.542969 8.3300781 C
18.895877 7.9770126 19.030492 7.5200648 18.966797 7.1269531 C 18.396436
6.3122027 17.687797 5.6035638 16.873047 5.0332031 C 16.772829 5.0168634
16.672741 4.995984 16.566406 5.0078125 z M 13.449219 7.6777344 L 11.978516
9.1503906 L 14.849609 12.021484 L 16.320312 10.548828 L 13.449219 7.6777344 z M
9.1484375 11.980469 L 5.65625 15.472656 L 5.015625 18.390625 C 4.9299361
18.781059 5.2170219 19.069875 5.6074219 18.984375 L 8.5253906 18.345703 L
12.019531 14.851562 L 9.1484375 11.980469 z" />
</group>
</vector>

View File

@@ -1,25 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="24"
android:viewportHeight="24"
android:width="24dp"
android:height="24dp">
<group
android:translateY="-8">
<group
android:scaleX="1.777778"
android:scaleY="1.777778"
android:translateX="-205.4844"
android:translateY="-31.99788">
<group
android:scaleX="0.5625"
android:scaleY="0.5625"
android:translateX="115.585"
android:translateY="22.49881">
<path
android:pathData="M4.375 3C2.5117466 3 1 4.5117271 1 6.375l0 11.25C1 19.488276 2.5117466 21 4.375 21l11.53125 0C16.373823 21 16.75 20.623825 16.75 20.15625l0 -0.5625c0 -0.263664 -0.122669 -0.503524 -0.3125 -0.658203 -0.147556 -0.54141 -0.147556 -2.08359 0 -2.625 0.189938 -0.151182 0.3125 -0.390619 0.3125 -0.654297l0 -0.669922 -2.378906 2.378906c-0.0089 0.500068 -0.0036 1.014773 0.03711 1.384766l-1.525391 0 -3.0507808 0.667969C9.333649 19.527124 8.7483479 19.391267 8.3632812 19.005859 8.2870146 18.929486 8.2287106 18.840044 8.171875 18.75L4.375 18.75C3.7527244 18.75 3.25 18.24727 3.25 17.625 3.25 17.006253 3.7562267 16.5 4.375 16.5l3.8027344 0L8.6503906 14.349609 12.125 10.875l-6.4140625 0C5.5948486 10.875 5.5 10.780031 5.5 10.664062l0 -0.7031245C5.5 9.8448664 5.5949375 9.75 5.7109375 9.75l7.4531245 0c0.02365 0 0.03921 0.018137 0.06055 0.025391L16.550781 6.4492188 16.75 6.25l0 -2.40625C16.75 3.3761713 16.373823 3 15.90625 3L4.375 3Zm16.015625 1.5898438c-0.30546 0.033979 -0.623906 0.1844704 -0.882813 0.4433593l-1.183593 1.1835938 2.828125 2.828125 1.183594 -1.1835938c0.517848 -0.5180889 0.601704 -1.2752558 0.1875 -1.6894531L21.195312 4.8457031C20.988219 4.6386018 20.696085 4.5558644 20.390625 4.5898438ZM17.320312 7.21875L9.6464844 14.894531 9.015625 17.767578c-0.08448 0.384462 0.1995577 0.670133 0.5839844 0.585938L12.472656 17.724609 20.148438 10.046875 17.320312 7.21875ZM5.7109375 7.5L13.164062 7.5C13.280151 7.5 13.375 7.594987 13.375 7.7109375l0 0.703125C13.375 8.5301336 13.28008 8.625 13.164062 8.625l-7.4531245 0C5.5948486 8.625 5.5 8.5300127 5.5 8.4140625l0 -0.703125C5.5 7.5948664 5.5949375 7.5 5.7109375 7.5Z"
android:fillColor="#ffffff" />
</group>
</group>
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<path
android:fillColor="#ffffff"
android:strokeWidth="1.01553178"
android:pathData="M 16.566406,5.0078125 C 15.782726,5.1133592 15.352352,5.8416456
14.802734,6.3261719 14.631179,6.503043 14.326338,6.6788074 14.664062,6.8554688 L
17.339844,9.53125 C 17.821017,9.0226367 18.368011,8.5699819 18.791016,8.0117188
19.125433,7.5187285 19.054409,6.8047721 18.564453,6.4453125 18.109555,6.0127792
17.694742,5.5314578 17.210938,5.1328125 17.01828,5.0182169 16.787242,4.9827103
16.566406,5.0078125 Z M 13.449219,7.6777344 C 10.851548,10.276042
8.2539034,12.874349 5.65625,15.472656 5.4423833,16.493124 5.1885689,17.506656
5,18.53125 c 0.019556,0.598212 0.681759,0.466668 1.0722656,0.351562
0.8179378,-0.179027 1.6351872,-0.358083 2.453125,-0.537109 2.5983114,-2.598958
5.1966104,-5.197918 7.7949214,-7.796875 z" />
</group>
</vector>

View File

@@ -33,7 +33,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toTopOf="@+id/disclaimer">
app:layout_constraintBottom_toBottomOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@@ -96,53 +96,85 @@
</LinearLayout>
</RelativeLayout>
<TextView android:id="@+id/about_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/about_description"/>
<TextView android:id="@+id/homepage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/about_homepage"
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
<TextView
android:id="@+id/textView"
android:id="@+id/activity_about_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="18dp"
android:layout_marginStart="18dp"
android:autoLink="web"
android:text="@string/homepage"/>
android:layout_marginTop="8dp"
android:text="@string/about_description"/>
<TextView android:id="@+id/feedback"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/about_feedback"
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="18dp"
android:layout_marginStart="18dp"
android:text="@string/issues"
android:autoLink="web"/>
<TextView
android:id="@+id/activity_about_about_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/about"
android:textStyle="bold"
style="@style/KeepassDXStyle.TextAppearance.Title"/>
<TextView
android:id="@+id/activity_about_licence_text"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/activity_about_contribution_text"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/activity_about_contact_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/contact"
android:textStyle="bold"
style="@style/KeepassDXStyle.TextAppearance.Title"/>
<TextView
android:id="@+id/activity_about_homepage_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/homepage"
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
<TextView
android:id="@+id/activity_about_homepage_link"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="web"
android:text="@string/homepage_url"/>
<TextView
android:id="@+id/activity_about_feedback_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/feedback"
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
<TextView
android:id="@+id/activity_about_feedback_link"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/issues_url"
android:autoLink="web"/>
<TextView
android:id="@+id/activity_about_contribution_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/contribution"
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
<TextView
android:id="@+id/activity_about_contribution_link"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/contribution_url"
android:autoLink="web"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
<TextView
android:id="@+id/disclaimer"
android:padding="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_alignParentBottom="true"
android:text="@string/disclaimer_formal"
style="@style/KeepassDXStyle.TextAppearance.TinyText"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -17,63 +17,100 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout
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:importantForAutofill="noExcludeDescendants"
tools:targetApi="o"
android:id="@+id/entry_edit_coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o" >
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="match_parent" >
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<include
android:id="@+id/toolbar"
layout="@layout/toolbar_default"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:titleEnabled="false"
app:toolbarId="@+id/toolbar"
app:layout_scrollFlags="enterAlways|enterAlwaysCollapsed|scroll|exitUntilCollapsed|snap">
<ScrollView
android:id="@+id/entry_edit_scroll"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:fillViewport="true"
android:scrollbars="none">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.kunzisoft.keepass.view.EntryEditContentsView
android:id="@+id/entry_edit_contents"
android:layout_width="0dp"
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize">
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
android:background="?attr/colorPrimaryDark"
android:theme="?attr/toolbarAppearance"
android:popupTheme="?attr/toolbarPopupAppearance">
<androidx.appcompat.widget.ActionMenuView
android:id="@+id/entry_edit_bottom_bar"
android:layout_width="wrap_content"
android:layout_height="?attr/actionBarSize"/>
</androidx.appcompat.widget.Toolbar>
<View
android:id="@+id/biometric_delimiter"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/colorAccent"/>
</FrameLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="?attr/toolbarAppearance"
android:popupTheme="?attr/toolbarPopupAppearance"
app:layout_collapseMode="pin"
tools:targetApi="lollipop" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/entry_edit_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbarStyle="insideOverlay"
android:scrollbars="none"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.kunzisoft.keepass.view.EntryEditContentsView
android:id="@+id/entry_edit_contents"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="@dimen/content_percent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/entry_edit_save"
android:layout_width="wrap_content"
android:id="@+id/entry_edit_validate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:src="@drawable/ic_save_white_24dp"
android:contentDescription="@string/content_description_entry_save"
android:layout_margin="16dp"
app:layout_anchorGravity="bottom|end"
app:layout_anchor="@+id/entry_edit_scroll"
android:src="@drawable/ic_check_white_24dp"
android:contentDescription="@string/validate"
app:useCompatPadding="true"
style="@style/KeepassDXStyle.Fab"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -32,7 +32,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/pass_ok">
app:layout_constraintBottom_toTopOf="@+id/activity_password_info_container">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
@@ -101,11 +101,10 @@
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_container"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="fill_vertical"
android:fillViewport="true">
android:scrollbarStyle="insideOverlay"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@@ -223,8 +222,36 @@
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<LinearLayout
android:id="@+id/activity_password_info_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/activity_password_open_button">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/activity_password_info_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:minHeight="24dp"
android:paddingStart="24dp"
android:paddingLeft="24dp"
android:paddingEnd="24dp"
android:paddingRight="24dp"
style="@style/KeepassDXStyle.TextAppearance.TinyText"
android:text="@string/warning_database_read_only"
android:textColor="?attr/textColorInverse"
android:background="?attr/colorAccent"
app:layout_constraintBottom_toTopOf="@+id/activity_password_info_delimiter"
android:layout_gravity="bottom"/>
<View
android:id="@+id/activity_password_info_delimiter"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/colorAccentLight"/>
</LinearLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/pass_ok"
android:id="@+id/activity_password_open_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -17,18 +17,25 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/toolbar_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/toolbar"
layout="@layout/toolbar_default" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent">
</LinearLayout>
<include
android:id="@+id/toolbar"
layout="@layout/toolbar_default" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Some files were not shown because too many files have changed in this diff Show More