Compare commits

..

443 Commits

Author SHA1 Message Date
J-Jamet
7750843b04 Merge branch 'release/3.0.2' 2021-09-24 19:34:58 +02:00
J-Jamet
d7da1ce333 Upgrade to 3.0.2 and update CHANGELOG 2021-09-24 12:55:08 +02:00
J-Jamet
dd9ee8c3f8 Merge branch 'chenxiaolong-samsung_dex' into develop 2021-09-24 12:50:40 +02:00
Andrew Gunnerson
c0ac01a34a Add workaround to support Samsung DeX
This commit changes the Magikeyboard service behavior so that KeePassDX
is able to run in Samsung DeX mode. Currently, the app cannot run in
DeX mode because apps which have services using `BIND_INPUT_METHOD` are
blocked.

A new broadcast receiver has been added to listen for DeX's enter/leave
events [1] and disable/enable the `Magikeyboard` service appropriately.
The enabled state of a service lives in the Android framework's
`PackageManager` and survives app crashes and device reboots (though it
does get reset when app data is cleared).

Additionally, an extra check is added to `FileDatabaseSelectActivity` to
ensure the service's enabled state is correct. This is necessary if the
app crashes or is force quit within DeX mode and then the user exits DeX
mode. Otherwise, the service would stay disabled until the user entered
and exited DeX again.

With the new behavior, KeePassDX will generally just work with DeX,
though there's one caveat: after the initial installation, the user must
open the app once outside of DeX. Otherwise, Android will not trigger
the broadcast receiver. This could be fixed by making the service
intially disabled in the manifest with `android:enabled="false"`, but
Android's Settings app in SDK 15 through 25 does not correctly refresh
the keyboard list when changing the service from disabled to enabled.
I opted *not* to introduce different behavior based on the API version.

[1] https://developer.samsung.com/sdp/blog/en-us/2017/07/27/samsung-dex-how-to-detect-the-samsung-dex-mode

Fixes: #245
Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com>
2021-09-18 23:30:37 -04:00
J-Jamet
1b88f2ddf0 Merge tag '3.0.1' into develop
3.0.1
2021-09-15 13:25:03 +02:00
J-Jamet
b4f2a1eb89 Merge branch 'release/3.0.1' 2021-09-15 13:24:53 +02:00
J-Jamet
e9fc9cbc2a Capture cast exception 2021-09-15 12:21:44 +02:00
J-Jamet
b809180a1b Small changes 2021-09-15 11:25:15 +02:00
J-Jamet
ecc75df3a1 Fix search actions #1091 #1092 2021-09-15 11:16:32 +02:00
J-Jamet
d1b6863143 Update CHANGELOG 2021-09-15 10:36:41 +02:00
J-Jamet
faf27143aa Fix timeout reset #1107 2021-09-15 10:33:37 +02:00
J-Jamet
aee58a4475 Fix exception after group name change and save #1112 2021-09-15 09:48:48 +02:00
J-Jamet
c5e07f643f Fix Magikeyboard URL auto action #1100 2021-09-14 20:40:22 +02:00
J-Jamet
818f5820d5 Update CHANGELOG 2021-09-14 20:13:35 +02:00
J-Jamet
77c6e28876 Fix max lines #1073 2021-09-14 20:06:49 +02:00
J-Jamet
fb7f66012d Upgrade to 3.0.1 2021-09-14 19:35:30 +02:00
J-Jamet
0f7f7bbe6c Min height to 48dp 2021-09-14 19:30:50 +02:00
J-Jamet
8dedb8deb4 Fix text dimension 2021-09-14 19:10:48 +02:00
J-Jamet
d284db4d3c Merge tag '3.0.0' into develop
3.0.0
2021-09-07 18:43:40 +02:00
J-Jamet
82450c0ae8 Merge branch 'release/3.0.0' 2021-09-07 18:43:31 +02:00
J-Jamet
8dd6c33901 Fix add entry education hint with default templates 2021-09-07 14:00:02 +02:00
J-Jamet
f920d40db5 Fix autofill popup window application id #1046 2021-09-07 13:45:35 +02:00
J-Jamet
19be6c1acc Upgrade to 3.0.0 2021-09-07 13:17:34 +02:00
J-Jamet
7d9d8ad0e4 Manage magikeyboard in landscape 2021-09-07 13:09:24 +02:00
J-Jamet
85f8237d5f Merge branch 'fullscreen' of git://github.com/chenxiaolong/KeePassDX into chenxiaolong-fullscreen 2021-09-07 12:41:24 +02:00
J-Jamet
c542894734 Small change in progress dialog 2021-09-07 12:18:21 +02:00
J-Jamet
d348987077 Remove unused code 2021-09-07 11:21:28 +02:00
J-Jamet
3718610595 Copy OTP Token from list to provide suitable alternative to #553 2021-09-07 10:44:34 +02:00
J-Jamet
9c36ec0623 Fix datetime font size 2021-09-07 10:29:08 +02:00
J-Jamet
c6917b5d74 Update CHANGELOG 2021-09-07 10:16:38 +02:00
Andrew Gunnerson
4eaa179789 magikeyboard: Don't force full screen EditTexts on large screen devices
Per [1], Android defaults to showing EditTexts in full screen mode when
the device is in landscape orientation. This makes sense for phones,
but not so much for larger screen devices, like tablets.

This commit updates MagiKeyboard to not use full screen mode on large
screen devices. The condition for disabling full screen mode is the same
as in the AOSP keyboard and should match what other OEM keyboards do as
well.

[1] https://developer.android.com/reference/android/inputmethodservice/InputMethodService#fullscreen-mode
2021-09-06 13:01:56 -04:00
J-Jamet
9008cd4549 Remove max lines in TextFieldView #1073 #1076 2021-09-06 12:18:17 +02:00
J-Jamet
cc3204453e Upgrade to 3.0.0_beta03 2021-09-03 15:31:17 +02:00
J-Jamet
5ef8d3b7b9 Update CHANGELOG 2021-09-03 15:25:30 +02:00
J-Jamet
2a9de97a19 Default manual selection to true 2021-09-03 15:14:39 +02:00
J-Jamet
9cecfed417 Add dots 2021-09-03 15:10:18 +02:00
J-Jamet
319715918a Small change to merge views 2021-09-03 15:02:40 +02:00
J-Jamet
a3bf6e8b6d Small change for consistency 2021-09-03 14:41:45 +02:00
J-Jamet
c4062658ce Fix search info parcelable 2021-09-03 14:39:19 +02:00
J-Jamet
01a5de413e Merge branch 'develop' of git://github.com/uduerholz/KeePassDX into uduerholz-develop 2021-09-03 14:18:51 +02:00
J-Jamet
e4c22b1f29 Change remote views when the database is open 2021-09-03 12:44:01 +02:00
J-Jamet
b10e60126f Fix UUID view 2021-09-02 17:39:08 +02:00
J-Jamet
ef1f27f421 Check null view model callback 2021-09-02 17:31:51 +02:00
J-Jamet
0ed208675c Fix reloading from history 2021-09-02 17:18:01 +02:00
J-Jamet
00f7a0a194 Better entry activity view model to fix reloading 2021-09-02 17:05:07 +02:00
J-Jamet
935d4f4a64 Unused throw 2021-09-02 16:13:25 +02:00
J-Jamet
dc4d88260d Fix database reload 2021-09-02 16:13:08 +02:00
J-Jamet
18934601da Fix education 2021-09-02 14:26:08 +02:00
J-Jamet
4ea811aeda Fix menu in template creation 2021-09-02 13:48:48 +02:00
J-Jamet
f8fdecdc8f Fix multiple loading by move variables in entry edit view model 2021-09-02 11:12:19 +02:00
J-Jamet
5467c61137 Add equals in node info 2021-09-02 11:10:51 +02:00
J-Jamet
9c72b4cc56 Fix timeout switch 2021-09-01 18:51:18 +02:00
J-Jamet
9102217bc3 Fix template lost after orientation change #1069 2021-09-01 17:38:29 +02:00
Uli
0e8fd7b2c4 Merge branch 'Kunzisoft:develop' into develop 2021-09-01 14:41:07 +02:00
J-Jamet
a06ea8fe55 Fix warning 2021-08-30 11:13:38 +02:00
J-Jamet
31eb0fb48a Upgrade to 3.0.0_beta02 2021-08-30 11:05:38 +02:00
J-Jamet
d6a012e85f Fix Permissions #1066 2021-08-30 11:05:08 +02:00
J-Jamet
11c1cc7c72 Change name to beta 01 2021-08-29 20:02:02 +02:00
J-Jamet
6b7acb7bd5 Fix translations 2021-08-29 19:57:34 +02:00
Hosted Weblate
bdebf19d7b Merge branch 'origin/develop' into Weblate. 2021-08-29 19:42:45 +02:00
J-Jamet
cb1973ffb5 Fix status bar in Lollipop 2021-08-29 19:29:19 +02:00
Uli
c6e2342ab4 Merge branch 'Kunzisoft:develop' into develop 2021-08-29 17:04:31 +02:00
J-Jamet
2447599364 Add view divider for better visibility in tablet 2021-08-29 16:59:29 +02:00
J-Jamet
6a2cda74f1 Fix color in lollipop and remove unnecessary otp counter 2021-08-29 16:44:19 +02:00
J-Jamet
8385d55d69 Fix template container view in lollipop 2021-08-29 16:07:04 +02:00
J-Jamet
85e3464a15 Fix check registration when database not yet retrieved #1064 2021-08-29 15:34:56 +02:00
J-Jamet
6680039de7 Build default templates if setting enable #1062 2021-08-29 13:56:41 +02:00
J-Jamet
9935826877 Fix warning dialog during creation state 2021-08-29 12:11:06 +02:00
Ulrich Dürholz
b977792168 Autofill manual selection for all form fields 2021-08-29 11:23:22 +02:00
Uli
2595cf87d8 Merge branch 'Kunzisoft:develop' into develop 2021-08-29 10:50:08 +02:00
Ulrich Dürholz
f4342f1448 Manual selection for inline suggestions 2021-08-29 10:16:15 +02:00
J-Jamet
84c26b7c40 Little adjustment 2021-08-28 18:35:32 +02:00
J-Jamet
1cd7940a17 Replace constraint by relative layout 2021-08-28 18:02:29 +02:00
J-Jamet
9514032f25 Refresh otp every second is sufficient 2021-08-28 17:52:23 +02:00
J-Jamet
c7d6da2373 Simpler activity group layout 2021-08-28 17:30:06 +02:00
J-Jamet
41b822fb6c Replace linear progress bar 2021-08-28 16:58:50 +02:00
J-Jamet
69bf098c84 Fix Kitkat OTP token 2021-08-28 16:44:39 +02:00
J-Jamet
b4283ed4dc Fix field view padding 2021-08-28 14:09:06 +02:00
J-Jamet
0fa0cac9e6 Fix copy field reference #1027 2021-08-28 13:45:35 +02:00
Ulrich Dürholz
c71ef24052 Add icon for manual autofill selection 2021-08-28 12:48:03 +02:00
J-Jamet
cf0f665b14 Add new source icon 2021-08-28 12:25:16 +02:00
J-Jamet
2034e3ab78 Dropdown list as outlined box 2021-08-28 12:12:32 +02:00
J-Jamet
89cfeec1b3 New icons for saving and reloading database 2021-08-28 11:14:26 +02:00
J-Jamet
d8ae212df0 Simpler entry edit viewModel 2021-08-27 22:07:39 +02:00
Ulrich Dürholz
39b817bc69 Let user select entry for autofill 2021-08-27 18:23:32 +02:00
J-Jamet
09d79d52ae Replace ConstraintLayout in activity password 2021-08-27 17:40:04 +02:00
J-Jamet
5c4b98d0e9 Fast loading #1021 2021-08-27 17:34:24 +02:00
J-Jamet
e5d6fc0604 Replace entry edit view constraint layout by linear layout 2021-08-27 17:26:59 +02:00
J-Jamet
5e656ebfba Replace attachment view constraint layout by relative layout 2021-08-27 17:04:53 +02:00
J-Jamet
58fb75e55d Replace history view constraint layout by linear layout 2021-08-27 16:48:50 +02:00
J-Jamet
e01621e658 Replace template view constraint layout as linear layout 2021-08-27 16:30:43 +02:00
J-Jamet
b4aee17f53 Simpler load entry implementation 2021-08-27 16:16:55 +02:00
J-Jamet
dc70918648 Encapsulate URL in viewModel 2021-08-27 16:03:47 +02:00
J-Jamet
69772edfa3 custom field 20 lines max 2021-08-27 15:52:59 +02:00
J-Jamet
dd224cab05 Entry field as programmatic view 2021-08-27 15:41:12 +02:00
J-Jamet
b62873129e ConstraintLayout as RelativeLayout for much better performance 2021-08-27 14:57:00 +02:00
J-Jamet
5052a1f564 Faster animation 2021-08-27 13:51:44 +02:00
J-Jamet
2c36163e7a Fix nodes if group modified in background 2021-08-27 13:51:33 +02:00
J-Jamet
1bf912d6f0 Fix launch application #996 2021-08-27 13:32:41 +02:00
J-Jamet
d290259075 Change outlinedbox padding 2021-08-27 11:18:43 +02:00
J-Jamet
1689672faf Change password form margin 2021-08-27 11:10:22 +02:00
J-Jamet
8196e05679 Show counter and fix HOTP 2021-08-26 20:09:49 +02:00
J-Jamet
fe0235da43 Setting to display OTP Token in list of entries #655 2021-08-26 18:50:49 +02:00
J-Jamet
0895a73546 Fix dialogs 2021-08-26 18:30:35 +02:00
J-Jamet
f06821e35b Fix refresh and colors 2021-08-26 18:06:53 +02:00
J-Jamet
9cce5f645f Show OTP Token in entry list 2021-08-26 17:17:59 +02:00
Francesco MDE
c1a46408e9 Translated using Weblate (Italian)
Currently translated at 99.8% (561 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-08-26 15:34:30 +02:00
J-Jamet
21c3ccd637 Fix view events 2021-08-26 13:51:18 +02:00
J-Jamet
3b9b034d80 Fix replace field 2021-08-26 12:03:33 +02:00
J-Jamet
348a5c3eb7 Fix save entry #1057 2021-08-26 10:57:35 +02:00
Uli
e3adaba3b3 Merge branch 'Kunzisoft:develop' into develop 2021-08-24 16:39:45 +02:00
J-Jamet
4c5be658c3 Fix small bugs 2021-08-23 18:29:40 +02:00
J-Jamet
b6517a449b Fix crash 2021-08-23 18:22:24 +02:00
J-Jamet
ae0b8db0b0 Autofill during save 2021-08-23 18:15:58 +02:00
J-Jamet
56f0f8a299 Fix autofill search 2021-08-23 17:25:55 +02:00
J-Jamet
c9af786b79 Fix focus 2021-08-23 11:29:07 +02:00
J-Jamet
f34f615b80 Fix focus 2021-08-23 11:11:11 +02:00
J-Jamet
aef2ef8479 Request focus after error 2021-08-23 10:04:24 +02:00
J-Jamet
7afbc9f5a4 Fix small bugs 2021-08-23 10:00:36 +02:00
J-Jamet
df51b62041 Validate credential with enter button #1043 2021-08-23 09:38:22 +02:00
J-Jamet
045abc54fb Checkboxes as Switches 2021-08-22 15:22:49 +02:00
J-Jamet
9b2d9683eb Add padding in field view 2021-08-22 14:34:08 +02:00
J-Jamet
3b0dd4a36c Edit text as outline box 2021-08-22 14:30:06 +02:00
J-Jamet
5e15f82313 Fix upload attachment 2021-08-22 12:23:35 +02:00
J-Jamet
d841c25bd3 Check URI permissions #626 2021-08-21 16:52:13 +02:00
J-Jamet
8d3f1fe179 Show UUID in each group and entry 2021-08-21 14:34:59 +02:00
J-Jamet
130ec130cc Add link to download icon 2021-08-21 13:21:24 +02:00
J-Jamet
5e7a95eac0 Fix loading file database list 2021-08-21 12:50:42 +02:00
J-Jamet
a8cb49d12d Fix template crash 2021-08-20 20:49:25 +02:00
J-Jamet
c179ac626a Fix recreate activity loop in kitkat 2021-08-20 20:44:54 +02:00
J-Jamet
041583bf96 Fix crash in kitkat 2021-08-20 19:57:01 +02:00
J-Jamet
ed710335b3 Merge branch 'feature/DatabaseProvider' into develop 2021-08-20 19:49:20 +02:00
J-Jamet
b556581a87 Remove TODO 2021-08-20 19:46:20 +02:00
J-Jamet
77a1b7918c Check database loaded and not read only 2021-08-20 19:43:39 +02:00
J-Jamet
45149e1b28 Encapsulate database functions 2021-08-20 19:11:08 +02:00
J-Jamet
932338a25a Fix appearance setting changed 2021-08-20 16:16:17 +02:00
J-Jamet
925509e5a0 Better item list view implementation 2021-08-19 17:11:42 +02:00
J-Jamet
25646fbad7 Fix opening SettingsActivity 2021-08-19 15:17:06 +02:00
J-Jamet
e1733512c4 Fix show error 2021-08-19 15:04:16 +02:00
J-Jamet
8379ffe1ce Add group loading 2021-08-19 14:44:59 +02:00
J-Jamet
c77537ecee Fix open database when back on password activity 2021-08-19 14:39:27 +02:00
J-Jamet
2192d97c69 Encapsulate database and scroll to new entry 2021-08-18 20:52:13 +02:00
J-Jamet
a0dc76bda8 Refactoring EntryEditViewModel 2021-08-18 20:35:05 +02:00
J-Jamet
7fe177edc6 Remove clipboard notification if setting off 2021-08-18 15:49:08 +02:00
J-Jamet
1f5e6f1e17 Fix education hint in actionNodeMode 2021-08-18 15:32:12 +02:00
J-Jamet
bf0aa295b0 Fix task request 2021-08-18 15:19:54 +02:00
J-Jamet
649dffc3e0 Close service after save if task removed by the user 2021-08-18 15:10:37 +02:00
J-Jamet
a0f5ed66e2 Fix lock bug 2021-08-18 14:53:41 +02:00
Éfrit
7df3b95c22 Translated using Weblate (French)
Currently translated at 100.0% (562 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-08-18 00:10:54 +02:00
J-Jamet
0756474d40 Allow to stop service after action task 2021-08-17 18:36:44 +02:00
J-Jamet
60747db945 Prevent remove service during database registration 2021-08-17 18:26:10 +02:00
J-Jamet
afcfad162e Remove unused methods 2021-08-17 18:16:51 +02:00
J-Jamet
63f15bdc9e Add search parameter to not search in templates 2021-08-17 17:46:46 +02:00
J-Jamet
3b826869e9 Fix search orientation change 2021-08-17 17:42:02 +02:00
J-Jamet
af0256add0 Fix nodes refresh 2021-08-17 13:20:00 +02:00
J-Jamet
b8d8cba12c Fix nodes same content conditions 2021-08-17 12:12:38 +02:00
J-Jamet
616e9a0ec2 Fix natural order when update entry 2021-08-17 12:00:11 +02:00
J-Jamet
366434cbd7 Fix update entry 2021-08-17 11:46:26 +02:00
J-Jamet
f6d4046af6 Better update implementation 2021-08-17 11:33:58 +02:00
J-Jamet
82932f002e Refactoring database locking activity and read only check 2021-08-14 13:28:59 +02:00
J-Jamet
7593a05953 Better readOnly implementation 2021-08-14 12:37:20 +02:00
J-Jamet
3026a9e3e4 Fix reloading activity 2021-08-13 18:23:28 +02:00
J-Jamet
362939eab9 Fix reloading activity in entry 2021-08-13 17:43:31 +02:00
J-Jamet
61d52731a5 Change special mode to database mode 2021-08-13 17:37:00 +02:00
J-Jamet
6aecc6521c Fix timeout in dialogs #716 2021-08-13 17:24:31 +02:00
zeritti
ef5829593e Translated using Weblate (Czech)
Currently translated at 100.0% (562 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-08-12 17:33:42 +02:00
J-Jamet
4a8f67093f Refactoring lock timer code 2021-08-11 14:05:00 +02:00
J-Jamet
9cbe0664f6 Fix attachments after orientation change 2021-08-11 12:35:32 +02:00
Hisikawa Mizuki
965d6e4e8e Translated using Weblate (Japanese)
Currently translated at 98.2% (552 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2021-08-11 09:32:44 +02:00
J-Jamet
eefdeb0bb7 Fix add attachments 2021-08-10 19:31:08 +02:00
J-Jamet
a904a51293 Fix new entry edition empty 2021-08-10 12:33:16 +02:00
John Doe
cce377d70d Translated using Weblate (French)
Currently translated at 99.8% (561 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2021-08-09 17:35:03 +02:00
J-Jamet
5721bca5a3 Use DatabaseTaskProvider to retrieve database from service 2021-08-09 17:00:34 +02:00
J-Jamet
bcd5b024f0 Simpler action mode callback 2021-08-09 15:18:50 +02:00
J-Jamet
571f257c17 Delegate onDatabaseActionFinished in Fragment 2021-08-09 12:46:03 +02:00
J-Jamet
3451135800 Fix icons pack change 2021-08-09 12:01:24 +02:00
J-Jamet
f426a78a94 Rename fragment group layout 2021-08-08 20:24:56 +02:00
J-Jamet
3d65236e63 Keep scroll position after orientation change 2021-08-08 20:15:51 +02:00
J-Jamet
7b51b5005a Remove TODO 2021-08-08 10:49:14 +02:00
J-Jamet
3d9cf16960 Fix search 2021-08-07 22:18:36 +02:00
J-Jamet
35def53666 Fix entry deletion 2021-08-07 21:08:32 +02:00
J-Jamet
5c46a89ddc Change launch methods 2021-08-07 18:39:51 +02:00
J-Jamet
4e429025bf Fix add new entry 2021-08-07 16:32:57 +02:00
J-Jamet
95fae11eee Fix group elements loading 2021-08-07 16:25:11 +02:00
Gabe Pérez
9a22a9fb8b Translated using Weblate (Spanish)
Currently translated at 100.0% (562 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-08-06 22:34:19 +02:00
J-Jamet
f60e2e2ca6 Fix reload activity 2021-08-06 20:40:40 +02:00
J-Jamet
deb9101335 Add edit group view model 2021-08-06 11:37:02 +02:00
J-Jamet
407f93ac43 Refactor magikeyboard service name 2021-08-05 20:21:08 +02:00
J-Jamet
78c39edceb Pass group icon selection and time through viewmodel 2021-08-05 20:09:38 +02:00
J-Jamet
c8445fb711 Delete nodes with viewmodel 2021-08-05 12:46:40 +02:00
J-Jamet
7c0e7347c8 Better icon chooser and activity lock 2021-08-05 11:01:47 +02:00
J-Jamet
12f37d0931 Close database in right activity 2021-08-05 10:41:23 +02:00
J-Jamet
9a5086d9ba Remove unused database deletion 2021-08-05 10:34:02 +02:00
Gabe Pérez
3222c7e677 Translated using Weblate (Spanish)
Currently translated at 98.9% (556 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-08-04 23:33:36 +02:00
J-Jamet
632e0648d4 Fix autofill style 2021-08-04 18:54:34 +02:00
J-Jamet
e3198031e3 Fix database loading 2021-08-04 18:46:56 +02:00
J-Jamet
0ced9c8e26 Fix clear database 2021-08-04 17:41:38 +02:00
J-Jamet
65f4a708cd Fix color selection 2021-08-04 14:08:52 +02:00
J-Jamet
36e7b00d9a Fix activity settings back pressed 2021-08-04 13:58:04 +02:00
J-Jamet
8b2c48f5ca Fix relaunch current screen 2021-08-04 13:51:05 +02:00
J-Jamet
9f7a0d4f17 Better inheritance 2021-08-04 13:24:45 +02:00
J-Jamet
fa5ae17621 Fix database settings 2021-08-04 12:11:24 +02:00
J-Jamet
7a2536c559 Merge branch 'develop' into feature/DatabaseProvider 2021-08-02 18:47:10 +02:00
J-Jamet
96d2edb641 Fix template after autofill search save 2021-08-02 18:36:42 +02:00
J-Jamet
8a2bd23c32 Fix default list persistence 2021-08-02 17:17:00 +02:00
WaldiS
d3b935ea7f Translated using Weblate (Polish)
Currently translated at 100.0% (562 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-07-31 21:53:13 +02:00
J-Jamet
e53bc3b048 Fix menu 2021-07-31 00:16:42 +02:00
J-Jamet
5f1cfc9dda Fix group activity 2021-07-31 00:07:23 +02:00
J-Jamet
43207b316f Fix entry history 2021-07-30 23:00:28 +02:00
J-Jamet
96ed4c419a Fix entry edition 2021-07-30 21:34:08 +02:00
J-Jamet
840a2253e2 First refactoring pass 2021-07-30 18:11:15 +02:00
Oğuz Ersen
18db9b0a77 Translated using Weblate (Turkish)
Currently translated at 100.0% (562 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-07-30 08:35:05 +02:00
random r
6c7a5292a4 Translated using Weblate (Italian)
Currently translated at 100.0% (562 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-07-30 08:35:04 +02:00
Milo Ivir
bef1c74226 Translated using Weblate (Croatian)
Currently translated at 99.8% (561 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-07-27 19:34:29 +02:00
VfBFan
176ec8bace Translated using Weblate (German)
Currently translated at 100.0% (562 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-07-27 19:34:28 +02:00
Ulrich Dürholz
c62064002f Fix small issue with credit card autofill 2021-07-27 17:36:56 +02:00
Eric
45b7800a68 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (562 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-07-26 00:34:43 +02:00
Ihor Hordiichuk
fa761ac69b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (562 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-07-26 00:34:42 +02:00
solokot
cc11e98aa6 Translated using Weblate (Russian)
Currently translated at 100.0% (562 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-07-26 00:34:41 +02:00
Matthaiks
8f1c71137a Translated using Weblate (Polish)
Currently translated at 100.0% (562 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-07-26 00:34:40 +02:00
Retrial
8fdf2dcb7a Translated using Weblate (Greek)
Currently translated at 100.0% (562 of 562 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-07-26 00:34:39 +02:00
J-Jamet
d4cd5b73bd Fix duplicate groups with settings 2021-07-25 14:37:23 +02:00
Hosted Weblate
77975aed2a Merge branch 'origin/develop' into Weblate. 2021-07-24 17:23:46 +02:00
J-Jamet
a7700ce27e Don't show link if url empty 2021-07-23 19:07:24 +02:00
J-Jamet
726ff1a126 Add timestamp in database file export #1035 2021-07-23 18:42:22 +02:00
J-Jamet
e24269c452 Update to 3.0.0 2021-07-23 18:31:52 +02:00
J-Jamet
686f4656ec Merge branch 'feature/Templates' into develop 2021-07-23 18:15:13 +02:00
J-Jamet
55fe10d2dc Allow null default item in selection 2021-07-23 18:11:16 +02:00
J-Jamet
422984ac41 Manually change templates group and recyclebin group 2021-07-23 18:02:41 +02:00
J-Jamet
706d117d80 Fix template entry edition 2021-07-20 19:53:52 +02:00
J-Jamet
13f8df4e0d Fix template field in magikeyboard 2021-07-20 19:36:53 +02:00
J-Jamet
263d433193 Disable autofill in form fields 2021-07-20 19:10:22 +02:00
J-Jamet
c15c11f3b1 Add expiration day 2021-07-20 17:38:46 +02:00
J-Jamet
524c8ccfc5 Fix small TODO 2021-07-20 17:12:48 +02:00
J-Jamet
902392ea30 Fix registration expiration 2021-07-20 17:07:10 +02:00
J-Jamet
bef179187f Refactoring cc names 2021-07-20 15:53:02 +02:00
J-Jamet
ea7221c39a Merge branch 'develop' into feature/Templates 2021-07-20 14:44:23 +02:00
J-Jamet
edaf9f6296 Fix default values 2021-07-18 20:51:33 +02:00
J-Jamet
0d83725b77 Simpler template option 2021-07-18 20:31:53 +02:00
J-Jamet
6ce31305c6 Fix password generator 2021-07-18 19:26:08 +02:00
J-Jamet
90935c033d Fix template format 2021-07-18 18:36:04 +02:00
J-Jamet
b4c3f831a7 Set date time from string 2021-07-18 14:52:23 +02:00
J-Jamet
f0e25e8198 Fix date time string representation 2021-07-18 14:47:58 +02:00
J-Jamet
d800082621 Fix date time expiration 2021-07-18 14:23:10 +02:00
J-Jamet
653d3da718 Recognize others date format 2021-07-18 13:54:13 +02:00
J-Jamet
0f39409386 Add version 2021-07-18 12:27:23 +02:00
J-Jamet
ccae0d1a57 Add template decorator in new template field 2021-07-18 12:10:25 +02:00
J-Jamet
257992d314 Refactor prefix and suffix template 2021-07-18 11:48:35 +02:00
Tur
5eb843b63d Translated using Weblate (Turkish)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2021-07-15 22:32:52 +02:00
J-Jamet
3929b478a7 Fix span in datetime view 2021-07-09 20:05:47 +02:00
J-Jamet
18734ed822 Change datetime expiration view 2021-07-09 14:13:09 +02:00
J-Jamet
876e749b31 Set max chars 2021-07-09 13:54:46 +02:00
J-Jamet
32b8c505d9 Change text appearance for selection view 2021-07-08 19:50:45 +02:00
J-Jamet
37a0dce7c5 Add selection view 2021-07-08 19:37:10 +02:00
Ngô Ngọc Đức Huy
2332f36b56 Translated using Weblate (Vietnamese)
Currently translated at 16.8% (89 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/vi/
2021-07-08 11:34:29 +02:00
solokot
21cc9cc026 Translated using Weblate (Russian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-07-08 11:34:27 +02:00
J-Jamet
db4d76502e Add Type to Wifi 2021-07-07 21:55:11 +02:00
J-Jamet
1578ea7590 Change build icon 2021-07-07 21:32:21 +02:00
J-Jamet
7405de01fe Fix list recognition 2021-07-07 21:16:56 +02:00
J-Jamet
77dc5943e5 Fix unit test 2021-07-07 21:03:11 +02:00
J-Jamet
f8a2748ede Refactor options 2021-07-07 20:59:33 +02:00
J-Jamet
a99ca00bb3 Add default option 2021-07-07 19:26:32 +02:00
J-Jamet
6eb80eea2f Fix title in template 2021-07-07 18:19:13 +02:00
J-Jamet
82828f7f82 Fix label and alias 2021-07-07 17:49:59 +02:00
J-Jamet
23c9a5963a Fix template order 2021-07-07 16:02:24 +02:00
J-Jamet
7595f113ec Fix many template options 2021-07-07 14:06:26 +02:00
Alexander
e9e5a4ee0d Translated using Weblate (Russian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-07-06 22:51:10 +02:00
J-Jamet
1947fc3e83 Fix dateTime visibility 2021-07-06 20:20:18 +02:00
J-Jamet
8844689482 Fix dateTime type 2021-07-06 19:34:29 +02:00
J-Jamet
0d2ba54c10 Fix dateTime type 2021-07-06 19:12:21 +02:00
Huy Ngo
a4bb5137ea Added translation using Weblate (Vietnamese) 2021-07-06 17:29:35 +02:00
J-Jamet
b8aea1f97a Add section name 2021-07-05 16:59:48 +02:00
J-Jamet
120e1893bd Better options parser 2021-07-05 14:44:38 +02:00
Ihor Hordiichuk
836df52a50 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-07-04 16:33:49 +02:00
J-Jamet
00498aaeac Fix options recognition 2021-07-03 20:49:04 +02:00
J-Jamet
f4f47cff75 Fix URL memory 2021-07-03 20:22:48 +02:00
J-Jamet
40dc3d45fc Better template types 2021-07-03 20:13:02 +02:00
J-Jamet
b89d2a6da1 Encapsulate local Template types 2021-07-03 15:52:30 +02:00
J-Jamet
55a4af9f00 Encode and decode labels 2021-07-03 14:42:35 +02:00
J-Jamet
719d45e75e Manage sections 2021-07-03 14:30:10 +02:00
J-Jamet
8703684740 Replace ArrayList by mutableList 2021-07-02 19:25:30 +02:00
J-Jamet
e95e7218f6 Merge debit and credit cards 2021-07-02 16:11:55 +02:00
J-Jamet
e9d4711978 Change E-mail to Email 2021-07-02 15:59:09 +02:00
J-Jamet
9309506e97 Encapsulate template name 2021-07-02 15:56:34 +02:00
J-Jamet
00d2a80e95 Add default templates 2021-07-02 13:58:00 +02:00
J-Jamet
c1e62b7d90 Fix template in history 2021-07-01 16:09:27 +02:00
Alexander
84775d36dc Translated using Weblate (Russian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-06-30 22:32:18 +02:00
Milo Ivir
fc4eb11fd8 Translated using Weblate (Croatian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hr/
2021-06-28 16:32:58 +02:00
Muhammad Raihan Divanda
ce70ce6c76 Translated using Weblate (Indonesian)
Currently translated at 82.1% (434 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-06-26 15:33:03 +02:00
J-Jamet
ffd404ec1b Try to fix progress dialog launch 2021-06-25 10:14:44 +02:00
J-Jamet
6f943db012 Fix not referenced fields 2021-06-24 17:19:42 +02:00
J-Jamet
12342ac426 Fix field selection and copy 2021-06-24 17:02:36 +02:00
J-Jamet
489ddc3f56 Fix datetime type 2021-06-24 16:33:25 +02:00
J-Jamet
02a266cbea Show expiration not referenced 2021-06-24 15:38:07 +02:00
J-Jamet
bdc9facd41 Better header implementation 2021-06-24 15:14:43 +02:00
J-Jamet
7d679aac0b Add not referenced fields section and encapsulate code 2021-06-24 14:18:55 +02:00
VfBFan
ae1719e795 Translated using Weblate (German)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-06-23 12:32:06 +02:00
Neko Nekowazarashi
6cea05e9f4 Translated using Weblate (Indonesian)
Currently translated at 78.5% (415 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-06-22 11:33:03 +02:00
zeritti
cda84d4e64 Translated using Weblate (Czech)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-06-22 11:33:01 +02:00
Martin
b3f63a85d5 Translated using Weblate (Czech)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2021-06-22 11:33:01 +02:00
J-Jamet
196903cb12 Merge tag '2.10.5' into develop
2.10.5
2021-06-21 17:57:14 +02:00
J-Jamet
e6f082adfd Merge branch 'release/2.10.5' 2021-06-21 17:57:03 +02:00
J-Jamet
bb2f641073 Update to 2.10.5 2021-06-19 20:50:28 +02:00
J-Jamet
de6312d317 Fix binding service #1028 2021-06-19 19:42:26 +02:00
J-Jamet
887b0f3119 Revert #1018 and change runOnUIThread by lifecyclescope(main) 2021-06-19 12:12:26 +02:00
J-Jamet
f13c8d7884 Fix date time selection 2021-06-17 21:04:30 +02:00
J-Jamet
a10fdb260d Fix cancel password generation 2021-06-17 20:50:09 +02:00
J-Jamet
0f8b1790f3 Remove not necessary first launch boolean 2021-06-17 20:40:12 +02:00
J-Jamet
531e345dd9 Fix OTP runnable 2021-06-17 20:35:42 +02:00
Eric
c1942759d4 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-06-17 20:34:53 +02:00
Ihor Hordiichuk
eca9e573de Translated using Weblate (Ukrainian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/uk/
2021-06-17 20:34:52 +02:00
solokot
61376f8d68 Translated using Weblate (Russian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-06-17 20:34:52 +02:00
Matthaiks
292e2c60e2 Translated using Weblate (Polish)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2021-06-17 20:34:52 +02:00
Stephan Paternotte
2ffaf81109 Translated using Weblate (Dutch)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2021-06-17 20:34:51 +02:00
Oliver Cervera
da37381678 Translated using Weblate (Italian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2021-06-17 20:34:51 +02:00
Adolfo Jayme Barrientos
3236bf6122 Translated using Weblate (Spanish)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2021-06-17 20:34:51 +02:00
Retrial
a352ae6922 Translated using Weblate (Greek)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/el/
2021-06-17 20:34:50 +02:00
Adolfo Jayme Barrientos
f3631a6a09 Translated using Weblate (Catalan)
Currently translated at 55.8% (295 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ca/
2021-06-17 20:34:50 +02:00
J-Jamet
0d376bd5b7 Fix template view settings initialization 2021-06-17 19:02:53 +02:00
J-Jamet
fd57bd0378 Add loading 2021-06-17 14:42:36 +02:00
J-Jamet
5a11882d1b Faster template creation 2021-06-17 13:16:57 +02:00
J-Jamet
0f041da548 Fix parcelable 2021-06-17 11:18:17 +02:00
J-Jamet
00a23119c5 Load history with entry info 2021-06-17 09:50:34 +02:00
J-Jamet
cdeb49473b Better async loading 2021-06-16 20:22:47 +02:00
J-Jamet
b55b3731a5 Fix template creation 2021-06-16 19:40:41 +02:00
J-Jamet
4afbb688ba Add template / edit / view 2021-06-16 19:18:49 +02:00
J-Jamet
f289a921f1 Fix entry history 2021-06-16 11:53:45 +02:00
J-Jamet
f2165bc4c1 Encapsulate entry methods in ViewModel 2021-06-16 11:28:54 +02:00
J-Jamet
73b20bfe4a Change load entry code 2021-06-16 09:26:03 +02:00
J-Jamet
fbf2006e3f First pass load entry refactoring 2021-06-15 20:03:01 +02:00
J-Jamet
358b701396 Entry history as fragment, entry fragment with only EntryInfo 2021-06-15 17:49:43 +02:00
J-Jamet
d3caae3a2d Add password generator education 2021-06-15 16:48:33 +02:00
J-Jamet
300062d3ac Fix cardview margin 2021-06-15 16:09:07 +02:00
J-Jamet
bb0aaad383 Fix new custom field after orientation change 2021-06-15 13:48:58 +02:00
J-Jamet
e9aa8609f1 Fix focus and orientation change 2021-06-15 13:41:32 +02:00
J-Jamet
37cbb18626 Merge branch 'develop' into feature/Templates_ViewModel 2021-06-15 13:11:41 +02:00
J-Jamet
3dad6daa2e Merge tag '2.10.4' into develop
2.10.4
2021-06-15 12:57:35 +02:00
J-Jamet
f49485e161 Merge branch 'release/2.10.4' 2021-06-15 12:57:28 +02:00
J-Jamet
f3f268742f Hot fix to increase the opening speed of database #1028 2021-06-15 12:52:16 +02:00
J-Jamet
99e76f2254 Fix orientation for attachments and entry info as template property 2021-06-15 10:33:02 +02:00
Hosted Weblate
a7801e376b Merge branch 'origin/develop' into Weblate. 2021-06-14 23:53:09 +02:00
VfBFan
90000aa1fb Translated using Weblate (German)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-06-14 23:53:09 +02:00
J-Jamet
a2a26cd058 Fix title 2021-06-14 22:56:59 +02:00
J-Jamet
e3cbefc5b6 Fix multiline 2021-06-14 22:27:15 +02:00
J-Jamet
c6ceb25ee8 Manage OTP 2021-06-14 21:57:04 +02:00
J-Jamet
022777888c Fix font in visibility 2021-06-14 21:22:37 +02:00
J-Jamet
ad5e644362 Fix DateTime view id 2021-06-14 21:12:22 +02:00
J-Jamet
bf6cb04fe8 Manage attachments 2021-06-14 20:21:54 +02:00
J-Jamet
dbde23fb7b Init template one time 2021-06-14 19:25:47 +02:00
J-Jamet
c9cb469d65 Title text view as other views 2021-06-14 17:35:42 +02:00
J-Jamet
2e37c20e55 Add saved instance state in template view 2021-06-14 17:07:27 +02:00
J-Jamet
ade665d228 Merge branch 'feature/Templates' into feature/Templates_ViewModel 2021-06-14 10:41:48 +02:00
J-Jamet
a62f0cfd3b Merge branch 'develop' into feature/Templates 2021-06-14 10:41:33 +02:00
J-Jamet
8b867f78fe Merge tag '2.10.3' into develop
2.10.3
2021-06-14 10:14:05 +02:00
J-Jamet
d6a43fd8e5 Merge branch 'release/2.10.3' 2021-06-14 10:13:56 +02:00
J-Jamet
9887b8142d Fix service starting #1025 2021-06-13 17:57:30 +02:00
J-Jamet
4d92d6dc2b Update CHANGELOG 2021-06-13 17:15:14 +02:00
J-Jamet
d8cd84ed9e Remove special chars 2021-06-13 17:09:43 +02:00
J-Jamet
6bc740e881 Merge branch 'djibux-master' into develop 2021-06-13 16:53:46 +02:00
J-Jamet
db348cc368 Merge branch 'translations' into develop 2021-06-13 16:51:56 +02:00
J-Jamet
6ebf59d7ff Merge branch 'develop' of https://hosted.weblate.org/git/keepass-dx/strings into translations 2021-06-13 16:51:21 +02:00
J-Jamet
d35e31d128 Fix biometric prompt #1018 2021-06-12 12:25:04 +02:00
J-Jamet
b9b6d3d2cb Fix database opened without notification (Database is now closed when screen is killed in background #1025) 2021-06-12 11:39:41 +02:00
J-Jamet
728b111ac9 Upgrade to 2.10.3 2021-06-12 10:31:18 +02:00
J-Jamet
5afbfbfd43 Create TemplateView 2021-06-12 10:23:14 +02:00
Reza Almanda
9e5ce589ae Translated using Weblate (Indonesian)
Currently translated at 74.8% (395 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/id/
2021-06-11 03:36:00 +02:00
djib
f486150a0f Fix a typo 2021-06-09 20:58:08 +02:00
djib
075ee815f0 Improve French translation mostly for Magikeyboard 2021-06-09 20:54:19 +02:00
djib
d321283b13 Improve Magikeyboard options descriptions 2021-06-09 20:42:16 +02:00
J-Jamet
0ec72bb013 Move temp attachments in view model 2021-06-09 17:34:14 +02:00
J-Jamet
cdfbcd873c Template and Attachment listener as view model 2021-06-09 17:06:06 +02:00
solokot
8be382fa7e Translated using Weblate (Russian)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2021-06-09 12:33:30 +02:00
J-Jamet
82f5ab1446 First pass to entry edit as ViewModel 2021-06-08 22:13:21 +02:00
J-Jamet
c97f8c31ce Better entry parameter lisibility 2021-06-08 18:12:47 +02:00
J-Jamet
92e5b5e9c3 Add EntryEditViewModel 2021-06-07 20:40:57 +02:00
J-Jamet
a70fca493d Better template engine encapsulation 2021-06-07 14:54:03 +02:00
J-Jamet
42c8f0c345 Fix and add each label translation 2021-06-07 13:27:28 +02:00
J-Jamet
d3d5a1745d Manage template fields 2021-06-06 21:52:17 +02:00
J-Jamet
8392d8b684 Add prefix to fix custom label with standard label 2021-06-06 21:05:52 +02:00
J-Jamet
1abd13c6e0 Fix standard field 2021-06-06 19:37:54 +02:00
J-Jamet
e126b66e19 Change LinkedHashMap by list for Fields 2021-06-06 19:12:16 +02:00
J-Jamet
47449d93db Move progress bar 2021-06-06 19:07:33 +02:00
VfBFan
13002f96f1 Translated using Weblate (German)
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2021-06-06 17:34:13 +02:00
J-Jamet
1985a035be Template edit configuration 2021-06-04 22:54:11 +02:00
J-Jamet
cdfc7e4158 Add creation template 2021-06-04 12:46:42 +02:00
J-Jamet
5050052710 Add progress bar during fragment loading 2021-06-03 11:53:43 +02:00
J-Jamet
48e39d2ffa Entry activity with fragment 2021-06-02 21:29:05 +02:00
J-Jamet
a8d053e82a Fix icons loading 2021-06-02 12:17:17 +02:00
J-Jamet
2154945c3c Merge branch 'feature/Database_Call' into feature/Templates_Database 2021-06-02 12:07:31 +02:00
J-Jamet
8d83a0a86a Merge branch 'develop' into feature/Database_Call 2021-06-02 11:41:07 +02:00
J-Jamet
c196cdf405 Refactor TemplateField and Template name as resource 2021-06-02 11:40:44 +02:00
J-Jamet
b6a5f43176 Merge branch 'develop' into feature/Templates 2021-06-02 11:09:52 +02:00
J-Jamet
008ded4a5c Merge tag '2.10.2' into develop
2.10.2
2021-06-02 10:37:27 +02:00
Yudong
371b3813d4 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (528 of 528 strings)

Translation: KeePassDX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2021-06-02 10:34:27 +02:00
J-Jamet
e4c3c224d9 Adapt code for each custom fields 2021-06-01 13:52:49 +02:00
J-Jamet
69bc697568 Merge branch 'develop' into feature/Templates 2021-05-31 16:36:38 +02:00
J-Jamet
36b37b3b8f Hide template UUID field 2021-05-17 19:17:04 +02:00
J-Jamet
ce9931d8a3 Create new templates group in settings 2021-05-17 13:12:23 +02:00
J-Jamet
66f5ff35d3 Add templates settings 2021-05-17 12:39:41 +02:00
J-Jamet
80c0152d46 Change credit card implementation 2021-05-16 23:19:45 +02:00
J-Jamet
fac727cd3d Manage Templates with TemplateEngine 2021-05-16 22:37:16 +02:00
J-Jamet
9c30183068 Fix template selection view 2021-05-16 18:25:05 +02:00
J-Jamet
9c28d5c5c5 Show view by fading 2021-05-16 17:52:10 +02:00
J-Jamet
3c55e3a3f0 Save entry info during template change 2021-05-16 14:16:02 +02:00
J-Jamet
ba6e3b801d Spinner template selection as cardview and fix margin in kitakt 2021-05-16 14:07:10 +02:00
J-Jamet
4064ab47ac Hide password with setting configuration 2021-05-16 12:45:15 +02:00
J-Jamet
b6005fcb56 Rollback : prevent focus after orientation change 2021-05-15 23:30:11 +02:00
J-Jamet
71de526366 Fix field after template change 2021-05-15 23:16:05 +02:00
J-Jamet
a3e5d8448b Fix custom fields actions 2021-05-15 22:57:51 +02:00
J-Jamet
f65b6e5484 Better cardview implementation 2021-05-15 22:29:35 +02:00
J-Jamet
05d0d9e501 Fix focus by manual view creation 2021-05-15 15:35:24 +02:00
J-Jamet
d107beadf2 EntryEditFieldView in manual configuration to avoid view id bugs 2021-05-15 14:55:24 +02:00
J-Jamet
9c2fd26579 Fix date time selection orientation change 2021-05-14 19:17:22 +02:00
J-Jamet
d796ea6324 Password manager for custom fields 2021-05-14 17:15:24 +02:00
J-Jamet
8386c9e729 Smooth view visibility 2021-05-12 22:58:36 +02:00
J-Jamet
00ca4524b5 Fix crashes and templates implementation 2021-05-12 21:42:58 +02:00
J-Jamet
f4d4853319 Add Section view 2021-05-12 20:22:21 +02:00
J-Jamet
00678cc9ca Dynamic edit field size 2021-05-12 13:48:01 +02:00
J-Jamet
2845486a3f Uniformize EntryEditFieldView 2021-05-12 13:42:56 +02:00
J-Jamet
a815e6447d Select date and time for each field 2021-05-11 22:06:40 +02:00
J-Jamet
5b71a30ee9 Date time view refactoring 2021-05-11 21:33:33 +02:00
J-Jamet
ac2d94420b Merge branch 'develop' into feature/Templates 2021-05-11 13:37:13 +02:00
J-Jamet
e344cafd9b Fix expiration 2021-05-09 20:16:13 +02:00
J-Jamet
4b0d16cad1 Move expires view 2021-05-09 15:34:12 +02:00
J-Jamet
da4d8629bd Remove unused credit card fragment 2021-05-09 15:27:05 +02:00
J-Jamet
68ae3b79ab Change template view implementation 2021-05-09 15:17:49 +02:00
J-Jamet
78e0336c1b Small change to fix card view margin 2021-05-08 13:20:02 +02:00
J-Jamet
e439f4d643 Merge branch 'develop' into feature/Templates 2021-05-07 17:46:11 +02:00
J-Jamet
39d95105e1 Merge branch 'develop' into feature/Templates 2021-05-07 10:44:40 +02:00
J-Jamet
4c99923467 Select template from entry edition screen 2021-05-06 14:16:42 +02:00
J-Jamet
38dac3803b Fix menu 2021-05-03 17:55:19 +02:00
J-Jamet
c0d4ad2042 First pass to prepare templates 2021-05-03 17:34:43 +02:00
J-Jamet
c669d5657a Fix compilation after merge 2021-05-03 15:59:54 +02:00
J-Jamet
98c44fa578 Merge branch 'master' of git://github.com/uduerholz/KeePassDX into uduerholz-master 2021-05-03 15:48:04 +02:00
J-Jamet
839375fcf1 Merge branch 'develop' into feature/Database_Call 2021-04-29 22:41:32 +02:00
J-Jamet
ed82c36628 Encapsulate database in fragments 2021-04-29 21:28:01 +02:00
J-Jamet
3536629dd9 Encapsulate database in sort enum 2021-04-29 18:27:28 +02:00
J-Jamet
c87696696e Merge branch 'develop' into feature/Database_Call 2021-04-29 17:54:23 +02:00
J-Jamet
b5ba03df4d Pass to encapsulate database instance calling 2021-04-29 12:31:52 +02:00
J-Jamet
0d37a59a5c Encapsulate database call as inheritance 2021-04-29 11:24:27 +02:00
Uli
2d528db054 Merge branch 'master' into master 2021-04-18 14:37:43 +02:00
Ulrich Dürholz
80c4ba6723 Let user save credit card details after filling out new form 2021-04-18 14:32:29 +02:00
Ulrich Dürholz
2ba8702787 Manage and autofill credit card details 2021-03-31 22:11:05 +02:00
274 changed files with 14233 additions and 6266 deletions

View File

@@ -1,3 +1,36 @@
KeePassDX(3.0.2)
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
KeePassDX(3.0.1)
* Fix text size and smallest margin #1085
* Fix number of lines during an edition #1073
* Fix Magikeyboard URL auto action #1100
* Fix exception after group name change and save #1112
* Fix timeout reset #1107
* Fix search actions #1091 #1092
* Small changes #1106 #1085
KeePassDX(3.0.0)
* Add / Manage dynamic templates #191
* Manually select RecycleBin group and Templates group #191
* Setting to display OTP Token in list #655
* Fix timeout in dialogs #716
* Check URI permissions #626
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
* Improvements #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong)
KeePassDX(2.10.5)
* Increase the saving speed of database #1028
* Fix advanced unlocking by device credential #1029
KeePassDX(2.10.4)
* Hot fix to increase the opening speed of database #1028
KeePassDX(2.10.3)
* Improve Magikeyboard options description #1022 #1023 (Thx @djibux)
* Fix database opened without notification (database is now closed when screen is killed in background #1025)
* Fix biometric prompt #1018
KeePassDX(2.10.2) KeePassDX(2.10.2)
* Fix search fields references #987 * Fix search fields references #987
* Fix Auto-Types with same key #997 * Fix Auto-Types with same key #997

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 30 targetSdkVersion 30
versionCode = 80 versionCode = 89
versionName = "2.10.2" versionName = "3.0.2"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"

View File

@@ -0,0 +1,24 @@
package com.kunzisoft.keepass.tests.template
import com.kunzisoft.keepass.database.element.template.TemplateAttributeOption
import junit.framework.TestCase
import org.junit.Assert
class TemplateAttributeOptionTest: TestCase() {
fun testSerializeOptions() {
val options = TemplateAttributeOption().apply {
put("TestA", "TestB")
put("{D", "}C")
put("E,gyu", "15,jk")
put("ù*:**", "78:96?545")
}
val strings = TemplateAttributeOption.getStringFromOptions(options)
val optionsAfterSerialization = TemplateAttributeOption.getOptionsFromString(strings)
val otherString = TemplateAttributeOption.getStringFromOptions(optionsAfterSerialization)
Assert.assertEquals("Output not equal to input", strings, otherString)
}
}

View File

@@ -45,7 +45,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTop" android:launchMode="singleTop"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="stateHidden" > android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@@ -112,8 +112,7 @@
<activity <activity
android:name="com.kunzisoft.keepass.activities.GroupActivity" android:name="com.kunzisoft.keepass.activities.GroupActivity"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustPan" android:windowSoftInputMode="adjustPan">
android:launchMode="singleTask">
<meta-data <meta-data
android:name="android.app.default_searchable" android:name="android.app.default_searchable"
android:value="com.kunzisoft.keepass.search.SearchResults" android:value="com.kunzisoft.keepass.search.SearchResults"
@@ -209,7 +208,7 @@
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name="com.kunzisoft.keepass.magikeyboard.MagikIME" android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
android:label="@string/keyboard_label" android:label="@string/keyboard_label"
android:permission="android.permission.BIND_INPUT_METHOD" > android:permission="android.permission.BIND_INPUT_METHOD" >
<meta-data android:name="android.view.im" <meta-data android:name="android.view.im"
@@ -222,6 +221,14 @@
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService" android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<receiver
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.app.action.ENTER_KNOX_DESKTOP_MODE" />
<action android:name="android.app.action.EXIT_KNOX_DESKTOP_MODE" />
</intent-filter>
</receiver>
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" /> <meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
</application> </application>

View File

@@ -30,6 +30,7 @@ package com.igreenwood.loupe
import android.animation.Animator import android.animation.Animator
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.graphics.Matrix import android.graphics.Matrix
import android.graphics.PointF import android.graphics.PointF
import android.graphics.Rect import android.graphics.Rect
@@ -108,6 +109,8 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION
// drag distance threshold in dp for swipe to dismiss // drag distance threshold in dp for swipe to dismiss
var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP
// on view touched
var onViewTouchedListener: View.OnTouchListener? = null
// on view translate listener // on view translate listener
var onViewTranslateListener: OnViewTranslateListener? = null var onViewTranslateListener: OnViewTranslateListener? = null
// on scale changed // on scale changed
@@ -272,7 +275,10 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
private var imageViewRef: WeakReference<ImageView> = WeakReference(imageView) private var imageViewRef: WeakReference<ImageView> = WeakReference(imageView)
private var containerRef: WeakReference<ViewGroup> = WeakReference(container) private var containerRef: WeakReference<ViewGroup> = WeakReference(container)
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(view: View?, event: MotionEvent?): Boolean { override fun onTouch(view: View?, event: MotionEvent?): Boolean {
onViewTouchedListener?.onTouch(view, event)
event ?: return false event ?: return false
val imageView = imageViewRef.get() ?: return false val imageView = imageViewRef.get() ?: return false
val container = containerRef.get() ?: return false val container = containerRef.get() ?: return false

View File

@@ -25,14 +25,13 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentSender import android.content.IntentSender
import android.os.Build import android.os.Build
import android.os.Bundle
import android.view.inputmethod.InlineSuggestionsRequest import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
import com.kunzisoft.keepass.autofill.KeeAutofillService import com.kunzisoft.keepass.autofill.KeeAutofillService
@@ -44,9 +43,18 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : AppCompatActivity() { class AutofillLauncherActivity : DatabaseModeActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun applyCustomStyle(): Boolean {
return false
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
// Retrieve selection mode // Retrieve selection mode
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode -> EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
@@ -57,10 +65,11 @@ class AutofillLauncherActivity : AppCompatActivity() {
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID) applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN) webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME) webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false)
} }
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain searchInfo.webDomain = concreteWebDomain
launchSelection(searchInfo) launchSelection(database, searchInfo)
} }
} }
SpecialMode.REGISTRATION -> { SpecialMode.REGISTRATION -> {
@@ -69,7 +78,7 @@ class AutofillLauncherActivity : AppCompatActivity() {
val searchInfo = SearchInfo(registerInfo?.searchInfo) val searchInfo = SearchInfo(registerInfo?.searchInfo)
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain searchInfo.webDomain = concreteWebDomain
launchRegistration(searchInfo, registerInfo) launchRegistration(database, searchInfo, registerInfo)
} }
} }
else -> { else -> {
@@ -79,11 +88,10 @@ class AutofillLauncherActivity : AppCompatActivity() {
} }
} }
} }
super.onCreate(savedInstanceState)
} }
private fun launchSelection(searchInfo: SearchInfo) { private fun launchSelection(database: Database?,
searchInfo: SearchInfo) {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE) // Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent) val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
@@ -98,24 +106,22 @@ class AutofillLauncherActivity : AppCompatActivity() {
setResult(Activity.RESULT_CANCELED) setResult(Activity.RESULT_CANCELED)
finish() finish()
} else { } else {
val database = Database.getInstance()
val readOnly = database.isReadOnly
// If database is open // If database is open
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(), database,
searchInfo, searchInfo,
{ items -> { openedDatabase, items ->
// Items found // Items found
AutofillHelper.buildResponseAndSetResult(this, items) AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
finish() finish()
}, },
{ { openedDatabase ->
// Show the database UI to select the entry // Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this, GroupActivity.launchForAutofillResult(this,
readOnly, openedDatabase,
autofillComponent, autofillComponent,
searchInfo, searchInfo,
false) false)
}, },
{ {
// If database not open // If database not open
@@ -127,7 +133,9 @@ class AutofillLauncherActivity : AppCompatActivity() {
} }
} }
private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) { private fun launchRegistration(database: Database?,
searchInfo: SearchInfo,
registerInfo: RegisterInfo?) {
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId, if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
PreferencesUtil.applicationIdBlocklist(this)) PreferencesUtil.applicationIdBlocklist(this))
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain, || !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
@@ -135,25 +143,26 @@ class AutofillLauncherActivity : AppCompatActivity() {
showBlockRestartMessage() showBlockRestartMessage()
setResult(Activity.RESULT_CANCELED) setResult(Activity.RESULT_CANCELED)
} else { } else {
val database = Database.getInstance() val readOnly = database?.isReadOnly != false
val readOnly = database.isReadOnly
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
database, database,
searchInfo, searchInfo,
{ _ -> { openedDatabase, _ ->
if (!readOnly) { if (!readOnly) {
// Show the database UI to select the entry // Show the database UI to select the entry
GroupActivity.launchForRegistration(this, GroupActivity.launchForRegistration(this,
registerInfo) openedDatabase,
registerInfo)
} else { } else {
showReadOnlySaveMessage() showReadOnlySaveMessage()
} }
}, },
{ { openedDatabase ->
if (!readOnly) { if (!readOnly) {
// Show the database UI to select the entry // Show the database UI to select the entry
GroupActivity.launchForRegistration(this, GroupActivity.launchForRegistration(this,
registerInfo) openedDatabase,
registerInfo)
} else { } else {
showReadOnlySaveMessage() showReadOnlySaveMessage()
} }
@@ -190,15 +199,16 @@ class AutofillLauncherActivity : AppCompatActivity() {
companion object { companion object {
private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION"
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID" private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN" private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME" private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO" private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
fun getAuthIntentSenderForSelection(context: Context, fun getPendingIntentForSelection(context: Context,
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender { inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent {
return PendingIntent.getActivity(context, 0, return PendingIntent.getActivity(context, 0,
// Doesn't work with Parcelable (don't know why?) // Doesn't work with Parcelable (don't know why?)
Intent(context, AutofillLauncherActivity::class.java).apply { Intent(context, AutofillLauncherActivity::class.java).apply {
@@ -206,6 +216,7 @@ class AutofillLauncherActivity : AppCompatActivity() {
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId) putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
putExtra(KEY_SEARCH_DOMAIN, it.webDomain) putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
putExtra(KEY_SEARCH_SCHEME, it.webScheme) putExtra(KEY_SEARCH_SCHEME, it.webScheme)
putExtra(KEY_MANUAL_SELECTION, it.manualSelection)
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let { inlineSuggestionsRequest?.let {
@@ -213,17 +224,17 @@ class AutofillLauncherActivity : AppCompatActivity() {
} }
} }
}, },
PendingIntent.FLAG_CANCEL_CURRENT).intentSender PendingIntent.FLAG_CANCEL_CURRENT)
} }
fun getAuthIntentSenderForRegistration(context: Context, fun getPendingIntentForRegistration(context: Context,
registerInfo: RegisterInfo): IntentSender { registerInfo: RegisterInfo): PendingIntent {
return PendingIntent.getActivity(context, 0, return PendingIntent.getActivity(context, 0,
Intent(context, AutofillLauncherActivity::class.java).apply { Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION) EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo) putExtra(KEY_REGISTER_INFO, registerInfo)
}, },
PendingIntent.FLAG_CANCEL_CURRENT).intentSender PendingIntent.FLAG_CANCEL_CURRENT)
} }
fun launchForRegistration(context: Context, fun launchForRegistration(context: Context,

View File

@@ -32,71 +32,63 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.Toast import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.appbar.CollapsingToolbarLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.fragments.EntryFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.services.AttachmentFileNotificationService import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.EntryContentsView import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.* import java.util.*
import kotlin.collections.HashMap import kotlin.collections.HashMap
class EntryActivity : LockingActivity() { class EntryActivity : DatabaseLockActivity() {
private var coordinatorLayout: CoordinatorLayout? = null private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var titleIconView: ImageView? = null private var titleIconView: ImageView? = null
private var historyView: View? = null private var historyView: View? = null
private var entryContentsView: EntryContentsView? = null
private var entryProgress: ProgressBar? = null private var entryProgress: ProgressBar? = null
private var lockView: View? = null private var lockView: View? = null
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
private var loadingView: ProgressBar? = null
private var mDatabase: Database? = null private val mEntryViewModel: EntryViewModel by viewModels()
private var mEntry: Entry? = null private var mMainEntryId: NodeId<UUID>? = null
private var mHistoryPosition: Int = -1
private var mIsHistory: Boolean = false private var mEntryIsHistory: Boolean = false
private var mEntryLastVersion: Entry? = null private var mUrl: String? = null
private var mEntryHistoryPosition: Int = -1 private var mEntryLoaded = false
private var mShowPassword: Boolean = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap() private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
private var clipboardHelper: ClipboardHelper? = null
private var mFirstLaunchOfActivity: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var iconColor: Int = 0 private var mIcon: IconImage? = null
private var mIconColor: Int = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -108,63 +100,168 @@ class EntryActivity : LockingActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
mDatabase = Database.getInstance()
mReadOnly = mDatabase!!.isReadOnly || mReadOnly
mShowPassword = !PreferencesUtil.isPasswordMask(this)
// Retrieve the textColor to tint the icon
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
iconColor = taIconColor.getColor(0, Color.BLACK)
taIconColor.recycle()
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
invalidateOptionsMenu()
// Get views // Get views
coordinatorLayout = findViewById(R.id.toolbar_coordinator) coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout) collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
titleIconView = findViewById(R.id.entry_icon) titleIconView = findViewById(R.id.entry_icon)
historyView = findViewById(R.id.history_container) historyView = findViewById(R.id.history_container)
entryContentsView = findViewById(R.id.entry_contents)
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
entryContentsView?.setAttachmentCipherKey(mDatabase)
entryProgress = findViewById(R.id.entry_progress) entryProgress = findViewById(R.id.entry_progress)
lockView = findViewById(R.id.lock_button) lockView = findViewById(R.id.lock_button)
loadingView = findViewById(R.id.loading)
// Empty title
collapsingToolbarLayout?.title = " "
toolbar?.title = " "
// Retrieve the textColor to tint the icon
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
mIconColor = taIconColor.getColor(0, Color.BLACK)
taIconColor.recycle()
// Get Entry from UUID
try {
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
intent.removeExtra(KEY_ENTRY)
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
}
} catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
// Init SAF manager
mExternalFileHelper = ExternalFileHelper(this)
// Init attachment service binder manager
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
lockView?.setOnClickListener { lockView?.setOnClickListener {
lockAndExit() lockAndExit()
} }
// Focus view to reinitialize timeout mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this) if (entryInfoHistory != null) {
this.mMainEntryId = entryInfoHistory.mainEntryId
// Init the clipboard helper // Manage history position
clipboardHelper = ClipboardHelper(this) val historyPosition = entryInfoHistory.historyPosition
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true this.mHistoryPosition = historyPosition
val entryIsHistory = historyPosition > -1
// Init SAF manager this.mEntryIsHistory = entryIsHistory
mExternalFileHelper = ExternalFileHelper(this) // Assign history dedicated view
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
// Init attachment service binder manager if (entryIsHistory) {
mAttachmentFileBinderManager = AttachmentFileBinderManager(this) val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
collapsingToolbarLayout?.contentScrim =
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result -> ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
when (actionTask) { taColorAccent.recycle()
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
// Close the current activity after an history action
if (result.isSuccess)
finish()
} }
ACTION_DATABASE_RELOAD_TASK -> {
// Close the current activity val entryInfo = entryInfoHistory.entryInfo
this.showActionErrorIfNeeded(result) // Manage entry copy to start notification if allowed (at the first start)
finish() if (savedInstanceState == null) {
// Manage entry to launch copying notification if allowed
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
}
}
// Assign title icon
mIcon = entryInfo.icon
titleIconView?.let { iconView ->
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
}
// Assign title text
val entryTitle =
if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString()
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
mUrl = entryInfo.url
loadingView?.hideByFading()
mEntryLoaded = true
} else {
finish()
}
// Refresh Menu
invalidateOptionsMenu()
}
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
if (otpElement == null)
entryProgress?.visibility = View.GONE
when (otpElement?.type) {
// Only add token if HOTP
OtpType.HOTP -> {
entryProgress?.visibility = View.GONE
}
// Refresh view if TOTP
OtpType.TOTP -> {
entryProgress?.apply {
max = otpElement.period
progress = otpElement.secondsRemaining
visibility = View.VISIBLE
}
} }
} }
coordinatorLayout?.showActionErrorIfNeeded(result)
} }
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentSelected
}
}
mEntryViewModel.historySelected.observe(this) { historySelected ->
mDatabase?.let { database ->
launch(
this,
database,
historySelected.nodeId,
historySelected.historyPosition
)
}
}
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun viewToInvalidateTimeout(): View? {
return coordinatorLayout
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
mEntryViewModel.loadDatabase(database)
// Assign title icon
mIcon?.let { icon ->
titleIconView?.let { iconView ->
mIconDrawableFactory?.assignDatabaseIcon(iconView, icon, mIconColor)
}
}
}
override fun onDatabaseActionFinished(
database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
// Close the current activity after an history action
if (result.isSuccess)
finish()
}
}
coordinatorLayout?.showActionErrorIfNeeded(result)
} }
override fun onResume() { override fun onResume() {
@@ -177,63 +274,14 @@ class EntryActivity : LockingActivity() {
View.GONE View.GONE
} }
// Get Entry from UUID
try {
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")
}
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
mEntryHistoryPosition = historyPosition
if (historyPosition >= 0) {
mIsHistory = true
mEntry = mEntry?.getHistory()?.get(historyPosition)
}
if (mEntry == null) {
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
finish()
return
}
// Update last access time.
mEntry?.touch(modified = false, touchParents = false)
mEntry?.let { entry ->
// Fill data in resume to update from EntryEditActivity
fillEntryDataInContentsView(entry)
// Refresh Menu
invalidateOptionsMenu()
val entryInfo = entry.getEntryInfo(mDatabase)
// Manage entry copy to start notification if allowed
if (mFirstLaunchOfActivity) {
// Manage entry to launch copying notification if allowed
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
}
}
}
mAttachmentFileBinderManager?.apply { mAttachmentFileBinderManager?.apply {
registerProgressTask() registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener { onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) { override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) { mEntryViewModel.onAttachmentAction(entryAttachmentState)
entryContentsView?.putAttachment(entryAttachmentState)
}
} }
} }
} }
mFirstLaunchOfActivity = false
} }
override fun onPause() { override fun onPause() {
@@ -242,148 +290,14 @@ class EntryActivity : LockingActivity() {
super.onPause() super.onPause()
} }
private fun fillEntryDataInContentsView(entry: Entry) {
val entryInfo = entry.getEntryInfo(mDatabase)
// Assign title icon
titleIconView?.let { iconView ->
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor)
}
// Assign title text
val entryTitle = entryInfo.title
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
// Assign basic fields
entryContentsView?.assignUserName(entryInfo.username) {
clipboardHelper?.timeoutCopyToClipboard(entryInfo.username,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
}
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
val allowCopyPasswordAndProtectedFields =
PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
AlertDialog.Builder(this@EntryActivity)
.setMessage(getString(R.string.allow_copy_password_warning) +
"\n\n" +
getString(R.string.clipboard_warning))
.create().apply {
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
show()
}
}
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
View.OnClickListener {
clipboardHelper?.timeoutCopyToClipboard(entryInfo.password,
getString(R.string.copy_field,
getString(R.string.entry_password)))
}
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
showWarningClipboardDialogOnClickListener
} else {
null
}
}
entryContentsView?.assignPassword(entryInfo.password,
allowCopyPasswordAndProtectedFields,
onPasswordCopyClickListener)
//Assign OTP field
entry.getOtpElement()?.let { otpElement ->
entryContentsView?.assignOtp(otpElement, entryProgress) {
clipboardHelper?.timeoutCopyToClipboard(
otpElement.token,
getString(R.string.copy_field, getString(R.string.entry_otp))
)
}
}
entryContentsView?.assignURL(entryInfo.url)
entryContentsView?.assignNotes(entryInfo.notes)
// Assign custom fields
if (mDatabase?.allowEntryCustomFields() == true) {
entryContentsView?.clearExtraFields()
entryInfo.customFields.forEach { field ->
val label = field.name
// OTP field is already managed in dedicated view
if (label != OtpEntryFields.OTP_TOKEN_FIELD) {
val value = field.protectedValue
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
if (allowCopyProtectedField) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
clipboardHelper?.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
)
}
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
} else {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
}
}
}
}
}
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
// Manage attachments
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
mExternalFileHelper?.createDocument(attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem
}
}
// Assign dates
entryContentsView?.assignCreationDate(entryInfo.creationTime)
entryContentsView?.assignModificationDate(entryInfo.lastModificationTime)
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
// Manage history
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
if (mIsHistory) {
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
taColorAccent.recycle()
}
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
launch(this, historyItem, mReadOnly, position)
}
// Assign special data
entryContentsView?.assignUUID(entry.nodeId.id)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
when (requestCode) { when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
// Not directly get the entry from intent data but from database // Reload the current id from database
mEntry?.let { mEntryViewModel.loadDatabase(mDatabase)
fillEntryDataInContentsView(it) }
}
} }
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri -> mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
@@ -398,56 +312,57 @@ class EntryActivity : LockingActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
if (mEntryLoaded) {
val inflater = menuInflater
MenuUtil.contributionMenuInflater(inflater, menu)
val inflater = menuInflater inflater.inflate(R.menu.entry, menu)
MenuUtil.contributionMenuInflater(inflater, menu) inflater.inflate(R.menu.database, menu)
inflater.inflate(R.menu.entry, menu)
inflater.inflate(R.menu.database, menu)
if (mIsHistory && !mReadOnly) {
inflater.inflate(R.menu.entry_history, menu)
}
if (mIsHistory || mReadOnly) {
menu.findItem(R.id.menu_save_database)?.isVisible = false
menu.findItem(R.id.menu_edit)?.isVisible = false
}
if (mSpecialMode != SpecialMode.DEFAULT) {
menu.findItem(R.id.menu_reload_database)?.isVisible = false
}
val gotoUrl = menu.findItem(R.id.menu_goto_url) if (mEntryIsHistory && !mDatabaseReadOnly) {
gotoUrl?.apply { inflater.inflate(R.menu.entry_history, menu)
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes }
// so mEntry may not be set
if (mEntry == null) { // Show education views
isVisible = false Handler(Looper.getMainLooper()).post {
} else { performedNextEducation(
if (mEntry?.url?.isEmpty() != false) { EntryActivityEducation(
// disable button if url is not available this
isVisible = false ), menu
} )
} }
} }
// Show education views
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
return true return true
} }
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
if (mUrl?.isEmpty() != false) {
menu?.findItem(R.id.menu_goto_url)?.isVisible = false
}
if (mEntryIsHistory || mDatabaseReadOnly) {
menu?.findItem(R.id.menu_save_database)?.isVisible = false
menu?.findItem(R.id.menu_edit)?.isVisible = false
}
if (mSpecialMode != SpecialMode.DEFAULT) {
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
}
return super.onPrepareOptionsMenu(menu)
}
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation, private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
menu: Menu) { menu: Menu) {
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView() val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
as? EntryFragment?
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
val entryCopyEducationPerformed = entryFieldCopyView != null val entryCopyEducationPerformed = entryFieldCopyView != null
&& entryActivityEducation.checkAndPerformedEntryCopyEducation( && entryActivityEducation.checkAndPerformedEntryCopyEducation(
entryFieldCopyView, entryFieldCopyView,
{ {
val appNameString = getString(R.string.app_name) entryFragment.launchEntryCopyEducationAction()
clipboardHelper?.timeoutCopyToClipboard(appNameString, },
getString(R.string.copy_field, appNameString)) {
}, performedNextEducation(entryActivityEducation, menu)
{ })
performedNextEducation(entryActivityEducation, menu)
})
if (!entryCopyEducationPerformed) { if (!entryCopyEducationPerformed) {
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit) val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
@@ -471,60 +386,53 @@ class EntryActivity : LockingActivity() {
return true return true
} }
R.id.menu_edit -> { R.id.menu_edit -> {
mEntry?.let { mDatabase?.let { database ->
EntryEditActivity.launch(this@EntryActivity, it) mMainEntryId?.let { entryId ->
EntryEditActivity.launchToUpdate(
this,
database,
entryId
)
}
} }
return true return true
} }
R.id.menu_goto_url -> { R.id.menu_goto_url -> {
var url: String = mEntry?.url ?: "" mUrl?.let { url ->
UriUtil.gotoUrl(this, url)
// Default http:// if no protocol specified
if (!url.contains("://")) {
url = "http://$url"
} }
UriUtil.gotoUrl(this, url)
return true return true
} }
R.id.menu_restore_entry_history -> { R.id.menu_restore_entry_history -> {
mEntryLastVersion?.let { mainEntry -> mMainEntryId?.let { mainEntryId ->
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory( restoreEntryHistory(
mainEntry, mainEntryId,
mEntryHistoryPosition, mHistoryPosition)
!mReadOnly && mAutoSaveEnable)
} }
} }
R.id.menu_delete_entry_history -> { R.id.menu_delete_entry_history -> {
mEntryLastVersion?.let { mainEntry -> mMainEntryId?.let { mainEntryId ->
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory( deleteEntryHistory(
mainEntry, mainEntryId,
mEntryHistoryPosition, mHistoryPosition)
!mReadOnly && mAutoSaveEnable)
} }
} }
R.id.menu_save_database -> { R.id.menu_save_database -> {
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly) saveDatabase()
} }
R.id.menu_reload_database -> { R.id.menu_reload_database -> {
mProgressDatabaseTaskProvider?.startDatabaseReload(false) reloadDatabase()
} }
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any) android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(KEY_FIRST_LAUNCH_ACTIVITY, mFirstLaunchOfActivity)
}
override fun finish() { override fun finish() {
// Transit data in previous Activity after an update // Transit data in previous Activity after an update
Intent().apply { Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry) putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this) setResult(EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE, this)
} }
super.finish() super.finish()
} }
@@ -532,19 +440,46 @@ class EntryActivity : LockingActivity() {
companion object { companion object {
private val TAG = EntryActivity::class.java.name private val TAG = EntryActivity::class.java.name
private const val KEY_FIRST_LAUNCH_ACTIVITY = "KEY_FIRST_LAUNCH_ACTIVITY"
const val KEY_ENTRY = "KEY_ENTRY" const val KEY_ENTRY = "KEY_ENTRY"
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION" const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
fun launch(activity: Activity, entry: Entry, readOnly: Boolean, historyPosition: Int? = null) { const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java) /**
intent.putExtra(KEY_ENTRY, entry.nodeId) * Open standard Entry activity
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly) */
if (historyPosition != null) fun launch(activity: Activity,
database: Database,
entryId: NodeId<UUID>) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
activity.startActivityForResult(
intent,
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
)
}
}
}
/**
* Open history Entry activity
*/
fun launch(activity: Activity,
database: Database,
entryId: NodeId<UUID>,
historyPosition: Int) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition) intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE) activity.startActivityForResult(
intent,
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
)
}
} }
} }
} }

View File

@@ -22,14 +22,13 @@ package com.kunzisoft.keepass.activities
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
@@ -39,10 +38,18 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
* Activity to search or select entry in database, * Activity to search or select entry in database,
* Commonly used with Magikeyboard * Commonly used with Magikeyboard
*/ */
class EntrySelectionLauncherActivity : AppCompatActivity() { class EntrySelectionLauncherActivity : DatabaseModeActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun applyCustomStyle(): Boolean {
return false
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
var sharedWebDomain: String? = null var sharedWebDomain: String? = null
var otpString: String? = null var otpString: String? = null
@@ -68,39 +75,39 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
else -> {} else -> {}
} }
// Build domain search param // Build domain search param
val searchInfo = SearchInfo().apply { val searchInfo = SearchInfo().apply {
this.webDomain = sharedWebDomain this.webDomain = sharedWebDomain
this.otpString = otpString this.otpString = otpString
} }
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain searchInfo.webDomain = concreteWebDomain
launch(searchInfo) launch(database, searchInfo)
} }
super.onCreate(savedInstanceState)
} }
private fun launch(searchInfo: SearchInfo) { private fun launch(database: Database?,
searchInfo: SearchInfo) {
if (!searchInfo.containsOnlyNullValues()) { if (!searchInfo.containsOnlyNullValues()) {
// Setting to integrate Magikeyboard // Setting to integrate Magikeyboard
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this) val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
// If database is open // If database is open
val database = Database.getInstance() val readOnly = database?.isReadOnly != false
val readOnly = database.isReadOnly
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
database, database,
searchInfo, searchInfo,
{ items -> { openedDatabase, items ->
// Items found // Items found
if (searchInfo.otpString != null) { if (searchInfo.otpString != null) {
if (!readOnly) { if (!readOnly) {
GroupActivity.launchForSaveResult(this, GroupActivity.launchForSaveResult(
searchInfo, this,
false) openedDatabase,
searchInfo,
false)
} else { } else {
Toast.makeText(applicationContext, Toast.makeText(applicationContext,
R.string.autofill_read_only_save, R.string.autofill_read_only_save,
@@ -111,30 +118,32 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
if (items.size == 1) { if (items.size == 1) {
// Automatically populate keyboard // Automatically populate keyboard
val entryPopulate = items[0] val entryPopulate = items[0]
populateKeyboardAndMoveAppToBackground(this, populateKeyboardAndMoveAppToBackground(
this,
entryPopulate, entryPopulate,
intent) intent)
} else { } else {
// Select the one we want // Select the one we want
GroupActivity.launchForKeyboardSelectionResult(this, GroupActivity.launchForKeyboardSelectionResult(this,
readOnly, openedDatabase,
searchInfo, searchInfo,
true) true)
} }
} else { } else {
GroupActivity.launchForSearchResult(this, GroupActivity.launchForSearchResult(this,
readOnly, openedDatabase,
searchInfo, searchInfo,
true) true)
} }
}, },
{ { openedDatabase ->
// Show the database UI to select the entry // Show the database UI to select the entry
if (searchInfo.otpString != null) { if (searchInfo.otpString != null) {
if (!readOnly) { if (!readOnly) {
GroupActivity.launchForSaveResult(this, GroupActivity.launchForSaveResult(this,
searchInfo, openedDatabase,
false) searchInfo,
false)
} else { } else {
Toast.makeText(applicationContext, Toast.makeText(applicationContext,
R.string.autofill_read_only_save, R.string.autofill_read_only_save,
@@ -143,13 +152,14 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
} }
} else if (readOnly || searchShareForMagikeyboard) { } else if (readOnly || searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult(this, GroupActivity.launchForKeyboardSelectionResult(this,
readOnly, openedDatabase,
searchInfo, searchInfo,
false) false)
} else { } else {
GroupActivity.launchForSaveResult(this, GroupActivity.launchForSaveResult(this,
searchInfo, openedDatabase,
false) searchInfo,
false)
} }
}, },
{ {
@@ -183,7 +193,7 @@ fun populateKeyboardAndMoveAppToBackground(activity: Activity,
intent: Intent, intent: Intent,
toast: Boolean = true) { toast: Boolean = true) {
// Populate Magikeyboard with entry // Populate Magikeyboard with entry
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast) MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
// Consume the selection mode // Consume the selection mode
EntrySelectionHelper.removeModesFromIntent(intent) EntrySelectionHelper.removeModesFromIntent(intent)
activity.moveTaskToBack(true) activity.moveTaskToBack(true)

View File

@@ -45,12 +45,11 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
@@ -61,12 +60,13 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
import java.io.FileNotFoundException import java.io.FileNotFoundException
class FileDatabaseSelectActivity : SpecialModeActivity(), class FileDatabaseSelectActivity : DatabaseModeActivity(),
AssignMasterKeyDialogFragment.AssignPasswordDialogListener { AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
// Views // Views
@@ -85,11 +85,15 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Enabling/disabling MagikeyboardService is normally done by DexModeReceiver, but this
// additional check will allow the keyboard to be reenabled more easily if the app crashes
// or is force quit within DeX mode and then the user leaves DeX mode. Without this, the
// user would need to enter and exit DeX mode once to reenable the service.
MagikeyboardUtil.setEnabled(this, !DexUtil.isDexMode(resources.configuration))
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext) mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
setContentView(R.layout.activity_file_selection) setContentView(R.layout.activity_file_selection)
@@ -127,7 +131,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
} }
} }
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete -> mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
// Remove from app database
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete) databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
true true
} }
@@ -190,39 +193,62 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
// Retrieve settings for default database // Retrieve settings for default database
mAdapterDatabaseHistory?.setDefaultDatabase(it) mAdapterDatabaseHistory?.setDefaultDatabase(it)
} }
}
// Attach the dialog thread to this activity override fun onDatabaseRetrieved(database: Database?) {
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply { super.onDatabaseRetrieved(database)
onActionFinish = { actionTask, result -> if (database != null) {
when (actionTask) { launchGroupActivityIfLoaded(database)
ACTION_DATABASE_CREATE_TASK -> { }
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri -> }
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri) override fun onDatabaseActionFinished(
} database: Database,
GroupActivity.launch(this@FileDatabaseSelectActivity, actionTask: String,
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)) result: ActionRunnable.Result
} ) {
ACTION_DATABASE_LOAD_TASK -> { super.onDatabaseActionFinished(database, actionTask, result)
val database = Database.getInstance()
if (result.isSuccess if (result.isSuccess) {
&& database.loaded) { // Update list
launchGroupActivity(database) when (actionTask) {
} else { ACTION_DATABASE_CREATE_TASK,
var resultError = "" ACTION_DATABASE_LOAD_TASK -> {
val resultMessage = result.message result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
// Show error message val mainCredential =
if (resultMessage != null && resultMessage.isNotEmpty()) { result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
resultError = "$resultError $resultMessage" ?: MainCredential()
} databaseFilesViewModel.addDatabaseFile(
Log.e(TAG, resultError) databaseUri,
Snackbar.make(coordinatorLayout, mainCredential.keyFileUri
resultError, )
Snackbar.LENGTH_LONG).asError().show()
}
} }
} }
} }
// Launch activity
when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> {
GroupActivity.launch(
this@FileDatabaseSelectActivity,
database,
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)
)
}
ACTION_DATABASE_LOAD_TASK -> {
launchGroupActivityIfLoaded(database)
}
}
} else {
var resultError = ""
val resultMessage = result.message
// Show error message
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError)
Snackbar.make(coordinatorLayout,
resultError,
Snackbar.LENGTH_LONG).asError().show()
} }
} }
@@ -251,12 +277,14 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
{ onLaunchActivitySpecialMode() }) { onLaunchActivitySpecialMode() })
} }
private fun launchGroupActivity(database: Database) { private fun launchGroupActivityIfLoaded(database: Database) {
GroupActivity.launch(this, if (database.loaded) {
database.isReadOnly, GroupActivity.launch(this,
database,
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }) { onLaunchActivitySpecialMode() })
}
} }
override fun onValidateSpecialMode() { override fun onValidateSpecialMode() {
@@ -296,28 +324,16 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
} }
} }
val database = Database.getInstance() mDatabase?.let { database ->
if (database.loaded) { launchGroupActivityIfLoaded(database)
launchGroupActivity(database)
} else {
// Construct adapter with listeners
if (PreferencesUtil.showRecentFiles(this)) {
databaseFilesViewModel.loadListOfDatabases()
} else {
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
mAdapterDatabaseHistory?.notifyDataSetChanged()
}
// Register progress task
mProgressDatabaseTaskProvider?.registerProgressTask()
} }
}
override fun onPause() { // Show recent files if allowed
// Unregister progress task if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
mProgressDatabaseTaskProvider?.unregisterProgressTask() databaseFilesViewModel.loadListOfDatabases()
} else {
super.onPause() mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
}
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@@ -329,15 +345,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
} }
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) { override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
try { try {
mDatabaseFileUri?.let { databaseUri -> mDatabaseFileUri?.let { databaseUri ->
// Create the new database // Create the new database
mProgressDatabaseTaskProvider?.startDatabaseCreate( createDatabase(databaseUri, mainCredential)
databaseUri,
mainCredential
)
} }
} catch (e: Exception) { } catch (e: Exception) {
val error = getString(R.string.error_create_database_file) val error = getString(R.string.error_create_database_file)

View File

@@ -36,8 +36,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
@@ -50,7 +49,7 @@ import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
import kotlinx.coroutines.* import kotlinx.coroutines.*
class IconPickerActivity : LockingActivity() { class IconPickerActivity : DatabaseLockActivity() {
private lateinit var toolbar: Toolbar private lateinit var toolbar: Toolbar
private lateinit var coordinatorLayout: CoordinatorLayout private lateinit var coordinatorLayout: CoordinatorLayout
@@ -65,8 +64,6 @@ class IconPickerActivity : LockingActivity() {
private var mCustomIconsSelectionMode = false private var mCustomIconsSelectionMode = false
private var mIconsSelected: List<IconImageCustom> = ArrayList() private var mIconsSelected: List<IconImageCustom> = ArrayList()
private var mDatabase: Database? = null
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -74,8 +71,6 @@ class IconPickerActivity : LockingActivity() {
setContentView(R.layout.activity_icon_picker) setContentView(R.layout.activity_icon_picker)
mDatabase = Database.getInstance()
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
toolbar.title = " " toolbar.title = " "
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
@@ -88,11 +83,6 @@ class IconPickerActivity : LockingActivity() {
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
uploadButton = findViewById(R.id.icon_picker_upload) uploadButton = findViewById(R.id.icon_picker_upload)
if (mDatabase?.allowCustomIcons == true) {
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
} else {
uploadButton.visibility = View.GONE
}
lockView = findViewById(R.id.lock_button) lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener { lockView?.setOnClickListener {
@@ -118,9 +108,6 @@ class IconPickerActivity : LockingActivity() {
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
} }
// Focus view to reinitialize timeout
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard -> iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
mIconImage.standard = iconStandard mIconImage.standard = iconStandard
// Remove the custom icon if a standard one is selected // Remove the custom icon if a standard one is selected
@@ -154,6 +141,24 @@ class IconPickerActivity : LockingActivity() {
} }
} }
override fun viewToInvalidateTimeout(): View? {
return findViewById<ViewGroup>(R.id.icon_picker_container)
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
if (database?.allowCustomIcons == true) {
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
} else {
uploadButton.visibility = View.GONE
}
}
private fun updateIconsSelectedViews() { private fun updateIconsSelectedViews() {
if (mIconsSelected.isEmpty()) { if (mIconsSelected.isEmpty()) {
mCustomIconsSelectionMode = false mCustomIconsSelectionMode = false
@@ -187,13 +192,18 @@ class IconPickerActivity : LockingActivity() {
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.icon, menu)
if (mCustomIconsSelectionMode) {
menuInflater.inflate(R.menu.icon, menu)
}
return true return true
} }
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menu?.findItem(R.id.menu_delete)?.apply {
isEnabled = mCustomIconsSelectionMode
isVisible = isEnabled
}
return super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
@@ -208,6 +218,9 @@ class IconPickerActivity : LockingActivity() {
removeCustomIcon(iconToRemove) removeCustomIcon(iconToRemove)
} }
} }
R.id.menu_external_icon -> {
UriUtil.gotoUrl(this, R.string.external_icon_url)
}
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)

View File

@@ -19,6 +19,7 @@
*/ */
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@@ -31,16 +32,19 @@ import android.widget.ImageView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import com.igreenwood.loupe.Loupe import com.igreenwood.loupe.Loupe
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import kotlin.math.max import kotlin.math.max
class ImageViewerActivity : LockingActivity() { class ImageViewerActivity : DatabaseLockActivity() {
private var mDatabase: Database? = null private var imageContainerView: ViewGroup? = null
private lateinit var imageView: ImageView
private lateinit var progressView: View
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -50,49 +54,21 @@ class ImageViewerActivity : LockingActivity() {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
toolbar.setOnTouchListener { _, _ ->
val imageContainerView: ViewGroup = findViewById(R.id.image_viewer_container) resetAppTimeout()
val imageView: ImageView = findViewById(R.id.image_viewer_image) false
val progressView: View = findViewById(R.id.image_viewer_progress)
// Approximately, to not OOM and allow a zoom
val mImagePreviewMaxWidth = max(
resources.displayMetrics.widthPixels * 2,
resources.displayMetrics.heightPixels * 2
)
mDatabase = Database.getInstance()
try {
progressView.visibility = View.VISIBLE
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
supportActionBar?.title = attachment.name
val size = attachment.binaryData.getSize()
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
mDatabase?.let { database ->
BinaryDatabaseManager.loadBitmap(
database,
attachment.binaryData,
mImagePreviewMaxWidth
) { bitmapLoaded ->
if (bitmapLoaded == null) {
finish()
} else {
progressView.visibility = View.GONE
imageView.setImageBitmap(bitmapLoaded)
}
}
}
} ?: finish()
} catch (e: Exception) {
Log.e(TAG, "Unable to view the binary", e)
finish()
} }
Loupe.create(imageView, imageContainerView) { imageContainerView = findViewById(R.id.image_viewer_container)
imageView = findViewById(R.id.image_viewer_image)
progressView = findViewById(R.id.image_viewer_progress)
Loupe.create(imageView, imageContainerView!!) {
onViewTouchedListener = View.OnTouchListener { _, _ ->
// to reset timeout when Loupe image view touched
resetAppTimeout()
false
}
onViewTranslateListener = object : Loupe.OnViewTranslateListener { onViewTranslateListener = object : Loupe.OnViewTranslateListener {
override fun onStart(view: ImageView) { override fun onStart(view: ImageView) {
@@ -115,6 +91,54 @@ class ImageViewerActivity : LockingActivity() {
} }
} }
override fun viewToInvalidateTimeout(): View? {
// Null to manually manage events
return null
}
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
try {
progressView.visibility = View.VISIBLE
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
supportActionBar?.title = attachment.name
val size = attachment.binaryData.getSize()
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
// Approximately, to not OOM and allow a zoom
val mImagePreviewMaxWidth = max(
resources.displayMetrics.widthPixels * 2,
resources.displayMetrics.heightPixels * 2
)
database?.let { database ->
BinaryDatabaseManager.loadBitmap(
database,
attachment.binaryData,
mImagePreviewMaxWidth
) { bitmapLoaded ->
if (bitmapLoaded == null) {
finish()
} else {
progressView.visibility = View.GONE
imageView.setImageBitmap(bitmapLoaded)
}
}
}
} ?: finish()
} catch (e: Exception) {
Log.e(TAG, "Unable to view the binary", e)
finish()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> finish() android.R.id.home -> finish()

View File

@@ -19,36 +19,41 @@
*/ */
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities
import android.os.Bundle import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
/** /**
* Activity to select entry in database and populate it in Magikeyboard * Activity to select entry in database and populate it in Magikeyboard
*/ */
class MagikeyboardLauncherActivity : AppCompatActivity() { class MagikeyboardLauncherActivity : DatabaseModeActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun applyCustomStyle(): Boolean {
val database = Database.getInstance() return false
val readOnly = database.isReadOnly }
override fun finishActivityIfReloadRequested(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
database, database,
null, null,
{ { _, _ ->
// Not called // Not called
// if items found directly returns before calling this activity // if items found directly returns before calling this activity
}, },
{ { openedDatabase ->
// Select if not found // Select if not found
GroupActivity.launchForKeyboardSelectionResult(this, readOnly) GroupActivity.launchForKeyboardSelectionResult(this, openedDatabase)
}, },
{ {
// Pass extra to get entry // Pass extra to get entry
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this) FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
} }
) )
finish() finish()
super.onCreate(savedInstanceState)
} }
} }

View File

@@ -31,8 +31,11 @@ import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import android.view.KeyEvent.KEYCODE_ENTER
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.view.inputmethod.InputMethodManager
import android.widget.* import android.widget.*
import android.widget.TextView.OnEditorActionListener
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
@@ -43,13 +46,12 @@ import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.* import com.kunzisoft.keepass.activities.helpers.*
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
@@ -63,6 +65,7 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
@@ -71,7 +74,8 @@ import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import java.io.FileNotFoundException import java.io.FileNotFoundException
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
// Views // Views
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
@@ -95,11 +99,11 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mPermissionAsked = false private var mPermissionAsked = false
private var readOnly: Boolean = false private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false private var mForceReadOnly: Boolean = false
set(value) { set(value) {
infoContainerView?.visibility = if (value) { infoContainerView?.visibility = if (value) {
readOnly = true mReadOnly = true
View.VISIBLE View.VISIBLE
} else { } else {
View.GONE View.GONE
@@ -107,8 +111,6 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
field = value field = value
} }
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
private var mAllowAutoOpenBiometricPrompt: Boolean = true private var mAllowAutoOpenBiometricPrompt: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -132,7 +134,11 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout) coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState) mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
savedInstanceState.getBoolean(KEY_READ_ONLY)
} else {
PreferencesUtil.enableReadOnlyDatabase(this)
}
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity) mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
@@ -149,6 +155,15 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
checkboxPasswordView?.isChecked = true checkboxPasswordView?.isChecked = true
} }
}) })
passwordView?.setOnKeyListener { _, _, keyEvent ->
var handled = false
if (keyEvent.action == KeyEvent.ACTION_DOWN
&& keyEvent?.keyCode == KEYCODE_ENTER) {
verifyCheckboxesAndLoadDatabase()
handled = true
}
handled
}
// If is a view intent // If is a view intent
getUriFromIntent(intent) getUriFromIntent(intent)
@@ -204,72 +219,114 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri) onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
} }
}
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply { override fun onResume() {
onActionFinish = { actionTask, result -> super.onResume()
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
// Recheck advanced unlock if error
advancedUnlockFragment?.initAdvancedUnlockMode()
if (result.isSuccess) { mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@PasswordActivity)
mDatabaseKeyFileUri = null
clearCredentialsViews(true)
launchGroupActivity()
} else {
var resultError = ""
val resultException = result.exception
val resultMessage = result.message
if (resultException != null) { // Back to previous keyboard is setting activated
resultError = resultException.getLocalizedMessage(resources) if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@PasswordActivity)) {
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
}
when (resultException) { // Don't allow auto open prompt if lock become when UI visible
is DuplicateUuidDatabaseException -> { mAllowAutoOpenBiometricPrompt = if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
// Relaunch loading if we need to fix UUID false
showLoadDatabaseDuplicateUuidMessage { else
mAllowAutoOpenBiometricPrompt
mDatabaseFileUri?.let { databaseFileUri ->
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
var databaseUri: Uri? = null checkPermission()
var mainCredential: MainCredential = MainCredential()
var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null
result.data?.let { resultData -> mDatabase?.let { database ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY) launchGroupActivityIfLoaded(database)
mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential }
readOnly = resultData.getBoolean(READ_ONLY_KEY) }
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
}
databaseUri?.let { databaseFileUri -> override fun onDatabaseRetrieved(database: Database?) {
showProgressDialogAndLoadDatabase( super.onDatabaseRetrieved(database)
databaseFileUri, if (database != null) {
mainCredential, launchGroupActivityIfLoaded(database)
readOnly, }
cipherEntity, }
true)
} override fun onDatabaseActionFinished(
} database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
// Recheck advanced unlock if error
advancedUnlockFragment?.initAdvancedUnlockMode()
if (result.isSuccess) {
launchGroupActivityIfLoaded(database)
} else {
passwordView?.requestFocusFromTouch()
var resultError = ""
val resultException = result.exception
val resultMessage = result.message
if (resultException != null) {
resultError = resultException.getLocalizedMessage(resources)
when (resultException) {
is DuplicateUuidDatabaseException -> {
// Relaunch loading if we need to fix UUID
showLoadDatabaseDuplicateUuidMessage {
var databaseUri: Uri? = null
var mainCredential = MainCredential()
var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null
result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
mainCredential =
resultData.getParcelable(MAIN_CREDENTIAL_KEY)
?: mainCredential
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity =
resultData.getParcelable(CIPHER_ENTITY_KEY)
} }
is FileNotFoundDatabaseException -> {
// Remove this default database inaccessible databaseUri?.let { databaseFileUri ->
if (mDefaultDatabase) { showProgressDialogAndLoadDatabase(
databaseFileViewModel.removeDefaultDatabase() databaseFileUri,
} mainCredential,
readOnly,
cipherEntity,
true
)
} }
} }
} }
is FileNotFoundDatabaseException -> {
// Show error message // Remove this default database inaccessible
if (resultMessage != null && resultMessage.isNotEmpty()) { if (mDefaultDatabase) {
resultError = "$resultError $resultMessage" databaseFileViewModel.removeDefaultDatabase()
}
} }
Log.e(TAG, resultError)
Snackbar.make(coordinatorLayout,
resultError,
Snackbar.LENGTH_LONG).asError().show()
} }
} }
// Show error message
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError)
Snackbar.make(
coordinatorLayout,
resultError,
Snackbar.LENGTH_LONG
).asError().show()
} }
} }
} }
@@ -296,13 +353,17 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
getUriFromIntent(intent) getUriFromIntent(intent)
} }
private fun launchGroupActivity() { private fun launchGroupActivityIfLoaded(database: Database) {
GroupActivity.launch(this, // Check if database really loaded
readOnly, if (database.loaded) {
clearCredentialsViews(true)
GroupActivity.launch(this,
database,
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() } { onLaunchActivitySpecialMode() }
) )
}
} }
override fun onValidateSpecialMode() { override fun onValidateSpecialMode() {
@@ -352,40 +413,6 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
} }
} }
override fun onResume() {
super.onResume()
if (Database.getInstance().loaded) {
launchGroupActivity()
} else {
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
// 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) {
clearCredentialsViews()
}
mProgressDatabaseTaskProvider?.registerProgressTask()
// Back to previous keyboard is setting activated
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
}
// Don't allow auto open prompt if lock become when UI visible
mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
false
else
mAllowAutoOpenBiometricPrompt
mDatabaseFileUri?.let { databaseFileUri ->
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
checkPermission()
}
}
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) { private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
// Define Key File text // Define Key File text
if (mRememberKeyFile) { if (mRememberKeyFile) {
@@ -409,11 +436,17 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
} else { } else {
// Init Biometric elements // Init Biometric elements
advancedUnlockFragment?.loadDatabase(databaseFileUri, advancedUnlockFragment?.loadDatabase(databaseFileUri,
mAllowAutoOpenBiometricPrompt mAllowAutoOpenBiometricPrompt)
&& mProgressDatabaseTaskProvider?.isBinded() != true)
} }
enableOrNotTheConfirmationButton() enableOrNotTheConfirmationButton()
// Auto select the password field and open keyboard
passwordView?.postDelayed({
passwordView?.requestFocusFromTouch()
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager?
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
}, 100)
} }
private fun enableOrNotTheConfirmationButton() { private fun enableOrNotTheConfirmationButton() {
@@ -431,6 +464,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) { private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
populatePasswordTextView(null) populatePasswordTextView(null)
if (clearKeyFile) { if (clearKeyFile) {
mDatabaseKeyFileUri = null
populateKeyFileTextView(null) populateKeyFileTextView(null)
} }
} }
@@ -460,10 +494,8 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
} }
override fun onPause() { override fun onPause() {
mProgressDatabaseTaskProvider?.unregisterProgressTask()
// Reinit locking activity UI variable // Reinit locking activity UI variable
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
mAllowAutoOpenBiometricPrompt = true mAllowAutoOpenBiometricPrompt = true
super.onPause() super.onPause()
@@ -474,7 +506,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
mDatabaseKeyFileUri?.let { mDatabaseKeyFileUri?.let {
outState.putString(KEY_KEYFILE, it.toString()) outState.putString(KEY_KEYFILE, it.toString())
} }
ReadOnlyHelper.onSaveInstanceState(outState, readOnly) outState.putBoolean(KEY_READ_ONLY, mReadOnly)
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false) outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
@@ -512,7 +544,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
clearCredentialsViews() clearCredentialsViews()
} }
if (readOnly && ( if (mReadOnly && (
mSpecialMode == SpecialMode.SAVE mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION) || mSpecialMode == SpecialMode.REGISTRATION)
) { ) {
@@ -526,7 +558,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
showProgressDialogAndLoadDatabase( showProgressDialogAndLoadDatabase(
databaseUri, databaseUri,
MainCredential(password, keyFileUri), MainCredential(password, keyFileUri),
readOnly, mReadOnly,
cipherDatabaseEntity, cipherDatabaseEntity,
false) false)
} }
@@ -538,7 +570,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
readOnly: Boolean, readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?, cipherDatabaseEntity: CipherDatabaseEntity?,
fixDuplicateUUID: Boolean) { fixDuplicateUUID: Boolean) {
mProgressDatabaseTaskProvider?.startDatabaseLoad( loadDatabase(
databaseUri, databaseUri,
mainCredential, mainCredential,
readOnly, readOnly,
@@ -577,7 +609,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
// Check permission // Check permission
private fun checkPermission() { private fun checkPermission() {
if (Build.VERSION.SDK_INT in 23..28 if (Build.VERSION.SDK_INT in 23..28
&& !readOnly && !mReadOnly
&& !mPermissionAsked) { && !mPermissionAsked) {
mPermissionAsked = true mPermissionAsked = true
// Check self permission to show or not the dialog // Check self permission to show or not the dialog
@@ -654,7 +686,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
} }
private fun changeOpenFileReadIcon(togglePassword: MenuItem) { private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
if (readOnly) { if (mReadOnly) {
togglePassword.setTitle(R.string.menu_file_selection_read_only) togglePassword.setTitle(R.string.menu_file_selection_read_only)
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp) togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
} else { } else {
@@ -668,7 +700,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
when (item.itemId) { when (item.itemId) {
android.R.id.home -> finish() android.R.id.home -> finish()
R.id.menu_open_file_read_mode_key -> { R.id.menu_open_file_read_mode_key -> {
readOnly = !readOnly mReadOnly = !mReadOnly
changeOpenFileReadIcon(item) changeOpenFileReadIcon(item)
} }
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item) else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
@@ -695,8 +727,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
var keyFileResult = false var keyFileResult = false
mExternalFileHelper?.let { mExternalFileHelper?.let {
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
) { uri ->
if (uri != null) { if (uri != null) {
mDatabaseKeyFileUri = uri mDatabaseKeyFileUri = uri
populateKeyFileTextView(uri) populateKeyFileTextView(uri)
@@ -706,9 +737,9 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
if (!keyFileResult) { if (!keyFileResult) {
// this block if not a key file response // this block if not a key file response
when (resultCode) { when (resultCode) {
LockingActivity.RESULT_EXIT_LOCK -> { DatabaseLockActivity.RESULT_EXIT_LOCK -> {
clearCredentialsViews() clearCredentialsViews()
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this)) closeDatabase()
} }
Activity.RESULT_CANCELED -> { Activity.RESULT_CANCELED -> {
clearCredentialsViews() clearCredentialsViews()
@@ -727,6 +758,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
private const val KEY_KEYFILE = "keyFile" private const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW" private const val VIEW_INTENT = "android.intent.action.VIEW"
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
private const val KEY_PASSWORD = "password" private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED" private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"

View File

@@ -32,7 +32,6 @@ import android.view.View
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
@@ -41,7 +40,7 @@ import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
class AssignMasterKeyDialogFragment : DialogFragment() { class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
private var mMasterPassword: String? = null private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null private var mKeyFile: Uri? = null

View File

@@ -23,12 +23,11 @@ import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
class DatabaseChangedDialogFragment : DialogFragment() { class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
var actionDatabaseListener: ActionDatabaseChangedListener? = null var actionDatabaseListener: ActionDatabaseChangedListener? = null

View File

@@ -0,0 +1,71 @@
package com.kunzisoft.keepass.activities.dialogs
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: Database? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseViewModel.database.observe(this) { database ->
this.mDatabase = database
resetAppTimeoutOnTouchOrFocus()
onDatabaseRetrieved(database)
}
mDatabaseViewModel.actionFinished.observe(this) { result ->
onDatabaseActionFinished(result.database, result.actionTask, result.result)
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
resetAppTimeoutOnTouchOrFocus()
}
override fun onDatabaseRetrieved(database: Database?) {
// Can be overridden by a subclass
}
override fun onDatabaseActionFinished(
database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
// Can be overridden by a subclass
}
fun resetAppTimeout() {
context?.let {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(it,
mDatabase?.loaded ?: false)
}
}
open fun overrideTimeoutTouchAndFocusEvents(): Boolean {
return false
}
private fun resetAppTimeoutOnTouchOrFocus() {
if (!overrideTimeoutTouchAndFocusEvents()) {
context?.let {
dialog?.window?.decorView?.resetAppTimeoutWhenViewTouchedOrFocused(
it,
mDatabase?.loaded
)
}
}
}
}

View File

@@ -6,6 +6,7 @@ import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
// Not as DatabaseDialogFragment because crash on KitKat
class DatePickerFragment : DialogFragment() { class DatePickerFragment : DialogFragment() {
private var mDefaultYear: Int = 2000 private var mDefaultYear: Int = 2000

View File

@@ -20,61 +20,38 @@
package com.kunzisoft.keepass.activities.dialogs package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.viewmodels.NodesViewModel
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
open class DeleteNodesDialogFragment : DialogFragment() { class DeleteNodesDialogFragment : DatabaseDialogFragment() {
private var mNodesToDelete: List<Node> = ArrayList() private var mNodesToDelete: List<Node> = listOf()
private var mListener: DeleteNodeListener? = null private val mNodesViewModel: NodesViewModel by activityViewModels()
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as DeleteNodeListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + DeleteNodeListener::class.java.name)
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
protected open fun retrieveMessage(): String {
return getString(R.string.warning_permanently_delete_nodes)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
mNodesViewModel.nodesToDelete.observe(this) { nodes ->
this.mNodesToDelete = nodes
}
var recycleBin = false
arguments?.apply { arguments?.apply {
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY) if (containsKey(RECYCLE_BIN_TAG)) {
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) { recycleBin = this.getBoolean(RECYCLE_BIN_TAG)
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), this)
}
} ?: savedInstanceState?.apply {
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), savedInstanceState)
} }
} }
activity?.let { activity -> activity?.let { activity ->
// Use the Builder class for convenient dialog construction // Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
builder.setMessage(retrieveMessage()) builder.setMessage(if (recycleBin)
getString(R.string.warning_empty_recycle_bin)
else
getString(R.string.warning_permanently_delete_nodes))
builder.setPositiveButton(android.R.string.ok) { _, _ -> builder.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.permanentlyDeleteNodes(mNodesToDelete) mNodesViewModel.permanentlyDeleteNodes(mNodesToDelete)
} }
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() } builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
// Create the AlertDialog object and return it // Create the AlertDialog object and return it
@@ -83,19 +60,14 @@ open class DeleteNodesDialogFragment : DialogFragment() {
return super.onCreateDialog(savedInstanceState) return super.onCreateDialog(savedInstanceState)
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putAll(getBundleFromListNodes(mNodesToDelete))
}
interface DeleteNodeListener {
fun permanentlyDeleteNodes(nodes: List<Node>)
}
companion object { companion object {
fun getInstance(nodesToDelete: List<Node>): DeleteNodesDialogFragment { private const val RECYCLE_BIN_TAG = "RECYCLE_BIN_TAG"
fun getInstance(recycleBin: Boolean): DeleteNodesDialogFragment {
return DeleteNodesDialogFragment().apply { return DeleteNodesDialogFragment().apply {
arguments = getBundleFromListNodes(nodesToDelete) arguments = Bundle().apply {
putBoolean(RECYCLE_BIN_TAG, recycleBin)
}
} }
} }
} }

View File

@@ -31,14 +31,13 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.Field
class EntryCustomFieldDialogFragment: DialogFragment() { class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
private var oldField: Field? = null private var oldField: Field? = null

View File

@@ -22,18 +22,18 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import com.google.android.material.textfield.TextInputLayout
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.password.PasswordGenerator import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.applyFontVisibility import com.kunzisoft.keepass.view.applyFontVisibility
class GeneratePasswordDialogFragment : DialogFragment() { class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
private var mListener: GeneratePasswordListener? = null private var mListener: GeneratePasswordListener? = null
@@ -42,6 +42,8 @@ class GeneratePasswordDialogFragment : DialogFragment() {
private var passwordInputLayoutView: TextInputLayout? = null private var passwordInputLayoutView: TextInputLayout? = null
private var passwordView: EditText? = null private var passwordView: EditText? = null
private var mPasswordField: Field? = null
private var uppercaseBox: CompoundButton? = null private var uppercaseBox: CompoundButton? = null
private var lowercaseBox: CompoundButton? = null private var lowercaseBox: CompoundButton? = null
private var digitsBox: CompoundButton? = null private var digitsBox: CompoundButton? = null
@@ -77,7 +79,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
passwordView = root?.findViewById(R.id.password) passwordView = root?.findViewById(R.id.password)
passwordView?.applyFontVisibility() passwordView?.applyFontVisibility()
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button) val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyPasswordAndProtectedFields(activity)) passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(activity))
View.VISIBLE else View.GONE View.VISIBLE else View.GONE
val clipboardHelper = ClipboardHelper(activity) val clipboardHelper = ClipboardHelper(activity)
passwordCopyView?.setOnClickListener { passwordCopyView?.setOnClickListener {
@@ -98,6 +100,8 @@ class GeneratePasswordDialogFragment : DialogFragment() {
bracketsBox = root?.findViewById(R.id.cb_brackets) bracketsBox = root?.findViewById(R.id.cb_brackets)
extendedBox = root?.findViewById(R.id.cb_extended) extendedBox = root?.findViewById(R.id.cb_extended)
mPasswordField = arguments?.getParcelable(KEY_PASSWORD_FIELD)
assignDefaultCharacters() assignDefaultCharacters()
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length) val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
@@ -120,16 +124,18 @@ class GeneratePasswordDialogFragment : DialogFragment() {
builder.setView(root) builder.setView(root)
.setPositiveButton(R.string.accept) { _, _ -> .setPositiveButton(R.string.accept) { _, _ ->
val bundle = Bundle() mPasswordField?.let { passwordField ->
bundle.putString(KEY_PASSWORD_ID, passwordView!!.text.toString()) passwordView?.text?.toString()?.let { passwordValue ->
mListener?.acceptPassword(bundle) passwordField.protectedValue.stringValue = passwordValue
}
mListener?.acceptPassword(passwordField)
}
dismiss() dismiss()
} }
.setNegativeButton(android.R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
val bundle = Bundle() mPasswordField?.let { passwordField ->
mListener?.cancelPassword(bundle) mListener?.cancelPassword(passwordField)
}
dismiss() dismiss()
} }
@@ -200,11 +206,19 @@ class GeneratePasswordDialogFragment : DialogFragment() {
} }
interface GeneratePasswordListener { interface GeneratePasswordListener {
fun acceptPassword(bundle: Bundle) fun acceptPassword(passwordField: Field)
fun cancelPassword(bundle: Bundle) fun cancelPassword(passwordField: Field)
} }
companion object { companion object {
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID" private const val KEY_PASSWORD_FIELD = "KEY_PASSWORD_FIELD"
fun getInstance(field: Field): GeneratePasswordDialogFragment {
return GeneratePasswordDialogFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_PASSWORD_FIELD, field)
}
}
}
} }
} }

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.activities.dialogs package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
@@ -28,35 +27,34 @@ import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.IconPickerActivity import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.*
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.view.ExpirationView import com.kunzisoft.keepass.view.DateTimeEditFieldView
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import org.joda.time.DateTime import org.joda.time.DateTime
class GroupEditDialogFragment : DialogFragment() { class GroupEditDialogFragment : DatabaseDialogFragment() {
private var mDatabase: Database? = null private val mGroupEditViewModel: GroupEditViewModel by activityViewModels()
private var mEditGroupListener: EditGroupListener? = null private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
private var mEditGroupDialogAction = NONE
private var mEditGroupDialogAction = EditGroupDialogAction.NONE
private var mGroupInfo = GroupInfo() private var mGroupInfo = GroupInfo()
private var mGroupNamesNotAllowed: List<String>? = null
private lateinit var iconButtonView: ImageView private lateinit var iconButtonView: ImageView
private var iconColor: Int = 0 private var mIconColor: Int = 0
private lateinit var nameTextLayoutView: TextInputLayout private lateinit var nameTextLayoutView: TextInputLayout
private lateinit var nameTextView: TextView private lateinit var nameTextView: TextView
private lateinit var notesTextLayoutView: TextInputLayout private lateinit var notesTextLayoutView: TextInputLayout
private lateinit var notesTextView: TextView private lateinit var notesTextView: TextView
private lateinit var expirationView: ExpirationView private lateinit var expirationView: DateTimeEditFieldView
enum class EditGroupDialogAction { enum class EditGroupDialogAction {
CREATION, UPDATE, NONE; CREATION, UPDATE, NONE;
@@ -68,22 +66,51 @@ class GroupEditDialogFragment : DialogFragment() {
} }
} }
override fun onAttach(context: Context) { override fun onCreate(savedInstanceState: Bundle?) {
super.onAttach(context) super.onCreate(savedInstanceState)
// Verify that the host activity implements the callback interface
try { mGroupEditViewModel.onIconSelected.observe(this) { iconImage ->
// Instantiate the NoticeDialogListener so we can send events to the host mGroupInfo.icon = iconImage
mEditGroupListener = context as EditGroupListener mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
} catch (e: ClassCastException) { }
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString() mGroupEditViewModel.onDateSelected.observe(this) { viewModelDate ->
+ " must implement " + GroupEditDialogFragment::class.java.name) // Save the date
mGroupInfo.expiryTime = DateInstant(
DateTime(mGroupInfo.expiryTime.date)
.withYear(viewModelDate.year)
.withMonthOfYear(viewModelDate.month + 1)
.withDayOfMonth(viewModelDate.day)
.toDate())
expirationView.dateTime = mGroupInfo.expiryTime
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
val instantTime = DateInstant(mGroupInfo.expiryTime.date, DateInstant.Type.TIME)
// Trick to recall selection with time
mGroupEditViewModel.requestDateTimeSelection(instantTime)
}
}
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
// Save the time
mGroupInfo.expiryTime = DateInstant(
DateTime(mGroupInfo.expiryTime.date)
.withHourOfDay(viewModelTime.hours)
.withMinuteOfHour(viewModelTime.minutes)
.toDate(), mGroupInfo.expiryTime.type)
expirationView.dateTime = mGroupInfo.expiryTime
}
mGroupEditViewModel.groupNamesNotAllowed.observe(this) { namesNotAllowed ->
this.mGroupNamesNotAllowed = namesNotAllowed
} }
} }
override fun onDetach() { override fun onDatabaseRetrieved(database: Database?) {
mEditGroupListener = null super.onDatabaseRetrieved(database)
super.onDetach() mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
}
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -98,12 +125,9 @@ class GroupEditDialogFragment : DialogFragment() {
// Retrieve the textColor to tint the icon // Retrieve the textColor to tint the icon
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
iconColor = ta.getColor(0, Color.WHITE) mIconColor = ta.getColor(0, Color.WHITE)
ta.recycle() ta.recycle()
// Init elements
mDatabase = Database.getInstance()
if (savedInstanceState != null if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_ACTION_ID) && savedInstanceState.containsKey(KEY_ACTION_ID)
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) { && savedInstanceState.containsKey(KEY_GROUP_INFO)) {
@@ -120,32 +144,22 @@ class GroupEditDialogFragment : DialogFragment() {
} }
// populate info in views // populate info in views
populateInfoToViews() populateInfoToViews(mGroupInfo)
expirationView.setOnDateClickListener = {
expirationView.expiryTime.date.let { expiresDate -> iconButtonView.setOnClickListener { _ ->
val dateTime = DateTime(expiresDate) mGroupEditViewModel.requestIconSelection(mGroupInfo.icon)
val defaultYear = dateTime.year }
val defaultMonth = dateTime.monthOfYear-1 expirationView.setOnDateClickListener = { dateInstant ->
val defaultDay = dateTime.dayOfMonth mGroupEditViewModel.requestDateTimeSelection(dateInstant)
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
.show(parentFragmentManager, "DatePickerFragment")
}
} }
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
builder.setView(root) builder.setView(root)
.setPositiveButton(android.R.string.ok, null) .setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
retrieveGroupInfoFromViews() // Do nothing
mEditGroupListener?.cancelEditGroup(
mEditGroupDialogAction,
mGroupInfo)
} }
iconButtonView.setOnClickListener { _ ->
IconPickerActivity.launch(activity, mGroupInfo.icon)
}
return builder.create() return builder.create()
} }
return super.onCreateDialog(savedInstanceState) return super.onCreateDialog(savedInstanceState)
@@ -155,40 +169,34 @@ class GroupEditDialogFragment : DialogFragment() {
super.onResume() super.onResume()
// To prevent auto dismiss // To prevent auto dismiss
val d = dialog as AlertDialog? val alertDialog = dialog as AlertDialog?
if (d != null) { if (alertDialog != null) {
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button val positiveButton = alertDialog.getButton(Dialog.BUTTON_POSITIVE) as Button
positiveButton.setOnClickListener { positiveButton.setOnClickListener {
retrieveGroupInfoFromViews() retrieveGroupInfoFromViews()
if (isValid()) { if (isValid()) {
mEditGroupListener?.approveEditGroup( when (mEditGroupDialogAction) {
mEditGroupDialogAction, CREATION ->
mGroupInfo) mGroupEditViewModel.approveGroupCreation(mGroupInfo)
d.dismiss() UPDATE ->
mGroupEditViewModel.approveGroupUpdate(mGroupInfo)
NONE -> {}
}
alertDialog.dismiss()
} }
} }
} }
} }
fun getExpiryTime(): DateInstant { private fun populateInfoToViews(groupInfo: GroupInfo) {
retrieveGroupInfoFromViews() mGroupEditViewModel.selectIcon(groupInfo.icon)
return mGroupInfo.expiryTime nameTextView.text = groupInfo.title
} notesTextLayoutView.visibility = if (groupInfo.notes == null) View.GONE else View.VISIBLE
groupInfo.notes?.let {
fun setExpiryTime(expiryTime: DateInstant) {
mGroupInfo.expiryTime = expiryTime
populateInfoToViews()
}
private fun populateInfoToViews() {
assignIconView()
nameTextView.text = mGroupInfo.title
notesTextLayoutView.visibility = if (mGroupInfo.notes == null) View.GONE else View.VISIBLE
mGroupInfo.notes?.let {
notesTextView.text = it notesTextView.text = it
} }
expirationView.expires = mGroupInfo.expires expirationView.activation = groupInfo.expires
expirationView.expiryTime = mGroupInfo.expiryTime expirationView.dateTime = groupInfo.expiryTime
} }
private fun retrieveGroupInfoFromViews() { private fun retrieveGroupInfoFromViews() {
@@ -198,17 +206,8 @@ class GroupEditDialogFragment : DialogFragment() {
if (newNotes.isNotEmpty()) { if (newNotes.isNotEmpty()) {
mGroupInfo.notes = newNotes mGroupInfo.notes = newNotes
} }
mGroupInfo.expires = expirationView.expires mGroupInfo.expires = expirationView.activation
mGroupInfo.expiryTime = expirationView.expiryTime mGroupInfo.expiryTime = expirationView.dateTime
}
private fun assignIconView() {
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconButtonView, mGroupInfo.icon, iconColor)
}
fun setIcon(icon: IconImage) {
mGroupInfo.icon = icon
assignIconView()
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@@ -219,7 +218,21 @@ class GroupEditDialogFragment : DialogFragment() {
} }
private fun isValid(): Boolean { private fun isValid(): Boolean {
val error = mEditGroupListener?.isValidGroupName(nameTextView.text.toString()) ?: Error(false, null) val name = nameTextView.text.toString()
val error = when {
name.isEmpty() -> {
Error(true, R.string.error_no_name)
}
mGroupNamesNotAllowed == null -> {
Error(true, R.string.error_word_reserved)
}
mGroupNamesNotAllowed?.find { it.equals(name, ignoreCase = true) } != null -> {
Error(true, R.string.error_word_reserved)
}
else -> {
Error(false, null)
}
}
error.messageId?.let { messageId -> error.messageId?.let { messageId ->
nameTextLayoutView.error = getString(messageId) nameTextLayoutView.error = getString(messageId)
} ?: kotlin.run { } ?: kotlin.run {
@@ -230,14 +243,6 @@ class GroupEditDialogFragment : DialogFragment() {
data class Error(val isError: Boolean, val messageId: Int?) data class Error(val isError: Boolean, val messageId: Int?)
interface EditGroupListener {
fun isValidGroupName(name: String): Error
fun approveEditGroup(action: EditGroupDialogAction,
groupInfo: GroupInfo)
fun cancelEditGroup(action: EditGroupDialogAction,
groupInfo: GroupInfo)
}
companion object { companion object {
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP" const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"

View File

@@ -19,11 +19,11 @@
*/ */
package com.kunzisoft.keepass.activities.dialogs package com.kunzisoft.keepass.activities.dialogs
import android.app.AlertDialog
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential

View File

@@ -25,14 +25,13 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
/** /**
* Custom Dialog to confirm big file to upload * Custom Dialog to confirm big file to upload
*/ */
class ReplaceFileDialogFragment : DialogFragment() { class ReplaceFileDialogFragment : DatabaseDialogFragment() {
private var mActionChooseListener: ActionChooseListener? = null private var mActionChooseListener: ActionChooseListener? = null

View File

@@ -31,7 +31,6 @@ import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.* import android.widget.*
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -49,7 +48,7 @@ import com.kunzisoft.keepass.otp.TokenCalculator
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import java.util.* import java.util.*
class SetOTPDialogFragment : DialogFragment() { class SetOTPDialogFragment : DatabaseDialogFragment() {
private var mCreateOTPElementListener: CreateOtpListener? = null private var mCreateOTPElementListener: CreateOtpListener? = null
@@ -80,11 +79,15 @@ class SetOTPDialogFragment : DialogFragment() {
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus -> private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
if (!isFocus) if (!isFocus)
mManualEvent = true mManualEvent = true
else
resetAppTimeout()
} }
@SuppressLint("ClickableViewAccessibility")
private var mOnTouchListener = View.OnTouchListener { _, event -> private var mOnTouchListener = View.OnTouchListener { _, event ->
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
mManualEvent = true mManualEvent = true
resetAppTimeout()
} }
} }
false false
@@ -95,6 +98,10 @@ class SetOTPDialogFragment : DialogFragment() {
private var mPeriodWellFormed = false private var mPeriodWellFormed = false
private var mDigitsWellFormed = false private var mDigitsWellFormed = false
override fun overrideTimeoutTouchAndFocusEvents(): Boolean {
return true
}
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
// Verify that the host activity implements the callback interface // Verify that the host activity implements the callback interface
@@ -225,8 +232,11 @@ class SetOTPDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
builder.apply { builder.apply {
setView(root) setView(root)
.setPositiveButton(android.R.string.ok) {_, _ -> } .setPositiveButton(android.R.string.ok) { _, _ ->
resetAppTimeout()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
resetAppTimeout()
} }
} }

View File

@@ -22,16 +22,15 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.annotation.IdRes
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.view.View import android.view.View
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.RadioGroup import android.widget.RadioGroup
import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.SortNodeEnum
class SortDialogFragment : DialogFragment() { class SortDialogFragment : DatabaseDialogFragment() {
private var mListener: SortSelectionListener? = null private var mListener: SortSelectionListener? = null

View File

@@ -8,6 +8,7 @@ import android.os.Bundle
import android.text.format.DateFormat import android.text.format.DateFormat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
// Not as DatabaseDialogFragment because crash on KitKat
class TimePickerFragment : DialogFragment() { class TimePickerFragment : DialogFragment() {
private var defaultHour: Int = 0 private var defaultHour: Int = 0

View File

@@ -0,0 +1,51 @@
package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle
import android.view.View
import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseFragment : StylishFragment(), DatabaseRetrieval {
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
protected var mDatabase: Database? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
if (mDatabase == null || mDatabase != database) {
this.mDatabase = database
onDatabaseRetrieved(database)
}
}
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
onDatabaseActionFinished(result.database, result.actionTask, result.result)
}
}
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
context?.let {
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
}
}
override fun onDatabaseActionFinished(
database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
// Can be overridden by a subclass
}
protected fun buildNewBinaryAttachment(): BinaryData? {
return mDatabase?.buildNewBinaryAttachment()
}
}

View File

@@ -19,431 +19,264 @@
*/ */
package com.kunzisoft.keepass.activities.fragments package com.kunzisoft.keepass.activities.fragments
import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import androidx.core.view.isVisible
import android.widget.EditText import androidx.fragment.app.activityViewModels
import android.widget.ImageView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.EntryEditActivity import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.education.EntryEditActivityEducation import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.view.TemplateEditView
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.ExpirationView
import com.kunzisoft.keepass.view.applyFontVisibility
import com.kunzisoft.keepass.view.collapse import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand import com.kunzisoft.keepass.view.expand
import com.kunzisoft.keepass.view.showByFading
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
class EntryEditFragment: StylishFragment() { class EntryEditFragment: DatabaseFragment() {
private lateinit var entryTitleLayoutView: TextInputLayout private val mEntryEditViewModel: EntryEditViewModel by activityViewModels()
private lateinit var entryTitleView: EditText
private lateinit var entryIconView: ImageView private lateinit var rootView: View
private lateinit var entryUserNameView: EditText private lateinit var templateView: TemplateEditView
private lateinit var entryUrlView: EditText private lateinit var attachmentsContainerView: ViewGroup
private lateinit var entryPasswordLayoutView: TextInputLayout
private lateinit var entryPasswordView: EditText
private lateinit var entryPasswordGeneratorView: View
private lateinit var entryExpirationView: ExpirationView
private lateinit var entryNotesView: EditText
private lateinit var extraFieldsContainerView: View
private lateinit var extraFieldsListView: ViewGroup
private lateinit var attachmentsContainerView: View
private lateinit var attachmentsListView: RecyclerView private lateinit var attachmentsListView: RecyclerView
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter private var mTemplate: Template? = null
private var mAllowMultipleAttachments: Boolean = false
private var fontInVisibility: Boolean = false private var mIconColor: Int = 0
private var iconColor: Int = 0
var drawFactory: IconDrawableFactory? = null override fun onCreateView(inflater: LayoutInflater,
var setOnDateClickListener: (() -> Unit)? = null container: ViewGroup?,
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null savedInstanceState: Bundle?): View? {
var setOnIconViewClickListener: ((IconImage) -> Unit)? = null
var setOnEditCustomField: ((Field) -> Unit)? = null
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
// Elements to modify the current entry
private var mEntryInfo = EntryInfo()
private var mLastFocusedEditField: FocusedEditField? = null
private var mExtraViewToRequestFocus: EditText? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
val rootView = inflater.cloneInContext(contextThemed) // Retrieve the textColor to tint the icon
.inflate(R.layout.fragment_entry_edit_contents, container, false) val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
mIconColor = taIconColor?.getColor(0, Color.BLACK) ?: Color.BLACK
taIconColor?.recycle()
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext()) return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry_edit, container, false)
}
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title) override fun onViewCreated(view: View,
entryTitleView = rootView.findViewById(R.id.entry_edit_title) savedInstanceState: Bundle?) {
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button) super.onViewCreated(view, savedInstanceState)
entryIconView.setOnClickListener {
setOnIconViewClickListener?.invoke(mEntryInfo.icon) rootView = view
// Hide only the first time
if (savedInstanceState == null) {
view.isVisible = false
} }
templateView = view.findViewById(R.id.template_view)
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
entryUrlView = rootView.findViewById(R.id.entry_edit_url)
entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password)
entryPasswordView = rootView.findViewById(R.id.entry_edit_password)
entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button)
entryPasswordGeneratorView.setOnClickListener {
setOnPasswordGeneratorClickListener?.onClick(it)
}
entryExpirationView = rootView.findViewById(R.id.entry_edit_expiration)
entryExpirationView.setOnDateClickListener = setOnDateClickListener
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container)
extraFieldsListView = rootView.findViewById(R.id.extra_fields_list)
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext()) attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
// TODO retrieve current database with its unique key
attachmentsAdapter.database = Database.getInstance()
//attachmentsAdapter.database = arguments?.getInt(KEY_DATABASE)
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) {
attachmentsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
attachmentsContainerView.expand(true)
}
}
attachmentsListView.apply { attachmentsListView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = attachmentsAdapter adapter = attachmentsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
} }
// Retrieve the textColor to tint the icon templateView.apply {
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) setOnIconClickListener {
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE mEntryEditViewModel.requestIconSelection(templateView.getIcon())
taIconColor?.recycle() }
setOnCustomEditionActionClickListener { field ->
rootView?.resetAppTimeoutWhenViewFocusedOrChanged(requireContext()) mEntryEditViewModel.requestCustomFieldEdition(field)
}
// Retrieve the new entry after an orientation change setOnPasswordGenerationActionClickListener { field ->
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true) mEntryEditViewModel.requestPasswordSelection(field)
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo }
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) { setOnDateInstantClickListener { dateInstant ->
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo mEntryEditViewModel.requestDateTimeSelection(dateInstant)
}
} }
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) { if (savedInstanceState != null) {
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField val attachments: List<Attachment> =
savedInstanceState.getParcelableArrayList(ATTACHMENTS_TAG) ?: listOf()
setAttachments(attachments)
} }
populateViewsWithEntry() mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template ->
this.mTemplate = template
templateView.setTemplate(template)
}
return rootView mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry ->
} if (templateEntry != null) {
val selectedTemplate = if (mTemplate != null)
override fun onDetach() { mTemplate
super.onDetach() else
templateEntry.defaultTemplate
drawFactory = null templateView.setTemplate(selectedTemplate)
setOnDateClickListener = null // Load entry info only the first time to keep change locally
setOnPasswordGeneratorClickListener = null if (savedInstanceState == null) {
setOnIconViewClickListener = null assignEntryInfo(templateEntry.entryInfo)
setOnRemoveAttachment = null
setOnEditCustomField = null
}
fun getEntryInfo(): EntryInfo {
populateEntryWithViews()
return mEntryInfo
}
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
entryPasswordGeneratorView,
{
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
},
{
try {
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
} catch (ignore: Exception) {}
} }
) // To prevent flickering
} rootView.showByFading()
// Apply timeout reset
private fun populateViewsWithEntry() { resetAppTimeoutWhenViewFocusedOrChanged(rootView)
// Set info in view
icon = mEntryInfo.icon
title = mEntryInfo.title
username = mEntryInfo.username
url = mEntryInfo.url
password = mEntryInfo.password
expires = mEntryInfo.expires
expiryTime = mEntryInfo.expiryTime
notes = mEntryInfo.notes
assignExtraFields(mEntryInfo.customFields) { fields ->
setOnEditCustomField?.invoke(fields)
}
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
setOnRemoveAttachment?.invoke(attachment)
}
}
private fun populateEntryWithViews() {
// Icon already populate
mEntryInfo.title = title
mEntryInfo.username = username
mEntryInfo.url = url
mEntryInfo.password = password
mEntryInfo.expires = expires
mEntryInfo.expiryTime = expiryTime
mEntryInfo.notes = notes
mEntryInfo.customFields = getExtraFields()
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
}?.otpModel
mEntryInfo.attachments = getAttachments()
}
var title: String
get() {
return entryTitleView.text.toString()
}
set(value) {
entryTitleView.setText(value)
if (fontInVisibility)
entryTitleView.applyFontVisibility()
}
var icon: IconImage
get() {
return mEntryInfo.icon
}
set(value) {
mEntryInfo.icon = value
drawFactory?.assignDatabaseIcon(entryIconView, value, iconColor)
}
var username: String
get() {
return entryUserNameView.text.toString()
}
set(value) {
entryUserNameView.setText(value)
if (fontInVisibility)
entryUserNameView.applyFontVisibility()
}
var url: String
get() {
return entryUrlView.text.toString()
}
set(value) {
entryUrlView.setText(value)
if (fontInVisibility)
entryUrlView.applyFontVisibility()
}
var password: String
get() {
return entryPasswordView.text.toString()
}
set(value) {
entryPasswordView.setText(value)
if (fontInVisibility) {
entryPasswordView.applyFontVisibility()
} }
} }
var expires: Boolean mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
get() { mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, retrieveEntryInfo())
return entryExpirationView.expires
}
set(value) {
entryExpirationView.expires = value
} }
var expiryTime: DateInstant mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { iconImage ->
get() { templateView.setIcon(iconImage)
return entryExpirationView.expiryTime
}
set(value) {
entryExpirationView.expiryTime = value
} }
var notes: String mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
get() { templateView.setPasswordField(passwordField)
return entryNotesView.text.toString()
}
set(value) {
entryNotesView.setText(value)
if (fontInVisibility)
entryNotesView.applyFontVisibility()
} }
/* ------------- mEntryEditViewModel.onDateSelected.observe(viewLifecycleOwner) { viewModelDate ->
* Extra Fields // Save the date
* ------------- templateView.setCurrentDateTimeValue(viewModelDate)
*/ }
private var mExtraFieldsList: MutableList<Field> = ArrayList() mEntryEditViewModel.onTimeSelected.observe(viewLifecycleOwner) { viewModelTime ->
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null // Save the time
templateView.setCurrentTimeValue(viewModelTime)
}
private fun buildViewFromField(extraField: Field): View? { mEntryEditViewModel.onCustomFieldEdited.observe(viewLifecycleOwner) { fieldAction ->
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? val oldField = fieldAction.oldField
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false) val newField = fieldAction.newField
itemView?.id = View.NO_ID // Field to add
if (oldField == null) {
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container) newField?.let {
extraFieldValueContainer?.endIconMode = if (extraField.protectedValue.isProtected) if (!templateView.putCustomField(it)) {
TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE mEntryEditViewModel.showCustomFieldEditionError()
extraFieldValueContainer?.hint = extraField.name }
extraFieldValueContainer?.id = View.NO_ID }
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
extraFieldValue?.apply {
if (extraField.protectedValue.isProtected) {
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
} }
setText(extraField.protectedValue.toString()) // Field to replace
if (fontInVisibility) oldField?.let {
applyFontVisibility() newField?.let {
} if (!templateView.replaceCustomField(oldField, newField)) {
extraFieldValue?.id = View.NO_ID mEntryEditViewModel.showCustomFieldEditionError()
extraFieldValue?.tag = "FIELD_VALUE_TAG" }
if (mLastFocusedEditField?.field == extraField) { }
mExtraViewToRequestFocus = extraFieldValue }
} // Field to remove
if (newField == null) {
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit) oldField?.let {
extraFieldEditButton?.setOnClickListener { templateView.removeCustomField(it)
mOnEditButtonClickListener?.invoke(extraField)
}
extraFieldEditButton?.id = View.NO_ID
return itemView
}
fun getExtraFields(): List<Field> {
mLastFocusedEditField = null
for (index in 0 until extraFieldsListView.childCount) {
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
.findViewWithTag("FIELD_VALUE_TAG")
val extraField = mExtraFieldsList[index]
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
if (extraFieldValue.isFocused) {
mLastFocusedEditField = FocusedEditField().apply {
field = extraField
cursorSelectionStart = extraFieldValue.selectionStart
cursorSelectionEnd = extraFieldValue.selectionEnd
} }
} }
} }
return mExtraFieldsList
}
/** mEntryEditViewModel.requestSetupOtp.observe(viewLifecycleOwner) {
* Remove all children and add new views for each field // Retrieve the current otpElement if exists
*/ // and open the dialog to set up the OTP
fun assignExtraFields(fields: List<Field>, SetOTPDialogFragment.build(templateView.getEntryInfo().otpModel)
onEditButtonClickListener: ((item: Field)->Unit)?) { .show(parentFragmentManager, "addOTPDialog")
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
// Reinit focused field
mExtraFieldsList.clear()
mExtraFieldsList.addAll(fields)
extraFieldsListView.removeAllViews()
fields.forEach {
extraFieldsListView.addView(buildViewFromField(it))
} }
// Request last focus
mLastFocusedEditField?.let { focusField -> mEntryEditViewModel.onOtpCreated.observe(viewLifecycleOwner) {
mExtraViewToRequestFocus?.apply { // Update the otp field with otpauth:// url
requestFocus() templateView.putOtpElement(it)
setSelection(focusField.cursorSelectionStart,
focusField.cursorSelectionEnd)
}
} }
mLastFocusedEditField = null
mOnEditButtonClickListener = onEditButtonClickListener
}
/** mEntryEditViewModel.onBuildNewAttachment.observe(viewLifecycleOwner) {
* Update an extra field or create a new one if doesn't exists, the old value is lost val attachmentToUploadUri = it.attachmentToUploadUri
*/ val fileName = it.fileName
fun putExtraField(extraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
oldField?.let {
val index = mExtraFieldsList.indexOf(oldField)
mExtraFieldsList.removeAt(index)
mExtraFieldsList.add(index, extraField)
extraFieldsListView.removeViewAt(index)
val newView = buildViewFromField(extraField)
extraFieldsListView.addView(newView, index)
newView?.requestFocus()
} ?: kotlin.run {
mExtraFieldsList.add(extraField)
val newView = buildViewFromField(extraField)
extraFieldsListView.addView(newView)
newView?.requestFocus()
}
}
/** buildNewBinaryAttachment()?.let { binaryAttachment ->
* Update an extra field and keep the old value val entryAttachment = Attachment(fileName, binaryAttachment)
*/ // Ask to replace the current attachment
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) { if ((!mAllowMultipleAttachments
extraFieldsContainerView.visibility = View.VISIBLE && containsAttachment()) ||
val index = mExtraFieldsList.indexOf(oldExtraField) containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD))) {
val oldValueEditText: EditText = extraFieldsListView.getChildAt(index) ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
.findViewWithTag("FIELD_VALUE_TAG") .show(parentFragmentManager, "replacementFileFragment")
val oldValue = oldValueEditText.text.toString() } else {
val newExtraFieldWithOldValue = Field(newExtraField).apply { mEntryEditViewModel.startUploadAttachment(attachmentToUploadUri, entryAttachment)
this.protectedValue.stringValue = oldValue
}
mExtraFieldsList.removeAt(index)
mExtraFieldsList.add(index, newExtraFieldWithOldValue)
extraFieldsListView.removeViewAt(index)
val newView = buildViewFromField(newExtraFieldWithOldValue)
extraFieldsListView.addView(newView, index)
newView?.requestFocus()
}
fun removeExtraField(oldExtraField: Field) {
val previousSize = mExtraFieldsList.size
val index = mExtraFieldsList.indexOf(oldExtraField)
extraFieldsListView.getChildAt(index)?.let {
it.collapse(true) {
mExtraFieldsList.removeAt(index)
extraFieldsListView.removeViewAt(index)
val newSize = mExtraFieldsList.size
if (previousSize > 0 && newSize == 0) {
extraFieldsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
extraFieldsContainerView.expand(true)
} }
} }
} }
mEntryEditViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
when (entryAttachmentState?.downloadState) {
AttachmentState.START -> {
putAttachment(entryAttachmentState)
getAttachmentViewPosition(entryAttachmentState) { attachment, position ->
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
}
}
AttachmentState.IN_PROGRESS -> {
putAttachment(entryAttachmentState)
}
AttachmentState.COMPLETE -> {
putAttachment(entryAttachmentState) { entryAttachment ->
getAttachmentViewPosition(entryAttachment) { attachment, position ->
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
}
}
mEntryEditViewModel.onAttachmentAction(null)
}
AttachmentState.CANCELED,
AttachmentState.ERROR -> {
removeAttachment(entryAttachmentState)
mEntryEditViewModel.onAttachmentAction(null)
}
else -> {}
}
}
}
override fun onDatabaseRetrieved(database: Database?) {
templateView.populateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
}
mAllowMultipleAttachments = database?.allowMultipleAttachments == true
attachmentsAdapter?.database = database
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) {
attachmentsContainerView.collapse(true)
} else if (previousSize == 0 && newSize == 1) {
attachmentsContainerView.expand(true)
}
}
}
private fun assignEntryInfo(entryInfo: EntryInfo?) {
// Populate entry views
templateView.setEntryInfo(entryInfo)
// Manage attachments
setAttachments(entryInfo?.attachments ?: listOf())
}
private fun retrieveEntryInfo(): EntryInfo {
val entryInfo = templateView.getEntryInfo()
entryInfo.attachments = getAttachments().toMutableList()
return entryInfo
} }
/* ------------- /* -------------
@@ -451,78 +284,84 @@ class EntryEditFragment: StylishFragment() {
* ------------- * -------------
*/ */
fun getAttachments(): List<Attachment> { private fun getAttachments(): List<Attachment> {
return attachmentsAdapter.itemsList.map { it.attachment } return attachmentsAdapter?.itemsList?.map { it.attachment } ?: listOf()
} }
fun assignAttachments(attachments: List<Attachment>, private fun setAttachments(attachments: List<Attachment>) {
streamDirection: StreamDirection,
onDeleteItem: (attachment: Attachment)->Unit) {
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) }) attachmentsAdapter?.assignItems(attachments.map {
attachmentsAdapter.onDeleteButtonClickListener = { item -> EntryAttachmentState(it, StreamDirection.UPLOAD)
onDeleteItem.invoke(item.attachment) })
attachmentsAdapter?.onDeleteButtonClickListener = { item ->
val attachment = item.attachment
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
mEntryEditViewModel.deleteAttachment(attachment)
} }
} }
fun containsAttachment(): Boolean { private fun containsAttachment(): Boolean {
return !attachmentsAdapter.isEmpty() return attachmentsAdapter?.isEmpty() != true
} }
fun containsAttachment(attachment: EntryAttachmentState): Boolean { private fun containsAttachment(attachment: EntryAttachmentState): Boolean {
return attachmentsAdapter.contains(attachment) return attachmentsAdapter?.contains(attachment) ?: false
} }
fun putAttachment(attachment: EntryAttachmentState, private fun putAttachment(attachment: EntryAttachmentState,
onPreviewLoaded: (()-> Unit)? = null) { onPreviewLoaded: ((attachment: EntryAttachmentState) -> Unit)? = null) {
// When only one attachment is allowed
if (!mAllowMultipleAttachments
&& attachment.downloadState == AttachmentState.START) {
attachmentsAdapter?.clear()
}
attachmentsContainerView.visibility = View.VISIBLE attachmentsContainerView.visibility = View.VISIBLE
attachmentsAdapter.putItem(attachment) attachmentsAdapter?.putItem(attachment)
attachmentsAdapter.onBinaryPreviewLoaded = { attachmentsAdapter?.onBinaryPreviewLoaded = {
onPreviewLoaded?.invoke() onPreviewLoaded?.invoke(attachment)
} }
} }
fun removeAttachment(attachment: EntryAttachmentState) { private fun removeAttachment(attachment: EntryAttachmentState) {
attachmentsAdapter.removeItem(attachment) attachmentsAdapter?.removeItem(attachment)
} }
fun clearAttachments() { private fun getAttachmentViewPosition(attachment: EntryAttachmentState,
attachmentsAdapter.clear() position: (attachment: EntryAttachmentState, Float) -> Unit) {
}
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
attachmentsListView.postDelayed({ attachmentsListView.postDelayed({
position.invoke(attachmentsContainerView.y attachmentsAdapter?.indexOf(attachment)?.let { index ->
+ attachmentsListView.y position.invoke(attachment,
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y attachmentsContainerView.y
?: 0F) + attachmentsListView.y
) + (attachmentsListView.getChildAt(index)?.y
?: 0F)
)
}
}, 250) }, 250)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
populateEntryWithViews()
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putParcelableArrayList(ATTACHMENTS_TAG, ArrayList(getAttachments()))
}
/* -------------
* Education
* -------------
*/
fun getActionImageView(): View? {
return templateView.getActionImageView()
}
fun launchGeneratePasswordEductionAction() {
mEntryEditViewModel.requestPasswordSelection(templateView.getPasswordField())
} }
companion object { companion object {
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO" private val TAG = EntryEditFragment::class.java.name
const val KEY_DATABASE = "KEY_DATABASE"
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment { private const val ATTACHMENTS_TAG = "ATTACHMENTS_TAG"
//database: Database?): EntryEditFragment {
return EntryEditFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
// TODO Unique database key database.key
putInt(KEY_DATABASE, 0)
}
}
}
} }
} }

View File

@@ -0,0 +1,253 @@
package com.kunzisoft.keepass.activities.fragments
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
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.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.view.TemplateView
import com.kunzisoft.keepass.view.showByFading
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.*
class EntryFragment: DatabaseFragment() {
private lateinit var rootView: View
private lateinit var templateView: TemplateView
private lateinit var creationDateView: TextView
private lateinit var modificationDateView: TextView
private lateinit var attachmentsContainerView: View
private lateinit var attachmentsListView: RecyclerView
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private lateinit var uuidContainerView: View
private lateinit var uuidView: TextView
private lateinit var uuidReferenceView: TextView
private var mClipboardHelper: ClipboardHelper? = null
private val mEntryViewModel: EntryViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry, container, false)
}
override fun onViewCreated(view: View,
savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.let { context ->
mClipboardHelper = ClipboardHelper(context)
}
rootView = view
// Hide only the first time
if (savedInstanceState == null) {
view.isVisible = false
}
templateView = view.findViewById(R.id.entry_template)
loadTemplateSettings()
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
attachmentsListView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
creationDateView = view.findViewById(R.id.entry_created)
modificationDateView = view.findViewById(R.id.entry_modified)
uuidContainerView = view.findViewById(R.id.entry_UUID_container)
uuidContainerView.apply {
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
}
uuidView = view.findViewById(R.id.entry_UUID)
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
if (entryInfoHistory != null) {
templateView.setTemplate(entryInfoHistory.template)
assignEntryInfo(entryInfoHistory.entryInfo)
// Smooth appearing
rootView.showByFading()
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
}
}
mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
entryAttachmentState?.let {
if (it.streamDirection != StreamDirection.UPLOAD) {
putAttachment(it)
}
}
}
}
override fun onDatabaseRetrieved(database: Database?) {
context?.let { context ->
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
attachmentsAdapter?.database = database
}
attachmentsListView.adapter = attachmentsAdapter
}
private fun loadTemplateSettings() {
context?.let { context ->
templateView.setFirstTimeAskAllowCopyProtectedFields(PreferencesUtil.isFirstTimeAskAllowCopyProtectedFields(context))
templateView.setAllowCopyProtectedFields(PreferencesUtil.allowCopyProtectedFields(context))
}
}
private fun assignEntryInfo(entryInfo: EntryInfo?) {
// Set copy buttons
templateView.apply {
setOnAskCopySafeClickListener {
showClipboardDialog()
}
setOnCopyActionClickListener { field ->
mClipboardHelper?.timeoutCopyToClipboard(
field.protectedValue.stringValue,
getString(
R.string.copy_field,
TemplateField.getLocalizedName(context, field.name)
)
)
}
}
// Populate entry views
templateView.setEntryInfo(entryInfo)
// OTP timer updated
templateView.setOnOtpElementUpdated { otpElementUpdated ->
mEntryViewModel.onOtpElementUpdated(otpElementUpdated)
}
// Manage attachments
assignAttachments(entryInfo?.attachments ?: listOf())
// Assign dates
assignCreationDate(entryInfo?.creationTime)
assignModificationDate(entryInfo?.lastModificationTime)
// Assign special data
assignUUID(entryInfo?.id)
}
private fun showClipboardDialog() {
context?.let {
AlertDialog.Builder(it)
.setMessage(
getString(R.string.allow_copy_password_warning) +
"\n\n" +
getString(R.string.clipboard_warning)
)
.create().apply {
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, true)
finishDialog(dialog)
}
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, false)
finishDialog(dialog)
}
show()
}
}
}
private fun finishDialog(dialog: DialogInterface) {
dialog.dismiss()
loadTemplateSettings()
templateView.reload()
}
private fun assignCreationDate(date: DateInstant?) {
creationDateView.text = date?.getDateTimeString(resources)
}
private fun assignModificationDate(date: DateInstant?) {
modificationDateView.text = date?.getDateTimeString(resources)
}
private fun assignUUID(uuid: UUID?) {
uuidView.text = uuid?.toString()
uuidReferenceView.text = UuidUtil.toHexString(uuid)
}
/* -------------
* Attachments
* -------------
*/
private fun assignAttachments(attachments: List<Attachment>) {
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter?.assignItems(attachments.map {
EntryAttachmentState(it, StreamDirection.DOWNLOAD)
})
attachmentsAdapter?.onItemClickListener = { item ->
mEntryViewModel.onAttachmentSelected(item.attachment)
}
}
fun putAttachment(attachmentToDownload: EntryAttachmentState) {
attachmentsAdapter?.putItem(attachmentToDownload)
}
/* -------------
* Education
* -------------
*/
fun firstEntryFieldCopyView(): View? {
return try {
templateView.getActionImageView()
} catch (e: Exception) {
null
}
}
fun launchEntryCopyEducationAction() {
val appNameString = getString(R.string.app_name)
mClipboardHelper?.timeoutCopyToClipboard(appNameString,
getString(R.string.copy_field, appNameString))
}
companion object {
fun getInstance(): EntryFragment {
return EntryFragment().apply {
arguments = Bundle()
}
}
}
}

View File

@@ -0,0 +1,72 @@
package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.viewmodels.EntryViewModel
class EntryHistoryFragment: StylishFragment() {
private lateinit var historyContainerView: View
private lateinit var historyListView: RecyclerView
private var historyAdapter: EntryHistoryAdapter? = null
private val mEntryViewModel: EntryViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_entry_history, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.let { context ->
historyAdapter = EntryHistoryAdapter(context)
}
historyContainerView = view.findViewById(R.id.entry_history_container)
historyListView = view.findViewById(R.id.entry_history_list)
historyListView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
adapter = historyAdapter
}
mEntryViewModel.entryHistory.observe(viewLifecycleOwner) {
assignHistory(it)
}
}
/* -------------
* History
* -------------
*/
private fun assignHistory(history: List<EntryInfo>?) {
historyAdapter?.clear()
history?.let {
historyAdapter?.entryHistoryList?.addAll(history)
}
historyAdapter?.onItemClickListener = { item, position ->
mEntryViewModel.onHistorySelected(item, position)
}
historyContainerView.visibility = if (historyAdapter?.entryHistoryList?.isEmpty() != false)
View.GONE
else
View.VISIBLE
historyAdapter?.notifyDataSetChanged()
}
}

View File

@@ -25,34 +25,40 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.EntryEditActivity import com.kunzisoft.keepass.activities.EntryEditActivity
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.NodeAdapter import com.kunzisoft.keepass.adapters.NodeAdapter
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node 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.node.Type
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.GroupViewModel
import java.util.* import java.util.*
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener { class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListener {
private var nodeClickListener: NodeClickListener? = null private var nodeClickListener: NodeClickListener? = null
private var onScrollListener: OnScrollListener? = null private var onScrollListener: OnScrollListener? = null
private var mNodesRecyclerView: RecyclerView? = null private var mNodesRecyclerView: RecyclerView? = null
var mainGroup: Group? = null private var mLayoutManager: LinearLayoutManager? = null
private set
private var mAdapter: NodeAdapter? = null private var mAdapter: NodeAdapter? = null
private val mGroupViewModel: GroupViewModel by activityViewModels()
private var mCurrentGroup: Group? = null
var nodeActionSelectionMode = false var nodeActionSelectionMode = false
private set private set
var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED
@@ -63,12 +69,23 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
private var notFoundView: View? = null private var notFoundView: View? = null
private var isASearchResult: Boolean = false private var isASearchResult: Boolean = false
private var readOnly: Boolean = false
private var specialMode: SpecialMode = SpecialMode.DEFAULT private var specialMode: SpecialMode = SpecialMode.DEFAULT
val isEmpty: Boolean private var mRecycleBinEnable: Boolean = false
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0 private var mRecycleBin: Group? = null
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == SCROLL_STATE_IDLE) {
mGroupViewModel.assignPosition(getFirstVisiblePosition())
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
onScrollListener?.onScrolled(dy)
}
}
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
@@ -100,128 +117,135 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
}
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments) override fun onDatabaseRetrieved(database: Database?) {
mRecycleBinEnable = database?.isRecycleBinEnabled == true
arguments?.let { args -> mRecycleBin = database?.recycleBin
// Contains all the group in element
if (args.containsKey(GROUP_KEY)) {
mainGroup = args.getParcelable(GROUP_KEY)
}
if (args.containsKey(IS_SEARCH)) {
isASearchResult = args.getBoolean(IS_SEARCH)
}
}
contextThemed?.let { context -> contextThemed?.let { context ->
mAdapter = NodeAdapter(context) database?.let { database ->
mAdapter?.apply { mAdapter = NodeAdapter(context, database).apply {
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback { setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
override fun onNodeClick(node: Node) { override fun onNodeClick(database: Database, node: Node) {
if (nodeActionSelectionMode) { if (nodeActionSelectionMode) {
if (listActionNodes.contains(node)) { if (listActionNodes.contains(node)) {
// Remove selected item if already selected // Remove selected item if already selected
listActionNodes.remove(node) listActionNodes.remove(node)
} else {
// Add selected item if not already selected
listActionNodes.add(node)
}
nodeClickListener?.onNodeSelected(database, listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
} else { } else {
// Add selected item if not already selected nodeClickListener?.onNodeClick(database, node)
listActionNodes.add(node)
} }
nodeClickListener?.onNodeSelected(listActionNodes)
setActionNodes(listActionNodes)
notifyNodeChanged(node)
} else {
nodeClickListener?.onNodeClick(node)
} }
}
override fun onNodeLongClick(node: Node): Boolean { override fun onNodeLongClick(database: Database, node: Node): Boolean {
if (nodeActionPasteMode == PasteMode.UNDEFINED) { if (nodeActionPasteMode == PasteMode.UNDEFINED) {
// Select the first item after a long click // Select the first item after a long click
if (!listActionNodes.contains(node)) if (!listActionNodes.contains(node))
listActionNodes.add(node) listActionNodes.add(node)
nodeClickListener?.onNodeSelected(listActionNodes) nodeClickListener?.onNodeSelected(database, listActionNodes)
setActionNodes(listActionNodes) setActionNodes(listActionNodes)
notifyNodeChanged(node) notifyNodeChanged(node)
}
return true
} }
return true })
} }
}) mNodesRecyclerView?.adapter = mAdapter
} }
} }
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onDatabaseActionFinished(
ReadOnlyHelper.onSaveInstanceState(outState, readOnly) database: Database,
super.onSaveInstanceState(outState) actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
// Too many special cases to make specific additions or deletions,
// rebuilt the list works well.
if (result.isSuccess) {
rebuildList()
}
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
// To apply theme // To apply theme
val rootView = inflater.cloneInContext(contextThemed) return inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_list_nodes, container, false) .inflate(R.layout.fragment_group, container, false)
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list) }
notFoundView = rootView.findViewById(R.id.not_found_container)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mNodesRecyclerView = view.findViewById(R.id.nodes_list)
notFoundView = view.findViewById(R.id.not_found_container)
mLayoutManager = LinearLayoutManager(context)
mNodesRecyclerView?.apply { mNodesRecyclerView?.apply {
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
layoutManager = LinearLayoutManager(context) layoutManager = mLayoutManager
adapter = mAdapter adapter = mAdapter
} }
resetAppTimeoutWhenViewFocusedOrChanged(view)
onScrollListener?.let { onScrollListener -> mGroupViewModel.group.observe(viewLifecycleOwner) {
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() { mCurrentGroup = it.group
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { isASearchResult = it.group.isVirtual
super.onScrolled(recyclerView, dx, dy) rebuildList()
onScrollListener.onScrolled(dy) it.showFromPosition?.let { position ->
} mNodesRecyclerView?.scrollToPosition(position)
}) }
} }
return rootView
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
activity?.intent?.let { activity?.intent?.let {
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it) specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
} }
// Refresh data rebuildList()
try {
rebuildList()
} catch (e: Exception) {
Log.e(TAG, "Unable to rebuild the list during resume")
e.printStackTrace()
}
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
// To show the " no search entry found "
mNodesRecyclerView?.visibility = View.GONE
notFoundView?.visibility = View.VISIBLE
} else {
mNodesRecyclerView?.visibility = View.VISIBLE
notFoundView?.visibility = View.GONE
}
} }
@Throws(IllegalArgumentException::class) override fun onPause() {
fun rebuildList() {
// Add elements to the list mNodesRecyclerView?.removeOnScrollListener(mRecycleViewScrollListener)
mainGroup?.let { mainGroup -> super.onPause()
mAdapter?.apply { }
fun getFirstVisiblePosition(): Int {
return mLayoutManager?.findFirstVisibleItemPosition() ?: 0
}
private fun rebuildList() {
try {
// Add elements to the list
mCurrentGroup?.let { mainGroup ->
// Thrown an exception when sort cannot be performed // Thrown an exception when sort cannot be performed
rebuildList(mainGroup) mAdapter?.rebuildList(mainGroup)
// To visually change the elements
if (PreferencesUtil.APPEARANCE_CHANGED) {
notifyDataSetChanged()
PreferencesUtil.APPEARANCE_CHANGED = false
}
} }
} catch (e:Exception) {
Log.e(TAG, "Unable to rebuild the list", e)
}
if (isASearchResult && mAdapter != null && mAdapter!!.isEmpty) {
// To show the " no search entry found "
notFoundView?.visibility = View.VISIBLE
} else {
notFoundView?.visibility = View.GONE
} }
} }
@@ -237,8 +261,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters) mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
rebuildList() rebuildList()
} catch (e:Exception) { } catch (e:Exception) {
Log.e(TAG, "Unable to rebuild the list with the sort") Log.e(TAG, "Unable to sort the list", e)
e.printStackTrace()
} }
} }
@@ -254,7 +277,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
R.id.menu_sort -> { R.id.menu_sort -> {
context?.let { context -> context?.let { context ->
val sortDialogFragment: SortDialogFragment = val sortDialogFragment: SortDialogFragment =
if (Database.getInstance().isRecycleBinEnabled) { if (mRecycleBinEnable) {
SortDialogFragment.getInstance( SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context), PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context), PreferencesUtil.getAscendingSort(context),
@@ -276,34 +299,32 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
} }
} }
fun actionNodesCallback(nodes: List<Node>, fun actionNodesCallback(database: Database,
nodes: List<Node>,
menuListener: NodesActionMenuListener?, menuListener: NodesActionMenuListener?,
actionModeCallback: ActionMode.Callback) : ActionMode.Callback { onDestroyActionMode: (mode: ActionMode?) -> Unit) : ActionMode.Callback {
return object : ActionMode.Callback { return object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
nodeActionSelectionMode = false nodeActionSelectionMode = false
nodeActionPasteMode = PasteMode.UNDEFINED nodeActionPasteMode = PasteMode.UNDEFINED
return actionModeCallback.onCreateActionMode(mode, menu) return true
} }
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
menu?.clear() menu?.clear()
if (nodeActionPasteMode != PasteMode.UNDEFINED) { if (nodeActionPasteMode != PasteMode.UNDEFINED) {
mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu) mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu)
} else { } else {
nodeActionSelectionMode = true nodeActionSelectionMode = true
mode?.menuInflater?.inflate(R.menu.node_menu, menu) mode?.menuInflater?.inflate(R.menu.node_menu, menu)
val database = Database.getInstance()
// Open and Edit for a single item // Open and Edit for a single item
if (nodes.size == 1) { if (nodes.size == 1) {
// Edition // Edition
if (readOnly if (database.isReadOnly
|| (database.isRecycleBinEnabled && nodes[0] == database.recycleBin)) { || (mRecycleBinEnable && nodes[0] == mRecycleBin)) {
menu?.removeItem(R.id.menu_edit) menu?.removeItem(R.id.menu_edit)
} }
} else { } else {
@@ -312,59 +333,58 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
} }
// Move // Move
if (readOnly if (database.isReadOnly
|| isASearchResult) { || isASearchResult) {
menu?.removeItem(R.id.menu_move) menu?.removeItem(R.id.menu_move)
} }
// Copy (not allowed for group) // Copy (not allowed for group)
if (readOnly if (database.isReadOnly
|| isASearchResult || isASearchResult
|| nodes.any { it.type == Type.GROUP }) { || nodes.any { it.type == Type.GROUP }) {
menu?.removeItem(R.id.menu_copy) menu?.removeItem(R.id.menu_copy)
} }
// Deletion // Deletion
if (readOnly if (database.isReadOnly
|| (database.isRecycleBinEnabled && nodes.any { it == database.recycleBin })) { || (mRecycleBinEnable && nodes.any { it == mRecycleBin })) {
menu?.removeItem(R.id.menu_delete) menu?.removeItem(R.id.menu_delete)
} }
} }
// Add the number of items selected in title // Add the number of items selected in title
mode?.title = nodes.size.toString() mode?.title = nodes.size.toString()
return true
return actionModeCallback.onPrepareActionMode(mode, menu)
} }
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
if (menuListener == null) if (menuListener == null)
return false return false
return when (item?.itemId) { return when (item?.itemId) {
R.id.menu_open -> menuListener.onOpenMenuClick(nodes[0]) R.id.menu_open -> menuListener.onOpenMenuClick(database, nodes[0])
R.id.menu_edit -> menuListener.onEditMenuClick(nodes[0]) R.id.menu_edit -> menuListener.onEditMenuClick(database, nodes[0])
R.id.menu_copy -> { R.id.menu_copy -> {
nodeActionPasteMode = PasteMode.PASTE_FROM_COPY nodeActionPasteMode = PasteMode.PASTE_FROM_COPY
mAdapter?.unselectActionNodes() mAdapter?.unselectActionNodes()
val returnValue = menuListener.onCopyMenuClick(nodes) val returnValue = menuListener.onCopyMenuClick(database, nodes)
nodeActionSelectionMode = false nodeActionSelectionMode = false
returnValue returnValue
} }
R.id.menu_move -> { R.id.menu_move -> {
nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE
mAdapter?.unselectActionNodes() mAdapter?.unselectActionNodes()
val returnValue = menuListener.onMoveMenuClick(nodes) val returnValue = menuListener.onMoveMenuClick(database, nodes)
nodeActionSelectionMode = false nodeActionSelectionMode = false
returnValue returnValue
} }
R.id.menu_delete -> menuListener.onDeleteMenuClick(nodes) R.id.menu_delete -> menuListener.onDeleteMenuClick(database, nodes)
R.id.menu_paste -> { R.id.menu_paste -> {
val returnValue = menuListener.onPasteMenuClick(nodeActionPasteMode, nodes) val returnValue = menuListener.onPasteMenuClick(database, nodeActionPasteMode, nodes)
nodeActionPasteMode = PasteMode.UNDEFINED nodeActionPasteMode = PasteMode.UNDEFINED
nodeActionSelectionMode = false nodeActionSelectionMode = false
returnValue returnValue
} }
else -> actionModeCallback.onActionItemClicked(mode, item) else -> false
} }
} }
@@ -374,7 +394,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
mAdapter?.unselectActionNodes() mAdapter?.unselectActionNodes()
nodeActionPasteMode = PasteMode.UNDEFINED nodeActionPasteMode = PasteMode.UNDEFINED
nodeActionSelectionMode = false nodeActionSelectionMode = false
actionModeCallback.onDestroyActionMode(mode) onDestroyActionMode(mode)
} }
} }
} }
@@ -384,73 +404,40 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
when (requestCode) { when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> { EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE if (resultCode == EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE) {
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) { data?.getParcelableExtra<NodeId<UUID>>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let {
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode -> // Simply refresh the list
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE) rebuildList()
addNode(changedNode) // Scroll to the new entry
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) mDatabase?.getEntryById(it)?.let { entry ->
mAdapter?.notifyDataSetChanged() mAdapter?.indexOf(entry)?.let { position ->
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result") mNodesRecyclerView?.scrollToPosition(position)
}
}
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
} }
} }
} }
} }
fun contains(node: Node): Boolean {
return mAdapter?.contains(node) ?: false
}
fun addNode(newNode: Node) {
mAdapter?.addNode(newNode)
}
fun addNodes(newNodes: List<Node>) {
mAdapter?.addNodes(newNodes)
}
fun updateNode(oldNode: Node, newNode: Node? = null) {
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
}
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
mAdapter?.updateNodes(oldNodes, newNodes)
}
fun removeNode(pwNode: Node) {
mAdapter?.removeNode(pwNode)
}
fun removeNodes(nodes: List<Node>) {
mAdapter?.removeNodes(nodes)
}
fun removeNodeAt(position: Int) {
mAdapter?.removeNodeAt(position)
}
fun removeNodesAt(positions: IntArray) {
mAdapter?.removeNodesAt(positions)
}
/** /**
* Callback listener to redefine to do an action when a node is click * Callback listener to redefine to do an action when a node is click
*/ */
interface NodeClickListener { interface NodeClickListener {
fun onNodeClick(node: Node) fun onNodeClick(database: Database, node: Node)
fun onNodeSelected(nodes: List<Node>): Boolean fun onNodeSelected(database: Database, nodes: List<Node>): Boolean
} }
/** /**
* Menu listener to redefine to do an action in menu * Menu listener to redefine to do an action in menu
*/ */
interface NodesActionMenuListener { interface NodesActionMenuListener {
fun onOpenMenuClick(node: Node): Boolean fun onOpenMenuClick(database: Database, node: Node): Boolean
fun onEditMenuClick(node: Node): Boolean fun onEditMenuClick(database: Database, node: Node): Boolean
fun onCopyMenuClick(nodes: List<Node>): Boolean fun onCopyMenuClick(database: Database, nodes: List<Node>): Boolean
fun onMoveMenuClick(nodes: List<Node>): Boolean fun onMoveMenuClick(database: Database, nodes: List<Node>): Boolean
fun onDeleteMenuClick(nodes: List<Node>): Boolean fun onDeleteMenuClick(database: Database, nodes: List<Node>): Boolean
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<Node>): Boolean fun onPasteMenuClick(database: Database, pasteMode: PasteMode?, nodes: List<Node>): Boolean
} }
enum class PasteMode { enum class PasteMode {
@@ -469,22 +456,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
} }
companion object { companion object {
private val TAG = GroupFragment::class.java.name
private val TAG = ListNodesFragment::class.java.name
private const val GROUP_KEY = "GROUP_KEY"
private const val IS_SEARCH = "IS_SEARCH"
fun newInstance(group: Group?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
val bundle = Bundle()
if (group != null) {
bundle.putParcelable(GROUP_KEY, group)
}
bundle.putBoolean(IS_SEARCH, isASearch)
ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly)
val listNodesFragment = ListNodesFragment()
listNodesFragment.arguments = bundle
return listNodesFragment
}
} }
} }

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
@@ -31,8 +32,8 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
return R.layout.fragment_icon_grid return R.layout.fragment_icon_grid
} }
override fun defineIconList() { override fun defineIconList(database: Database?) {
mDatabase?.doForEachCustomIcons { customIcon, _ -> database?.doForEachCustomIcons { customIcon, _ ->
iconPickerAdapter.addIcon(customIcon, false) iconPickerAdapter.addIcon(customIcon, false)
} }
} }

View File

@@ -19,7 +19,6 @@
*/ */
package com.kunzisoft.keepass.activities.fragments package com.kunzisoft.keepass.activities.fragments
import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -28,7 +27,6 @@ import android.view.ViewGroup
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.IconPickerAdapter import com.kunzisoft.keepass.adapters.IconPickerAdapter
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageDraw import com.kunzisoft.keepass.database.element.icon.IconImageDraw
@@ -38,39 +36,48 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
abstract class IconFragment<T: IconImageDraw> : StylishFragment(), abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
IconPickerAdapter.IconPickerListener<T> { IconPickerAdapter.IconPickerListener<T> {
protected lateinit var iconsGridView: RecyclerView protected lateinit var iconsGridView: RecyclerView
protected lateinit var iconPickerAdapter: IconPickerAdapter<T> protected lateinit var iconPickerAdapter: IconPickerAdapter<T>
protected var iconActionSelectionMode = false protected var iconActionSelectionMode = false
protected var mDatabase: Database? = null
protected val iconPickerViewModel: IconPickerViewModel by activityViewModels() protected val iconPickerViewModel: IconPickerViewModel by activityViewModels()
abstract fun retrieveMainLayoutId(): Int abstract fun retrieveMainLayoutId(): Int
abstract fun defineIconList() abstract fun defineIconList(database: Database?)
override fun onAttach(context: Context) { override fun onCreateView(inflater: LayoutInflater,
super.onAttach(context) container: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflate(retrieveMainLayoutId(), container, false)
}
mDatabase = Database.getInstance() override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Retrieve the textColor to tint the icon // Retrieve the textColor to tint the icon
val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
ta?.recycle() ta?.recycle()
iconPickerAdapter = IconPickerAdapter<T>(context, tintColor).apply { iconsGridView = view.findViewById(R.id.icons_grid_view)
iconDrawableFactory = mDatabase?.iconDrawableFactory iconPickerAdapter = IconPickerAdapter(requireContext(), tintColor)
} iconPickerAdapter.iconPickerListener = this
iconsGridView.adapter = iconPickerAdapter
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
override fun onDatabaseRetrieved(database: Database?) {
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val populateList = launch { val populateList = launch {
iconPickerAdapter.clear() iconPickerAdapter.clear()
defineIconList() defineIconList(database)
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
populateList.join() populateList.join()
@@ -79,21 +86,6 @@ abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
} }
} }
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
val root = inflater.inflate(retrieveMainLayoutId(), container, false)
iconsGridView = root.findViewById(R.id.icons_grid_view)
iconsGridView.adapter = iconPickerAdapter
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
iconPickerAdapter.iconPickerListener = this
}
fun onIconDeleteClicked() { fun onIconDeleteClicked() {
iconActionSelectionMode = false iconActionSelectionMode = false
} }

View File

@@ -9,20 +9,18 @@ import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
class IconPickerFragment : StylishFragment() { class IconPickerFragment : DatabaseFragment() {
private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null
private lateinit var viewPager: ViewPager2 private lateinit var viewPager: ViewPager2
private lateinit var tabLayout: TabLayout
private val iconPickerViewModel: IconPickerViewModel by activityViewModels() private val iconPickerViewModel: IconPickerViewModel by activityViewModels()
private var mDatabase: Database? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -32,19 +30,11 @@ class IconPickerFragment : StylishFragment() {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mDatabase = Database.getInstance() super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.icon_picker_pager) viewPager = view.findViewById(R.id.icon_picker_pager)
val tabLayout = view.findViewById<TabLayout>(R.id.icon_picker_tabs) tabLayout = view.findViewById(R.id.icon_picker_tabs)
iconPickerPagerAdapter = IconPickerPagerAdapter(this, resetAppTimeoutWhenViewFocusedOrChanged(view)
if (mDatabase?.allowCustomIcons == true) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {
1 -> getString(R.string.icon_section_custom)
else -> getString(R.string.icon_section_standard)
}
}.attach()
arguments?.apply { arguments?.apply {
if (containsKey(ICON_TAB_ARG)) { if (containsKey(ICON_TAB_ARG)) {
@@ -58,6 +48,18 @@ class IconPickerFragment : StylishFragment() {
} }
} }
override fun onDatabaseRetrieved(database: Database?) {
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
if (database?.allowCustomIcons == true) 2 else 1)
viewPager.adapter = iconPickerPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {
1 -> getString(R.string.icon_section_custom)
else -> getString(R.string.icon_section_standard)
}
}.attach()
}
enum class IconTab { enum class IconTab {
STANDARD, CUSTOM STANDARD, CUSTOM
} }

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.activities.fragments package com.kunzisoft.keepass.activities.fragments
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.icon.IconImageStandard
@@ -29,8 +30,8 @@ class IconStandardFragment : IconFragment<IconImageStandard>() {
return R.layout.fragment_icon_grid return R.layout.fragment_icon_grid
} }
override fun defineIconList() { override fun defineIconList(database: Database?) {
mDatabase?.doForEachStandardIcons { standardIcon -> database?.doForEachStandardIcons { standardIcon ->
iconPickerAdapter.addIcon(standardIcon, false) iconPickerAdapter.addIcon(standardIcon, false)
} }
} }

View File

@@ -127,17 +127,7 @@ class ExternalFileHelper {
if (data != null) { if (data != null) {
val uri = data.data val uri = data.data
if (uri != null) { if (uri != null) {
try { UriUtil.takeUriPermission(activity?.contentResolver, uri)
// try to persist read and write permissions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
activity?.contentResolver?.apply {
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
}
} catch (e: Exception) {
// nop
}
onFileSelected?.invoke(uri) onFileSelected?.invoke(uri)
} }
} }

View File

@@ -1,78 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.helpers
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.kunzisoft.keepass.settings.PreferencesUtil
object ReadOnlyHelper {
private const val READ_ONLY_KEY = "READ_ONLY_KEY"
const val READ_ONLY_DEFAULT = false
fun retrieveReadOnlyFromIntent(intent: Intent): Boolean {
return intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
}
fun retrieveReadOnlyFromInstanceStateOrPreference(context: Context, savedInstanceState: Bundle?): Boolean {
return if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
savedInstanceState.getBoolean(READ_ONLY_KEY)
} else {
PreferencesUtil.enableReadOnlyDatabase(context)
}
}
fun retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState: Bundle?, arguments: Bundle?): Boolean {
var readOnly = READ_ONLY_DEFAULT
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
} else if (arguments != null && arguments.containsKey(READ_ONLY_KEY)) {
readOnly = arguments.getBoolean(READ_ONLY_KEY)
}
return readOnly
}
fun retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState: Bundle?, intent: Intent?): Boolean {
var readOnly = READ_ONLY_DEFAULT
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
} else {
if (intent != null)
readOnly = intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
}
return readOnly
}
fun putReadOnlyInIntent(intent: Intent, readOnly: Boolean) {
intent.putExtra(READ_ONLY_KEY, readOnly)
}
fun putReadOnlyInBundle(bundle: Bundle, readOnly: Boolean) {
bundle.putBoolean(READ_ONLY_KEY, readOnly)
}
fun onSaveInstanceState(outState: Bundle, readOnly: Boolean) {
outState.putBoolean(READ_ONLY_KEY, readOnly)
}
}

View File

@@ -0,0 +1,80 @@
package com.kunzisoft.keepass.activities.legacy
import android.net.Uri
import android.os.Bundle
import androidx.activity.viewModels
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
protected var mDatabase: Database? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
val databaseWasReloaded = database?.wasReloaded == true
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
finish()
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
database?.wasReloaded = false
onDatabaseRetrieved(database)
}
}
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
onDatabaseActionFinished(database, actionTask, result)
}
}
override fun onDatabaseRetrieved(database: Database?) {
mDatabase = database
mDatabaseViewModel.defineDatabase(database)
// optional method implementation
}
override fun onDatabaseActionFinished(
database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
mDatabaseViewModel.onActionFinished(database, actionTask, result)
// optional method implementation
}
fun createDatabase(databaseUri: Uri,
mainCredential: MainCredential) {
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
}
fun loadDatabase(databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEntity: CipherDatabaseEntity?,
fixDuplicateUuid: Boolean) {
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEntity, fixDuplicateUuid)
}
protected fun closeDatabase() {
mDatabase?.clearAndClose(this)
}
override fun onResume() {
super.onResume()
mDatabaseTaskProvider?.registerProgressTask()
}
override fun onPause() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
}
}

View File

@@ -0,0 +1,479 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.legacy
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.viewModels
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.NodesViewModel
import java.util.*
abstract class DatabaseLockActivity : DatabaseModeActivity(),
PasswordEncodingDialogFragment.Listener {
private val mNodesViewModel: NodesViewModel by viewModels()
protected var mTimeoutEnable: Boolean = true
private var mLockReceiver: LockReceiver? = null
private var mExitLock: Boolean = false
protected var mDatabaseReadOnly: Boolean = true
private var mAutoSaveEnable: Boolean = true
protected var mIconDrawableFactory: IconDrawableFactory? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)
) {
mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
} else {
if (intent != null)
mTimeoutEnable =
intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
}
mNodesViewModel.nodesToPermanentlyDelete.observe(this) { nodes ->
deleteDatabaseNodes(nodes)
}
mDatabaseViewModel.saveDatabase.observe(this) { save ->
mDatabaseTaskProvider?.startDatabaseSave(save)
}
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
}
mDatabaseViewModel.saveName.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveDescription.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveDefaultUsername.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveColor.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveCompression.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.removeUnlinkData.observe(this) {
mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it)
}
mDatabaseViewModel.saveRecycleBin.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveTemplatesGroup.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMaxHistoryItems.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMaxHistorySize.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveEncryption.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveKeyDerivation.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveIterations.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveMemoryUsage.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveParallelism.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save)
}
mExitLock = false
}
open fun finishActivityIfDatabaseNotLoaded(): Boolean {
return true
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
// End activity if database not loaded
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) {
finish()
}
// Focus view to reinitialize timeout,
// view is not necessary loaded so retry later in resume
viewToInvalidateTimeout()
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
database?.let {
// check timeout
if (mTimeoutEnable) {
if (mLockReceiver == null) {
mLockReceiver = LockReceiver {
mDatabase = null
closeDatabase(database)
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
// Add onActivityForResult response
setResult(RESULT_EXIT_LOCK)
closeOptionsMenu()
finish()
}
registerLockReceiver(mLockReceiver)
}
// After the first creation
// or If simply swipe with another application
// If the time is out -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
// If onCreate already record time
if (!mExitLock)
TimeoutHelper.recordTime(this, database.loaded)
}
mDatabaseReadOnly = database.isReadOnly
mIconDrawableFactory = database.iconDrawableFactory
checkRegister()
}
}
abstract fun viewToInvalidateTimeout(): View?
override fun onDatabaseActionFinished(
database: Database,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity
if (result.isSuccess) {
reloadActivity()
} else {
this.showActionErrorIfNeeded(result)
finish()
}
}
}
}
override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
mainCredential: MainCredential) {
assignDatabasePassword(databaseUri, mainCredential)
}
private fun assignDatabasePassword(databaseUri: Uri?,
mainCredential: MainCredential) {
if (databaseUri != null) {
mDatabaseTaskProvider?.startDatabaseAssignPassword(databaseUri, mainCredential)
}
}
fun assignPassword(mainCredential: MainCredential) {
mDatabase?.let { database ->
database.fileUri?.let { databaseUri ->
// Show the progress dialog now or after dialog confirmation
if (database.validatePasswordEncoding(mainCredential)) {
assignDatabasePassword(databaseUri, mainCredential)
} else {
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
.show(supportFragmentManager, "passwordEncodingTag")
}
}
}
}
fun saveDatabase() {
mDatabaseTaskProvider?.startDatabaseSave(true)
}
fun reloadDatabase() {
mDatabaseTaskProvider?.startDatabaseReload(false)
}
fun createEntry(newEntry: Entry,
parent: Group) {
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable)
}
fun updateEntry(oldEntry: Entry,
entryToUpdate: Entry) {
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
}
fun copyNodes(nodesToCopy: List<Node>,
newParent: Group) {
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable)
}
fun moveNodes(nodesToMove: List<Node>,
newParent: Group) {
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
}
private fun eachNodeRecyclable(database: Database, nodes: List<Node>): Boolean {
return nodes.find { node ->
var cannotRecycle = true
if (node is Entry) {
cannotRecycle = !database.canRecycle(node)
} else if (node is Group) {
cannotRecycle = !database.canRecycle(node)
}
cannotRecycle
} == null
}
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
mDatabase?.let { database ->
// If recycle bin enabled, ensure it exists
if (database.isRecycleBinEnabled) {
database.ensureRecycleBinExists(resources)
}
// If recycle bin enabled and not in recycle bin, move in recycle bin
if (eachNodeRecyclable(database, nodes)) {
deleteDatabaseNodes(nodes)
}
// else open the dialog to confirm deletion
else {
DeleteNodesDialogFragment.getInstance(recycleBin)
.show(supportFragmentManager, "deleteNodesDialogFragment")
mNodesViewModel.deleteNodes(nodes)
}
}
}
private fun deleteDatabaseNodes(nodes: List<Node>) {
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable)
}
fun createGroup(parent: Group,
groupInfo: GroupInfo?) {
// Build the group
mDatabase?.createGroup()?.let { newGroup ->
groupInfo?.let { info ->
newGroup.setGroupInfo(info)
}
// Not really needed here because added in runnable but safe
newGroup.parent = parent
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable)
}
}
fun updateGroup(oldGroup: Group,
groupInfo: GroupInfo) {
// If group updated save it in the database
val updateGroup = Group(oldGroup).let { updateGroup ->
updateGroup.apply {
// WARNING remove parent and children to keep memory
removeParent()
removeChildren()
this.setGroupInfo(groupInfo)
}
}
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable)
}
fun restoreEntryHistory(mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int) {
mDatabaseTaskProvider
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
}
fun deleteEntryHistory(mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int) {
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_EXIT_LOCK) {
mExitLock = true
lockAndExit()
}
}
private fun checkRegister() {
// If in ave or registration mode, don't allow read only
if ((mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
&& mDatabaseReadOnly) {
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
EntrySelectionHelper.removeModesFromIntent(intent)
finish()
}
}
override fun onResume() {
super.onResume()
// To refresh when back to normal workflow from selection workflow
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
// Invalidate timeout by touch
mDatabase?.let { database ->
viewToInvalidateTimeout()
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
}
invalidateOptionsMenu()
LOCKING_ACTIVITY_UI_VISIBLE = true
}
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
mDatabase?.loaded == true,
action)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
super.onSaveInstanceState(outState)
}
override fun onPause() {
LOCKING_ACTIVITY_UI_VISIBLE = false
super.onPause()
if (mTimeoutEnable) {
// If the time is out during our navigation in activity -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
}
}
override fun onDestroy() {
unregisterLockReceiver(mLockReceiver)
super.onDestroy()
}
protected fun lockAndExit() {
sendBroadcast(Intent(LOCK_ACTION))
}
fun resetAppTimeout() {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
mDatabase?.loaded ?: false)
}
override fun onBackPressed() {
if (mTimeoutEnable) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
mDatabase?.loaded == true) {
super.onBackPressed()
}
} else {
super.onBackPressed()
}
}
companion object {
const val TAG = "LockingActivity"
const val RESULT_EXIT_LOCK = 1450
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
private var LOCKING_ACTIVITY_UI_VISIBLE = false
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
}
}
/**
* To reset the app timeout when a view is focused or changed
*/
@SuppressLint("ClickableViewAccessibility")
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
databaseLoaded ?: false)
}
}
false
}
setOnFocusChangeListener { _, _ ->
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
databaseLoaded ?: false)
}
if (this is ViewGroup) {
for (i in 0..childCount) {
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
}
}
}

View File

@@ -1,4 +1,4 @@
package com.kunzisoft.keepass.activities.selection package com.kunzisoft.keepass.activities.legacy
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
@@ -7,15 +7,14 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.helpers.TypeMode import com.kunzisoft.keepass.activities.helpers.TypeMode
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.SpecialModeView import com.kunzisoft.keepass.view.SpecialModeView
/** /**
* Activity to manage special mode (ie: selection mode) * Activity to manage database special mode (ie: selection mode)
*/ */
abstract class SpecialModeActivity : StylishActivity() { abstract class DatabaseModeActivity : DatabaseActivity() {
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
private var mTypeMode: TypeMode = TypeMode.DEFAULT private var mTypeMode: TypeMode = TypeMode.DEFAULT

View File

@@ -0,0 +1,11 @@
package com.kunzisoft.keepass.activities.legacy
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.tasks.ActionRunnable
interface DatabaseRetrieval {
fun onDatabaseRetrieved(database: Database?)
fun onDatabaseActionFinished(database: Database,
actionTask: String,
result: ActionRunnable.Result)
}

View File

@@ -1,215 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.lock
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.*
abstract class LockingActivity : SpecialModeActivity() {
protected var mTimeoutEnable: Boolean = true
private var mLockReceiver: LockReceiver? = null
private var mExitLock: Boolean = false
// Force readOnly if Entry Selection mode
protected var mReadOnly: Boolean
get() {
return mReadOnlyToSave
}
set(value) {
mReadOnlyToSave = value
}
private var mReadOnlyToSave: Boolean = false
protected var mAutoSaveEnable: Boolean = true
var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
private set
override fun onCreate(savedInstanceState: Bundle?) {
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
super.onCreate(savedInstanceState)
if (savedInstanceState != null
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)) {
mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
} else {
if (intent != null)
mTimeoutEnable = intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
}
if (mTimeoutEnable) {
mLockReceiver = LockReceiver {
closeDatabase()
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
// Add onActivityForResult response
setResult(RESULT_EXIT_LOCK)
closeOptionsMenu()
finish()
}
registerLockReceiver(mLockReceiver)
}
mExitLock = false
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_EXIT_LOCK) {
mExitLock = true
if (Database.getInstance().loaded) {
lockAndExit()
}
}
}
override fun onResume() {
super.onResume()
// If in ave or registration mode, don't allow read only
if ((mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
&& mReadOnly) {
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
EntrySelectionHelper.removeModesFromIntent(intent)
finish()
}
mProgressDatabaseTaskProvider?.registerProgressTask()
// To refresh when back to normal workflow from selection workflow
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
invalidateOptionsMenu()
if (mTimeoutEnable) {
// End activity if database not loaded
if (!Database.getInstance().loaded) {
finish()
return
}
// After the first creation
// or If simply swipe with another application
// If the time is out -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
// If onCreate already record time
if (!mExitLock)
TimeoutHelper.recordTime(this)
}
LOCKING_ACTIVITY_UI_VISIBLE = true
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
super.onSaveInstanceState(outState)
}
override fun onPause() {
LOCKING_ACTIVITY_UI_VISIBLE = false
mProgressDatabaseTaskProvider?.unregisterProgressTask()
super.onPause()
if (mTimeoutEnable) {
// If the time is out during our navigation in activity -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
}
}
override fun onDestroy() {
unregisterLockReceiver(mLockReceiver)
super.onDestroy()
}
protected fun lockAndExit() {
sendBroadcast(Intent(LOCK_ACTION))
}
override fun onBackPressed() {
if (mTimeoutEnable) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
super.onBackPressed()
}
} else {
super.onBackPressed()
}
}
companion object {
const val TAG = "LockingActivity"
const val RESULT_EXIT_LOCK = 1450
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
private var LOCKING_ACTIVITY_UI_VISIBLE = false
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
}
}
/**
* To reset the app timeout when a view is focused or changed
*/
@SuppressLint("ClickableViewAccessibility")
fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context) {
setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//Log.d(LockingActivity.TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
}
}
false
}
setOnFocusChangeListener { _, _ ->
//Log.d(LockingActivity.TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
}
if (this is ViewGroup) {
for (i in 0..childCount) {
getChildAt(i)?.resetAppTimeoutWhenViewFocusedOrChanged(context)
}
}
}

View File

@@ -22,10 +22,13 @@ package com.kunzisoft.keepass.activities.stylish
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.annotation.StyleRes import android.os.Handler
import androidx.appcompat.app.AppCompatActivity import android.os.Looper
import android.util.Log import android.util.Log
import android.view.WindowManager import android.view.WindowManager
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_APPEARANCE_PREFERENCE_CHANGED
/** /**
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from * Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
@@ -35,6 +38,7 @@ abstract class StylishActivity : AppCompatActivity() {
@StyleRes @StyleRes
private var themeId: Int = 0 private var themeId: Int = 0
private var customStyle = true
/* (non-Javadoc) Workaround for HTC Linkify issues /* (non-Javadoc) Workaround for HTC Linkify issues
* @see android.app.Activity#startActivity(android.content.Intent) * @see android.app.Activity#startActivity(android.content.Intent)
@@ -52,10 +56,30 @@ abstract class StylishActivity : AppCompatActivity() {
} }
} }
open fun applyCustomStyle(): Boolean {
return true
}
open fun finishActivityIfReloadRequested(): Boolean {
return false
}
open fun reloadActivity() {
if (!finishActivityIfReloadRequested()) {
startActivity(intent)
}
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
this.themeId = Stylish.getThemeId(this)
setTheme(themeId) customStyle = applyCustomStyle()
if (customStyle) {
this.themeId = Stylish.getThemeId(this)
setTheme(themeId)
}
// Several gingerbread devices have problems with FLAG_SECURE // Several gingerbread devices have problems with FLAG_SECURE
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
@@ -63,9 +87,17 @@ abstract class StylishActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (Stylish.getThemeId(this) != this.themeId) {
if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|| DATABASE_APPEARANCE_PREFERENCE_CHANGED) {
DATABASE_APPEARANCE_PREFERENCE_CHANGED = false
Log.d(this.javaClass.name, "Theme change detected, restarting activity") Log.d(this.javaClass.name, "Theme change detected, restarting activity")
this.recreate() recreateActivity()
} }
} }
private fun recreateActivity() {
// To prevent KitKat bugs
Handler(Looper.getMainLooper()).post { recreate() }
}
} }

View File

@@ -26,13 +26,13 @@ import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.model.EntryInfo
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() { class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context) private val inflater: LayoutInflater = LayoutInflater.from(context)
var entryHistoryList: MutableList<Entry> = ArrayList() var entryHistoryList: MutableList<EntryInfo> = ArrayList()
var onItemClickListener: ((item: Entry, position: Int)->Unit)? = null var onItemClickListener: ((item: EntryInfo, position: Int)->Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder {
return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false)) return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false))
@@ -44,7 +44,6 @@ class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHist
holder.lastModifiedView.text = entryHistory.lastModificationTime.getDateTimeString(context.resources) holder.lastModifiedView.text = entryHistory.lastModificationTime.getDateTimeString(context.resources)
holder.titleView.text = entryHistory.title holder.titleView.text = entryHistory.title
holder.usernameView.text = entryHistory.username holder.usernameView.text = entryHistory.username
holder.urlView.text = entryHistory.url
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
onItemClickListener?.invoke(entryHistory, position) onItemClickListener?.invoke(entryHistory, position)
@@ -64,6 +63,5 @@ class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHist
var lastModifiedView: TextView = itemView.findViewById(R.id.entry_history_last_modified) var lastModifiedView: TextView = itemView.findViewById(R.id.entry_history_last_modified)
var titleView: TextView = itemView.findViewById(R.id.entry_history_title) var titleView: TextView = itemView.findViewById(R.id.entry_history_title)
var usernameView: TextView = itemView.findViewById(R.id.entry_history_username) var usernameView: TextView = itemView.findViewById(R.id.entry_history_username)
var urlView: TextView = itemView.findViewById(R.id.entry_history_url)
} }
} }

View File

@@ -27,7 +27,7 @@ import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.Field import com.kunzisoft.keepass.database.element.Field
import java.util.ArrayList import java.util.ArrayList

View File

@@ -30,6 +30,8 @@ import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.DatabaseFile import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.view.collapse import com.kunzisoft.keepass.view.collapse
@@ -44,11 +46,43 @@ class FileDatabaseHistoryAdapter(context: Context)
private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null
private var saveAliasListener: ((DatabaseFile)->Unit)? = null private var saveAliasListener: ((DatabaseFile)->Unit)? = null
private val listDatabaseFiles = ArrayList<DatabaseFile>() private var mDefaultDatabase: DatabaseFile? = null
private var mExpandedDatabaseFile: SuperDatabaseFile? = null
private var mPreviousExpandedDatabaseFile: SuperDatabaseFile? = null
private var mDefaultDatabaseFile: DatabaseFile? = null private val mListPosition = mutableListOf<SuperDatabaseFile>()
private var mExpandedDatabaseFile: DatabaseFile? = null private val mSortedListDatabaseFiles = SortedList(SuperDatabaseFile::class.java,
private var mPreviousExpandedDatabaseFile: DatabaseFile? = null object: SortedListAdapterCallback<SuperDatabaseFile>(this) {
override fun compare(item1: SuperDatabaseFile, item2: SuperDatabaseFile): Int {
val indexItem1 = mListPosition.indexOf(item1)
val indexItem2 = mListPosition.indexOf(item2)
return if (indexItem1 == -1 && indexItem2 == -1)
-1
else if (indexItem1 < indexItem2)
-1
else if (indexItem1 > indexItem2)
1
else
0
}
override fun areContentsTheSame(oldItem: SuperDatabaseFile, newItem: SuperDatabaseFile): Boolean {
val oldDatabaseFile = oldItem.databaseFile
val newDatabaseFile = newItem.databaseFile
return oldDatabaseFile.databaseUri == newDatabaseFile.databaseUri
&& oldDatabaseFile.databaseDecodedPath == newDatabaseFile.databaseDecodedPath
&& oldDatabaseFile.databaseAlias == newDatabaseFile.databaseAlias
&& oldDatabaseFile.databaseFileExists == newDatabaseFile.databaseFileExists
&& oldDatabaseFile.databaseLastModified == newDatabaseFile.databaseLastModified
&& oldDatabaseFile.databaseSize == newDatabaseFile.databaseSize
&& oldItem.default == newItem.default
}
override fun areItemsTheSame(item1: SuperDatabaseFile, item2: SuperDatabaseFile): Boolean {
return item1.databaseFile == item2.databaseFile
}
}
)
@ColorInt @ColorInt
private val defaultColor: Int private val defaultColor: Int
@@ -71,7 +105,8 @@ class FileDatabaseHistoryAdapter(context: Context)
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) { override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
// Get info from position // Get info from position
val databaseFile = listDatabaseFiles[position] val superDatabaseFile = mSortedListDatabaseFiles[position]
val databaseFile = superDatabaseFile.databaseFile
// Click item to open file // Click item to open file
holder.fileContainer.setOnClickListener { holder.fileContainer.setOnClickListener {
@@ -80,7 +115,7 @@ class FileDatabaseHistoryAdapter(context: Context)
// Default database // Default database
holder.defaultFileButton.apply { holder.defaultFileButton.apply {
this.isChecked = mDefaultDatabaseFile == databaseFile this.isChecked = superDatabaseFile.default
setOnClickListener { setOnClickListener {
defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null) defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null)
} }
@@ -115,7 +150,7 @@ class FileDatabaseHistoryAdapter(context: Context)
} }
// Click on information // Click on information
val isExpanded = databaseFile == mExpandedDatabaseFile val isExpanded = superDatabaseFile == mExpandedDatabaseFile
// Hides or shows info // Hides or shows info
holder.fileExpandContainer.apply { holder.fileExpandContainer.apply {
if (isExpanded) { if (isExpanded) {
@@ -151,16 +186,16 @@ class FileDatabaseHistoryAdapter(context: Context)
} }
if (isExpanded) { if (isExpanded) {
mPreviousExpandedDatabaseFile = databaseFile mPreviousExpandedDatabaseFile = superDatabaseFile
} }
holder.fileInformationButton.apply { holder.fileInformationButton.apply {
animate().rotation(if (isExpanded) 180F else 0F).start() animate().rotation(if (isExpanded) 180F else 0F).start()
setOnClickListener { setOnClickListener {
mExpandedDatabaseFile = if (isExpanded) null else databaseFile mExpandedDatabaseFile = if (isExpanded) null else superDatabaseFile
// Notify change // Notify change
val previousExpandedPosition = listDatabaseFiles.indexOf(mPreviousExpandedDatabaseFile) val previousExpandedPosition = mListPosition.indexOf(mPreviousExpandedDatabaseFile)
notifyItemChanged(previousExpandedPosition) notifyItemChanged(previousExpandedPosition)
val expandedPosition = listDatabaseFiles.indexOf(mExpandedDatabaseFile) val expandedPosition = mListPosition.indexOf(mExpandedDatabaseFile)
notifyItemChanged(expandedPosition) notifyItemChanged(expandedPosition)
} }
} }
@@ -172,50 +207,67 @@ class FileDatabaseHistoryAdapter(context: Context)
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
return listDatabaseFiles.size return mSortedListDatabaseFiles.size()
} }
fun clearDatabaseFileHistoryList() { fun clearDatabaseFileHistoryList() {
listDatabaseFiles.clear() mListPosition.clear()
mSortedListDatabaseFiles.clear()
} }
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) { fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
listDatabaseFiles.add(0, fileDatabaseHistoryToAdd) val superToAdd = SuperDatabaseFile(fileDatabaseHistoryToAdd)
notifyItemInserted(0) mListPosition.add(0, superToAdd)
mSortedListDatabaseFiles.add(superToAdd)
} }
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) { fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToUpdate) val superToUpdate = SuperDatabaseFile(fileDatabaseHistoryToUpdate)
if (listDatabaseFiles.remove(fileDatabaseHistoryToUpdate)) { val index = mListPosition.indexOf(superToUpdate)
listDatabaseFiles.add(index, fileDatabaseHistoryToUpdate) if (mListPosition.remove(superToUpdate)) {
notifyItemChanged(index) mListPosition.add(index, superToUpdate)
} }
mSortedListDatabaseFiles.updateItemAt(index, superToUpdate)
} }
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) { fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) {
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToDelete) val superToDelete = SuperDatabaseFile(fileDatabaseHistoryToDelete)
if (listDatabaseFiles.remove(fileDatabaseHistoryToDelete)) { val index = mListPosition.indexOf(superToDelete)
notifyItemRemoved(index) mListPosition.remove(superToDelete)
} mSortedListDatabaseFiles.removeItemAt(index)
} }
fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) { fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) {
if (listDatabaseFiles.isEmpty()) { val superMapToReplace = listFileDatabaseHistoryToAdd.map {
listFileDatabaseHistoryToAdd.forEach { SuperDatabaseFile(it)
listDatabaseFiles.add(it)
notifyItemInserted(listDatabaseFiles.size)
}
} else {
listDatabaseFiles.clear()
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
notifyDataSetChanged()
} }
mListPosition.clear()
mListPosition.addAll(superMapToReplace)
mSortedListDatabaseFiles.replaceAll(superMapToReplace)
} }
fun setDefaultDatabase(databaseUri: Uri?) { fun setDefaultDatabase(databaseUri: Uri?) {
val defaultDatabaseFile = listDatabaseFiles.firstOrNull { it.databaseUri == databaseUri } // Remove default from last item
mDefaultDatabaseFile = defaultDatabaseFile val oldDefaultDatabasePosition = mListPosition.indexOfFirst {
notifyDataSetChanged() it.default
}
if (oldDefaultDatabasePosition >= 0) {
val oldDefaultDatabase = mListPosition[oldDefaultDatabasePosition].apply {
default = false
}
mSortedListDatabaseFiles.updateItemAt(oldDefaultDatabasePosition, oldDefaultDatabase)
}
// Add default to new item
val newDefaultDatabaseFilePosition = mListPosition.indexOfFirst {
it.databaseFile.databaseUri == databaseUri
}
if (newDefaultDatabaseFilePosition >= 0) {
val newDefaultDatabase = mListPosition[newDefaultDatabaseFilePosition].apply {
default = true
}
mDefaultDatabase = newDefaultDatabase.databaseFile
mSortedListDatabaseFiles.updateItemAt(newDefaultDatabaseFilePosition, newDefaultDatabase)
}
} }
fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) { fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) {
@@ -234,6 +286,30 @@ class FileDatabaseHistoryAdapter(context: Context)
this.saveAliasListener = listener this.saveAliasListener = listener
} }
private inner class SuperDatabaseFile(
var databaseFile: DatabaseFile,
var default: Boolean = false
) {
init {
if (mDefaultDatabase == databaseFile)
this.default = true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SuperDatabaseFile) return false
if (databaseFile != other.databaseFile) return false
return true
}
override fun hashCode(): Int {
return databaseFile.hashCode()
}
}
class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info) var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)

View File

@@ -26,7 +26,9 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -40,7 +42,11 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.setTextSize import com.kunzisoft.keepass.view.setTextSize
import com.kunzisoft.keepass.view.strikeOut import com.kunzisoft.keepass.view.strikeOut
import java.util.* import java.util.*
@@ -49,7 +55,8 @@ import java.util.*
* Create node list adapter with contextMenu or not * Create node list adapter with contextMenu or not
* @param context Context to use * @param context Context to use
*/ */
class NodeAdapter (private val context: Context) class NodeAdapter (private val context: Context,
private val database: Database)
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() { : RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
@@ -60,19 +67,22 @@ class NodeAdapter (private val context: Context)
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
private var mPrefSizeMultiplier: Float = 0F private var mPrefSizeMultiplier: Float = 0F
private var mSubtextDefaultDimension: Float = 0F private var mTextDefaultDimension: Float = 0F
private var mInfoTextDefaultDimension: Float = 0F private var mSubTextDefaultDimension: Float = 0F
private var mMetaTextDefaultDimension: Float = 0F
private var mOtpTokenTextDefaultDimension: Float = 0F
private var mNumberChildrenTextDefaultDimension: Float = 0F private var mNumberChildrenTextDefaultDimension: Float = 0F
private var mIconDefaultDimension: Float = 0F private var mIconDefaultDimension: Float = 0F
private var mShowUserNames: Boolean = true private var mShowUserNames: Boolean = true
private var mShowNumberEntries: Boolean = true private var mShowNumberEntries: Boolean = true
private var mShowOTP: Boolean = false
private var mShowUUID: Boolean = false
private var mEntryFilters = arrayOf<Group.ChildFilter>() private var mEntryFilters = arrayOf<Group.ChildFilter>()
private var mActionNodesList = LinkedList<Node>() private var mActionNodesList = LinkedList<Node>()
private var mNodeClickCallback: NodeClickCallback? = null private var mNodeClickCallback: NodeClickCallback? = null
private var mClipboardHelper = ClipboardHelper(context)
private val mDatabase: Database
@ColorInt @ColorInt
private val mContentSelectionColor: Int private val mContentSelectionColor: Int
@@ -96,9 +106,6 @@ class NodeAdapter (private val context: Context)
this.mNodeSortedListCallback = NodeSortedListCallback() this.mNodeSortedListCallback = NodeSortedListCallback()
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback) this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
// Database
this.mDatabase = Database.getInstance()
// Color of content selection // Color of content selection
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white) this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
// Retrieve the color to tint the icon // Retrieve the color to tint the icon
@@ -111,7 +118,7 @@ class NodeAdapter (private val context: Context)
taTextColor.recycle() taTextColor.recycle()
} }
fun assignPreferences() { private fun assignPreferences() {
this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context) this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context)
notifyChangeSort( notifyChangeSort(
@@ -125,6 +132,8 @@ class NodeAdapter (private val context: Context)
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context) this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context) this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
this.mShowOTP = PreferencesUtil.showOTPToken(context)
this.mShowUUID = PreferencesUtil.showUUID(context)
this.mEntryFilters = Group.ChildFilter.getDefaults(context) this.mEntryFilters = Group.ChildFilter.getDefaults(context)
@@ -146,9 +155,21 @@ class NodeAdapter (private val context: Context)
} }
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean { override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
return oldItem.type == newItem.type var typeContentTheSame = true
if (oldItem is Entry && newItem is Entry) {
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
&& oldItem.username == newItem.username
&& oldItem.getOtpElement() == newItem.getOtpElement()
&& oldItem.containsAttachment() == newItem.containsAttachment()
} else if (oldItem is Group && newItem is Group) {
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
}
return typeContentTheSame
&& oldItem.nodeId == newItem.nodeId
&& oldItem.type == newItem.type
&& oldItem.title == newItem.title && oldItem.title == newItem.title
&& oldItem.icon == newItem.icon && oldItem.icon == newItem.icon
&& oldItem.isCurrentlyExpires == newItem.isCurrentlyExpires
} }
override fun areItemsTheSame(item1: Node, item2: Node): Boolean { override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
@@ -241,6 +262,10 @@ class NodeAdapter (private val context: Context)
mNodeSortedList.endBatchedUpdates() mNodeSortedList.endBatchedUpdates()
} }
fun indexOf(node: Node): Int {
return mNodeSortedList.indexOf(node)
}
fun notifyNodeChanged(node: Node) { fun notifyNodeChanged(node: Node) {
notifyItemChanged(mNodeSortedList.indexOf(node)) notifyItemChanged(mNodeSortedList.indexOf(node))
} }
@@ -266,7 +291,7 @@ class NodeAdapter (private val context: Context)
*/ */
fun notifyChangeSort(sortNodeEnum: SortNodeEnum, fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
sortNodeParameters: SortNodeEnum.SortNodeParameters) { sortNodeParameters: SortNodeEnum.SortNodeParameters) {
this.mNodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters) this.mNodeComparator = sortNodeEnum.getNodeComparator(database, sortNodeParameters)
} }
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
@@ -280,8 +305,10 @@ class NodeAdapter (private val context: Context)
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false) mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
} }
val nodeViewHolder = NodeViewHolder(view) val nodeViewHolder = NodeViewHolder(view)
mInfoTextDefaultDimension = nodeViewHolder.text.textSize mTextDefaultDimension = nodeViewHolder.text.textSize
mSubtextDefaultDimension = nodeViewHolder.subText.textSize mSubTextDefaultDimension = nodeViewHolder.subText?.textSize ?: mSubTextDefaultDimension
mMetaTextDefaultDimension = nodeViewHolder.meta.textSize
mOtpTokenTextDefaultDimension = nodeViewHolder.otpToken?.textSize ?: mOtpTokenTextDefaultDimension
nodeViewHolder.numberChildren?.let { nodeViewHolder.numberChildren?.let {
mNumberChildrenTextDefaultDimension = it.textSize mNumberChildrenTextDefaultDimension = it.textSize
} }
@@ -292,7 +319,9 @@ class NodeAdapter (private val context: Context)
val subNode = mNodeSortedList.get(position) val subNode = mNodeSortedList.get(position)
// Node selection // Node selection
holder.container.isSelected = mActionNodesList.contains(subNode) holder.container.apply {
isSelected = mActionNodesList.contains(subNode)
}
// Assign image // Assign image
val iconColor = if (holder.container.isSelected) val iconColor = if (holder.container.isSelected)
@@ -303,7 +332,7 @@ class NodeAdapter (private val context: Context)
} }
holder.imageIdentifier?.setColorFilter(iconColor) holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply { holder.icon.apply {
mDatabase.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor) database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
// Relative size of the icon // Relative size of the icon
layoutParams?.apply { layoutParams?.apply {
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt() height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
@@ -314,35 +343,62 @@ class NodeAdapter (private val context: Context)
// Assign text // Assign text
holder.text.apply { holder.text.apply {
text = subNode.title text = subNode.title
setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier) setTextSize(mTextSizeUnit, mTextDefaultDimension, mPrefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires) strikeOut(subNode.isCurrentlyExpires)
} }
// Add subText with username // Add meta text to show UUID
holder.subText.apply { holder.meta.apply {
text = "" if (mShowUUID) {
strikeOut(subNode.isCurrentlyExpires) text = subNode.nodeId.toString()
visibility = View.GONE setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
} }
// Specific elements for entry // Specific elements for entry
if (subNode.type == Type.ENTRY) { if (subNode.type == Type.ENTRY) {
val entry = subNode as Entry val entry = subNode as Entry
mDatabase.startManageEntry(entry) database.startManageEntry(entry)
holder.text.text = entry.getVisualTitle() holder.text.text = entry.getVisualTitle()
holder.subText.apply { // Add subText with username
holder.subText?.apply {
val username = entry.username val username = entry.username
if (mShowUserNames && username.isNotEmpty()) { if (mShowUserNames && username.isNotEmpty()) {
visibility = View.VISIBLE visibility = View.VISIBLE
text = username text = username
setTextSize(mTextSizeUnit, mSubtextDefaultDimension, mPrefSizeMultiplier) setTextSize(mTextSizeUnit, mSubTextDefaultDimension, mPrefSizeMultiplier)
strikeOut(subNode.isCurrentlyExpires)
} else {
visibility = View.GONE
} }
} }
val otpElement = entry.getOtpElement()
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
if (otpElement != null
&& mShowOTP
&& otpElement.token.isNotEmpty()) {
// Execute runnable to show progress
holder.otpRunnable.action = {
populateOtpView(holder, otpElement)
}
if (otpElement.type == OtpType.TOTP) {
holder.otpRunnable.postDelayed()
}
populateOtpView(holder, otpElement)
holder.otpContainer?.visibility = View.VISIBLE
} else {
holder.otpContainer?.visibility = View.GONE
}
holder.attachmentIcon?.visibility = holder.attachmentIcon?.visibility =
if (entry.containsAttachment()) View.VISIBLE else View.GONE if (entry.containsAttachment()) View.VISIBLE else View.GONE
mDatabase.stopManageEntry(entry) database.stopManageEntry(entry)
} }
// Add number of entries in groups // Add number of entries in groups
@@ -350,7 +406,7 @@ class NodeAdapter (private val context: Context)
if (mShowNumberEntries) { if (mShowNumberEntries) {
holder.numberChildren?.apply { holder.numberChildren?.apply {
text = (subNode as Group) text = (subNode as Group)
.getNumberOfChildEntries(mEntryFilters) .numberOfChildEntries
.toString() .toString()
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier) setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE visibility = View.VISIBLE
@@ -362,13 +418,59 @@ class NodeAdapter (private val context: Context)
// Assign click // Assign click
holder.container.setOnClickListener { holder.container.setOnClickListener {
mNodeClickCallback?.onNodeClick(subNode) mNodeClickCallback?.onNodeClick(database, subNode)
} }
holder.container.setOnLongClickListener { holder.container.setOnLongClickListener {
mNodeClickCallback?.onNodeLongClick(subNode) ?: false mNodeClickCallback?.onNodeLongClick(database, subNode) ?: false
} }
} }
private fun populateOtpView(holder: NodeViewHolder?, otpElement: OtpElement?) {
when (otpElement?.type) {
OtpType.HOTP -> {
holder?.otpProgress?.apply {
max = 100
progress = 100
}
}
OtpType.TOTP -> {
holder?.otpProgress?.apply {
max = otpElement.period
progress = otpElement.secondsRemaining
}
}
}
holder?.otpToken?.apply {
text = otpElement?.token
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
}
holder?.otpContainer?.setOnClickListener {
otpElement?.token?.let { token ->
Toast.makeText(
context,
context.getString(R.string.copy_field,
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)),
Toast.LENGTH_LONG
).show()
mClipboardHelper.copyToClipboard(token)
}
}
}
class OtpRunnable(val view: View?): Runnable {
var action: (() -> Unit)? = null
override fun run() {
action?.invoke()
postDelayed()
}
fun postDelayed() {
view?.postDelayed(this, 1000)
}
}
override fun getItemCount(): Int { override fun getItemCount(): Int {
return mNodeSortedList.size() return mNodeSortedList.size()
} }
@@ -384,8 +486,8 @@ class NodeAdapter (private val context: Context)
* Callback listener to redefine to do an action when a node is click * Callback listener to redefine to do an action when a node is click
*/ */
interface NodeClickCallback { interface NodeClickCallback {
fun onNodeClick(node: Node) fun onNodeClick(database: Database, node: Node)
fun onNodeLongClick(node: Node): Boolean fun onNodeLongClick(database: Database, node: Node): Boolean
} }
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@@ -393,7 +495,12 @@ class NodeAdapter (private val context: Context)
var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier) var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
var icon: ImageView = itemView.findViewById(R.id.node_icon) var icon: ImageView = itemView.findViewById(R.id.node_icon)
var text: TextView = itemView.findViewById(R.id.node_text) var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView = itemView.findViewById(R.id.node_subtext) var subText: TextView? = itemView.findViewById(R.id.node_subtext)
var meta: TextView = itemView.findViewById(R.id.node_meta)
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress)
var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token)
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers) var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon) var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
} }

View File

@@ -0,0 +1,70 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.icons.IconDrawableFactory
class TemplatesSelectorAdapter(private val context: Context,
private val iconDrawableFactory: IconDrawableFactory?,
private var templates: List<Template>): BaseAdapter() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var mIconColor = Color.BLACK
init {
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
mIconColor = taIconColor.getColor(0, Color.BLACK)
taIconColor.recycle()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val template: Template = getItem(position)
val holder: TemplateSelectorViewHolder
var templateView = convertView
if (templateView == null) {
holder = TemplateSelectorViewHolder()
templateView = inflater.inflate(R.layout.item_template, parent, false)
holder.icon = templateView?.findViewById(R.id.template_image)
holder.name = templateView?.findViewById(R.id.template_name)
templateView?.tag = holder
} else {
holder = templateView.tag as TemplateSelectorViewHolder
}
holder.icon?.let { icon ->
iconDrawableFactory?.assignDatabaseIcon(icon, template.icon, mIconColor)
}
holder.name?.text = TemplateField.getLocalizedName(context, template.title)
return templateView!!
}
override fun getCount(): Int {
return templates.size
}
override fun getItem(position: Int): Template {
return templates[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
inner class TemplateSelectorViewHolder {
var icon: ImageView? = null
var name: TextView? = null
}
}

View File

@@ -21,8 +21,6 @@ package com.kunzisoft.keepass.app
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import com.kunzisoft.keepass.activities.stylish.Stylish import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.utils.UriUtil
class App : MultiDexApplication() { class App : MultiDexApplication() {
@@ -32,9 +30,4 @@ class App : MultiDexApplication() {
Stylish.load(this) Stylish.load(this)
PRNGFixes.apply() PRNGFixes.apply()
} }
override fun onTerminate() {
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
super.onTerminate()
}
} }

View File

@@ -189,26 +189,36 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
).execute() ).execute()
} }
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) { fun deleteKeyFileByDatabaseUri(databaseUri: Uri,
result: (() ->Unit)? = null) {
IOActionTask( IOActionTask(
{ {
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString()) databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
},
{
result?.invoke()
} }
).execute() ).execute()
} }
fun deleteAllKeyFiles() { fun deleteAllKeyFiles(result: (() ->Unit)? = null) {
IOActionTask( IOActionTask(
{ {
databaseFileHistoryDao.deleteAllKeyFiles() databaseFileHistoryDao.deleteAllKeyFiles()
},
{
result?.invoke()
} }
).execute() ).execute()
} }
fun deleteAll() { fun deleteAll(result: (() ->Unit)? = null) {
IOActionTask( IOActionTask(
{ {
databaseFileHistoryDao.deleteAll() databaseFileHistoryDao.deleteAll()
},
{
result?.invoke()
} }
).execute() ).execute()
} }

View File

@@ -25,6 +25,7 @@ import android.app.PendingIntent
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentSender
import android.graphics.BlendMode import android.graphics.BlendMode
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
@@ -37,19 +38,23 @@ import android.view.autofill.AutofillValue
import android.view.inputmethod.InlineSuggestionsRequest import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.Toast import android.widget.Toast
import android.widget.inline.InlinePresentationSpec
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import kotlin.collections.ArrayList
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
@@ -85,13 +90,14 @@ object AutofillHelper {
} }
private fun newRemoteViews(context: Context, private fun newRemoteViews(context: Context,
database: Database,
remoteViewsText: String, remoteViewsText: String,
remoteViewsIcon: IconImage? = null): RemoteViews { remoteViewsIcon: IconImage? = null): RemoteViews {
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry) val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText) presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
if (remoteViewsIcon != null) { if (remoteViewsIcon != null) {
try { try {
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context, database.iconDrawableFactory.getBitmapFromIcon(context,
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap -> remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap) presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
} }
@@ -103,19 +109,96 @@ object AutofillHelper {
} }
private fun buildDataset(context: Context, private fun buildDataset(context: Context,
entryInfo: EntryInfo, database: Database,
struct: StructureParser.Result, entryInfo: EntryInfo,
inlinePresentation: InlinePresentation?): Dataset? { struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset? {
val title = makeEntryTitle(entryInfo) val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context, title, entryInfo.icon) val views = newRemoteViews(context, database, title, entryInfo.icon)
val builder = Dataset.Builder(views) val builder = Dataset.Builder(views)
builder.setId(entryInfo.id) builder.setId(entryInfo.id.toString())
struct.usernameId?.let { usernameId -> struct.usernameId?.let { usernameId ->
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username)) builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
} }
struct.passwordId?.let { password -> struct.passwordId?.let { passwordId ->
builder.setValue(password, AutofillValue.forText(entryInfo.password)) builder.setValue(passwordId, AutofillValue.forText(entryInfo.password))
}
if (entryInfo.expires) {
val year = entryInfo.expiryTime.getYearInt()
val month = entryInfo.expiryTime.getMonthInt()
val monthString = month.toString().padStart(2, '0')
val day = entryInfo.expiryTime.getDay()
val dayString = day.toString().padStart(2, '0')
struct.creditCardExpirationDateId?.let {
if (struct.isWebView) {
// set date string as defined in https://html.spec.whatwg.org
builder.setValue(it, AutofillValue.forText("$year\u002D$monthString"))
} else {
builder.setValue(it, AutofillValue.forDate(entryInfo.expiryTime.date.time))
}
}
struct.creditCardExpirationYearId?.let {
var autofillValue: AutofillValue? = null
struct.creditCardExpirationYearOptions?.let { options ->
var yearIndex = options.indexOf(year.toString().substring(0, 2))
if (yearIndex == -1) {
yearIndex = options.indexOf(year.toString())
}
if (yearIndex != -1) {
autofillValue = AutofillValue.forList(yearIndex)
builder.setValue(it, autofillValue)
}
}
if (autofillValue == null) {
builder.setValue(it, AutofillValue.forText(year.toString()))
}
}
struct.creditCardExpirationMonthId?.let {
if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(monthString))
} else {
if (struct.creditCardExpirationMonthOptions != null) {
// index starts at 0
builder.setValue(it, AutofillValue.forList(month - 1))
} else {
builder.setValue(it, AutofillValue.forText(monthString))
}
}
}
struct.creditCardExpirationDayId?.let {
if (struct.isWebView) {
builder.setValue(it, AutofillValue.forText(dayString))
} else {
if (struct.creditCardExpirationDayOptions != null) {
builder.setValue(it, AutofillValue.forList(day - 1))
} else {
builder.setValue(it, AutofillValue.forText(dayString))
}
}
}
}
for (field in entryInfo.customFields) {
if (field.name == TemplateField.LABEL_HOLDER) {
struct.creditCardHolderId?.let { ccNameId ->
builder.setValue(ccNameId, AutofillValue.forText(field.protectedValue.stringValue))
}
}
if (field.name == TemplateField.LABEL_NUMBER) {
struct.creditCardNumberId?.let { ccnId ->
builder.setValue(ccnId, AutofillValue.forText(field.protectedValue.stringValue))
}
}
if (field.name == TemplateField.LABEL_CVV) {
struct.cardVerificationValueId?.let { cvvId ->
builder.setValue(cvvId, AutofillValue.forText(field.protectedValue.stringValue))
}
}
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@@ -126,8 +209,8 @@ object AutofillHelper {
return try { return try {
builder.build() builder.build()
} catch (e: IllegalArgumentException) { } catch (e: Exception) {
// if not value be set // at least one value must be set
null null
} }
} }
@@ -135,9 +218,11 @@ object AutofillHelper {
/** /**
* Method to assign a drawable to a new icon from a database icon * Method to assign a drawable to a new icon from a database icon
*/ */
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? { private fun buildIconFromEntry(context: Context,
database: Database,
entryInfo: EntryInfo): Icon? {
try { try {
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context, database.iconDrawableFactory.getBitmapFromIcon(context,
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap -> entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
return Icon.createWithBitmap(bitmap) return Icon.createWithBitmap(bitmap)
} }
@@ -150,13 +235,14 @@ object AutofillHelper {
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun buildInlinePresentationForEntry(context: Context, private fun buildInlinePresentationForEntry(context: Context,
database: Database,
inlineSuggestionsRequest: InlineSuggestionsRequest, inlineSuggestionsRequest: InlineSuggestionsRequest,
positionItem: Int, positionItem: Int,
entryInfo: EntryInfo): InlinePresentation? { entryInfo: EntryInfo): InlinePresentation? {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
if (positionItem <= maxSuggestion-1 if (positionItem <= maxSuggestion - 1
&& inlinePresentationSpecs.size > positionItem) { && inlinePresentationSpecs.size > positionItem) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem] val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
@@ -178,7 +264,7 @@ object AutofillHelper {
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply { setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST) setTintBlendMode(BlendMode.DST)
}) })
buildIconFromEntry(context, entryInfo)?.let { icon -> buildIconFromEntry(context, database, entryInfo)?.let { icon ->
setEndIcon(icon.apply { setEndIcon(icon.apply {
setTintBlendMode(BlendMode.DST) setTintBlendMode(BlendMode.DST)
}) })
@@ -188,10 +274,32 @@ object AutofillHelper {
return null return null
} }
@RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi")
private fun buildInlinePresentationForManualSelection(context: Context,
inlinePresentationSpec: InlinePresentationSpec,
pendingIntent: PendingIntent): InlinePresentation? {
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
return null
// Build the content for IME UI
return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
setTitle(context.getString(R.string.autofill_select_entry))
setStartIcon(Icon.createWithResource(context, R.drawable.ic_arrow_right_green_24dp).apply {
setTintBlendMode(BlendMode.DST)
})
}.build().slice, inlinePresentationSpec, false)
}
fun buildResponse(context: Context, fun buildResponse(context: Context,
database: Database,
entriesInfo: List<EntryInfo>, entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse { inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? {
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
// Add Header // Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -208,31 +316,85 @@ object AutofillHelper {
} }
} }
} }
// Add inline suggestion for new IME and dataset // Add inline suggestion for new IME and dataset
entriesInfo.forEachIndexed { index, entryInfo -> var numberInlineSuggestions = 0
val inlinePresentation = inlineSuggestionsRequest?.let { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { inlineSuggestionsRequest?.let {
buildInlinePresentationForEntry(context, inlineSuggestionsRequest, index, entryInfo) numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
} else { if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
null if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
--numberInlineSuggestions
}
} }
} }
responseBuilder.addDataset(buildDataset(context, entryInfo, parseResult, inlinePresentation))
}
entriesInfo.forEachIndexed { _, entry ->
val inlinePresentation = if (numberInlineSuggestions > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let {
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, numberInlineSuggestions--, entry)
}
} else {
null
}
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult, inlinePresentation))
}
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
val searchInfo = SearchInfo().apply {
applicationId = parseResult.applicationId
webDomain = parseResult.webDomain
webScheme = parseResult.webScheme
manualSelection = true
}
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
searchInfo, inlineSuggestionsRequest)
parseResult.allAutofillIds().let { autofillIds ->
autofillIds.forEach { id ->
val builder = Dataset.Builder(manualSelectionView)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let {
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
inlinePresentation?.let {
builder.setInlinePresentation(it)
}
}
}
builder.setValue(id, null)
builder.setAuthentication(pendingIntent.intentSender)
responseBuilder.addDataset(builder.build())
}
}
}
return try {
responseBuilder.build()
} catch (e: Exception) {
null
} }
return responseBuilder.build()
} }
/** /**
* Build the Autofill response for one entry * Build the Autofill response for one entry
*/ */
fun buildResponseAndSetResult(activity: Activity, entryInfo: EntryInfo) { fun buildResponseAndSetResult(activity: Activity,
buildResponseAndSetResult(activity, ArrayList<EntryInfo>().apply { add(entryInfo) }) database: Database,
entryInfo: EntryInfo) {
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
} }
/** /**
* Build the Autofill response for many entry * Build the Autofill response for many entry
*/ */
fun buildResponseAndSetResult(activity: Activity, entriesInfo: List<EntryInfo>) { fun buildResponseAndSetResult(activity: Activity,
database: Database,
entriesInfo: List<EntryInfo>) {
if (entriesInfo.isEmpty()) { if (entriesInfo.isEmpty()) {
activity.setResult(Activity.RESULT_CANCELED) activity.setResult(Activity.RESULT_CANCELED)
} else { } else {
@@ -245,9 +407,9 @@ object AutofillHelper {
if (inlineSuggestionsRequest != null) { if (inlineSuggestionsRequest != null) {
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show() Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
} }
buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest) buildResponse(activity, database, entriesInfo, result, inlineSuggestionsRequest)
} else { } else {
buildResponse(activity, entriesInfo, result, null) buildResponse(activity, database, entriesInfo, result, null)
} }
val mReplyIntent = Intent() val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.") Log.d(activity.javaClass.name, "Successed Autofill auth.")

View File

@@ -36,29 +36,47 @@ import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.autofill.inline.v1.InlineSuggestionUi
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.CreditCard
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import org.joda.time.DateTime
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() { class KeeAutofillService : AutofillService() {
var applicationIdBlocklist: Set<String>? = null private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
var webDomainBlocklist: Set<String>? = null private var mDatabase: Database? = null
var askToSaveData: Boolean = false private var applicationIdBlocklist: Set<String>? = null
var autofillInlineSuggestionsEnabled: Boolean = false private var webDomainBlocklist: Set<String>? = null
private var askToSaveData: Boolean = false
private var autofillInlineSuggestionsEnabled: Boolean = false
private var mLock = AtomicBoolean() private var mLock = AtomicBoolean()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.registerProgressTask()
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
this.mDatabase = database
}
getPreferences() getPreferences()
} }
override fun onDestroy() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onDestroy()
}
private fun getPreferences() { private fun getPreferences() {
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this) applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this) webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
@@ -95,7 +113,8 @@ class KeeAutofillService : AutofillService() {
} else { } else {
null null
} }
launchSelection(searchInfo, launchSelection(mDatabase,
searchInfo,
parseResult, parseResult,
inlineSuggestionsRequest, inlineSuggestionsRequest,
callback) callback)
@@ -105,27 +124,28 @@ class KeeAutofillService : AutofillService() {
} }
} }
private fun launchSelection(searchInfo: SearchInfo, private fun launchSelection(database: Database?,
searchInfo: SearchInfo,
parseResult: StructureParser.Result, parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?, inlineSuggestionsRequest: InlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(this, SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(), database,
searchInfo, searchInfo,
{ items -> { openedDatabase, items ->
callback.onSuccess( callback.onSuccess(
AutofillHelper.buildResponse(this, AutofillHelper.buildResponse(this, openedDatabase,
items, parseResult, inlineSuggestionsRequest) items, parseResult, inlineSuggestionsRequest)
) )
}, },
{ { openedDatabase ->
// Show UI if no search result // Show UI if no search result
showUIForEntrySelection(parseResult, showUIForEntrySelection(parseResult, openedDatabase,
searchInfo, inlineSuggestionsRequest, callback) searchInfo, inlineSuggestionsRequest, callback)
}, },
{ {
// Show UI if database not open // Show UI if database not open
showUIForEntrySelection(parseResult, showUIForEntrySelection(parseResult, null,
searchInfo, inlineSuggestionsRequest, callback) searchInfo, inlineSuggestionsRequest, callback)
} }
) )
@@ -133,6 +153,7 @@ class KeeAutofillService : AutofillService() {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private fun showUIForEntrySelection(parseResult: StructureParser.Result, private fun showUIForEntrySelection(parseResult: StructureParser.Result,
database: Database?,
searchInfo: SearchInfo, searchInfo: SearchInfo,
inlineSuggestionsRequest: InlineSuggestionsRequest?, inlineSuggestionsRequest: InlineSuggestionsRequest?,
callback: FillCallback) { callback: FillCallback) {
@@ -140,38 +161,87 @@ class KeeAutofillService : AutofillService() {
if (autofillIds.isNotEmpty()) { if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used // If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response. // to generate Response.
val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this, val intentSender = AutofillLauncherActivity.getPendingIntentForSelection(this,
searchInfo, inlineSuggestionsRequest) searchInfo, inlineSuggestionsRequest).intentSender
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) { val remoteViewsUnlock: RemoteViews = if (database == null) {
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply { if (!parseResult.webDomain.isNullOrEmpty()) {
setTextViewText(R.id.autofill_web_domain_text, parseResult.webDomain) RemoteViews(
} packageName,
} else if (!parseResult.applicationId.isNullOrEmpty()) { R.layout.item_autofill_unlock_web_domain
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply { ).apply {
setTextViewText(R.id.autofill_app_id_text, parseResult.applicationId) setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_unlock)
} }
} else { } else {
RemoteViews(packageName, R.layout.item_autofill_unlock) if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(
packageName,
R.layout.item_autofill_select_entry_web_domain
).apply {
setTextViewText(
R.id.autofill_web_domain_text,
parseResult.webDomain
)
}
} else if (!parseResult.applicationId.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_select_entry_app_id).apply {
setTextViewText(
R.id.autofill_app_id_text,
parseResult.applicationId
)
}
} else {
RemoteViews(packageName, R.layout.item_autofill_select_entry)
}
} }
// Tell to service the interest to save credentials // Tell the autofill framework the interest to save credentials
if (askToSaveData) { if (askToSaveData) {
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
val info = ArrayList<AutofillId>() val requiredIds = ArrayList<AutofillId>()
val optionalIds = ArrayList<AutofillId>()
// Only if at least a password // Only if at least a password
parseResult.passwordId?.let { passwordInfo -> parseResult.passwordId?.let { passwordInfo ->
parseResult.usernameId?.let { usernameInfo -> parseResult.usernameId?.let { usernameInfo ->
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
info.add(usernameInfo) requiredIds.add(usernameInfo)
} }
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
info.add(passwordInfo) requiredIds.add(passwordInfo)
} }
if (info.isNotEmpty()) { // or a credit card form
responseBuilder.setSaveInfo( if (requiredIds.isEmpty()) {
SaveInfo.Builder(types, info.toTypedArray()).build() parseResult.creditCardNumberId?.let { numberId ->
) types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
requiredIds.add(numberId)
Log.d(TAG, "Asking to save credit card number")
}
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
}
if (requiredIds.isNotEmpty()) {
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
if (optionalIds.isNotEmpty()) {
builder.setOptionalIds(optionalIds.toTypedArray())
}
responseBuilder.setSaveInfo(builder.build())
} }
} }
@@ -223,14 +293,35 @@ class KeeAutofillService : AutofillService() {
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) { && autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
Log.d(TAG, "autofill onSaveRequest password") Log.d(TAG, "autofill onSaveRequest password")
// Build expiration from date or from year and month
var expiration: DateTime? = parseResult.creditCardExpirationValue
if (parseResult.creditCardExpirationValue == null
&& parseResult.creditCardExpirationYearValue != 0
&& parseResult.creditCardExpirationMonthValue != 0) {
expiration = DateTime()
.withYear(parseResult.creditCardExpirationYearValue)
.withMonthOfYear(parseResult.creditCardExpirationMonthValue)
if (parseResult.creditCardExpirationDayValue != 0) {
expiration = expiration.withDayOfMonth(parseResult.creditCardExpirationDayValue)
}
}
// Show UI to save data // Show UI to save data
val registerInfo = RegisterInfo(SearchInfo().apply { val registerInfo = RegisterInfo(
applicationId = parseResult.applicationId SearchInfo().apply {
webDomain = parseResult.webDomain applicationId = parseResult.applicationId
webScheme = parseResult.webScheme webDomain = parseResult.webDomain
}, webScheme = parseResult.webScheme
},
parseResult.usernameValue?.textValue?.toString(), parseResult.usernameValue?.textValue?.toString(),
parseResult.passwordValue?.textValue?.toString()) parseResult.passwordValue?.textValue?.toString(),
CreditCard(
parseResult.creditCardHolder,
parseResult.creditCardNumber,
expiration,
parseResult.cardVerificationValue
))
// TODO Callback in each activity #765 // TODO Callback in each activity #765
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { //if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this, // callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,

View File

@@ -21,12 +21,14 @@ package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.os.Build import android.os.Build
import android.text.InputType import android.text.InputType
import androidx.annotation.RequiresApi
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.autofill.AutofillId import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue import android.view.autofill.AutofillValue
import androidx.annotation.RequiresApi
import org.joda.time.DateTime
import java.util.* import java.util.*
import kotlin.collections.ArrayList
/** /**
@@ -35,10 +37,8 @@ import java.util.*
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class StructureParser(private val structure: AssistStructure) { class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null private var result: Result? = null
private var usernameNeeded = true private var usernameNeeded = true
private var usernameIdCandidate: AutofillId? = null
private var usernameCandidate: AutofillId? = null
private var usernameValueCandidate: AutofillValue? = null private var usernameValueCandidate: AutofillValue? = null
fun parse(saveValue: Boolean = false): Result? { fun parse(saveValue: Boolean = false): Result? {
@@ -46,37 +46,42 @@ class StructureParser(private val structure: AssistStructure) {
result = Result() result = Result()
result?.apply { result?.apply {
allowSaveValues = saveValue allowSaveValues = saveValue
usernameCandidate = null usernameIdCandidate = null
usernameValueCandidate = null usernameValueCandidate = null
mainLoop@ for (i in 0 until structure.windowNodeCount) { mainLoop@ for (i in 0 until structure.windowNodeCount) {
val windowNode = structure.getWindowNodeAt(i) val windowNode = structure.getWindowNodeAt(i)
applicationId = windowNode.title.toString().split("/")[0] applicationId = windowNode.title.toString().split("/")[0]
Log.d(TAG, "Autofill applicationId: $applicationId") Log.d(TAG, "Autofill applicationId: $applicationId")
if (parseViewNode(windowNode.rootViewNode)) if (applicationId?.contains("PopupWindow:") == false) {
break@mainLoop if (parseViewNode(windowNode.rootViewNode))
break@mainLoop
}
} }
// If not explicit username field found, add the field just before password field. // If not explicit username field found, add the field just before password field.
if (usernameId == null && passwordId != null && usernameCandidate != null) { if (usernameId == null && passwordId != null && usernameIdCandidate != null) {
usernameId = usernameCandidate usernameId = usernameIdCandidate
if (allowSaveValues) { if (allowSaveValues) {
usernameValue = usernameValueCandidate usernameValue = usernameValueCandidate
} }
} }
} }
// Return the result only if password field is retrieved return if (result?.passwordId != null || result?.creditCardNumberId != null)
return if ((!usernameNeeded || result?.usernameId != null) result
&& result?.passwordId != null) else
result null
else
null
} catch (e: Exception) { } catch (e: Exception) {
return null return null
} }
} }
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean { private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
// remember this
if (node.className == "android.webkit.WebView") {
result?.isWebView = true
}
// Get the domain of a web app // Get the domain of a web app
node.webDomain?.let { webDomain -> node.webDomain?.let { webDomain ->
if (webDomain.isNotEmpty()) { if (webDomain.isNotEmpty()) {
@@ -97,8 +102,7 @@ class StructureParser(private val structure: AssistStructure) {
var returnValue = false var returnValue = false
// Only parse visible nodes // Only parse visible nodes
if (node.visibility == View.VISIBLE) { if (node.visibility == View.VISIBLE) {
if (node.autofillId != null if (node.autofillId != null) {
&& node.autofillType == View.AUTOFILL_TYPE_TEXT) {
// Parse methods // Parse methods
val hints = node.autofillHints val hints = node.autofillHints
if (hints != null && hints.isNotEmpty()) { if (hints != null && hints.isNotEmpty()) {
@@ -130,7 +134,7 @@ class StructureParser(private val structure: AssistStructure) {
it.contains(View.AUTOFILL_HINT_USERNAME, true) it.contains(View.AUTOFILL_HINT_USERNAME, true)
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true) || it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|| it.contains("email", true) || it.contains("email", true)
|| it.contains(View.AUTOFILL_HINT_PHONE, true)-> { || it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
result?.usernameId = autofillId result?.usernameId = autofillId
result?.usernameValue = node.autofillValue result?.usernameValue = node.autofillValue
Log.d(TAG, "Autofill username hint") Log.d(TAG, "Autofill username hint")
@@ -139,14 +143,123 @@ class StructureParser(private val structure: AssistStructure) {
result?.passwordId = autofillId result?.passwordId = autofillId
result?.passwordValue = node.autofillValue result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password hint") Log.d(TAG, "Autofill password hint")
// Username not needed in this case
usernameNeeded = false
return true return true
} }
it.equals("cc-name", true) -> {
Log.d(TAG, "Autofill credit card name hint")
result?.creditCardHolderId = autofillId
result?.creditCardHolder = node.autofillValue?.textValue?.toString()
}
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, true)
|| it.equals("cc-number", true) -> {
Log.d(TAG, "Autofill credit card number hint")
result?.creditCardNumberId = autofillId
result?.creditCardNumber = node.autofillValue?.textValue?.toString()
}
// expect date string as defined in https://html.spec.whatwg.org, e.g. 2014-12
it.equals("cc-exp", true) -> {
Log.d(TAG, "Autofill credit card expiration date hint")
result?.creditCardExpirationDateId = autofillId
node.autofillValue?.let { value ->
if (value.isText && value.textValue.length == 7) {
value.textValue.let { date ->
try {
result?.creditCardExpirationValue = DateTime()
.withYear(date.substring(2, 4).toInt())
.withMonthOfYear(date.substring(5, 7).toInt())
} catch(e: Exception) {
Log.e(TAG, "Unable to retrieve expiration", e)
}
}
}
}
}
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE, true) -> {
Log.d(TAG, "Autofill credit card expiration date hint")
result?.creditCardExpirationDateId = autofillId
node.autofillValue?.let { value ->
if (value.isDate) {
result?.creditCardExpirationValue = DateTime(value.dateValue)
}
}
}
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, true)
|| it.equals("cc-exp-year", true) -> {
Log.d(TAG, "Autofill credit card expiration year hint")
result?.creditCardExpirationYearId = autofillId
if (node.autofillOptions != null) {
result?.creditCardExpirationYearOptions = node.autofillOptions
}
node.autofillValue?.let { value ->
var year = 0
try {
if (value.isText) {
year = value.textValue.toString().toInt()
}
if (value.isList) {
year = node.autofillOptions?.get(value.listValue).toString().toInt()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve expiration year", e)
}
result?.creditCardExpirationYearValue = year % 100
}
}
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, true)
|| it.equals("cc-exp-month", true) -> {
Log.d(TAG, "Autofill credit card expiration month hint")
result?.creditCardExpirationMonthId = autofillId
if (node.autofillOptions != null) {
result?.creditCardExpirationMonthOptions = node.autofillOptions
}
node.autofillValue?.let { value ->
var month = 0
try {
if (value.isText) {
month = value.textValue.toString().toInt()
}
if (value.isList) {
// assume list starts with January (index 0)
month = value.listValue + 1
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve expiration month", e)
}
result?.creditCardExpirationMonthValue = month
}
}
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY, true)
|| it.equals("cc-exp-day", true) -> {
Log.d(TAG, "Autofill credit card expiration day hint")
result?.creditCardExpirationDayId = autofillId
if (node.autofillOptions != null) {
result?.creditCardExpirationDayOptions = node.autofillOptions
}
node.autofillValue?.let { value ->
var day = 0
try {
if (value.isText) {
day = value.textValue.toString().toInt()
}
if (value.isList) {
day = node.autofillOptions?.get(value.listValue).toString().toInt()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve expiration day", e)
}
result?.creditCardExpirationDayValue = day
}
}
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE, true)
|| it.contains("cc-csc", true) -> {
Log.d(TAG, "Autofill card security code hint")
result?.cardVerificationValueId = autofillId
result?.cardVerificationValue = node.autofillValue?.textValue?.toString()
}
// Ignore autocomplete="off" // Ignore autocomplete="off"
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion // https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
it.equals("off", true) || it.equals("off", true) ||
it.equals("on", true) -> { it.equals("on", true) -> {
Log.d(TAG, "Autofill web hint") Log.d(TAG, "Autofill web hint")
return parseNodeByHtmlAttributes(node) return parseNodeByHtmlAttributes(node)
} }
@@ -171,7 +284,7 @@ class StructureParser(private val structure: AssistStructure) {
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}") Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
} }
"text" -> { "text" -> {
usernameCandidate = autofillId usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}") Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
} }
@@ -219,15 +332,15 @@ class StructureParser(private val structure: AssistStructure) {
InputType.TYPE_TEXT_VARIATION_NORMAL, InputType.TYPE_TEXT_VARIATION_NORMAL,
InputType.TYPE_TEXT_VARIATION_PERSON_NAME, InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> { InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
usernameCandidate = autofillId usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}") Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
} }
inputIsVariationType(inputType, inputIsVariationType(inputType,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> { InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
// Some forms used visible password as username // Some forms used visible password as username
if (usernameCandidate == null && usernameValueCandidate == null) { if (usernameIdCandidate == null && usernameValueCandidate == null) {
usernameCandidate = autofillId usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}") Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
} else if (result?.passwordId == null && result?.passwordValue == null) { } else if (result?.passwordId == null && result?.passwordValue == null) {
@@ -243,7 +356,6 @@ class StructureParser(private val structure: AssistStructure) {
result?.passwordId = autofillId result?.passwordId = autofillId
result?.passwordValue = node.autofillValue result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}") Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
usernameNeeded = false
return true return true
} }
inputIsVariationType(inputType, inputIsVariationType(inputType,
@@ -265,16 +377,15 @@ class StructureParser(private val structure: AssistStructure) {
when { when {
inputIsVariationType(inputType, inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> { InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
usernameCandidate = autofillId usernameIdCandidate = autofillId
usernameValueCandidate = node.autofillValue usernameValueCandidate = node.autofillValue
Log.d(TAG, "Autofill usernale candidate android number type: ${showHexInputType(inputType)}") Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
} }
inputIsVariationType(inputType, inputIsVariationType(inputType,
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> { InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
result?.passwordId = autofillId result?.passwordId = autofillId
result?.passwordValue = node.autofillValue result?.passwordValue = node.autofillValue
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}") Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
usernameNeeded = false
return true return true
} }
else -> { else -> {
@@ -288,8 +399,8 @@ class StructureParser(private val structure: AssistStructure) {
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
class Result { class Result {
var isWebView: Boolean = false
var applicationId: String? = null var applicationId: String? = null
var webDomain: String? = null var webDomain: String? = null
set(value) { set(value) {
if (field == null) if (field == null)
@@ -302,6 +413,12 @@ class StructureParser(private val structure: AssistStructure) {
field = value field = value
} }
// if the user selects the credit card expiration date from a list of options
// all options are stored here
var creditCardExpirationYearOptions: Array<CharSequence>? = null
var creditCardExpirationMonthOptions: Array<CharSequence>? = null
var creditCardExpirationDayOptions: Array<CharSequence>? = null
var usernameId: AutofillId? = null var usernameId: AutofillId? = null
set(value) { set(value) {
if (field == null) if (field == null)
@@ -314,6 +431,48 @@ class StructureParser(private val structure: AssistStructure) {
field = value field = value
} }
var creditCardHolderId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardNumberId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationDateId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationYearId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationMonthId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var creditCardExpirationDayId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
var cardVerificationValueId: AutofillId? = null
set(value) {
if (field == null)
field = value
}
fun allAutofillIds(): Array<AutofillId> { fun allAutofillIds(): Array<AutofillId> {
val all = ArrayList<AutofillId>() val all = ArrayList<AutofillId>()
usernameId?.let { usernameId?.let {
@@ -322,6 +481,15 @@ class StructureParser(private val structure: AssistStructure) {
passwordId?.let { passwordId?.let {
all.add(it) all.add(it)
} }
creditCardHolderId?.let {
all.add(it)
}
creditCardNumberId?.let {
all.add(it)
}
cardVerificationValueId?.let {
all.add(it)
}
return all.toTypedArray() return all.toTypedArray()
} }
@@ -339,6 +507,52 @@ class StructureParser(private val structure: AssistStructure) {
if (allowSaveValues && field == null) if (allowSaveValues && field == null)
field = value field = value
} }
var creditCardHolder: String? = null
set(value) {
if (allowSaveValues)
field = value
}
var creditCardNumber: String? = null
set(value) {
if (allowSaveValues)
field = value
}
// format MMYY
var creditCardExpirationValue: DateTime? = null
set(value) {
if (allowSaveValues)
field = value
}
// for year of CC expiration date: YY
var creditCardExpirationYearValue = 0
set(value) {
if (allowSaveValues)
field = value
}
// for month of CC expiration date: MM
var creditCardExpirationMonthValue = 0
set(value) {
if (allowSaveValues)
field = value
}
var creditCardExpirationDayValue = 0
set(value) {
if (allowSaveValues)
field = value
}
// the security code for the credit card (also called CVV)
var cardVerificationValue: String? = null
set(value) {
if (allowSaveValues)
field = value
}
} }
companion object { companion object {

View File

@@ -30,6 +30,7 @@ import android.view.*
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.lifecycle.lifecycleScope
import com.getkeepsafe.taptargetview.TapTargetView import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.activities.stylish.StylishFragment
@@ -38,6 +39,8 @@ import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback { class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
@@ -309,7 +312,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) { private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
activity?.runOnUiThread { lifecycleScope.launch(Dispatchers.Main) {
if (allowOpenBiometricPrompt) { if (allowOpenBiometricPrompt) {
if (cryptoPrompt.isDeviceCredentialOperation) if (cryptoPrompt.isDeviceCredentialOperation)
keepConnection = true keepConnection = true
@@ -450,7 +453,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
activity?.runOnUiThread { lifecycleScope.launch(Dispatchers.Main) {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
setAdvancedUnlockedMessageView(errString.toString()) setAdvancedUnlockedMessageView(errString.toString())
} }
@@ -458,7 +461,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationFailed() { override fun onAuthenticationFailed() {
activity?.runOnUiThread { lifecycleScope.launch(Dispatchers.Main) {
Log.e(TAG, "Biometric authentication failed, biometric not recognized") Log.e(TAG, "Biometric authentication failed, biometric not recognized")
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized) setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
} }
@@ -466,7 +469,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationSucceeded() { override fun onAuthenticationSucceeded() {
activity?.runOnUiThread { lifecycleScope.launch(Dispatchers.Main) {
when (biometricMode) { when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> { Mode.BIOMETRIC_UNAVAILABLE -> {
} }
@@ -524,7 +527,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
private fun showViews(show: Boolean) { private fun showViews(show: Boolean) {
activity?.runOnUiThread { lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.visibility = if (show) mAdvancedUnlockInfoView?.visibility = if (show)
View.VISIBLE View.VISIBLE
else { else {
@@ -535,20 +538,20 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedTitleView(textId: Int) { private fun setAdvancedUnlockedTitleView(textId: Int) {
activity?.runOnUiThread { lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.setTitle(textId) mAdvancedUnlockInfoView?.setTitle(textId)
} }
} }
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(textId: Int) { private fun setAdvancedUnlockedMessageView(textId: Int) {
activity?.runOnUiThread { lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.setMessage(textId) mAdvancedUnlockInfoView?.setMessage(textId)
} }
} }
private fun setAdvancedUnlockedMessageView(text: CharSequence) { private fun setAdvancedUnlockedMessageView(text: CharSequence) {
activity?.runOnUiThread { lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.message = text mAdvancedUnlockInfoView?.message = text
} }
} }

View File

@@ -26,13 +26,13 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
class CreateDatabaseRunnable(context: Context, class CreateDatabaseRunnable(context: Context,
private val mDatabase: Database, private val mDatabase: Database,
databaseUri: Uri, databaseUri: Uri,
private val databaseName: String, private val databaseName: String,
private val rootName: String, private val rootName: String,
private val templateGroupName: String?,
mainCredential: MainCredential, mainCredential: MainCredential,
private val createDatabaseResult: ((Result) -> Unit)?) private val createDatabaseResult: ((Result) -> Unit)?)
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) { : AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
@@ -41,10 +41,10 @@ class CreateDatabaseRunnable(context: Context,
try { try {
// Create new database record // Create new database record
mDatabase.apply { mDatabase.apply {
createData(mDatabaseUri, databaseName, rootName) createData(mDatabaseUri, databaseName, rootName, templateGroupName)
} }
} catch (e: Exception) { } catch (e: Exception) {
mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) mDatabase.clearAndClose(context)
setError(e) setError(e)
} }

View File

@@ -19,21 +19,23 @@
*/ */
package com.kunzisoft.keepass.database.action package com.kunzisoft.keepass.database.action
import android.app.Service
import android.content.* import android.content.*
import android.content.Context.BIND_ABOVE_CLIENT import android.content.Context.*
import android.content.Context.BIND_NOT_FOREGROUND
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
@@ -70,21 +72,35 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import kotlinx.coroutines.launch
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { /**
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
* Useful to retrieve a database instance and sending tasks commands
*/
class DatabaseTaskProvider {
var onActionFinish: ((actionTask: String, private var activity: FragmentActivity? = null
private var service: Service? = null
private var context: Context
var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null
var onActionFinish: ((database: Database,
actionTask: String,
result: ActionRunnable.Result) -> Unit)? = null result: ActionRunnable.Result) -> Unit)? = null
private var intentDatabaseTask = Intent(activity.applicationContext, DatabaseTaskNotificationService::class.java) private var intentDatabaseTask: Intent
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
@@ -94,17 +110,31 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
constructor(activity: FragmentActivity) {
this.activity = activity
this.context = activity
this.intentDatabaseTask = Intent(activity.applicationContext,
DatabaseTaskNotificationService::class.java)
}
constructor(service: Service) {
this.service = service
this.context = service
this.intentDatabaseTask = Intent(service.applicationContext,
DatabaseTaskNotificationService::class.java)
}
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener { private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) { override fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
startDialog(titleId, messageId, warningId) startDialog(titleId, messageId, warningId)
} }
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) { override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
updateDialog(titleId, messageId, warningId) updateDialog(titleId, messageId, warningId)
} }
override fun onStopAction(actionTask: String, result: ActionRunnable.Result) { override fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) {
onActionFinish?.invoke(actionTask, result) onActionFinish?.invoke(database, actionTask, result)
// Remove the progress task // Remove the progress task
stopDialog() stopDialog()
} }
@@ -119,31 +149,56 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
private var databaseInfoListener = object: DatabaseTaskNotificationService.DatabaseInfoListener { private var databaseInfoListener = object: DatabaseTaskNotificationService.DatabaseInfoListener {
override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo, override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo) { newDatabaseInfo: SnapFileDatabaseInfo) {
if (databaseChangedDialogFragment == null) { activity?.let { activity ->
databaseChangedDialogFragment = activity.supportFragmentManager activity.lifecycleScope.launch {
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment? if (databaseChangedDialogFragment == null) {
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener databaseChangedDialogFragment = activity.supportFragmentManager
} .findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
if (progressTaskDialogFragment == null) { databaseChangedDialogFragment?.actionDatabaseListener =
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(previousDatabaseInfo, newDatabaseInfo) mActionDatabaseListener
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener }
databaseChangedDialogFragment?.show(activity.supportFragmentManager, DATABASE_CHANGED_DIALOG_TAG) if (progressTaskDialogFragment == null) {
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
previousDatabaseInfo,
newDatabaseInfo
)
databaseChangedDialogFragment?.actionDatabaseListener =
mActionDatabaseListener
databaseChangedDialogFragment?.show(
activity.supportFragmentManager,
DATABASE_CHANGED_DIALOG_TAG
)
}
}
} }
} }
} }
private var databaseListener = object: DatabaseTaskNotificationService.DatabaseListener {
override fun onDatabaseRetrieved(database: Database?) {
onDatabaseRetrieved?.invoke(database)
}
}
private fun startDialog(titleId: Int? = null, private fun startDialog(titleId: Int? = null,
messageId: Int? = null, messageId: Int? = null,
warningId: Int? = null) { warningId: Int? = null) {
if (progressTaskDialogFragment == null) { activity?.let { activity ->
progressTaskDialogFragment = activity.supportFragmentManager activity.lifecycleScope.launch {
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment? if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = activity.supportFragmentManager
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
}
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = ProgressTaskDialogFragment()
progressTaskDialogFragment?.show(
activity.supportFragmentManager,
PROGRESS_TASK_DIALOG_TAG
)
}
updateDialog(titleId, messageId, warningId)
}
} }
if (progressTaskDialogFragment == null) {
progressTaskDialogFragment = ProgressTaskDialogFragment()
progressTaskDialogFragment?.show(activity.supportFragmentManager, PROGRESS_TASK_DIALOG_TAG)
}
updateDialog(titleId, messageId, warningId)
} }
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) { private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
@@ -170,16 +225,19 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
serviceConnection = object : ServiceConnection { serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) { override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply { mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addActionTaskListener(actionTaskListener) addDatabaseListener(databaseListener)
addDatabaseFileInfoListener(databaseInfoListener) addDatabaseFileInfoListener(databaseInfoListener)
getService().checkAction() addActionTaskListener(actionTaskListener)
getService().checkDatabase()
getService().checkDatabaseInfo() getService().checkDatabaseInfo()
getService().checkAction()
} }
} }
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) {
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeActionTaskListener(actionTaskListener) mBinder?.removeActionTaskListener(actionTaskListener)
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeDatabaseListener(databaseListener)
mBinder = null mBinder = null
} }
} }
@@ -189,7 +247,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
private fun bindService() { private fun bindService() {
initServiceConnection() initServiceConnection()
serviceConnection?.let { serviceConnection?.let {
activity.bindService(intentDatabaseTask, it, BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT) context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
} }
} }
@@ -198,7 +256,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
*/ */
private fun unBindService() { private fun unBindService() {
serviceConnection?.let { serviceConnection?.let {
activity.unbindService(it) context.unbindService(it)
} }
serviceConnection = null serviceConnection = null
} }
@@ -226,7 +284,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
} }
} }
} }
activity.registerReceiver(databaseTaskBroadcastReceiver, context.registerReceiver(databaseTaskBroadcastReceiver,
IntentFilter().apply { IntentFilter().apply {
addAction(DATABASE_START_TASK_ACTION) addAction(DATABASE_START_TASK_ACTION)
addAction(DATABASE_STOP_TASK_ACTION) addAction(DATABASE_STOP_TASK_ACTION)
@@ -240,14 +298,15 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
fun unregisterProgressTask() { fun unregisterProgressTask() {
stopDialog() stopDialog()
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeActionTaskListener(actionTaskListener) mBinder?.removeActionTaskListener(actionTaskListener)
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeDatabaseListener(databaseListener)
mBinder = null mBinder = null
unBindService() unBindService()
try { try {
activity.unregisterReceiver(databaseTaskBroadcastReceiver) context.unregisterReceiver(databaseTaskBroadcastReceiver)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
// If receiver not register, do nothing // If receiver not register, do nothing
} }
@@ -255,11 +314,10 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
private fun start(bundle: Bundle? = null, actionTask: String) { private fun start(bundle: Bundle? = null, actionTask: String) {
try { try {
activity.stopService(intentDatabaseTask)
if (bundle != null) if (bundle != null)
intentDatabaseTask.putExtras(bundle) intentDatabaseTask.putExtras(bundle)
intentDatabaseTask.action = actionTask intentDatabaseTask.action = actionTask
activity.startService(intentDatabaseTask) context.startService(intentDatabaseTask)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to perform database action", e) Log.e(TAG, "Unable to perform database action", e)
Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show() Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
@@ -372,9 +430,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
nodesPaste.forEach { nodeVersioned -> nodesPaste.forEach { nodeVersioned ->
when (nodeVersioned.type) { when (nodeVersioned.type) {
Type.GROUP -> { Type.GROUP -> {
(nodeVersioned as Group).nodeId?.let { groupId -> groupsIdToCopy.add((nodeVersioned as Group).nodeId)
groupsIdToCopy.add(groupId)
}
} }
Type.ENTRY -> { Type.ENTRY -> {
entriesIdToCopy.add((nodeVersioned as Entry).nodeId) entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
@@ -417,22 +473,22 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
----------------- -----------------
*/ */
fun startDatabaseRestoreEntryHistory(mainEntry: Entry, fun startDatabaseRestoreEntryHistory(mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int, entryHistoryPosition: Int,
save: Boolean) { save: Boolean) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId) putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition) putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY) , ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
} }
fun startDatabaseDeleteEntryHistory(mainEntry: Entry, fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int, entryHistoryPosition: Int,
save: Boolean) { save: Boolean) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId) putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition) putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
} }
@@ -507,6 +563,28 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK) , ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
} }
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
newRecycleBin: Group?,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
}
fun startDatabaseSaveTemplatesGroup(oldTemplatesGroup: Group?,
newTemplatesGroup: Group?,
save: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
}
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int, fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
newMaxHistoryItems: Int, newMaxHistoryItems: Int,
save: Boolean) { save: Boolean) {
@@ -601,6 +679,6 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
} }
companion object { companion object {
private val TAG = ProgressDatabaseTaskProvider::class.java.name private val TAG = DatabaseTaskProvider::class.java.name
} }
} }

View File

@@ -25,8 +25,8 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -47,7 +47,7 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onStartRun() { override fun onStartRun() {
// Clear before we load // Clear before we load
mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) mDatabase.clearAndClose(context)
} }
override fun onActionRun() { override fun onActionRun() {
@@ -85,7 +85,7 @@ class LoadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer // Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context) PreferencesUtil.saveCurrentTime(context)
} else { } else {
mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) mDatabase.clearAndClose(context)
} }
} }

View File

@@ -21,8 +21,8 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -62,7 +62,7 @@ class ReloadDatabaseRunnable(private val context: Context,
PreferencesUtil.saveCurrentTime(context) PreferencesUtil.saveCurrentTime(context)
} else { } else {
tempCipherKey = null tempCipherKey = null
mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) mDatabase.clearAndClose(context)
} }
} }

View File

@@ -34,54 +34,52 @@ class UpdateEntryRunnable constructor(
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
// Keep backup of original values in case save fails
private var mBackupEntryHistory: Entry = Entry(mOldEntry)
override fun nodeAction() { override fun nodeAction() {
// WARNING : Re attribute parent removed in entry edit activity to save memory if (mOldEntry.nodeId == mNewEntry.nodeId) {
mNewEntry.addParentFrom(mOldEntry) // WARNING : Re attribute parent removed in entry edit activity to save memory
mNewEntry.addParentFrom(mOldEntry)
// Build oldest attachments // Build oldest attachments
val oldEntryAttachments = mOldEntry.getAttachments(database.attachmentPool, true) val oldEntryAttachments = mOldEntry.getAttachments(database.attachmentPool, true)
val newEntryAttachments = mNewEntry.getAttachments(database.attachmentPool, true) val newEntryAttachments = mNewEntry.getAttachments(database.attachmentPool, true)
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments) val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
// Not use equals because only check name // Not use equals because only check name
newEntryAttachments.forEach { newAttachment -> newEntryAttachments.forEach { newAttachment ->
oldEntryAttachments.forEach { oldAttachment -> oldEntryAttachments.forEach { oldAttachment ->
if (oldAttachment.name == newAttachment.name if (oldAttachment.name == newAttachment.name
&& oldAttachment.binaryData == newAttachment.binaryData) && oldAttachment.binaryData == newAttachment.binaryData
attachmentsToRemove.remove(oldAttachment) )
attachmentsToRemove.remove(oldAttachment)
}
} }
}
// Update entry with new values // Update entry with new values
mOldEntry.updateWith(mNewEntry) mNewEntry.touch(modified = true, touchParents = true)
mNewEntry.touch(modified = true, touchParents = true)
// Create an entry history (an entry history don't have history) // Create an entry history (an entry history don't have history)
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false)) mNewEntry.addEntryToHistory(Entry(mOldEntry, copyHistory = false))
database.removeOldestEntryHistory(mOldEntry, database.attachmentPool) database.removeOldestEntryHistory(mNewEntry, database.attachmentPool)
// Only change data in index // Only change data in index
database.updateEntry(mOldEntry) database.updateEntry(mNewEntry)
// Remove oldest attachments // Remove oldest attachments
attachmentsToRemove.forEach { attachmentsToRemove.forEach {
database.removeAttachmentIfNotUsed(it) database.removeAttachmentIfNotUsed(it)
}
} }
} }
override fun nodeFinish(): ActionNodesValues { override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) { if (!result.isSuccess) {
mOldEntry.updateWith(mBackupEntryHistory)
// If we fail to save, back out changes to global structure // If we fail to save, back out changes to global structure
database.updateEntry(mOldEntry) database.updateEntry(mOldEntry)
} }
val oldNodesReturn = ArrayList<Node>() val oldNodesReturn = ArrayList<Node>()
oldNodesReturn.add(mBackupEntryHistory) oldNodesReturn.add(mOldEntry)
val newNodesReturn = ArrayList<Node>() val newNodesReturn = ArrayList<Node>()
newNodesReturn.add(mOldEntry) newNodesReturn.add(mNewEntry)
return ActionNodesValues(oldNodesReturn, newNodesReturn) return ActionNodesValues(oldNodesReturn, newNodesReturn)
} }
} }

View File

@@ -33,33 +33,30 @@ class UpdateGroupRunnable constructor(
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
// Keep backup of original values in case save fails
private val mBackupGroup: Group = Group(mOldGroup)
override fun nodeAction() { override fun nodeAction() {
// WARNING : Re attribute parent and children removed in group activity to save memory if (mOldGroup.nodeId == mNewGroup.nodeId) {
mNewGroup.addParentFrom(mOldGroup) // WARNING : Re attribute parent and children removed in group activity to save memory
mNewGroup.addChildrenFrom(mOldGroup) mNewGroup.addParentFrom(mOldGroup)
mNewGroup.addChildrenFrom(mOldGroup)
// Update group with new values // Update group with new values
mOldGroup.updateWith(mNewGroup) mNewGroup.touch(modified = true, touchParents = true)
mOldGroup.touch(modified = true, touchParents = true)
// Only change data in index // Only change data in index
database.updateGroup(mOldGroup) database.updateGroup(mNewGroup)
}
} }
override fun nodeFinish(): ActionNodesValues { override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) { if (!result.isSuccess) {
// If we fail to save, back out changes to global structure // If we fail to save, back out changes to global structure
mOldGroup.updateWith(mBackupGroup)
database.updateGroup(mOldGroup) database.updateGroup(mOldGroup)
} }
val oldNodesReturn = ArrayList<Node>() val oldNodesReturn = ArrayList<Node>()
oldNodesReturn.add(mBackupGroup) oldNodesReturn.add(mOldGroup)
val newNodesReturn = ArrayList<Node>() val newNodesReturn = ArrayList<Node>()
newNodesReturn.add(mOldGroup) newNodesReturn.add(mNewGroup)
return ActionNodesValues(oldNodesReturn, newNodesReturn) return ActionNodesValues(oldNodesReturn, newNodesReturn)
} }
} }

View File

@@ -45,8 +45,8 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
entry.expires entry.expires
)) ))
entry.doForEachDecodedCustomField { key, value -> entry.doForEachDecodedCustomField { field ->
extraFieldCursor.addExtraField(entryId, key, value) extraFieldCursor.addExtraField(entryId, field)
} }
entryId++ entryId++

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.cursor
import android.database.MatrixCursor import android.database.MatrixCursor
import android.provider.BaseColumns import android.provider.BaseColumns
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -36,8 +37,12 @@ class ExtraFieldCursor : MatrixCursor(arrayOf(
private var fieldId: Long = 0 private var fieldId: Long = 0
@Synchronized @Synchronized
fun addExtraField(entryId: Long, label: String, value: ProtectedString) { fun addExtraField(entryId: Long, field: Field) {
addRow(arrayOf(fieldId, entryId, label, if (value.isProtected) 1 else 0, value.toString())) addRow(arrayOf(fieldId,
entryId,
field.name,
if (field.protectedValue.isProtected) 1 else 0,
field.protectedValue.toString()))
fieldId++ fieldId++
} }

View File

@@ -20,9 +20,12 @@
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.net.Uri import android.net.Uri
import android.os.Build
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
@@ -39,6 +42,8 @@ import com.kunzisoft.keepass.database.element.icon.IconsManager
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateEngine
import com.kunzisoft.keepass.database.exception.* import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
@@ -142,6 +147,57 @@ class Database {
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid) iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
} }
fun getTemplates(templateCreation: Boolean): List<Template> {
return mDatabaseKDBX?.getTemplates(templateCreation) ?: listOf()
}
fun getTemplate(entry: Entry): Template? {
if (entryIsTemplate(entry))
return TemplateEngine.CREATION
entry.entryKDBX?.let { entryKDBX ->
return mDatabaseKDBX?.getTemplate(entryKDBX)
}
return null
}
fun entryIsTemplate(entry: Entry?): Boolean {
// Define is current entry is a template (in direct template group)
if (entry == null || templatesGroup == null)
return false
return templatesGroup == entry.parent
}
// Not the same as decode, here remove in all cases the template link in the entry data
fun removeTemplateConfiguration(entry: Entry): Entry {
entry.entryKDBX?.let {
mDatabaseKDBX?.decodeEntryWithTemplateConfiguration(it, false)?.let { decode ->
return Entry(decode)
}
}
return entry
}
// Remove the template link in the entry data if it's a basic entry
// or compress the template fields (as pseudo language) if it's a template entry
fun decodeEntryWithTemplateConfiguration(entry: Entry, lastEntryVersion: Entry? = null): Entry {
entry.entryKDBX?.let {
val lastEntry = lastEntryVersion ?: entry
mDatabaseKDBX?.decodeEntryWithTemplateConfiguration(it, entryIsTemplate(lastEntry))?.let { decode ->
return Entry(decode)
}
}
return entry
}
fun encodeEntryWithTemplateConfiguration(entry: Entry, template: Template): Entry {
entry.entryKDBX?.let {
mDatabaseKDBX?.encodeEntryWithTemplateConfiguration(it, entryIsTemplate(entry), template)?.let { encode ->
return Entry(encode)
}
}
return entry
}
val allowName: Boolean val allowName: Boolean
get() = mDatabaseKDBX != null get() = mDatabaseKDBX != null
@@ -315,6 +371,15 @@ class Database {
return null return null
} }
/**
* Do not modify groups here, used for read only
*/
fun getAllGroupsWithoutRoot(): List<Group> {
return mDatabaseKDB?.getAllGroupsWithoutRoot()?.map { Group(it) }
?: mDatabaseKDBX?.getAllGroupsWithoutRoot()?.map { Group(it) }
?: listOf()
}
val manageHistory: Boolean val manageHistory: Boolean
get() = mDatabaseKDBX != null get() = mDatabaseKDBX != null
@@ -341,12 +406,18 @@ class Database {
val allowConfigurableRecycleBin: Boolean val allowConfigurableRecycleBin: Boolean
get() = mDatabaseKDBX != null get() = mDatabaseKDBX != null
var isRecycleBinEnabled: Boolean val isRecycleBinEnabled: Boolean
// Backup is always enabled in KDB database // Backup is always enabled in KDB database
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
set(value) {
mDatabaseKDBX?.isRecycleBinEnabled = value fun enableRecycleBin(enable: Boolean, resources: Resources) {
mDatabaseKDBX?.isRecycleBinEnabled = enable
if (enable) {
ensureRecycleBinExists(resources)
} else {
mDatabaseKDBX?.removeRecycleBin()
} }
}
val recycleBin: Group? val recycleBin: Group?
get() { get() {
@@ -359,6 +430,47 @@ class Database {
return null return null
} }
fun setRecycleBin(group: Group?) {
// Only the kdbx recycle bin can be changed
if (group != null) {
mDatabaseKDBX?.recycleBinUUID = group.nodeIdKDBX.id
} else {
mDatabaseKDBX?.removeTemplatesGroup()
}
}
/**
* Determine if a configurable templates group is available or not for this version of database
* @return true if a configurable templates group available
*/
val allowConfigurableTemplatesGroup: Boolean
get() = mDatabaseKDBX != null
// Maybe another templates method with KDBX5
val isTemplatesEnabled: Boolean
get() = mDatabaseKDBX?.isTemplatesGroupEnabled() ?: false
fun enableTemplates(enable: Boolean, templatesGroupName: String) {
mDatabaseKDBX?.enableTemplatesGroup(enable, templatesGroupName)
}
val templatesGroup: Group?
get() {
mDatabaseKDBX?.getTemplatesGroup()?.let {
return Group(it)
}
return null
}
fun setTemplatesGroup(group: Group?) {
// Only the kdbx templates group can be changed
if (group != null) {
mDatabaseKDBX?.entryTemplatesGroup = group.nodeIdKDBX.id
} else {
mDatabaseKDBX?.entryTemplatesGroup
}
}
val groupNamesNotAllowed: List<String> val groupNamesNotAllowed: List<String>
get() { get() {
return mDatabaseKDB?.groupNamesNotAllowed ?: ArrayList() return mDatabaseKDB?.groupNamesNotAllowed ?: ArrayList()
@@ -374,8 +486,11 @@ class Database {
this.mDatabaseKDBX = databaseKDBX this.mDatabaseKDBX = databaseKDBX
} }
fun createData(databaseUri: Uri, databaseName: String, rootName: String) { fun createData(databaseUri: Uri,
val newDatabase = DatabaseKDBX(databaseName, rootName) databaseName: String,
rootName: String,
templateGroupName: String?) {
val newDatabase = DatabaseKDBX(databaseName, rootName, templateGroupName)
setDatabaseKDBX(newDatabase) setDatabaseKDBX(newDatabase)
this.fileUri = databaseUri this.fileUri = databaseUri
// Set Database state // Set Database state
@@ -557,6 +672,7 @@ class Database {
searchInOther = true searchInOther = true
searchInUUIDs = false searchInUUIDs = false
searchInTags = false searchInTags = false
searchInTemplates = false
}, omitBackup, max) }, omitBackup, max)
} }
@@ -574,10 +690,11 @@ class Database {
return false return false
} }
fun buildNewBinaryAttachment(compressed: Boolean = false, fun buildNewBinaryAttachment(): BinaryData? {
protected: Boolean = false): BinaryData? {
return mDatabaseKDB?.buildNewAttachment() return mDatabaseKDB?.buildNewAttachment()
?: mDatabaseKDBX?.buildNewAttachment( false, compressed, protected) ?: mDatabaseKDBX?.buildNewAttachment( false,
compressionForNewEntry(),
false)
} }
fun removeAttachmentIfNotUsed(attachment: Attachment) { fun removeAttachmentIfNotUsed(attachment: Attachment) {
@@ -668,8 +785,8 @@ class Database {
} }
} }
fun clearAndClose(filesDirectory: File? = null) { fun clearAndClose(context: Context? = null) {
clear(filesDirectory) clear(context?.let { UriUtil.getBinaryDir(context) })
this.mDatabaseKDB = null this.mDatabaseKDB = null
this.mDatabaseKDBX = null this.mDatabaseKDBX = null
this.fileUri = null this.fileUri = null
@@ -890,11 +1007,6 @@ class Database {
mDatabaseKDBX?.ensureRecycleBinExists(resources) mDatabaseKDBX?.ensureRecycleBinExists(resources)
} }
fun removeRecycleBin() {
// Don't allow remove backup in KDB
mDatabaseKDBX?.removeRecycleBin()
}
fun canRecycle(entry: Entry): Boolean { fun canRecycle(entry: Entry): Boolean {
var canRecycle: Boolean? = null var canRecycle: Boolean? = null
entry.entryKDB?.let { entry.entryKDB?.let {

View File

@@ -23,32 +23,85 @@ import android.content.res.Resources
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import androidx.core.os.ConfigurationCompat import androidx.core.os.ConfigurationCompat
import org.joda.time.Duration import com.kunzisoft.keepass.utils.readEnum
import org.joda.time.Instant import com.kunzisoft.keepass.utils.writeEnum
import org.joda.time.*
import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
class DateInstant : Parcelable { class DateInstant : Parcelable {
private var jDate: Date = Date() private var jDate: Date = Date()
private var mType: Type = Type.DATE_TIME
val date: Date val date: Date
get() = jDate get() = jDate
var type: Type
get() = mType
set(value) {
mType = value
}
constructor(source: DateInstant) { constructor(source: DateInstant) {
this.jDate = Date(source.jDate.time) this.jDate = Date(source.jDate.time)
this.mType = source.mType
} }
constructor(date: Date) { constructor(date: Date, type: Type = Type.DATE_TIME) {
jDate = Date(date.time) jDate = Date(date.time)
mType = type
} }
constructor(millis: Long) { constructor(millis: Long, type: Type = Type.DATE_TIME) {
jDate = Date(millis) jDate = Date(millis)
mType = type
} }
constructor(string: String) { private fun parse(value: String, type: Type): Date {
jDate = dateFormat.parse(string) ?: jDate return when (type) {
Type.DATE -> dateFormat.parse(value) ?: jDate
Type.TIME -> timeFormat.parse(value) ?: jDate
else -> dateTimeFormat.parse(value) ?: jDate
}
}
constructor(string: String, type: Type = Type.DATE_TIME) {
try {
jDate = parse(string, type)
mType = type
} catch (e: Exception) {
// Retry with second format
try {
when (type) {
Type.TIME -> {
jDate = parse(string, Type.DATE)
mType = Type.DATE
}
else -> {
jDate = parse(string, Type.TIME)
mType = Type.TIME
}
}
} catch (e: Exception) {
// Retry with third format
when (type) {
Type.DATE, Type.TIME -> {
jDate = parse(string, Type.DATE_TIME)
mType = Type.DATE_TIME
}
else -> {
jDate = parse(string, Type.DATE)
mType = Type.DATE
}
}
}
}
}
constructor(type: Type) {
mType = type
} }
constructor() { constructor() {
@@ -56,62 +109,123 @@ class DateInstant : Parcelable {
} }
constructor(parcel: Parcel) { constructor(parcel: Parcel) {
jDate = parcel.readSerializable() as Date jDate = parcel.readSerializable() as? Date? ?: jDate
mType = parcel.readEnum<Type>() ?: mType
} }
override fun describeContents(): Int { override fun describeContents(): Int {
return 0 return 0
} }
fun getDateTimeString(resources: Resources): String {
return Companion.getDateTimeString(resources, this.date)
}
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeSerializable(date) dest.writeSerializable(jDate)
dest.writeEnum(mType)
} }
override fun equals(other: Any?): Boolean { fun getDateTimeString(resources: Resources): String {
if (this === other) { return when (mType) {
return true Type.DATE -> DateFormat.getDateInstance(
DateFormat.MEDIUM,
ConfigurationCompat.getLocales(resources.configuration)[0])
.format(jDate)
Type.TIME -> DateFormat.getTimeInstance(
DateFormat.SHORT,
ConfigurationCompat.getLocales(resources.configuration)[0])
.format(jDate)
else -> DateFormat.getDateTimeInstance(
DateFormat.MEDIUM,
DateFormat.SHORT,
ConfigurationCompat.getLocales(resources.configuration)[0])
.format(jDate)
} }
if (other == null) {
return false
}
if (javaClass != other.javaClass) {
return false
}
val date = other as DateInstant
return isSameDate(jDate, date.jDate)
} }
override fun hashCode(): Int { fun getYearInt(): Int {
return jDate.hashCode() val dateFormat = SimpleDateFormat("yyyy", Locale.ENGLISH)
return dateFormat.format(date).toInt()
}
fun getMonthInt(): Int {
val dateFormat = SimpleDateFormat("MM", Locale.ENGLISH)
return dateFormat.format(date).toInt()
}
fun getDay(): Int {
val dateFormat = SimpleDateFormat("dd", Locale.ENGLISH)
return dateFormat.format(date).toInt()
}
// If expireDate is before NEVER_EXPIRE date less 1 month (to be sure)
// it is not expires
fun isNeverExpires(): Boolean {
return LocalDateTime(jDate)
.isBefore(
LocalDateTime.fromDateFields(NEVER_EXPIRES.date)
.minusMonths(1))
}
fun isCurrentlyExpire(): Boolean {
return when (type) {
Type.DATE -> LocalDate.fromDateFields(jDate).isBefore(LocalDate.now())
Type.TIME -> LocalTime.fromDateFields(jDate).isBefore(LocalTime.now())
else -> LocalDateTime.fromDateFields(jDate).isBefore(LocalDateTime.now())
}
} }
override fun toString(): String { override fun toString(): String {
return dateFormat.format(jDate) return when (type) {
Type.DATE -> dateFormat.format(jDate)
Type.TIME -> timeFormat.format(jDate)
else -> dateTimeFormat.format(jDate)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DateInstant) return false
if (jDate != other.jDate) return false
if (mType != other.mType) return false
return true
}
override fun hashCode(): Int {
var result = jDate.hashCode()
result = 31 * result + mType.hashCode()
return result
}
enum class Type {
DATE_TIME, DATE, TIME
} }
companion object { companion object {
val NEVER_EXPIRE = neverExpire val NEVER_EXPIRES = DateInstant(Calendar.getInstance().apply {
val IN_ONE_MONTH = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate()) set(Calendar.YEAR, 2999)
private val dateFormat = SimpleDateFormat.getDateTimeInstance() set(Calendar.MONTH, 11)
set(Calendar.DAY_OF_MONTH, 28)
set(Calendar.HOUR, 23)
set(Calendar.MINUTE, 59)
set(Calendar.SECOND, 59)
}.time)
val IN_ONE_MONTH_DATE_TIME = DateInstant(
Instant.now().plus(Duration.standardDays(30)).toDate(), Type.DATE_TIME)
val IN_ONE_MONTH_DATE = DateInstant(
Instant.now().plus(Duration.standardDays(30)).toDate(), Type.DATE)
val IN_ONE_HOUR_TIME = DateInstant(
Instant.now().plus(Duration.standardHours(1)).toDate(), Type.TIME)
private val neverExpire: DateInstant private val dateTimeFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.ROOT).apply {
get() { timeZone = TimeZone.getTimeZone("UTC")
val cal = Calendar.getInstance() }
cal.set(Calendar.YEAR, 2999) private val dateFormat = SimpleDateFormat("yyyy-MM-dd'Z'", Locale.ROOT).apply {
cal.set(Calendar.MONTH, 11) timeZone = TimeZone.getTimeZone("UTC")
cal.set(Calendar.DAY_OF_MONTH, 28) }
cal.set(Calendar.HOUR, 23) private val timeFormat = SimpleDateFormat("HH:mm'Z'", Locale.ROOT).apply {
cal.set(Calendar.MINUTE, 59) timeZone = TimeZone.getTimeZone("UTC")
cal.set(Calendar.SECOND, 59) }
return DateInstant(cal.time)
}
@JvmField @JvmField
val CREATOR: Parcelable.Creator<DateInstant> = object : Parcelable.Creator<DateInstant> { val CREATOR: Parcelable.Creator<DateInstant> = object : Parcelable.Creator<DateInstant> {
@@ -123,31 +237,5 @@ class DateInstant : Parcelable {
return arrayOfNulls(size) return arrayOfNulls(size)
} }
} }
private fun isSameDate(d1: Date, d2: Date): Boolean {
val cal1 = Calendar.getInstance()
cal1.time = d1
cal1.set(Calendar.MILLISECOND, 0)
val cal2 = Calendar.getInstance()
cal2.time = d2
cal2.set(Calendar.MILLISECOND, 0)
return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH) &&
cal1.get(Calendar.DAY_OF_MONTH) == cal2.get(Calendar.DAY_OF_MONTH) &&
cal1.get(Calendar.HOUR) == cal2.get(Calendar.HOUR) &&
cal1.get(Calendar.MINUTE) == cal2.get(Calendar.MINUTE) &&
cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND)
}
fun getDateTimeString(resources: Resources, date: Date): String {
return java.text.DateFormat.getDateTimeInstance(
java.text.DateFormat.MEDIUM,
java.text.DateFormat.SHORT,
ConfigurationCompat.getLocales(resources.configuration)[0])
.format(date)
}
} }
} }

View File

@@ -33,7 +33,6 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
import java.util.* import java.util.*
@@ -46,15 +45,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
var entryKDBX: EntryKDBX? = null var entryKDBX: EntryKDBX? = null
private set private set
fun updateWith(entry: Entry, copyHistory: Boolean = true) {
entry.entryKDB?.let {
this.entryKDB?.updateWith(it)
}
entry.entryKDBX?.let {
this.entryKDBX?.updateWith(it, copyHistory)
}
}
/** /**
* Use this constructor to copy an Entry with exact same values * Use this constructor to copy an Entry with exact same values
*/ */
@@ -65,7 +55,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
if (entry.entryKDBX != null) { if (entry.entryKDBX != null) {
this.entryKDBX = EntryKDBX() this.entryKDBX = EntryKDBX()
} }
updateWith(entry, copyHistory) entry.entryKDB?.let {
this.entryKDB?.updateWith(it)
}
entry.entryKDBX?.let {
this.entryKDBX?.updateWith(it, copyHistory)
}
} }
constructor(entry: EntryKDB) { constructor(entry: EntryKDB) {
@@ -283,8 +278,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
fun getExtraFields(): List<Field> { fun getExtraFields(): List<Field> {
val extraFields = ArrayList<Field>() val extraFields = ArrayList<Field>()
entryKDBX?.let { entryKDBX?.let {
it.doForEachDecodedCustomField { key, value -> it.doForEachDecodedCustomField { field ->
extraFields.add(Field(key, value)) extraFields.add(field)
} }
} }
return extraFields return extraFields
@@ -294,7 +289,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
* Update or add an extra field to the list (standard or custom) * Update or add an extra field to the list (standard or custom)
*/ */
fun putExtraField(field: Field) { fun putExtraField(field: Field) {
entryKDBX?.putField(field.name, field.protectedValue) entryKDBX?.putField(field)
} }
private fun addExtraFields(fields: List<Field>) { private fun addExtraFields(fields: List<Field>) {
@@ -310,7 +305,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
fun getOtpElement(): OtpElement? { fun getOtpElement(): OtpElement? {
entryKDBX?.let { entryKDBX?.let {
return OtpEntryFields.parseFields { key -> return OtpEntryFields.parseFields { key ->
it.getField(key)?.toString() it.getFieldValue(key)?.toString()
} }
} }
return null return null
@@ -398,37 +393,46 @@ class Entry : Node, EntryVersionedInterface<Group> {
* Retrieve generated entry info. * Retrieve generated entry info.
* If are not [raw] data, remove parameter fields and add auto generated elements in auto custom fields * If are not [raw] data, remove parameter fields and add auto generated elements in auto custom fields
*/ */
fun getEntryInfo(database: Database?, raw: Boolean = false): EntryInfo { fun getEntryInfo(database: Database?,
raw: Boolean = false,
removeTemplateConfiguration: Boolean = true): EntryInfo {
val entryInfo = EntryInfo() val entryInfo = EntryInfo()
if (raw) // Remove unwanted template fields
database?.stopManageEntry(this) val baseInfo = if (removeTemplateConfiguration)
database?.removeTemplateConfiguration(this) ?: this
else else
database?.startManageEntry(this) this
baseInfo.apply {
if (raw)
database?.stopManageEntry(this)
else
database?.startManageEntry(this)
entryInfo.id = nodeId.toString() entryInfo.id = nodeId.id
entryInfo.title = title entryInfo.title = title
entryInfo.icon = icon entryInfo.icon = icon
entryInfo.username = username entryInfo.username = username
entryInfo.password = password entryInfo.password = password
entryInfo.creationTime = creationTime entryInfo.creationTime = creationTime
entryInfo.lastModificationTime = lastModificationTime entryInfo.lastModificationTime = lastModificationTime
entryInfo.expires = expires entryInfo.expires = expires
entryInfo.expiryTime = expiryTime entryInfo.expiryTime = expiryTime
entryInfo.url = url entryInfo.url = url
entryInfo.notes = notes entryInfo.notes = notes
entryInfo.customFields = getExtraFields() entryInfo.customFields = getExtraFields().toMutableList()
// Add otpElement to generate token // Add otpElement to generate token
entryInfo.otpModel = getOtpElement()?.otpModel entryInfo.otpModel = getOtpElement()?.otpModel
if (!raw) { if (!raw) {
// Replace parameter fields by generated OTP fields // Replace parameter fields by generated OTP fields
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields) entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
} }
database?.attachmentPool?.let { binaryPool -> database?.attachmentPool?.let { binaryPool ->
entryInfo.attachments = getAttachments(binaryPool) entryInfo.attachments = getAttachments(binaryPool).toMutableList()
} }
if (!raw) if (!raw)
database?.stopManageEntry(this) database?.stopManageEntry(this)
}
return entryInfo return entryInfo
} }

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.model package com.kunzisoft.keepass.database.element
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable

View File

@@ -44,14 +44,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
// Virtual group is used to defined a detached database group // Virtual group is used to defined a detached database group
var isVirtual = false var isVirtual = false
fun updateWith(group: Group) { var numberOfChildEntries: Int = 0
group.groupKDB?.let {
this.groupKDB?.updateWith(it)
}
group.groupKDBX?.let {
this.groupKDBX?.updateWith(it)
}
}
/** /**
* Use this constructor to copy a Group * Use this constructor to copy a Group
@@ -65,7 +58,12 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
if (this.groupKDBX == null) if (this.groupKDBX == null)
this.groupKDBX = GroupKDBX() this.groupKDBX = GroupKDBX()
} }
updateWith(group) group.groupKDB?.let {
this.groupKDB?.updateWith(it)
}
group.groupKDBX?.let {
this.groupKDBX?.updateWith(it)
}
} }
constructor(group: GroupKDB) { constructor(group: GroupKDB) {
@@ -118,8 +116,8 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
dest.writeByte((if (isVirtual) 1 else 0).toByte()) dest.writeByte((if (isVirtual) 1 else 0).toByte())
} }
override val nodeId: NodeId<*>? override val nodeId: NodeId<*>
get() = groupKDBX?.nodeId ?: groupKDB?.nodeId get() = groupKDBX?.nodeId ?: groupKDB?.nodeId ?: NodeIdUUID()
override var title: String override var title: String
get() = groupKDB?.title ?: groupKDBX?.title ?: "" get() = groupKDB?.title ?: groupKDBX?.title ?: ""
@@ -179,16 +177,20 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
fun addChildrenFrom(group: Group) { fun addChildrenFrom(group: Group) {
group.groupKDB?.getChildEntries()?.forEach { entryToAdd -> group.groupKDB?.getChildEntries()?.forEach { entryToAdd ->
groupKDB?.addChildEntry(entryToAdd) groupKDB?.addChildEntry(entryToAdd)
entryToAdd.parent = groupKDB
} }
group.groupKDB?.getChildGroups()?.forEach { groupToAdd -> group.groupKDB?.getChildGroups()?.forEach { groupToAdd ->
groupKDB?.addChildGroup(groupToAdd) groupKDB?.addChildGroup(groupToAdd)
groupToAdd.parent = groupKDB
} }
group.groupKDBX?.getChildEntries()?.forEach { entryToAdd -> group.groupKDBX?.getChildEntries()?.forEach { entryToAdd ->
groupKDBX?.addChildEntry(entryToAdd) groupKDBX?.addChildEntry(entryToAdd)
entryToAdd.parent = groupKDBX
} }
group.groupKDBX?.getChildGroups()?.forEach { groupToAdd -> group.groupKDBX?.getChildGroups()?.forEach { groupToAdd ->
groupKDBX?.addChildGroup(groupToAdd) groupKDBX?.addChildGroup(groupToAdd)
groupToAdd.parent = groupKDBX
} }
} }
@@ -270,6 +272,20 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
ArrayList() ArrayList()
} }
fun getFilteredChildGroups(filters: Array<ChildFilter>): List<Group> {
return groupKDB?.getChildGroups()?.map {
Group(it).apply {
this.refreshNumberOfChildEntries(filters)
}
} ?:
groupKDBX?.getChildGroups()?.map {
Group(it).apply {
this.refreshNumberOfChildEntries(filters)
}
} ?:
ArrayList()
}
override fun getChildEntries(): List<Entry> { override fun getChildEntries(): List<Entry> {
return groupKDB?.getChildEntries()?.map { return groupKDB?.getChildEntries()?.map {
Entry(it) Entry(it)
@@ -306,8 +322,8 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
ArrayList() ArrayList()
} }
fun getNumberOfChildEntries(filters: Array<ChildFilter> = emptyArray()): Int { fun refreshNumberOfChildEntries(filters: Array<ChildFilter> = emptyArray()) {
return getFilteredChildEntries(filters).size this.numberOfChildEntries = getFilteredChildEntries(filters).size
} }
/** /**
@@ -319,7 +335,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
} }
fun getFilteredChildren(filters: Array<ChildFilter>): List<Node> { fun getFilteredChildren(filters: Array<ChildFilter>): List<Node> {
return getChildGroups() + getFilteredChildEntries(filters) val nodes = getFilteredChildGroups(filters) + getFilteredChildEntries(filters)
refreshNumberOfChildEntries(filters)
return nodes
} }
override fun addChildGroup(group: Group) { override fun addChildGroup(group: Group) {
@@ -340,6 +358,24 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
} }
} }
override fun updateChildGroup(group: Group) {
group.groupKDB?.let {
groupKDB?.updateChildGroup(it)
}
group.groupKDBX?.let {
groupKDBX?.updateChildGroup(it)
}
}
override fun updateChildEntry(entry: Entry) {
entry.entryKDB?.let {
groupKDB?.updateChildEntry(it)
}
entry.entryKDBX?.let {
groupKDBX?.updateChildEntry(it)
}
}
override fun removeChildGroup(group: Group) { override fun removeChildGroup(group: Group) {
group.groupKDB?.let { group.groupKDB?.let {
groupKDB?.removeChildGroup(it) groupKDB?.removeChildGroup(it)
@@ -455,4 +491,10 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
result = 31 * result + (groupKDBX?.hashCode() ?: 0) result = 31 * result + (groupKDBX?.hashCode() ?: 0)
return result return result
} }
override fun toString(): String {
return groupKDB?.toString() ?: groupKDBX?.toString() ?: "Undefined"
}
} }

View File

@@ -28,15 +28,17 @@ import java.util.*
enum class SortNodeEnum { enum class SortNodeEnum {
DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME; DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME;
fun <G: GroupVersionedInterface<G, *>> getNodeComparator(sortNodeParameters: SortNodeParameters) fun <G: GroupVersionedInterface<G, *>> getNodeComparator(
database: Database,
sortNodeParameters: SortNodeParameters)
: Comparator<NodeVersionedInterface<G>> { : Comparator<NodeVersionedInterface<G>> {
return when (this) { return when (this) {
DB -> NodeNaturalComparator(sortNodeParameters) // Force false because natural order contains recycle bin DB -> NodeNaturalComparator(database, sortNodeParameters) // Force false because natural order contains recycle bin
TITLE -> NodeTitleComparator(sortNodeParameters) TITLE -> NodeTitleComparator(database, sortNodeParameters)
USERNAME -> NodeUsernameComparator(sortNodeParameters) USERNAME -> NodeUsernameComparator(database, sortNodeParameters)
CREATION_TIME -> NodeCreationComparator(sortNodeParameters) CREATION_TIME -> NodeCreationComparator(database, sortNodeParameters)
LAST_MODIFY_TIME -> NodeLastModificationComparator(sortNodeParameters) LAST_MODIFY_TIME -> NodeLastModificationComparator(database, sortNodeParameters)
LAST_ACCESS_TIME -> NodeLastAccessComparator(sortNodeParameters) LAST_ACCESS_TIME -> NodeLastAccessComparator(database, sortNodeParameters)
} }
} }
@@ -48,11 +50,9 @@ enum class SortNodeEnum {
< <
G: GroupVersionedInterface<*, *>, G: GroupVersionedInterface<*, *>,
T: NodeVersionedInterface<G> T: NodeVersionedInterface<G>
>(var sortNodeParameters: SortNodeParameters) >(var database: Database, var sortNodeParameters: SortNodeParameters)
: Comparator<T> { : Comparator<T> {
val database = Database.getInstance()
abstract fun compareBySpecificOrder(object1: T, object2: T): Int abstract fun compareBySpecificOrder(object1: T, object2: T): Int
private fun specificOrderOrHashIfEquals(object1: T, object2: T): Int { private fun specificOrderOrHashIfEquals(object1: T, object2: T): Int {
@@ -110,8 +110,9 @@ enum class SortNodeEnum {
* Comparator of node by natural database placement * Comparator of node by natural database placement
*/ */
class NodeNaturalComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>( class NodeNaturalComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters) sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) { : NodeComparator<G, T>(database, sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int { override fun compareBySpecificOrder(object1: T, object2: T): Int {
return object1.nodeIndexInParentForNaturalOrder() return object1.nodeIndexInParentForNaturalOrder()
@@ -123,13 +124,14 @@ enum class SortNodeEnum {
* Comparator of Node by Title * Comparator of Node by Title
*/ */
class NodeTitleComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>( class NodeTitleComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters) sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) { : NodeComparator<G, T>(database, sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int { override fun compareBySpecificOrder(object1: T, object2: T): Int {
val titleCompare = object1.title.compareTo(object2.title, ignoreCase = true) val titleCompare = object1.title.compareTo(object2.title, ignoreCase = true)
return if (titleCompare == 0) return if (titleCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters) NodeNaturalComparator<G, T>(database, sortNodeParameters)
.compare(object1, object2) .compare(object1, object2)
else else
titleCompare titleCompare
@@ -140,8 +142,9 @@ enum class SortNodeEnum {
* Comparator of Node by Username, Groups by title * Comparator of Node by Username, Groups by title
*/ */
class NodeUsernameComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>( class NodeUsernameComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters) sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) { : NodeComparator<G, T>(database, sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int { override fun compareBySpecificOrder(object1: T, object2: T): Int {
return if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) { return if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
@@ -150,12 +153,12 @@ enum class SortNodeEnum {
.compareTo((object2 as Entry).getEntryInfo(database).username, .compareTo((object2 as Entry).getEntryInfo(database).username,
ignoreCase = true) ignoreCase = true)
if (usernameCompare == 0) if (usernameCompare == 0)
NodeTitleComparator<G, T>(sortNodeParameters) NodeTitleComparator<G, T>(database, sortNodeParameters)
.compare(object1, object2) .compare(object1, object2)
else else
usernameCompare usernameCompare
} else { } else {
NodeTitleComparator<G, T>(sortNodeParameters) NodeTitleComparator<G, T>(database, sortNodeParameters)
.compare(object1, object2) .compare(object1, object2)
} }
} }
@@ -165,14 +168,15 @@ enum class SortNodeEnum {
* Comparator of node by creation * Comparator of node by creation
*/ */
class NodeCreationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>( class NodeCreationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters) sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) { : NodeComparator<G, T>(database, sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int { override fun compareBySpecificOrder(object1: T, object2: T): Int {
val creationCompare = object1.creationTime.date val creationCompare = object1.creationTime.date
.compareTo(object2.creationTime.date) .compareTo(object2.creationTime.date)
return if (creationCompare == 0) return if (creationCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters) NodeNaturalComparator<G, T>(database, sortNodeParameters)
.compare(object1, object2) .compare(object1, object2)
else else
creationCompare creationCompare
@@ -183,14 +187,15 @@ enum class SortNodeEnum {
* Comparator of node by last modification * Comparator of node by last modification
*/ */
class NodeLastModificationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>( class NodeLastModificationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters) sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) { : NodeComparator<G, T>(database, sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int { override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastModificationCompare = object1.lastModificationTime.date val lastModificationCompare = object1.lastModificationTime.date
.compareTo(object2.lastModificationTime.date) .compareTo(object2.lastModificationTime.date)
return if (lastModificationCompare == 0) return if (lastModificationCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters) NodeNaturalComparator<G, T>(database, sortNodeParameters)
.compare(object1, object2) .compare(object1, object2)
else else
lastModificationCompare lastModificationCompare
@@ -201,14 +206,15 @@ enum class SortNodeEnum {
* Comparator of node by last access * Comparator of node by last access
*/ */
class NodeLastAccessComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>( class NodeLastAccessComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
database: Database,
sortNodeParameters: SortNodeParameters) sortNodeParameters: SortNodeParameters)
: NodeComparator<G, T>(sortNodeParameters) { : NodeComparator<G, T>(database, sortNodeParameters) {
override fun compareBySpecificOrder(object1: T, object2: T): Int { override fun compareBySpecificOrder(object1: T, object2: T): Int {
val lastAccessCompare = object1.lastAccessTime.date val lastAccessCompare = object1.lastAccessTime.date
.compareTo(object2.lastAccessTime.date) .compareTo(object2.lastAccessTime.date)
return if (lastAccessCompare == 0) return if (lastAccessCompare == 0)
NodeNaturalComparator<G, T>(sortNodeParameters) NodeNaturalComparator<G, T>(database, sortNodeParameters)
.compare(object1, object2) .compare(object1, object2)
else else
lastAccessCompare lastAccessCompare

View File

@@ -45,6 +45,8 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
import com.kunzisoft.keepass.database.exception.UnknownKDF import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
@@ -79,6 +81,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private var numKeyEncRounds: Long = 0 private var numKeyEncRounds: Long = 0
var publicCustomData = VariantDictionary() var publicCustomData = VariantDictionary()
private val mFieldReferenceEngine = FieldReferencesEngine(this) private val mFieldReferenceEngine = FieldReferencesEngine(this)
private val mTemplateEngine = TemplateEngineCompatible(this)
var kdbxVersion = UnsignedInt(0) var kdbxVersion = UnsignedInt(0)
var name = "" var name = ""
@@ -128,7 +131,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
/** /**
* Create a new database with a root group * Create a new database with a root group
*/ */
constructor(databaseName: String, rootName: String) { constructor(databaseName: String,
rootName: String,
templatesGroupName: String? = null) {
name = databaseName name = databaseName
kdbxVersion = FILE_VERSION_31 kdbxVersion = FILE_VERSION_31
val group = createGroup().apply { val group = createGroup().apply {
@@ -136,6 +141,11 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID) icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID)
} }
rootGroup = group rootGroup = group
if (templatesGroupName != null) {
val templatesGroup = mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
entryTemplatesGroup = templatesGroup.id
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
}
} }
override val version: String override val version: String
@@ -332,10 +342,77 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return this.iconsManager.getIcon(iconUuid) return this.iconsManager.getIcon(iconUuid)
} }
fun isTemplatesGroupEnabled(): Boolean {
return entryTemplatesGroup != UUID_ZERO
}
fun enableTemplatesGroup(enable: Boolean, templatesGroupName: String) {
// Create templates group only if a group with a valid name don't already exists
val firstGroupWithValidName = getGroupIndexes().firstOrNull {
it.title == templatesGroupName
}
if (enable) {
val templatesGroup = firstGroupWithValidName
?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
entryTemplatesGroup = templatesGroup.id
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
} else {
removeTemplatesGroup()
}
}
fun removeTemplatesGroup() {
entryTemplatesGroup = UUID_ZERO
entryTemplatesGroupChanged = DateInstant()
mTemplateEngine.clearCache()
}
fun getTemplatesGroup(): GroupKDBX? {
if (isTemplatesGroupEnabled()) {
return getGroupById(entryTemplatesGroup)
}
return null
}
fun getTemplates(templateCreation: Boolean): List<Template> {
return if (templateCreation)
listOf(mTemplateEngine.getTemplateCreation())
else
mTemplateEngine.getTemplates()
}
fun getTemplate(entry: EntryKDBX): Template? {
return mTemplateEngine.getTemplate(entry)
}
fun decodeEntryWithTemplateConfiguration(entryKDBX: EntryKDBX, entryIsTemplate: Boolean): EntryKDBX {
return if (entryIsTemplate) {
mTemplateEngine.decodeTemplateEntry(entryKDBX)
} else {
mTemplateEngine.removeMetaTemplateRecognitionFromEntry(entryKDBX)
}
}
fun encodeEntryWithTemplateConfiguration(entryKDBX: EntryKDBX, entryIsTemplate: Boolean, template: Template): EntryKDBX {
return if (entryIsTemplate) {
mTemplateEngine.encodeTemplateEntry(entryKDBX)
} else {
mTemplateEngine.addMetaTemplateRecognitionToEntry(template, entryKDBX)
}
}
/* /*
* Search methods * Search methods
*/ */
fun getGroupById(id: UUID): GroupKDBX? {
return this.getGroupById(NodeIdUUID(id))
}
fun getEntryById(id: UUID): EntryKDBX? {
return this.getEntryById(NodeIdUUID(id))
}
fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? { fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry -> return this.entryIndexes.values.find { entry ->
entry.decodeTitleKey(recursionLevel).equals(title, true) entry.decodeTitleKey(recursionLevel).equals(title, true)
@@ -629,15 +706,23 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
*/ */
fun ensureRecycleBinExists(resources: Resources) { fun ensureRecycleBinExists(resources: Resources) {
if (recycleBin == null) { if (recycleBin == null) {
// Create recycle bin // Create recycle bin only if a group with a valid name don't already exists
val recycleBinGroup = createGroup().apply { val firstGroupWithValidName = getGroupIndexes().firstOrNull {
title = resources.getString(R.string.recycle_bin) it.title == resources.getString(R.string.recycle_bin)
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID) }
enableAutoType = false val recycleBinGroup = if (firstGroupWithValidName == null) {
enableSearching = false val newRecycleBinGroup = createGroup().apply {
isExpanded = false title = resources.getString(R.string.recycle_bin)
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
enableAutoType = false
enableSearching = false
isExpanded = false
}
addGroupTo(newRecycleBinGroup, rootGroup)
newRecycleBinGroup
} else {
firstGroupWithValidName
} }
addGroupTo(recycleBinGroup, rootGroup)
recycleBinUUID = recycleBinGroup.id recycleBinUUID = recycleBinGroup.id
recycleBinChanged = recycleBinGroup.lastModificationTime recycleBinChanged = recycleBinGroup.lastModificationTime
} }

View File

@@ -93,6 +93,10 @@ abstract class DatabaseVersioned<
} }
} }
fun getAllGroupsWithoutRoot(): List<Group> {
return getGroupIndexes().filter { it != rootGroup }
}
@Throws(IOException::class) @Throws(IOException::class)
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
@@ -238,13 +242,6 @@ abstract class DatabaseVersioned<
} }
} }
fun updateGroupIndex(group: Group) {
val groupId = group.nodeId
if (groupIndexes.containsKey(groupId)) {
groupIndexes[groupId] = group
}
}
fun removeGroupIndex(group: Group) { fun removeGroupIndex(group: Group) {
this.groupIndexes.remove(group.nodeId) this.groupIndexes.remove(group.nodeId)
} }
@@ -287,13 +284,6 @@ abstract class DatabaseVersioned<
} }
} }
fun updateEntryIndex(entry: Entry) {
val entryId = entry.nodeId
if (entryIndexes.containsKey(entryId)) {
entryIndexes[entryId] = entry
}
}
fun removeEntryIndex(entry: Entry) { fun removeEntryIndex(entry: Entry) {
this.entryIndexes.remove(entry.nodeId) this.entryIndexes.remove(entry.nodeId)
} }
@@ -325,7 +315,11 @@ abstract class DatabaseVersioned<
} }
fun updateGroup(group: Group) { fun updateGroup(group: Group) {
updateGroupIndex(group) group.parent?.updateChildGroup(group)
val groupId = group.nodeId
if (groupIndexes.containsKey(groupId)) {
groupIndexes[groupId] = group
}
} }
fun removeGroupFrom(groupToRemove: Group, parent: Group?) { fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
@@ -342,7 +336,11 @@ abstract class DatabaseVersioned<
} }
open fun updateEntry(entry: Entry) { open fun updateEntry(entry: Entry) {
updateEntryIndex(entry) entry.parent?.updateChildEntry(entry)
val entryId = entry.nodeId
if (entryIndexes.containsKey(entryId)) {
entryIndexes[entryId] = entry
}
} }
open fun removeEntryFrom(entryToRemove: Entry, parent: Group?) { open fun removeEntryFrom(entryToRemove: Entry, parent: Group?) {

View File

@@ -22,10 +22,7 @@ package com.kunzisoft.keepass.database.element.entry
import android.os.Parcel import android.os.Parcel
import android.os.ParcelUuid import android.os.ParcelUuid
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
@@ -52,7 +49,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
override var usageCount = UnsignedLong(0) override var usageCount = UnsignedLong(0)
override var locationChanged = DateInstant() override var locationChanged = DateInstant()
override var customData = CustomData() override var customData = CustomData()
var fields = LinkedHashMap<String, ProtectedString>() private var fields = LinkedHashMap<String, ProtectedString>()
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId> var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
var foregroundColor = "" var foregroundColor = ""
var backgroundColor = "" var backgroundColor = ""
@@ -81,7 +78,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
autoType = parcel.readParcelable(AutoType::class.java.classLoader) ?: autoType autoType = parcel.readParcelable(AutoType::class.java.classLoader) ?: autoType
parcel.readTypedList(history, CREATOR) parcel.readTypedList(history, CREATOR)
url = parcel.readString() ?: url
additional = parcel.readString() ?: additional additional = parcel.readString() ?: additional
} }
@@ -107,7 +103,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
dest.writeParcelable(ParcelUuid(previousParentGroup), flags) dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
dest.writeParcelable(autoType, flags) dest.writeParcelable(autoType, flags)
dest.writeTypedList(history) dest.writeTypedList(history)
dest.writeString(url)
dest.writeString(additional) dest.writeString(additional)
} }
@@ -133,7 +128,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
history.clear() history.clear()
if (copyHistory) if (copyHistory)
history.addAll(source.history) history.addAll(source.history)
url = source.url
additional = source.additional additional = source.additional
} }
@@ -227,6 +221,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
fields[STR_NOTES] = ProtectedString(protect, value) fields[STR_NOTES] = ProtectedString(protect, value)
} }
fun getCustomFieldValue(label: String): String {
return decodeRefKey(mDecodeRef, label, 0)
}
fun getSize(attachmentPool: AttachmentPool): Long { fun getSize(attachmentPool: AttachmentPool): Long {
var size = FIXED_LENGTH_SIZE var size = FIXED_LENGTH_SIZE
@@ -265,28 +263,41 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|| key == STR_NOTES) || key == STR_NOTES)
} }
fun doForEachDecodedCustomField(action: (key: String, value: ProtectedString) -> Unit) { fun doForEachDecodedCustomField(action: (field: Field) -> Unit) {
val iterator = fields.entries.iterator() val iterator = fields.entries.iterator()
while (iterator.hasNext()) { while (iterator.hasNext()) {
val mapEntry = iterator.next() val mapEntry = iterator.next()
if (!isStandardField(mapEntry.key)) { if (!isStandardField(mapEntry.key)) {
action.invoke(mapEntry.key, action.invoke(Field(mapEntry.key,
ProtectedString(mapEntry.value.isProtected, ProtectedString(mapEntry.value.isProtected,
decodeRefKey(mDecodeRef, mapEntry.key, 0) decodeRefKey(mDecodeRef, mapEntry.key, 0)
) )
)
) )
} }
} }
} }
fun getField(key: String): ProtectedString? { fun getFieldValue(label: String): ProtectedString? {
return fields[key] return fields[label]
}
fun getFields(): List<Field> {
return fields.map { Field(it.key, it.value) }
}
fun putField(field: Field) {
putField(field.name, field.protectedValue)
} }
fun putField(label: String, value: ProtectedString) { fun putField(label: String, value: ProtectedString) {
fields[label] = value fields[label] = value
} }
fun removeField(name: String) {
fields.remove(name)
}
fun removeAllFields() { fun removeAllFields() {
fields.clear() fields.clear()
} }

View File

@@ -98,6 +98,24 @@ abstract class GroupVersioned
this.childEntries.add(entry) this.childEntries.add(entry)
} }
override fun updateChildGroup(group: Group) {
val index = this.childGroups.indexOfFirst { it.nodeId == group.nodeId }
if (index >= 0) {
val oldGroup = this.childGroups.removeAt(index)
group.nodeIndexInParentForNaturalOrder = oldGroup.nodeIndexInParentForNaturalOrder
this.childGroups.add(index, group)
}
}
override fun updateChildEntry(entry: Entry) {
val index = this.childEntries.indexOfFirst { it.nodeId == entry.nodeId }
if (index >= 0) {
val oldEntry = this.childEntries.removeAt(index)
entry.nodeIndexInParentForNaturalOrder = oldEntry.nodeIndexInParentForNaturalOrder
this.childEntries.add(index, entry)
}
}
override fun removeChildGroup(group: Group) { override fun removeChildGroup(group: Group) {
this.childGroups.remove(group) this.childGroups.remove(group)
} }
@@ -117,8 +135,4 @@ abstract class GroupVersioned
else else
nodeIndexInParentForNaturalOrder nodeIndexInParentForNaturalOrder
} }
override fun toString(): String {
return titleGroup
}
} }

View File

@@ -32,6 +32,10 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
fun addChildEntry(entry: Entry) fun addChildEntry(entry: Entry)
fun updateChildGroup(group: Group)
fun updateChildEntry(entry: Entry)
fun removeChildGroup(group: Group) fun removeChildGroup(group: Group)
fun removeChildEntry(entry: Entry) fun removeChildEntry(entry: Entry)

View File

@@ -75,8 +75,16 @@ class IconImageStandard : IconImageDraw {
companion object { companion object {
const val KEY_ID = 0 const val KEY_ID = 0
const val ID_CARD_ID = 9
const val WIRELESS_ID = 12
const val EMAIL_ID = 19
const val CREDIT_CARD_ID = 37
const val TRASH_ID = 43 const val TRASH_ID = 43
const val FOLDER_ID = 48 const val FOLDER_ID = 48
const val LIST_ID = 57
const val BUILD_ID = 59
const val STAR_ID = 61
const val DOLLAR_ID = 66
fun isCorrectIconId(iconId: Int): Boolean { fun isCorrectIconId(iconId: Int): Boolean {
return iconId in 0 until NB_ICONS return iconId in 0 until NB_ICONS

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.element.node
import android.os.Parcel import android.os.Parcel
import android.os.ParcelUuid import android.os.ParcelUuid
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.utils.UuidUtil
import java.util.* import java.util.*
class NodeIdUUID : NodeId<UUID> { class NodeIdUUID : NodeId<UUID> {
@@ -60,7 +61,7 @@ class NodeIdUUID : NodeId<UUID> {
} }
override fun toString(): String { override fun toString(): String {
return id.toString() return UuidUtil.toHexString(id) ?: id.toString()
} }
companion object { companion object {

View File

@@ -20,19 +20,16 @@
package com.kunzisoft.keepass.database.element.node package com.kunzisoft.keepass.database.element.node
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import org.joda.time.LocalDateTime
interface NodeKDBInterface : NodeTimeInterface { interface NodeKDBInterface : NodeTimeInterface {
override var expires: Boolean override var expires: Boolean
// If expireDate is before NEVER_EXPIRE date less 1 month (to be sure)
// it is not expires get() = expiryTime.isNeverExpires()
get() = LocalDateTime(expiryTime.date)
.isBefore(LocalDateTime.fromDateFields(DateInstant.NEVER_EXPIRE.date)
.minusMonths(1))
set(value) { set(value) {
if (!value) if (!value)
expiryTime = DateInstant.NEVER_EXPIRE expiryTime = DateInstant.NEVER_EXPIRES
} }
} }

View File

@@ -26,7 +26,6 @@ import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import org.joda.time.LocalDateTime
/** /**
* Abstract class who manage Groups and Entries * Abstract class who manage Groups and Entries
@@ -95,11 +94,10 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
final override var lastAccessTime: DateInstant = DateInstant() final override var lastAccessTime: DateInstant = DateInstant()
final override var expiryTime: DateInstant = DateInstant.NEVER_EXPIRE final override var expiryTime: DateInstant = DateInstant.NEVER_EXPIRES
final override val isCurrentlyExpires: Boolean final override val isCurrentlyExpires: Boolean
get() = expires get() = expires && expiryTime.isCurrentlyExpire()
&& LocalDateTime.fromDateFields(expiryTime.date).isBefore(LocalDateTime.now())
/** /**
* @return true if parent is present (false if not present, can be a root or a detach element) * @return true if parent is present (false if not present, can be a root or a detach element)
@@ -150,4 +148,8 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
override fun hashCode(): Int { override fun hashCode(): Int {
return nodeId.hashCode() return nodeId.hashCode()
} }
override fun toString(): String {
return "$title ($nodeId)"
}
} }

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.database.element.template
import android.os.Parcel
import android.os.ParcelUuid
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.LinkedHashMap
class Template : Parcelable {
var version = 1
var uuid: UUID = DatabaseVersioned.UUID_ZERO
var title = ""
var icon = IconImage()
var sections: MutableList<TemplateSection> = ArrayList()
private set
constructor(uuid: UUID,
title: String,
icon: IconImage,
section: TemplateSection,
version: Int = 1): this(uuid, title, icon, ArrayList<TemplateSection>().apply {
add(section)
}, version)
constructor(uuid: UUID,
title: String,
icon: IconImage,
sections: List<TemplateSection>,
version: Int = 1) {
this.version = version
this.uuid = uuid
this.title = title
this.icon = icon
this.sections.clear()
this.sections.addAll(sections)
}
constructor(template: Template) {
this.version = template.version
this.uuid = template.uuid
this.title = template.title
this.icon = template.icon
this.sections.clear()
this.sections.addAll(template.sections)
}
constructor(parcel: Parcel) {
version = parcel.readInt()
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: uuid
title = parcel.readString() ?: title
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
parcel.readList(sections, TemplateSection::class.java.classLoader)
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(version)
parcel.writeParcelable(ParcelUuid(uuid), flags)
parcel.writeString(title)
parcel.writeParcelable(icon, flags)
parcel.writeList(sections)
}
override fun describeContents(): Int {
return 0
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Template) return false
if (uuid != other.uuid) return false
return true
}
override fun hashCode(): Int {
return uuid.hashCode()
}
companion object CREATOR : Parcelable.Creator<Template> {
override fun createFromParcel(parcel: Parcel): Template {
return Template(parcel)
}
override fun newArray(size: Int): Array<Template?> {
return arrayOfNulls(size)
}
val TITLE_ATTRIBUTE = TemplateAttribute(
TemplateField.LABEL_TITLE,
TemplateAttributeType.TEXT)
val USERNAME_ATTRIBUTE = TemplateAttribute(
TemplateField.LABEL_USERNAME,
TemplateAttributeType.TEXT)
val PASSWORD_ATTRIBUTE = TemplateAttribute(
TemplateField.LABEL_PASSWORD,
TemplateAttributeType.TEXT,
true,
TemplateAttributeOption().apply {
setNumberLines(3)
associatePasswordGenerator()
}
)
val URL_ATTRIBUTE = TemplateAttribute(
TemplateField.LABEL_URL,
TemplateAttributeType.TEXT,
false,
TemplateAttributeOption().apply {
setLink(true)
})
val EXPIRATION_ATTRIBUTE = TemplateAttribute(
TemplateField.LABEL_EXPIRATION,
TemplateAttributeType.DATETIME,
false)
val NOTES_ATTRIBUTE = TemplateAttribute(
TemplateField.LABEL_NOTES,
TemplateAttributeType.TEXT,
false,
TemplateAttributeOption().apply {
setNumberLinesToMany()
})
val STANDARD: Template
get() {
val sections = mutableListOf<TemplateSection>()
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
add(USERNAME_ATTRIBUTE)
add(PASSWORD_ATTRIBUTE)
add(URL_ATTRIBUTE)
add(EXPIRATION_ATTRIBUTE)
add(NOTES_ATTRIBUTE)
})
sections.add(mainSection)
return Template(
DatabaseVersioned.UUID_ZERO,
TemplateField.LABEL_STANDARD,
IconImage(),
sections)
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.database.element.template
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.utils.ParcelableUtil
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
data class TemplateAttribute(var label: String,
var type: TemplateAttributeType,
var protected: Boolean = false,
var options: TemplateAttributeOption = TemplateAttributeOption(),
var action: TemplateAttributeAction = TemplateAttributeAction.NONE): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readEnum<TemplateAttributeType>() ?: TemplateAttributeType.TEXT,
parcel.readByte() != 0.toByte(),
parcel.readParcelable(TemplateAttributeOption::class.java.classLoader) ?: TemplateAttributeOption(),
parcel.readEnum<TemplateAttributeAction>() ?: TemplateAttributeAction.NONE)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(label)
parcel.writeEnum(type)
parcel.writeByte(if (protected) 1 else 0)
parcel.writeParcelable(options, flags)
parcel.writeEnum(action)
}
override fun describeContents(): Int {
return 0
}
var alias: String?
get() {
return options.alias
}
set(value) {
options.alias = value
}
var default: String
get() {
return this.options.default
}
set(value) {
this.options.default = value
}
companion object CREATOR : Parcelable.Creator<TemplateAttribute> {
override fun createFromParcel(parcel: Parcel): TemplateAttribute {
return TemplateAttribute(parcel)
}
override fun newArray(size: Int): Array<TemplateAttribute?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,5 @@
package com.kunzisoft.keepass.database.element.template
enum class TemplateAttributeAction {
NONE, CUSTOM_EDITION
}

View File

@@ -0,0 +1,270 @@
package com.kunzisoft.keepass.database.element.template
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.utils.ParcelableUtil
class TemplateAttributeOption() : Parcelable {
private val mOptions: MutableMap<String, String> = mutableMapOf()
constructor(parcel: Parcel) : this() {
ParcelableUtil.readStringParcelableMap(parcel)
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
ParcelableUtil.writeStringParcelableMap(parcel, LinkedHashMap(mOptions))
}
override fun describeContents(): Int {
return 0
}
var alias: String?
get() {
val tempAlias = mOptions[ALIAS_ATTR]
if (tempAlias.isNullOrEmpty())
return null
return tempAlias
}
set(value) {
if (value == null)
mOptions.remove(ALIAS_ATTR)
else
mOptions[ALIAS_ATTR] = value
}
var default: String
get() {
return mOptions[DEFAULT_ATTR] ?: DEFAULT_VALUE
}
set(value) {
mOptions[DEFAULT_ATTR] = value
}
fun getNumberChars(): Int {
return try {
if (mOptions[TEXT_NUMBER_CHARS_ATTR].equals(TEXT_NUMBER_CHARS_VALUE_MANY_STRING, true))
TEXT_NUMBER_CHARS_VALUE_MANY
else
mOptions[TEXT_NUMBER_CHARS_ATTR]?.toInt() ?: TEXT_NUMBER_CHARS_VALUE_DEFAULT
} catch (e: Exception) {
TEXT_NUMBER_CHARS_VALUE_DEFAULT
}
}
fun setNumberChars(numberChars: Int) {
mOptions[TEXT_NUMBER_CHARS_ATTR] = numberChars.toString()
}
fun setNumberCharsToMany() {
mOptions[TEXT_NUMBER_CHARS_ATTR] = TEXT_NUMBER_CHARS_VALUE_MANY_STRING
}
fun getNumberLines(): Int {
return try {
if (mOptions[TEXT_NUMBER_LINES_ATTR].equals(TEXT_NUMBER_LINES_VALUE_MANY_STRING, true))
TEXT_NUMBER_LINES_VALUE_MANY
else
mOptions[TEXT_NUMBER_LINES_ATTR]?.toInt() ?: TEXT_NUMBER_LINES_VALUE_DEFAULT
} catch (e: Exception) {
TEXT_NUMBER_LINES_VALUE_DEFAULT
}
}
fun setNumberLines(numberLines: Int) {
val lines = if (numberLines == 0) 1 else numberLines
mOptions[TEXT_NUMBER_LINES_ATTR] = lines.toString()
}
fun setNumberLinesToMany() {
mOptions[TEXT_NUMBER_LINES_ATTR] = TEXT_NUMBER_LINES_VALUE_MANY_STRING
}
fun isLink(): Boolean {
return try {
mOptions[TEXT_LINK_ATTR]?.toBoolean() ?: TEXT_LINK_VALUE_DEFAULT
} catch (e: Exception) {
TEXT_LINK_VALUE_DEFAULT
}
}
fun setLink(isLink: Boolean) {
mOptions[TEXT_LINK_ATTR] = isLink.toString()
}
fun isAssociatedWithPasswordGenerator(): Boolean {
return try {
mOptions[PASSWORD_GENERATOR_ATTR]?.toBoolean() ?: PASSWORD_GENERATOR_VALUE_DEFAULT
} catch (e: Exception) {
PASSWORD_GENERATOR_VALUE_DEFAULT
}
}
fun associatePasswordGenerator() {
mOptions[PASSWORD_GENERATOR_ATTR] = true.toString()
}
fun getListItems(): List<String> {
return mOptions[LIST_ITEMS]?.split(LIST_ITEMS_SEPARATOR) ?: listOf()
}
fun setListItems(vararg items: String) {
mOptions[LIST_ITEMS] = items.joinToString(LIST_ITEMS_SEPARATOR)
}
fun getDateFormat(): DateInstant.Type {
return when (mOptions[DATETIME_FORMAT_ATTR]) {
DATETIME_FORMAT_VALUE_DATE -> DateInstant.Type.DATE
DATETIME_FORMAT_VALUE_TIME -> DateInstant.Type.TIME
else -> DateInstant.Type.DATE_TIME
}
}
fun setDateFormatToDate() {
mOptions[DATETIME_FORMAT_ATTR] = DATETIME_FORMAT_VALUE_DATE
}
fun setDateFormatToTime() {
mOptions[DATETIME_FORMAT_ATTR] = DATETIME_FORMAT_VALUE_TIME
}
fun get(label: String): String? {
return mOptions[label]
}
fun put(label: String, value: String) {
mOptions[label] = value
}
fun remove(label: String) {
mOptions.remove(label)
}
companion object CREATOR : Parcelable.Creator<TemplateAttributeOption> {
override fun createFromParcel(parcel: Parcel): TemplateAttributeOption {
return TemplateAttributeOption(parcel)
}
override fun newArray(size: Int): Array<TemplateAttributeOption?> {
return arrayOfNulls(size)
}
/**
* Applicable to each type
* Define a text replacement for a label,
* Useful to keep compatibility with old keepass apps by replacing standard field label
*/
private const val ALIAS_ATTR = "alias"
/**
* Applicable to each type
* Define a default string element representation
* For a type LIST, represents a single string element representation
*/
private const val DEFAULT_ATTR = "default"
private const val DEFAULT_VALUE = ""
/**
* Applicable to type TEXT
* Define a number of chars
* Integer, can be "many" or "-1" to infinite value
* "1" if not defined
*/
private const val TEXT_NUMBER_CHARS_ATTR = "chars"
private const val TEXT_NUMBER_CHARS_VALUE_MANY = -1
private const val TEXT_NUMBER_CHARS_VALUE_MANY_STRING = "many"
private const val TEXT_NUMBER_CHARS_VALUE_DEFAULT = -1
/**
* Applicable to type TEXT
* Define a number of lines
* Integer, can be "-1" to infinite value
* "1" if not defined
*/
private const val TEXT_NUMBER_LINES_ATTR = "lines"
private const val TEXT_NUMBER_LINES_VALUE_MANY = -1
private const val TEXT_NUMBER_LINES_VALUE_MANY_STRING = "many"
private const val TEXT_NUMBER_LINES_VALUE_DEFAULT = 1
/**
* Applicable to type TEXT
* Define if a text is a link
* Boolean ("true" or "false")
* "true" if not defined
*/
private const val TEXT_LINK_ATTR = "link"
private const val TEXT_LINK_VALUE_DEFAULT = false
/**
* Applicable to type TEXT
* Define if a password generator is associated with the text
* Boolean ("true" or "false")
* "false" if not defined
*/
private const val PASSWORD_GENERATOR_ATTR = "generator"
private const val PASSWORD_GENERATOR_VALUE_DEFAULT = false
/**
* Applicable to type LIST
* Define items of a list
* List of items, separator is '|'
*/
private const val LIST_ITEMS = "items"
private const val LIST_ITEMS_SEPARATOR = "|"
/**
* Applicable to type DATETIME
* Define the type of date
* String ("date" or "time" or "datetime" or based on https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html)
* "datetime" if not defined
*/
private const val DATETIME_FORMAT_ATTR = "format"
private const val DATETIME_FORMAT_VALUE_DATE = "date"
private const val DATETIME_FORMAT_VALUE_TIME = "time"
private fun removeSpecialChars(string: String): String {
return string.filterNot { "{,:}".indexOf(it) > -1 }
}
fun getOptionsFromString(label: String): TemplateAttributeOption {
val options = TemplateAttributeOption()
val optionsMap = if (label.contains("{") || label.contains("}")) {
try {
label.trim().substringAfter("{").substringBefore("}")
.split(",").associate {
val keyValue = it.trim()
val (left, right) = keyValue.split(":")
left to right
}.toMutableMap()
} catch (e: Exception) {
mutableMapOf()
}
} else {
mutableMapOf()
}
options.mOptions.apply {
clear()
putAll(optionsMap)
}
return options
}
fun getStringFromOptions(options: TemplateAttributeOption): String {
var optionsString = ""
if (options.mOptions.isNotEmpty()) {
optionsString += " {"
var first = true
for ((key, value) in options.mOptions) {
if (!first)
optionsString += ", "
first = false
optionsString += "${removeSpecialChars(key)}:${removeSpecialChars(value)}"
}
optionsString += "}"
}
return optionsString
}
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2020 Jeremy Jamet / Kunzisoft. * Copyright 2021 Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePassDX. * This file is part of KeePassDX.
* *
@@ -15,25 +15,24 @@
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/ */
package com.kunzisoft.keepass.activities.dialogs package com.kunzisoft.keepass.database.element.template
import com.kunzisoft.keepass.R enum class TemplateAttributeType(val typeString: String) {
import com.kunzisoft.keepass.database.element.node.Node TEXT("text"),
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes LIST("list"),
DATETIME("datetime"),
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() { DIVIDER("divider");
override fun retrieveMessage(): String {
return getString(R.string.warning_empty_recycle_bin)
}
companion object { companion object {
fun getInstance(nodesToDelete: List<Node>): EmptyRecycleBinDialogFragment { fun getFromString(label: String): TemplateAttributeType {
return EmptyRecycleBinDialogFragment().apply { return when {
arguments = getBundleFromListNodes(nodesToDelete) label.contains(TEXT.typeString, true) -> TEXT
label.contains(LIST.typeString, true) -> LIST
label.contains(DATETIME.typeString, true) -> DATETIME
label.contains(DIVIDER.typeString, true) -> DIVIDER
else -> TEXT
} }
} }
} }
} }

View File

@@ -0,0 +1,178 @@
package com.kunzisoft.keepass.database.element.template
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import java.util.*
import kotlin.collections.LinkedHashMap
class TemplateBuilder {
private val urlAttribute = TemplateAttribute(TemplateField.LABEL_URL, TemplateAttributeType.TEXT)
private val usernameAttribute = TemplateAttribute(TemplateField.LABEL_USERNAME, TemplateAttributeType.TEXT)
private val notesAttribute = TemplateAttribute(
TemplateField.LABEL_NOTES,
TemplateAttributeType.TEXT,
false,
TemplateAttributeOption().apply {
setNumberLinesToMany()
})
private val holderAttribute = TemplateAttribute(TemplateField.LABEL_HOLDER, TemplateAttributeType.TEXT)
private val numberAttribute = TemplateAttribute(TemplateField.LABEL_NUMBER, TemplateAttributeType.TEXT)
private val cvvAttribute = TemplateAttribute(TemplateField.LABEL_CVV, TemplateAttributeType.TEXT, true)
private val pinAttribute = TemplateAttribute(TemplateField.LABEL_PIN, TemplateAttributeType.TEXT, true)
private val nameAttribute = TemplateAttribute(TemplateField.LABEL_NAME, TemplateAttributeType.TEXT)
private val placeOfIssueAttribute = TemplateAttribute(TemplateField.LABEL_PLACE_OF_ISSUE, TemplateAttributeType.TEXT)
private val dateOfIssueAttribute = TemplateAttribute(
TemplateField.LABEL_DATE_OF_ISSUE,
TemplateAttributeType.DATETIME,
false,
TemplateAttributeOption().apply {
setDateFormatToDate()
})
private val expirationDateAttribute = TemplateAttribute(
TemplateField.LABEL_EXPIRATION,
TemplateAttributeType.DATETIME,
false,
TemplateAttributeOption().apply {
setDateFormatToDate()
})
private val emailAddressAttribute = TemplateAttribute(TemplateField.LABEL_EMAIL_ADDRESS, TemplateAttributeType.TEXT)
private val passwordAttribute = TemplateAttribute(TemplateField.LABEL_PASSWORD, TemplateAttributeType.TEXT, true)
private val ssidAttribute = TemplateAttribute(TemplateField.LABEL_SSID, TemplateAttributeType.TEXT)
private val wirelessTypeAttribute = TemplateAttribute(
TemplateField.LABEL_TYPE,
TemplateAttributeType.LIST,
false,
TemplateAttributeOption().apply{
setListItems("WPA3", "WPA2", "WPA", "WEP")
default = "WPA2"
})
private val tokenAttribute = TemplateAttribute(TemplateField.LABEL_TOKEN, TemplateAttributeType.TEXT)
private val publicKeyAttribute = TemplateAttribute(TemplateField.LABEL_PUBLIC_KEY, TemplateAttributeType.TEXT)
private val privateKeyAttribute = TemplateAttribute(TemplateField.LABEL_PRIVATE_KEY, TemplateAttributeType.TEXT, true)
private val seedAttribute = TemplateAttribute(TemplateField.LABEL_SEED, TemplateAttributeType.TEXT, true)
private val bicAttribute = TemplateAttribute(TemplateField.LABEL_BIC, TemplateAttributeType.TEXT)
private val ibanAttribute = TemplateAttribute(TemplateField.LABEL_IBAN, TemplateAttributeType.TEXT)
val email: Template
get() {
val sections = mutableListOf<TemplateSection>()
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
add(emailAddressAttribute)
add(urlAttribute)
add(passwordAttribute)
})
sections.add(mainSection)
return Template(
UUID.randomUUID(),
TemplateField.LABEL_EMAIL,
IconImage(IconImageStandard(IconImageStandard.EMAIL_ID)),
sections)
}
val wifi: Template
get() {
val sections = mutableListOf<TemplateSection>()
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
add(ssidAttribute)
add(passwordAttribute)
add(wirelessTypeAttribute)
})
sections.add(mainSection)
return Template(
UUID.randomUUID(),
TemplateField.LABEL_WIRELESS,
IconImage(IconImageStandard(IconImageStandard.WIRELESS_ID)),
sections)
}
val notes: Template
get() {
val sections = mutableListOf<TemplateSection>()
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
add(notesAttribute)
})
sections.add(mainSection)
return Template(
UUID.randomUUID(),
TemplateField.LABEL_NOTES,
IconImage(IconImageStandard(IconImageStandard.LIST_ID)),
sections)
}
val idCard: Template
get() {
val sections = mutableListOf<TemplateSection>()
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
add(numberAttribute)
add(nameAttribute)
add(placeOfIssueAttribute)
add(dateOfIssueAttribute)
add(expirationDateAttribute)
})
sections.add(mainSection)
return Template(
UUID.randomUUID(),
TemplateField.LABEL_ID_CARD,
IconImage(IconImageStandard(IconImageStandard.ID_CARD_ID)),
sections)
}
val creditCard: Template
get() {
val sections = mutableListOf<TemplateSection>()
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
add(numberAttribute)
add(cvvAttribute)
add(pinAttribute)
add(holderAttribute)
add(expirationDateAttribute)
})
sections.add(mainSection)
return Template(
UUID.randomUUID(),
TemplateField.LABEL_DEBIT_CREDIT_CARD,
IconImage(IconImageStandard(IconImageStandard.CREDIT_CARD_ID)),
sections)
}
val bank: Template
get() {
val sections = mutableListOf<TemplateSection>()
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
add(nameAttribute)
add(urlAttribute)
add(usernameAttribute)
add(passwordAttribute)
})
val ibanSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
add(holderAttribute)
add(bicAttribute)
add(ibanAttribute)
}, TemplateField.LABEL_ACCOUNT)
sections.add(mainSection)
sections.add(ibanSection)
return Template(
UUID.randomUUID(),
TemplateField.LABEL_BANK,
IconImage(IconImageStandard(IconImageStandard.DOLLAR_ID)),
sections)
}
val cryptocurrency: Template
get() {
val sections = mutableListOf<TemplateSection>()
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
add(tokenAttribute)
add(publicKeyAttribute)
add(privateKeyAttribute)
add(seedAttribute)
})
sections.add(mainSection)
return Template(
UUID.randomUUID(),
TemplateField.LABEL_CRYPTOCURRENCY,
IconImage(IconImageStandard(IconImageStandard.STAR_ID)),
sections)
}
}

View File

@@ -0,0 +1,207 @@
package com.kunzisoft.keepass.database.element.template
import android.content.res.Resources
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.security.ProtectedString
import java.util.*
import kotlin.collections.HashMap
abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
private val mCacheTemplates = HashMap<UUID, Template>()
fun getTemplates(): List<Template> {
val templates = mutableListOf<Template>()
try {
val templateGroup = mDatabase.getTemplatesGroup()
mCacheTemplates.clear()
if (templateGroup != null) {
templates.add(Template.STANDARD)
templateGroup.getChildEntries().forEach { templateEntry ->
getTemplateFromTemplateEntry(templateEntry)?.let {
mCacheTemplates[templateEntry.id] = it
templates.add(it)
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to get templates from group", e)
}
return templates
}
fun getTemplateCreation(): Template {
return Template(CREATION)
}
fun createNewTemplatesGroup(templatesGroupName: String): GroupKDBX {
val newTemplatesGroup = mDatabase.createGroup().apply {
title = templatesGroupName
icon.standard = mDatabase.getStandardIcon(IconImageStandard.BUILD_ID)
enableAutoType = false
enableSearching = false
isExpanded = false
}
mDatabase.addGroupTo(newTemplatesGroup, mDatabase.rootGroup)
// Build default templates
getDefaults().forEach { defaultTemplate ->
createTemplateEntry(defaultTemplate).also {
mDatabase.addEntryTo(it, newTemplatesGroup)
}
}
return newTemplatesGroup
}
fun clearCache() {
mCacheTemplates.clear()
}
protected fun getTemplateByCache(uuid: UUID): Template? {
try {
if (mCacheTemplates.containsKey(uuid))
return mCacheTemplates[uuid]
else {
mDatabase.getEntryById(uuid)?.let { templateEntry ->
getTemplateFromTemplateEntry(templateEntry)?.let { newTemplate ->
mCacheTemplates[uuid] = newTemplate
return newTemplate
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to get Entry by $uuid", e)
}
return null
}
abstract fun getVersion(): Int
abstract fun getTemplate(entryKDBX: EntryKDBX): Template?
abstract fun removeMetaTemplateRecognitionFromEntry(entry: EntryKDBX): EntryKDBX
abstract fun addMetaTemplateRecognitionToEntry(template: Template, entry: EntryKDBX): EntryKDBX
abstract fun buildTemplateEntryField(attribute: TemplateAttribute): Field
abstract fun decodeTemplateEntry(templateEntry: EntryKDBX): EntryKDBX
abstract fun encodeTemplateEntry(templateEntry: EntryKDBX): EntryKDBX
fun createTemplateEntry(template: Template): EntryKDBX {
val newEntry = EntryKDBX().apply {
nodeId = NodeIdUUID(template.uuid)
title = template.title
icon = template.icon
template.sections.forEachIndexed { index, section ->
section.attributes.forEach { attribute ->
if (index > 0) {
// Label is not important with section => [Section_X]: Divider
val sectionName = if (section.name.isEmpty())
"$SECTION_DECODED_TEMPLATE_PREFIX${index-1}"
else
section.name
putField(Field(addTemplateDecorator(sectionName),
ProtectedString(false, TemplateAttributeType.DIVIDER.typeString))
)
}
putField(buildTemplateEntryField(attribute))
}
}
}
return encodeTemplateEntry(newEntry)
}
private fun buildTemplateSectionFromFields(fields: List<Field>): TemplateSection {
val sectionAttributes = mutableListOf<TemplateAttribute>()
fields.forEach { field ->
sectionAttributes.add(TemplateAttribute(
removeTemplateDecorator(field.name),
TemplateAttributeType.getFromString(field.protectedValue.stringValue),
field.protectedValue.isProtected,
TemplateAttributeOption.getOptionsFromString(field.protectedValue.stringValue))
)
}
return TemplateSection(sectionAttributes)
}
private fun getTemplateFromTemplateEntry(templateEntry: EntryKDBX): Template? {
val templateEntryDecoded = decodeTemplateEntry(templateEntry)
val templateSections = mutableListOf<TemplateSection>()
val sectionFields = mutableListOf<Field>()
templateEntryDecoded.doForEachDecodedCustomField { field ->
if (field.protectedValue.stringValue.contains(TemplateAttributeType.DIVIDER.typeString)) {
templateSections.add(buildTemplateSectionFromFields(sectionFields))
sectionFields.clear()
} else {
sectionFields.add(field)
}
}
templateSections.add(buildTemplateSectionFromFields(sectionFields))
return Template(templateEntry.id, templateEntry.title, templateEntry.icon, templateSections, getVersion())
}
companion object {
private val TAG = TemplateEngine::class.java.name
private const val PREFIX_DECODED_TEMPLATE = "["
private const val SUFFIX_DECODED_TEMPLATE = "]"
private const val SECTION_DECODED_TEMPLATE_PREFIX = "Section "
val CREATION: Template
get() {
val sections = mutableListOf<TemplateSection>()
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
// Dynamic part
})
sections.add(mainSection)
return Template(UUID(0, 1),
TemplateField.LABEL_TEMPLATE,
IconImage(IconImageStandard(IconImageStandard.BUILD_ID)),
sections)
}
fun getDefaultTemplateGroupName(resources: Resources): String {
return resources.getString(R.string.templates)
}
fun getDefaults(): List<Template> {
val templateBuilder = TemplateBuilder()
return listOf(
templateBuilder.email,
templateBuilder.wifi,
templateBuilder.notes,
templateBuilder.idCard,
templateBuilder.creditCard,
templateBuilder.bank,
templateBuilder.cryptocurrency)
}
fun containsTemplateDecorator(name: String): Boolean {
return name.startsWith(PREFIX_DECODED_TEMPLATE)
&& name.endsWith(SUFFIX_DECODED_TEMPLATE)
}
fun addTemplateDecorator(name: String): String {
return "$PREFIX_DECODED_TEMPLATE${name}$SUFFIX_DECODED_TEMPLATE"
}
fun removeTemplateDecorator(name: String): String {
return name
.removePrefix(PREFIX_DECODED_TEMPLATE)
.removeSuffix(SUFFIX_DECODED_TEMPLATE)
}
}
}

View File

@@ -0,0 +1,434 @@
package com.kunzisoft.keepass.database.element.template
import android.util.Log
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.utils.UuidUtil
class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database) {
override fun getVersion(): Int {
return 1
}
override fun getTemplate(entryKDBX: EntryKDBX): Template? {
UuidUtil.fromHexString(entryKDBX.getCustomFieldValue(TEMPLATE_ENTRY_UUID))?.let { templateUUID ->
return getTemplateByCache(templateUUID)
}
return null
}
override fun removeMetaTemplateRecognitionFromEntry(entry: EntryKDBX): EntryKDBX {
val entryCopy = EntryKDBX().apply {
updateWith(entry)
}
entryCopy.removeField(TEMPLATE_ENTRY_UUID)
return entryCopy
}
private fun getTemplateUUIDField(template: Template): Field? {
UuidUtil.toHexString(template.uuid)?.let { uuidString ->
return Field(TEMPLATE_ENTRY_UUID,
ProtectedString(false, uuidString))
}
return null
}
override fun addMetaTemplateRecognitionToEntry(template: Template, entry: EntryKDBX): EntryKDBX {
val entryCopy = EntryKDBX().apply {
updateWith(entry)
}
// Add template field
if (template != Template.STANDARD
&& template != CREATION) {
getTemplateUUIDField(template)?.let { templateField ->
entryCopy.putField(templateField)
}
} else {
entryCopy.removeField(TEMPLATE_ENTRY_UUID)
}
return entryCopy
}
private fun getOrRetrieveAttributeFromName(attributes: HashMap<String, TemplateAttributePosition>, name: String): TemplateAttributePosition {
return if (attributes.containsKey(name)) {
attributes[name]!!
} else {
val newAttribute = TemplateAttributePosition(
-1,
TemplateAttribute(name, TemplateAttributeType.TEXT)
)
attributes[name] = newAttribute
newAttribute
}
}
override fun buildTemplateEntryField(attribute: TemplateAttribute): Field {
val typeAndOptions = attribute.type.typeString +
TemplateAttributeOption.getStringFromOptions(attribute.options)
// PREFIX_DECODED_TEMPLATE to fix same label as standard fields
return Field(addTemplateDecorator(decodeTemplateAttribute(attribute.label)),
ProtectedString(attribute.protected, typeAndOptions))
}
override fun decodeTemplateEntry(templateEntry: EntryKDBX): EntryKDBX {
val attributes = HashMap<String, TemplateAttributePosition>()
val defaultValues = HashMap<String, String>()
val entryCopy = EntryKDBX().apply {
updateWith(templateEntry)
}
// Remove template version
entryCopy.getFieldValue(TEMPLATE_LABEL_VERSION)
try {
// value.toIntOrNull()
// At the moment, only the version 1 is known
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve template version", e)
}
entryCopy.removeField(TEMPLATE_LABEL_VERSION)
// Dynamic attributes
templateEntry.doForEachDecodedCustomField { field ->
val label = field.name
val value = field.protectedValue.stringValue
when {
label.startsWith(TEMPLATE_ATTRIBUTE_POSITION_PREFIX, true) -> {
try {
val attributeName = label.substring(TEMPLATE_ATTRIBUTE_POSITION_PREFIX.length)
val attribute = getOrRetrieveAttributeFromName(attributes, attributeName)
attribute.position = value.toInt()
entryCopy.removeField(field.name)
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve template position", e)
}
}
label.startsWith(TEMPLATE_ATTRIBUTE_TITLE_PREFIX, true) -> {
try {
val attributeName = label.substring(TEMPLATE_ATTRIBUTE_TITLE_PREFIX.length)
val attribute = getOrRetrieveAttributeFromName(attributes, attributeName)
// Here title is an alias if different (often the same)
if (attributeName != value) {
attribute.attribute.alias = value
}
entryCopy.removeField(field.name)
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve template title", e)
}
}
label.startsWith(TEMPLATE_ATTRIBUTE_TYPE_PREFIX, true) -> {
try {
val attributeName = label.substring(TEMPLATE_ATTRIBUTE_TYPE_PREFIX.length)
val attribute = getOrRetrieveAttributeFromName(attributes, attributeName)
if (value.contains(TEMPLATE_ATTRIBUTE_TYPE_PROTECTED, true)) {
attribute.attribute.protected = true
}
when {
value.contains(TEMPLATE_ATTRIBUTE_TYPE_INLINE_URL, true) -> {
attribute.attribute.type = TemplateAttributeType.TEXT
attribute.attribute.options.setLink(true)
}
value.contains(TEMPLATE_ATTRIBUTE_TYPE_INLINE, true) ||
value.contains(TEMPLATE_ATTRIBUTE_TYPE_POPOUT, true) -> {
attribute.attribute.type = TemplateAttributeType.TEXT
}
value.contains(TEMPLATE_ATTRIBUTE_TYPE_MULTILINE, true) -> {
attribute.attribute.type = TemplateAttributeType.TEXT
attribute.attribute.options.setNumberLinesToMany()
}
value.contains(TEMPLATE_ATTRIBUTE_TYPE_RICH_TEXTBOX, true) -> {
attribute.attribute.type = TemplateAttributeType.TEXT
attribute.attribute.options.setNumberLinesToMany()
attribute.attribute.options.setLink(true)
}
value.contains(TEMPLATE_ATTRIBUTE_TYPE_LISTBOX, true) -> {
attribute.attribute.type = TemplateAttributeType.LIST
}
value.contains(TEMPLATE_ATTRIBUTE_TYPE_DATE_TIME, true) -> {
attribute.attribute.type = TemplateAttributeType.DATETIME
}
value.contains(TEMPLATE_ATTRIBUTE_TYPE_DATE, true) -> {
attribute.attribute.type = TemplateAttributeType.DATETIME
attribute.attribute.options.setDateFormatToDate()
}
value.contains(TEMPLATE_ATTRIBUTE_TYPE_TIME, true) -> {
attribute.attribute.type = TemplateAttributeType.DATETIME
attribute.attribute.options.setDateFormatToTime()
}
value.contains(TEMPLATE_ATTRIBUTE_TYPE_DIVIDER, true) -> {
attribute.attribute.type = TemplateAttributeType.DIVIDER
}
}
entryCopy.removeField(field.name)
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve template type", e)
}
}
label.startsWith(TEMPLATE_ATTRIBUTE_OPTIONS_PREFIX, true) -> {
try {
val attributeName = label.substring(TEMPLATE_ATTRIBUTE_OPTIONS_PREFIX.length)
val attribute = getOrRetrieveAttributeFromName(attributes, attributeName)
if (value.isNotEmpty()) {
attribute.attribute.options.put(TEMPLATE_ATTRIBUTE_OPTIONS_TEMP, value)
}
entryCopy.removeField(field.name)
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve template options", e)
}
}
else -> {
// To retrieve default values
if (value.isNotEmpty()) {
defaultValues[label] = value
}
entryCopy.removeField(label)
}
}
}
val newFields = arrayOfNulls<Field>(attributes.size)
attributes.values.forEach {
val attribute = it.attribute
// Add password generator
if (attribute.label.equals(TEMPLATE_ATTRIBUTE_PASSWORD, true)) {
attribute.options.associatePasswordGenerator()
}
// Add default value
if (defaultValues.containsKey(attribute.label)) {
attribute.options.default = defaultValues[attribute.label]!!
}
// Recognize each temp option
attribute.options.get(TEMPLATE_ATTRIBUTE_OPTIONS_TEMP)?.let { defaultOption ->
when (attribute.type) {
TemplateAttributeType.TEXT -> {
try {
when (attribute.options.getNumberLines()) {
1 -> {
// If one line, default attribute option is number of chars
attribute.options.setNumberChars(defaultOption.toInt())
}
else -> {
// else it's number of lines
attribute.options.setNumberLines(defaultOption.toInt())
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to transform default text option", e)
}
}
TemplateAttributeType.LIST -> {
try {
// Default attribute option is items of the list
val items = defaultOption.split(",")
attribute.options.setListItems(*items.toTypedArray())
} catch (e: Exception) {
Log.e(TAG, "Unable to transform default list option", e)
}
}
TemplateAttributeType.DATETIME -> {
try {
// Default attribute option is datetime, date or time
when {
defaultOption.equals(TEMPLATE_ATTRIBUTE_TYPE_DATE_TIME, true) -> {
// Do not add option if it's datetime
}
defaultOption.equals(TEMPLATE_ATTRIBUTE_TYPE_DATE, true) -> {
attribute.options.setDateFormatToDate()
}
defaultOption.equals(TEMPLATE_ATTRIBUTE_TYPE_TIME, true) -> {
attribute.options.setDateFormatToTime()
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to transform default datetime", e)
}
}
TemplateAttributeType.DIVIDER -> {
// No option here
}
}
attribute.options.remove(TEMPLATE_ATTRIBUTE_OPTIONS_TEMP)
}
// Add position for each attribute
newFields[it.position] = Field(buildTemplateEntryField(attribute))
}
// Add custom fields to entry
newFields.forEach { field ->
field?.let {
entryCopy.putField(field)
}
}
return entryCopy
}
override fun encodeTemplateEntry(templateEntry: EntryKDBX): EntryKDBX {
val entryCopy = EntryKDBX().apply {
updateWith(templateEntry)
}
// Add template version
entryCopy.putField(TEMPLATE_LABEL_VERSION, ProtectedString(false, "1"))
// Dynamic attributes
var index = 0
templateEntry.doForEachDecodedCustomField { field ->
val label = removeTemplateDecorator(encodeTemplateAttribute(field.name))
val value = field.protectedValue
when {
label.equals(TEMPLATE_LABEL_VERSION, true) -> {
// Keep template version as is
}
else -> {
entryCopy.removeField(field.name)
val options = TemplateAttributeOption.getOptionsFromString(value.stringValue)
// Add Position attribute
entryCopy.putField(
TEMPLATE_ATTRIBUTE_POSITION_PREFIX + label,
ProtectedString(false, index.toString())
)
// Add Title attribute (or alias if defined)
val title = options.alias ?: label
entryCopy.putField(
TEMPLATE_ATTRIBUTE_TITLE_PREFIX + label,
ProtectedString(false, title)
)
// Add Type attribute
var typeString: String = when {
value.stringValue.contains(TemplateAttributeType.TEXT.typeString, true) -> {
when (options.getNumberLines()) {
1 -> TEMPLATE_ATTRIBUTE_TYPE_INLINE
else -> TEMPLATE_ATTRIBUTE_TYPE_MULTILINE
}
}
value.stringValue.contains(TemplateAttributeType.LIST.typeString, true) -> {
TEMPLATE_ATTRIBUTE_TYPE_LISTBOX
}
value.stringValue.contains(TemplateAttributeType.DATETIME.typeString, true) -> {
when (options.getDateFormat()) {
DateInstant.Type.DATE -> TEMPLATE_ATTRIBUTE_TYPE_DATE
DateInstant.Type.TIME -> TEMPLATE_ATTRIBUTE_TYPE_TIME
else -> TEMPLATE_ATTRIBUTE_TYPE_DATE_TIME
}
}
value.stringValue.contains(TemplateAttributeType.DIVIDER.typeString, true) -> {
TEMPLATE_ATTRIBUTE_TYPE_DIVIDER
}
else -> TEMPLATE_ATTRIBUTE_TYPE_INLINE
}
// Add protected string if needed
if (value.isProtected) {
typeString = "$TEMPLATE_ATTRIBUTE_TYPE_PROTECTED $typeString"
}
entryCopy.putField(
TEMPLATE_ATTRIBUTE_TYPE_PREFIX + label,
ProtectedString(false, typeString)
)
// Add Options attribute
// Here only number of chars, lines and list items are supported
var defaultOption = ""
try {
val listItems = options.getListItems()
if (listItems.isNotEmpty()) {
defaultOption = listItems.joinToString(",")
} else {
val numberChars = options.getNumberChars()
if (numberChars > 1) {
defaultOption = numberChars.toString()
} else {
val numberLines = options.getNumberLines()
if (numberLines > 1) {
defaultOption = numberLines.toString()
}
}
}
} catch (e: Exception) {
// Ignore, default option is defined
}
entryCopy.putField(
TEMPLATE_ATTRIBUTE_OPTIONS_PREFIX + label,
ProtectedString(false, defaultOption)
)
// Add default elements
if (options.default.isNotEmpty()) {
entryCopy.putField(
label,
ProtectedString(value.isProtected, options.default)
)
}
index++
}
}
}
return entryCopy
}
private data class TemplateAttributePosition(var position: Int, var attribute: TemplateAttribute)
companion object {
private val TAG = TemplateEngineCompatible::class.java.name
// Custom template ref
private const val TEMPLATE_ATTRIBUTE_TITLE = "Title"
private const val TEMPLATE_ATTRIBUTE_USERNAME = "UserName"
private const val TEMPLATE_ATTRIBUTE_PASSWORD = "Password"
private const val TEMPLATE_ATTRIBUTE_URL = "URL"
private const val TEMPLATE_ATTRIBUTE_EXP_DATE = "@exp_date"
private const val TEMPLATE_ATTRIBUTE_EXPIRES = "Expires"
private const val TEMPLATE_ATTRIBUTE_NOTES = "Notes"
private const val TEMPLATE_LABEL_VERSION = "_etm_template"
private const val TEMPLATE_ENTRY_UUID = "_etm_template_uuid"
private const val TEMPLATE_ATTRIBUTE_POSITION_PREFIX = "_etm_position_"
private const val TEMPLATE_ATTRIBUTE_TITLE_PREFIX = "_etm_title_"
private const val TEMPLATE_ATTRIBUTE_TYPE_PREFIX = "_etm_type_"
private const val TEMPLATE_ATTRIBUTE_OPTIONS_PREFIX = "_etm_options_"
private const val TEMPLATE_ATTRIBUTE_OPTIONS_TEMP = "temp_option"
private const val TEMPLATE_ATTRIBUTE_TYPE_PROTECTED = "Protected"
private const val TEMPLATE_ATTRIBUTE_TYPE_INLINE = "Inline"
private const val TEMPLATE_ATTRIBUTE_TYPE_INLINE_URL = "Inline URL"
private const val TEMPLATE_ATTRIBUTE_TYPE_MULTILINE = "Multiline"
private const val TEMPLATE_ATTRIBUTE_TYPE_LISTBOX = "Listbox"
private const val TEMPLATE_ATTRIBUTE_TYPE_DATE_TIME = "Date Time"
private const val TEMPLATE_ATTRIBUTE_TYPE_DATE = "Date"
private const val TEMPLATE_ATTRIBUTE_TYPE_TIME = "Time"
private const val TEMPLATE_ATTRIBUTE_TYPE_DIVIDER = "Divider"
private const val TEMPLATE_ATTRIBUTE_TYPE_POPOUT = "Popout"
private const val TEMPLATE_ATTRIBUTE_TYPE_RICH_TEXTBOX = "Rich Textbox"
private fun decodeTemplateAttribute(name: String): String {
return when {
TEMPLATE_LABEL_VERSION.equals(name, true) -> TemplateField.LABEL_VERSION
TEMPLATE_ATTRIBUTE_TITLE.equals(name, true) -> TemplateField.LABEL_TITLE
TEMPLATE_ATTRIBUTE_USERNAME.equals(name, true) -> TemplateField.LABEL_USERNAME
TEMPLATE_ATTRIBUTE_PASSWORD.equals(name, true) -> TemplateField.LABEL_PASSWORD
TEMPLATE_ATTRIBUTE_URL.equals(name, true) -> TemplateField.LABEL_URL
TEMPLATE_ATTRIBUTE_EXP_DATE.equals(name, true) -> TemplateField.LABEL_EXPIRATION
TEMPLATE_ATTRIBUTE_EXPIRES.equals(name, true) -> TemplateField.LABEL_EXPIRATION
TEMPLATE_ATTRIBUTE_NOTES.equals(name, true) -> TemplateField.LABEL_NOTES
else -> name
}
}
private fun encodeTemplateAttribute(name: String): String {
return when {
TemplateField.LABEL_VERSION.equals(name, true) -> TEMPLATE_LABEL_VERSION
TemplateField.LABEL_TITLE.equals(name, true) -> TEMPLATE_ATTRIBUTE_TITLE
TemplateField.LABEL_USERNAME.equals(name, true) -> TEMPLATE_ATTRIBUTE_USERNAME
TemplateField.LABEL_PASSWORD.equals(name, true) -> TEMPLATE_ATTRIBUTE_PASSWORD
TemplateField.LABEL_URL.equals(name, true) -> TEMPLATE_ATTRIBUTE_URL
TemplateField.LABEL_EXPIRATION.equals(name, true) -> TEMPLATE_ATTRIBUTE_EXP_DATE
TemplateField.LABEL_NOTES.equals(name, true) -> TEMPLATE_ATTRIBUTE_NOTES
else -> name
}
}
}
}

View File

@@ -0,0 +1,102 @@
package com.kunzisoft.keepass.database.element.template
import android.content.Context
import com.kunzisoft.keepass.R
object TemplateField {
const val LABEL_STANDARD = "Standard"
const val LABEL_TEMPLATE = "Template"
const val LABEL_VERSION = "Version"
const val LABEL_TITLE = "Title"
const val LABEL_USERNAME = "Username"
const val LABEL_PASSWORD = "Password"
const val LABEL_URL = "URL"
const val LABEL_EXPIRATION = "Expires"
const val LABEL_NOTES = "Notes"
const val LABEL_DEBIT_CREDIT_CARD = "Debit / Credit Card"
const val LABEL_HOLDER = "Holder"
const val LABEL_NUMBER = "Number"
const val LABEL_CVV = "CVV"
const val LABEL_PIN = "PIN"
const val LABEL_ID_CARD = "ID Card"
const val LABEL_NAME = "Name"
const val LABEL_PLACE_OF_ISSUE = "Place of issue"
const val LABEL_DATE_OF_ISSUE = "Date of issue"
const val LABEL_EMAIL = "Email"
const val LABEL_EMAIL_ADDRESS = "Email address"
const val LABEL_WIRELESS = "Wifi"
const val LABEL_SSID = "SSID"
const val LABEL_TYPE = "Type"
const val LABEL_CRYPTOCURRENCY = "Cryptocurrency wallet"
const val LABEL_TOKEN = "Token"
const val LABEL_PUBLIC_KEY = "Public key"
const val LABEL_PRIVATE_KEY = "Private key"
const val LABEL_SEED = "Seed"
const val LABEL_ACCOUNT = "Account"
const val LABEL_BANK = "Bank"
const val LABEL_BIC = "BIC"
const val LABEL_IBAN = "IBAN"
const val LABEL_SECURE_NOTE = "Secure Note"
const val LABEL_MEMBERSHIP = "Membership"
fun isStandardFieldName(name: String): Boolean {
return arrayOf(
LABEL_TITLE,
LABEL_USERNAME,
LABEL_PASSWORD,
LABEL_URL,
LABEL_EXPIRATION,
LABEL_NOTES
).firstOrNull { it.equals(name, true) } != null
}
fun getLocalizedName(context: Context?, name: String): String {
if (context == null
|| TemplateEngine.containsTemplateDecorator(name))
return name
return when {
LABEL_STANDARD.equals(name, true) -> context.getString(R.string.standard)
LABEL_TEMPLATE.equals(name, true) -> context.getString(R.string.template)
LABEL_VERSION.equals(name, true) -> context.getString(R.string.version)
LABEL_TITLE.equals(name, true) -> context.getString(R.string.entry_title)
LABEL_USERNAME.equals(name, true) -> context.getString(R.string.entry_user_name)
LABEL_PASSWORD.equals(name, true) -> context.getString(R.string.entry_password)
LABEL_URL.equals(name, true) -> context.getString(R.string.entry_url)
LABEL_EXPIRATION.equals(name, true) -> context.getString(R.string.entry_expires)
LABEL_NOTES.equals(name, true) -> context.getString(R.string.entry_notes)
LABEL_DEBIT_CREDIT_CARD.equals(name, true) -> context.getString(R.string.debit_credit_card)
LABEL_HOLDER.equals(name, true) -> context.getString(R.string.holder)
LABEL_NUMBER.equals(name, true) -> context.getString(R.string.number)
LABEL_CVV.equals(name, true) -> context.getString(R.string.card_verification_value)
LABEL_PIN.equals(name, true) -> context.getString(R.string.personal_identification_number)
LABEL_ID_CARD.equals(name, true) -> context.getString(R.string.id_card)
LABEL_NAME.equals(name, true) -> context.getString(R.string.name)
LABEL_PLACE_OF_ISSUE.equals(name, true) -> context.getString(R.string.place_of_issue)
LABEL_DATE_OF_ISSUE.equals(name, true) -> context.getString(R.string.date_of_issue)
LABEL_EMAIL.equals(name, true) -> context.getString(R.string.email)
LABEL_EMAIL_ADDRESS.equals(name, true) -> context.getString(R.string.email_address)
LABEL_WIRELESS.equals(name, true) -> context.getString(R.string.wireless)
LABEL_SSID.equals(name, true) -> context.getString(R.string.ssid)
LABEL_TYPE.equals(name, true) -> context.getString(R.string.type)
LABEL_CRYPTOCURRENCY.equals(name, true) -> context.getString(R.string.cryptocurrency)
LABEL_TOKEN.equals(name, true) -> context.getString(R.string.token)
LABEL_PUBLIC_KEY.equals(name, true) -> context.getString(R.string.public_key)
LABEL_PRIVATE_KEY.equals(name, true) -> context.getString(R.string.private_key)
LABEL_SEED.equals(name, true) -> context.getString(R.string.seed)
LABEL_ACCOUNT.equals(name, true) -> context.getString(R.string.account)
LABEL_BANK.equals(name, true) -> context.getString(R.string.bank)
LABEL_BIC.equals(name, true) -> context.getString(R.string.bank_identifier_code)
LABEL_IBAN.equals(name, true) -> context.getString(R.string.international_bank_account_number)
LABEL_SECURE_NOTE.equals(name, true) -> context.getString(R.string.secure_note)
LABEL_MEMBERSHIP.equals(name, true) -> context.getString(R.string.membership)
else -> name
}
}
}

View File

@@ -0,0 +1,40 @@
package com.kunzisoft.keepass.database.element.template
import android.os.Parcel
import android.os.Parcelable
class TemplateSection: Parcelable {
var name: String = ""
var attributes: List<TemplateAttribute> = ArrayList()
private set
constructor(attributes: List<TemplateAttribute>, name: String = "") {
this.name = name
this.attributes = attributes
}
constructor(parcel: Parcel) {
this.name = parcel.readString() ?: name
parcel.readList(this.attributes, TemplateAttribute::class.java.classLoader)
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(this.name)
parcel.writeList(this.attributes)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<TemplateSection> {
override fun createFromParcel(parcel: Parcel): TemplateSection {
return TemplateSection(parcel)
}
override fun newArray(size: Int): Array<TemplateSection?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -38,8 +38,8 @@ object DateKDBXUtil {
} else dt.toDate() } else dt.toDate()
} }
fun convertDateToKDBX4Time(dt: DateTime?): Long { fun convertDateToKDBX4Time(date: Date): Long {
val duration = Duration(javaEpoch, dt) val duration = Duration(javaEpoch, DateTime(date))
val seconds = duration.millis / 1000L val seconds = duration.millis / 1000L
return seconds + epochOffset return seconds + epochOffset
} }

View File

@@ -38,7 +38,6 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.exception.UnknownKDF import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
@@ -401,7 +400,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
writeTags(entry.tags) writeTags(entry.tags)
writePreviousParentGroup(entry.previousParentGroup) writePreviousParentGroup(entry.previousParentGroup)
writeTimes(entry) writeTimes(entry)
writeFields(entry.fields) writeFields(entry.getFields())
writeEntryBinaries(entry.binaries) writeEntryBinaries(entry.binaries)
writeCustomData(entry.customData) writeCustomData(entry.customData)
writeAutoType(entry.autoType) writeAutoType(entry.autoType)
@@ -433,7 +432,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
if (header!!.version.isBefore(FILE_VERSION_40)) { if (header!!.version.isBefore(FILE_VERSION_40)) {
writeString(name, DatabaseKDBXXML.DateFormatter.format(date)) writeString(name, DatabaseKDBXXML.DateFormatter.format(date))
} else { } else {
val buf = longTo8Bytes(DateKDBXUtil.convertDateToKDBX4Time(DateTime(date))) val buf = longTo8Bytes(DateKDBXUtil.convertDateToKDBX4Time(date))
val b64 = String(Base64.encode(buf, BASE_64_FLAG)) val b64 = String(Base64.encode(buf, BASE_64_FLAG))
writeString(name, b64) writeString(name, b64)
} }
@@ -548,25 +547,26 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} }
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeFields(fields: Map<String, ProtectedString>) { private fun writeFields(fields: List<Field>) {
for (field in fields) {
for ((key, value) in fields) { writeField(field)
writeField(key, value)
} }
} }
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeField(key: String, value: ProtectedString) { private fun writeField(field: Field) {
val label = field.name
val value = field.protectedValue
xml.startTag(null, DatabaseKDBXXML.ElemString) xml.startTag(null, DatabaseKDBXXML.ElemString)
xml.startTag(null, DatabaseKDBXXML.ElemKey) xml.startTag(null, DatabaseKDBXXML.ElemKey)
xml.text(safeXmlString(key)) xml.text(safeXmlString(label))
xml.endTag(null, DatabaseKDBXXML.ElemKey) xml.endTag(null, DatabaseKDBXXML.ElemKey)
xml.startTag(null, DatabaseKDBXXML.ElemValue) xml.startTag(null, DatabaseKDBXXML.ElemValue)
var protect = value.isProtected var protect = value.isProtected
when (key) { when (label) {
MemoryProtectionConfig.ProtectDefinition.TITLE_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectTitle MemoryProtectionConfig.ProtectDefinition.TITLE_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectTitle
MemoryProtectionConfig.ProtectDefinition.USERNAME_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectUserName MemoryProtectionConfig.ProtectDefinition.USERNAME_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectUserName
MemoryProtectionConfig.ProtectDefinition.PASSWORD_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectPassword MemoryProtectionConfig.ProtectDefinition.PASSWORD_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectPassword

View File

@@ -51,6 +51,8 @@ class SearchHelper {
override fun operate(node: Entry): Boolean { override fun operate(node: Entry): Boolean {
if (incrementEntry >= max) if (incrementEntry >= max)
return false return false
if (database.entryIsTemplate(node) && !searchParameters.searchInTemplates)
return false
if (entryContainsString(database, node, searchParameters)) { if (entryContainsString(database, node, searchParameters)) {
searchGroup?.addChildEntry(node) searchGroup?.addChildEntry(node)
incrementEntry++ incrementEntry++
@@ -70,6 +72,7 @@ class SearchHelper {
}, },
false) false)
searchGroup?.refreshNumberOfChildEntries()
return searchGroup return searchGroup
} }
@@ -92,15 +95,18 @@ class SearchHelper {
* Utility method to perform actions if item is found or not after an auto search in [database] * Utility method to perform actions if item is found or not after an auto search in [database]
*/ */
fun checkAutoSearchInfo(context: Context, fun checkAutoSearchInfo(context: Context,
database: Database, database: Database?,
searchInfo: SearchInfo?, searchInfo: SearchInfo?,
onItemsFound: (items: List<EntryInfo>) -> Unit, onItemsFound: (openedDatabase: Database,
onItemNotFound: () -> Unit, items: List<EntryInfo>) -> Unit,
onItemNotFound: (openedDatabase: Database) -> Unit,
onDatabaseClosed: () -> Unit) { onDatabaseClosed: () -> Unit) {
if (database.loaded && TimeoutHelper.checkTime(context)) { if (database == null || !database.loaded) {
onDatabaseClosed.invoke()
} else if (TimeoutHelper.checkTime(context)) {
var searchWithoutUI = false var searchWithoutUI = false
if (PreferencesUtil.isAutofillAutoSearchEnable(context) if (PreferencesUtil.isAutofillAutoSearchEnable(context)
&& searchInfo != null && searchInfo != null && !searchInfo.manualSelection
&& !searchInfo.containsOnlyNullValues()) { && !searchInfo.containsOnlyNullValues()) {
// If search provide results // If search provide results
database.createVirtualGroupFromSearchInfo( database.createVirtualGroupFromSearchInfo(
@@ -108,18 +114,16 @@ class SearchHelper {
PreferencesUtil.omitBackup(context), PreferencesUtil.omitBackup(context),
MAX_SEARCH_ENTRY MAX_SEARCH_ENTRY
)?.let { searchGroup -> )?.let { searchGroup ->
if (searchGroup.getNumberOfChildEntries() > 0) { if (searchGroup.numberOfChildEntries > 0) {
searchWithoutUI = true searchWithoutUI = true
onItemsFound.invoke( onItemsFound.invoke(database,
searchGroup.getChildEntriesInfo(database)) searchGroup.getChildEntriesInfo(database))
} }
} }
} }
if (!searchWithoutUI) { if (!searchWithoutUI) {
onItemNotFound.invoke() onItemNotFound.invoke(database)
} }
} else {
onDatabaseClosed.invoke()
} }
} }

View File

@@ -34,4 +34,6 @@ class SearchParameters {
var searchInOther = true var searchInOther = true
var searchInUUIDs = false var searchInUUIDs = false
var searchInTags = true var searchInTags = true
var searchInTemplates = false
} }

View File

@@ -33,6 +33,7 @@ import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.core.widget.ImageViewCompat import androidx.core.widget.ImageViewCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryCache import com.kunzisoft.keepass.database.element.binary.BinaryCache
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
@@ -63,10 +64,18 @@ class IconDrawableFactory(private val retrieveBinaryCache : () -> BinaryCache?,
*/ */
private val standardIconMap = HashMap<CacheKey, WeakReference<Drawable>>() private val standardIconMap = HashMap<CacheKey, WeakReference<Drawable>>()
/**
* To load an icon pack only if current one is different
*/
private var mCurrentIconPack: IconPack? = null
/** /**
* Get the [SuperDrawable] [iconDraw] (from cache, or build it and add it to the cache if not exists yet), then tint it with [tintColor] if needed * Get the [SuperDrawable] [iconDraw] (from cache, or build it and add it to the cache if not exists yet), then tint it with [tintColor] if needed
*/ */
private fun getIconSuperDrawable(context: Context, iconDraw: IconImageDraw, width: Int, tintColor: Int = Color.WHITE): SuperDrawable { private fun getIconSuperDrawable(context: Context,
iconDraw: IconImageDraw,
width: Int,
tintColor: Int = Color.WHITE): SuperDrawable {
val icon = iconDraw.getIconImageToDraw() val icon = iconDraw.getIconImageToDraw()
val customIconBinary = retrieveCustomIconBinary(icon.custom.uuid) val customIconBinary = retrieveCustomIconBinary(icon.custom.uuid)
val binaryCache = retrieveBinaryCache() val binaryCache = retrieveBinaryCache()
@@ -76,6 +85,10 @@ class IconDrawableFactory(private val retrieveBinaryCache : () -> BinaryCache?,
} }
} }
val iconPack = IconPackChooser.getSelectedIconPack(context) val iconPack = IconPackChooser.getSelectedIconPack(context)
if (mCurrentIconPack != iconPack) {
this.mCurrentIconPack = iconPack
this.clearCache()
}
iconPack?.iconToResId(icon.standard.id)?.let { iconId -> iconPack?.iconToResId(icon.standard.id)?.let { iconId ->
return SuperDrawable(getIconDrawable(context.resources, iconId, width, tintColor), iconPack.tintable()) return SuperDrawable(getIconDrawable(context.resources, iconId, width, tintColor), iconPack.tintable())
} ?: run { } ?: run {

View File

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.icons
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.* import java.util.*
@@ -93,7 +92,6 @@ object IconPackChooser {
fun setSelectedIconPack(iconPackIdString: String?) { fun setSelectedIconPack(iconPackIdString: String?) {
for (iconPack in iconPackList) { for (iconPack in iconPackList) {
if (iconPack.id == iconPackIdString) { if (iconPack.id == iconPackIdString) {
Database.getInstance().iconDrawableFactory.clearCache()
iconPackSelected = iconPack iconPackSelected = iconPack
break break
} }
@@ -108,8 +106,9 @@ object IconPackChooser {
*/ */
fun getSelectedIconPack(context: Context): IconPack? { fun getSelectedIconPack(context: Context): IconPack? {
build(context) build(context)
if (iconPackSelected == null) if (iconPackSelected == null) {
setSelectedIconPack(PreferencesUtil.getIconPackSelectedId(context)) setSelectedIconPack(PreferencesUtil.getIconPackSelectedId(context))
}
return iconPackSelected return iconPackSelected
} }

View File

@@ -38,15 +38,19 @@ import android.widget.TextView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity import com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity
import com.kunzisoft.keepass.adapters.FieldsAdapter import com.kunzisoft.keepass.adapters.FieldsAdapter
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*
class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener { class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionListener {
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
private var mDatabase: Database? = null
private var keyboardView: KeyboardView? = null private var keyboardView: KeyboardView? = null
private var entryText: TextView? = null private var entryText: TextView? = null
@@ -61,6 +65,11 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.registerProgressTask()
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
this.mDatabase = database
}
// Remove the entry and lock the keyboard when the lock signal is receive // Remove the entry and lock the keyboard when the lock signal is receive
lockReceiver = LockReceiver { lockReceiver = LockReceiver {
removeEntryInfo() removeEntryInfo()
@@ -111,13 +120,13 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
// Remove entry info if the database is not loaded // Remove entry info if the database is not loaded
// or if entry info timestamp is before database loaded timestamp // or if entry info timestamp is before database loaded timestamp
val database = Database.getInstance() val databaseTime = mDatabase?.loadTimestamp
val databaseTime = database.loadTimestamp
val entryTime = entryInfoTimestamp val entryTime = entryInfoTimestamp
if (!database.loaded if (mDatabase == null
|| databaseTime == null || mDatabase?.loaded != true
|| entryTime == null || databaseTime == null
|| entryTime < databaseTime) { || entryTime == null
|| entryTime < databaseTime) {
removeEntryInfo() removeEntryInfo()
} }
assignKeyboardView() assignKeyboardView()
@@ -169,6 +178,11 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
assignKeyboardView() assignKeyboardView()
} }
override fun onEvaluateFullscreenMode(): Boolean {
return resources.getBoolean(R.bool.magikeyboard_allow_fullscreen_mode)
&& super.onEvaluateFullscreenMode()
}
private fun playVibration(keyCode: Int) { private fun playVibration(keyCode: Int) {
when (keyCode) { when (keyCode) {
Keyboard.KEYCODE_DELETE -> {} Keyboard.KEYCODE_DELETE -> {}
@@ -258,7 +272,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
if (entryInfoKey != null) { if (entryInfoKey != null) {
currentInputConnection.commitText(entryInfoKey!!.url, 1) currentInputConnection.commitText(entryInfoKey!!.url, 1)
} }
actionTabAutomatically() actionGoAutomatically()
} }
KEY_FIELDS -> { KEY_FIELDS -> {
if (entryInfoKey != null) { if (entryInfoKey != null) {
@@ -323,11 +337,12 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
override fun onDestroy() { override fun onDestroy() {
dismissCustomKeys() dismissCustomKeys()
unregisterLockReceiver(lockReceiver) unregisterLockReceiver(lockReceiver)
mDatabaseTaskProvider?.unregisterProgressTask()
super.onDestroy() super.onDestroy()
} }
companion object { companion object {
private val TAG = MagikIME::class.java.name private val TAG = MagikeyboardService::class.java.name
const val KEY_BACK_KEYBOARD = 600 const val KEY_BACK_KEYBOARD = 600
const val KEY_CHANGE_KEYBOARD = 601 const val KEY_CHANGE_KEYBOARD = 601

View File

@@ -0,0 +1,39 @@
package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
import org.joda.time.DateTime
data class CreditCard(val cardholder: String?,
val number: String?,
val expiration: DateTime?,
val cvv: String?) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
parcel.readSerializable() as DateTime?,
parcel.readString()) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(cardholder)
parcel.writeString(number)
parcel.writeSerializable(expiration)
parcel.writeString(cvv)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CreditCard> {
override fun createFromParcel(parcel: Parcel): CreditCard {
return CreditCard(parcel)
}
override fun newArray(size: Int): Array<CreditCard?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -20,29 +20,35 @@
package com.kunzisoft.keepass.model package com.kunzisoft.keepass.model
import android.os.Parcel import android.os.Parcel
import android.os.ParcelUuid
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import java.util.*
class EntryInfo : NodeInfo { class EntryInfo : NodeInfo {
var id: String = "" var id: UUID = UUID.randomUUID()
var username: String = "" var username: String = ""
var password: String = "" var password: String = ""
var url: String = "" var url: String = ""
var notes: String = "" var notes: String = ""
var customFields: List<Field> = ArrayList() var customFields: MutableList<Field> = mutableListOf()
var attachments: List<Attachment> = ArrayList() var attachments: MutableList<Attachment> = mutableListOf()
var otpModel: OtpModel? = null var otpModel: OtpModel? = null
var isTemplate: Boolean = false
constructor(): super() constructor() : super()
constructor(parcel: Parcel): super(parcel) { constructor(parcel: Parcel) : super(parcel) {
id = parcel.readString() ?: id id = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: id
username = parcel.readString() ?: username username = parcel.readString() ?: username
password = parcel.readString() ?: password password = parcel.readString() ?: password
url = parcel.readString() ?: url url = parcel.readString() ?: url
@@ -50,6 +56,7 @@ class EntryInfo : NodeInfo {
parcel.readList(customFields, Field::class.java.classLoader) parcel.readList(customFields, Field::class.java.classLoader)
parcel.readList(attachments, Attachment::class.java.classLoader) parcel.readList(attachments, Attachment::class.java.classLoader)
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
isTemplate = parcel.readByte().toInt() != 0
} }
override fun describeContents(): Int { override fun describeContents(): Int {
@@ -58,14 +65,15 @@ class EntryInfo : NodeInfo {
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
super.writeToParcel(parcel, flags) super.writeToParcel(parcel, flags)
parcel.writeString(id) parcel.writeParcelable(ParcelUuid(id), flags)
parcel.writeString(username) parcel.writeString(username)
parcel.writeString(password) parcel.writeString(password)
parcel.writeString(url) parcel.writeString(url)
parcel.writeString(notes) parcel.writeString(notes)
parcel.writeArray(customFields.toTypedArray()) parcel.writeList(customFields)
parcel.writeArray(attachments.toTypedArray()) parcel.writeList(attachments)
parcel.writeParcelable(otpModel, flags) parcel.writeParcelable(otpModel, flags)
parcel.writeByte((if (isTemplate) 1 else 0).toByte())
} }
fun containsCustomFieldsProtected(): Boolean { fun containsCustomFieldsProtected(): Boolean {
@@ -133,8 +141,7 @@ class EntryInfo : NodeInfo {
val webDomainToStore = "$webScheme://$webDomain" val webDomainToStore = "$webScheme://$webDomain"
if (database?.allowEntryCustomFields() != true || url.isEmpty()) { if (database?.allowEntryCustomFields() != true || url.isEmpty()) {
url = webDomainToStore url = webDomainToStore
} } else if (url != webDomainToStore) {
else if (url != webDomainToStore){
// Save web domain in custom field // Save web domain in custom field
addUniqueField(Field(WEB_DOMAIN_FIELD_NAME, addUniqueField(Field(WEB_DOMAIN_FIELD_NAME,
ProtectedString(false, webDomainToStore)), ProtectedString(false, webDomainToStore)),
@@ -153,6 +160,65 @@ class EntryInfo : NodeInfo {
} }
} }
fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo) {
registerInfo.username?.let {
username = it
}
registerInfo.password?.let {
password = it
}
if (database?.allowEntryCustomFields() == true) {
val creditCard: CreditCard? = registerInfo.creditCard
creditCard?.cardholder?.let {
addUniqueField(Field(TemplateField.LABEL_HOLDER, ProtectedString(false, it)))
}
creditCard?.expiration?.let {
expires = true
expiryTime = DateInstant(creditCard.expiration.millis)
}
creditCard?.number?.let {
addUniqueField(Field(TemplateField.LABEL_NUMBER, ProtectedString(false, it)))
}
creditCard?.cvv?.let {
addUniqueField(Field(TemplateField.LABEL_CVV, ProtectedString(true, it)))
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is EntryInfo) return false
if (!super.equals(other)) return false
if (id != other.id) return false
if (username != other.username) return false
if (password != other.password) return false
if (url != other.url) return false
if (notes != other.notes) return false
if (customFields != other.customFields) return false
if (attachments != other.attachments) return false
if (otpModel != other.otpModel) return false
if (isTemplate != other.isTemplate) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + username.hashCode()
result = 31 * result + password.hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + notes.hashCode()
result = 31 * result + customFields.hashCode()
result = 31 * result + attachments.hashCode()
result = 31 * result + (otpModel?.hashCode() ?: 0)
result = 31 * result + isTemplate.hashCode()
return result
}
companion object { companion object {
const val WEB_DOMAIN_FIELD_NAME = "URL" const val WEB_DOMAIN_FIELD_NAME = "URL"

View File

@@ -2,6 +2,7 @@ package com.kunzisoft.keepass.model
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.Field
class FocusedEditField : Parcelable { class FocusedEditField : Parcelable {

View File

@@ -24,6 +24,22 @@ class GroupInfo : NodeInfo {
parcel.writeString(notes) parcel.writeString(notes)
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is GroupInfo) return false
if (!super.equals(other)) return false
if (notes != other.notes) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (notes?.hashCode() ?: 0)
return result
}
companion object CREATOR : Parcelable.Creator<GroupInfo> { companion object CREATOR : Parcelable.Creator<GroupInfo> {
override fun createFromParcel(parcel: Parcel): GroupInfo { override fun createFromParcel(parcel: Parcel): GroupInfo {
return GroupInfo(parcel) return GroupInfo(parcel)

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