mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
244 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7182c2e66d | ||
|
|
503316bc70 | ||
|
|
d5af59f2c7 | ||
|
|
cb3ac1ad3a | ||
|
|
e161080e4c | ||
|
|
ceab7f917b | ||
|
|
41d8066c4c | ||
|
|
e373cbd776 | ||
|
|
05ea6b6b10 | ||
|
|
28eed3ae71 | ||
|
|
535c67ac9b | ||
|
|
68027a6e15 | ||
|
|
12dea6b499 | ||
|
|
d8bd078a02 | ||
|
|
439bc109b0 | ||
|
|
b1ec93ceb5 | ||
|
|
44b9aa0e48 | ||
|
|
0a59063027 | ||
|
|
5db4608abd | ||
|
|
6ece2aa6cb | ||
|
|
95ee45f666 | ||
|
|
556e90b8d8 | ||
|
|
42841e6247 | ||
|
|
324c82248a | ||
|
|
94f5a47918 | ||
|
|
3f70990956 | ||
|
|
d77635e572 | ||
|
|
0f26f1b751 | ||
|
|
456bc22138 | ||
|
|
b6fe91e396 | ||
|
|
0fb4d26949 | ||
|
|
3b3583a416 | ||
|
|
cac1d576c8 | ||
|
|
9cba0d0a48 | ||
|
|
3bf11e9dc0 | ||
|
|
edbb160ac6 | ||
|
|
ee111dc63c | ||
|
|
b06edb756a | ||
|
|
fd745494e0 | ||
|
|
702bf3f479 | ||
|
|
6adc02a91f | ||
|
|
55bae5a130 | ||
|
|
1aae817b17 | ||
|
|
8afc8c23fb | ||
|
|
36e473b139 | ||
|
|
e529723f86 | ||
|
|
6c5112c142 | ||
|
|
77ac68a603 | ||
|
|
e816f40872 | ||
|
|
65313f114b | ||
|
|
ef1f1342f5 | ||
|
|
9205fe6c08 | ||
|
|
75df3e81fe | ||
|
|
47fffbadb5 | ||
|
|
78354473fa | ||
|
|
8d7efb44b5 | ||
|
|
cab00b3d8c | ||
|
|
8efab01336 | ||
|
|
66feb8beb4 | ||
|
|
ca4cccffeb | ||
|
|
51b1760c50 | ||
|
|
42e1bda365 | ||
|
|
194b6b557a | ||
|
|
e7bc439997 | ||
|
|
ef76cce0ac | ||
|
|
63eec6d969 | ||
|
|
719ae74c06 | ||
|
|
37cf424eb8 | ||
|
|
3a87f7ba9d | ||
|
|
275428d825 | ||
|
|
7b9dac86ca | ||
|
|
46496ee2cc | ||
|
|
110cc402cc | ||
|
|
587f006259 | ||
|
|
ec45c0df81 | ||
|
|
dc575aeca4 | ||
|
|
a3f790f000 | ||
|
|
4e9e188d02 | ||
|
|
abb17efae4 | ||
|
|
16be990502 | ||
|
|
d1a496f9a3 | ||
|
|
cd730fcfef | ||
|
|
1b65bf665b | ||
|
|
6f4735790c | ||
|
|
040666f89d | ||
|
|
227cb800c3 | ||
|
|
0052569a14 | ||
|
|
f7561c4888 | ||
|
|
92200f19e7 | ||
|
|
4c01f18a62 | ||
|
|
a6ce3e49fe | ||
|
|
1b3a5d1bf6 | ||
|
|
db7bdc63c8 | ||
|
|
7aca550f02 | ||
|
|
b60980b3fd | ||
|
|
b578c2c584 | ||
|
|
fc45bd624e | ||
|
|
17be3d9d2c | ||
|
|
5b3a38a7bc | ||
|
|
4403835d50 | ||
|
|
b7a3d3eb46 | ||
|
|
29846b22fe | ||
|
|
32d235e8c7 | ||
|
|
cb982b3513 | ||
|
|
d7ed6c26dd | ||
|
|
8ff19f7e68 | ||
|
|
729e062c3a | ||
|
|
7d0340ac07 | ||
|
|
01960e74c1 | ||
|
|
8e40250985 | ||
|
|
8ae2edb61a | ||
|
|
0baa7bcbf1 | ||
|
|
fffee48918 | ||
|
|
515abb6e14 | ||
|
|
6c1c3ff87f | ||
|
|
5b65575c7a | ||
|
|
0f258fc5f8 | ||
|
|
206bc661dc | ||
|
|
d0e35b109e | ||
|
|
61769c4f20 | ||
|
|
95778ee5f4 | ||
|
|
155030fdca | ||
|
|
98237ef76c | ||
|
|
f0a7b38199 | ||
|
|
9fc5e6751b | ||
|
|
4c1630312b | ||
|
|
d397c5c996 | ||
|
|
f6c41b5a60 | ||
|
|
06eb5c01c3 | ||
|
|
7df49f91e8 | ||
|
|
96dcbb0ce7 | ||
|
|
5f828fb986 | ||
|
|
533d663938 | ||
|
|
ae788503a9 | ||
|
|
cf0acd9c73 | ||
|
|
0857f2f1cf | ||
|
|
c05d412bdb | ||
|
|
c8e0ce717d | ||
|
|
cc3485b201 | ||
|
|
81ea7080c2 | ||
|
|
76ff6f5ae0 | ||
|
|
0c0d0b7a6f | ||
|
|
ec8cf1f6b7 | ||
|
|
da44310d1b | ||
|
|
4bd9c84bb0 | ||
|
|
3b1269a770 | ||
|
|
7c986ccee8 | ||
|
|
903bad8f36 | ||
|
|
4b9577437c | ||
|
|
c316011fbc | ||
|
|
3fb1f18c22 | ||
|
|
53935058f5 | ||
|
|
a3860c9581 | ||
|
|
dc20899d26 | ||
|
|
62ac3ddb75 | ||
|
|
b792a61bf9 | ||
|
|
aae9f9e1cb | ||
|
|
d098bf5e6a | ||
|
|
b0e8a3ecd9 | ||
|
|
4efa684022 | ||
|
|
f2c8082990 | ||
|
|
1abba80045 | ||
|
|
68564a2b75 | ||
|
|
385b701b38 | ||
|
|
11c9a1d707 | ||
|
|
c5aef6b561 | ||
|
|
dfcf73cfd0 | ||
|
|
7fc9389700 | ||
|
|
d1af7349bc | ||
|
|
0fd955197d | ||
|
|
cbf33507d1 | ||
|
|
bc60a5d97e | ||
|
|
57596b2991 | ||
|
|
43b3602a52 | ||
|
|
c09ec961b8 | ||
|
|
d140b453b2 | ||
|
|
9eb42636ec | ||
|
|
ee2d663fce | ||
|
|
8e83615a22 | ||
|
|
0ff129c5ca | ||
|
|
e49858439f | ||
|
|
3df07f7f47 | ||
|
|
ff9e179593 | ||
|
|
7539fee04b | ||
|
|
71a339a58f | ||
|
|
9ef2ea016b | ||
|
|
de4936a16d | ||
|
|
574d2b8904 | ||
|
|
1f3f7634e7 | ||
|
|
3c0725baff | ||
|
|
b0e1411012 | ||
|
|
39daf4714d | ||
|
|
4706afa823 | ||
|
|
25977d389d | ||
|
|
f760110569 | ||
|
|
133e78fe97 | ||
|
|
d92e0c8620 | ||
|
|
62fdb69d6b | ||
|
|
def57c9fb2 | ||
|
|
21c9c898c3 | ||
|
|
1f03c922c2 | ||
|
|
3f6ae6bdac | ||
|
|
60615ee1eb | ||
|
|
92b0d1bfa9 | ||
|
|
237988dc1f | ||
|
|
a846ec29ca | ||
|
|
4533e96bff | ||
|
|
0a401c3ac9 | ||
|
|
468abaf077 | ||
|
|
4ccf2f641c | ||
|
|
34eb2785cf | ||
|
|
09dbfe323e | ||
|
|
1f06c5b425 | ||
|
|
b98e089f7a | ||
|
|
a0ad06ed0a | ||
|
|
ec63365429 | ||
|
|
2cb85e4346 | ||
|
|
0d7c479c51 | ||
|
|
5a6c21e662 | ||
|
|
d6cadac98f | ||
|
|
dac2fc2c37 | ||
|
|
0fb45cef0d | ||
|
|
5ebdbd4003 | ||
|
|
b30f1023cb | ||
|
|
e5f65a4d1e | ||
|
|
ab42a65aa4 | ||
|
|
e351456bfe | ||
|
|
452e68b08f | ||
|
|
d65beed7a1 | ||
|
|
f5a5a0e8cb | ||
|
|
98380a0906 | ||
|
|
22fe7508f3 | ||
|
|
c8e241fc76 | ||
|
|
2c943e00d0 | ||
|
|
7ddb83b72d | ||
|
|
50bac01699 | ||
|
|
e0a92dfadd | ||
|
|
b50c951091 | ||
|
|
2f589a95a9 | ||
|
|
53eac86a95 | ||
|
|
9412f8955e | ||
|
|
71c98d82b1 | ||
|
|
42c1a925b4 | ||
|
|
1e84534ffd |
90
CHANGELOG
90
CHANGELOG
@@ -1,4 +1,32 @@
|
|||||||
KeepassDX (2.5beta27)
|
KeePassDX(2.5beta31)
|
||||||
|
* Add write permission to keep compatibility with old file managers
|
||||||
|
* Fix autofill for apps
|
||||||
|
* Auto search for autofill
|
||||||
|
* New keyfile input
|
||||||
|
* Icon to hide keyfile input
|
||||||
|
* New lock button
|
||||||
|
* Setting to hide lock button in user interface
|
||||||
|
* Clickable links in notes
|
||||||
|
* Fix autofill for key-value pairs
|
||||||
|
|
||||||
|
KeePassDX(2.5beta30)
|
||||||
|
* Fix Lock after screen off (wait 1.5 seconds)
|
||||||
|
* Upgrade autofill algorithm
|
||||||
|
* Fix ANR during file verifications
|
||||||
|
|
||||||
|
KeePassDX(2.5beta29)
|
||||||
|
* Upgrade autofill algorithm
|
||||||
|
* Delete registered KeyFile after save new credentials
|
||||||
|
* Fix title and username entry view refresh after an update
|
||||||
|
* Fix database lock request (open notification always active)
|
||||||
|
* Allow empty title in entries
|
||||||
|
* Add expiration datetime
|
||||||
|
|
||||||
|
KeePassDX(2.5beta28)
|
||||||
|
* Fix read only database
|
||||||
|
* Upgrade to Android SDK 29
|
||||||
|
|
||||||
|
KeePassDX (2.5beta27)
|
||||||
* New setting to hide broken links
|
* New setting to hide broken links
|
||||||
* Show URL when title is empty
|
* Show URL when title is empty
|
||||||
* Setting to open search field at database opening
|
* Setting to open search field at database opening
|
||||||
@@ -7,7 +35,7 @@ KeepassDX (2.5beta27)
|
|||||||
* Fix appearance refresh settings
|
* Fix appearance refresh settings
|
||||||
* Sort optimization
|
* Sort optimization
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta26)
|
KeePassDX (2.5.0.0beta26)
|
||||||
* Download attachments
|
* Download attachments
|
||||||
* Change file size string format
|
* Change file size string format
|
||||||
* Prevent screenshot for all screen
|
* Prevent screenshot for all screen
|
||||||
@@ -20,7 +48,7 @@ KeepassDX (2.5.0.0beta26)
|
|||||||
* Fix dates
|
* Fix dates
|
||||||
* Fix UUID message for Database v1
|
* Fix UUID message for Database v1
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta25)
|
KeePassDX (2.5.0.0beta25)
|
||||||
* Setting for Recycle Bin
|
* Setting for Recycle Bin
|
||||||
* Fix Recycle bin issues
|
* Fix Recycle bin issues
|
||||||
* Fix TOTP
|
* Fix TOTP
|
||||||
@@ -28,7 +56,7 @@ KeepassDX (2.5.0.0beta25)
|
|||||||
* Fix update group
|
* Fix update group
|
||||||
* Fix OOM
|
* Fix OOM
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta24)
|
KeePassDX (2.5.0.0beta24)
|
||||||
* Add OTP (HOTP / TOTP)
|
* Add OTP (HOTP / TOTP)
|
||||||
* Add settings (Color, Security, Master Key)
|
* Add settings (Color, Security, Master Key)
|
||||||
* Show history of each entry
|
* Show history of each entry
|
||||||
@@ -38,7 +66,7 @@ KeepassDX (2.5.0.0beta24)
|
|||||||
* Open/Save database as service / Add persistent notification
|
* Open/Save database as service / Add persistent notification
|
||||||
* Fix settings / edit group / small bugs
|
* Fix settings / edit group / small bugs
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta23)
|
KeePassDX (2.5.0.0beta23)
|
||||||
* New, more secure database creation workflow
|
* New, more secure database creation workflow
|
||||||
* Recognize more database files
|
* Recognize more database files
|
||||||
* Add alias for history files (WARNING: history is erased)
|
* Add alias for history files (WARNING: history is erased)
|
||||||
@@ -47,14 +75,14 @@ KeepassDX (2.5.0.0beta23)
|
|||||||
* Fix OOM with KeyFile
|
* Fix OOM with KeyFile
|
||||||
* Fix small issues
|
* Fix small issues
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta22)
|
KeePassDX (2.5.0.0beta22)
|
||||||
* Rebuild code for actions
|
* Rebuild code for actions
|
||||||
* Add UUID as entry view
|
* Add UUID as entry view
|
||||||
* Fix bug with natural order
|
* Fix bug with natural order
|
||||||
* Fix number of entries in databaseV1
|
* Fix number of entries in databaseV1
|
||||||
* New entry views
|
* New entry views
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta21)
|
KeePassDX (2.5.0.0beta21)
|
||||||
* Fix nested groups no longer visible in V1 databases
|
* Fix nested groups no longer visible in V1 databases
|
||||||
* Improved data import algorithm for V1 databases
|
* Improved data import algorithm for V1 databases
|
||||||
* Add natural database sort
|
* Add natural database sort
|
||||||
@@ -62,10 +90,10 @@ KeepassDX (2.5.0.0beta21)
|
|||||||
* Fix button disabled with only KeyFile
|
* Fix button disabled with only KeyFile
|
||||||
* Show the number of entries in a group
|
* Show the number of entries in a group
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta20)
|
KeePassDX (2.5.0.0beta20)
|
||||||
* Fix a major bug that displays an entry history
|
* Fix a major bug that displays an entry history
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta19)
|
KeePassDX (2.5.0.0beta19)
|
||||||
* Add lock button always visible
|
* Add lock button always visible
|
||||||
* New connection workflow
|
* New connection workflow
|
||||||
* Code refactored in Kotlin
|
* Code refactored in Kotlin
|
||||||
@@ -76,7 +104,7 @@ KeepassDX (2.5.0.0beta19)
|
|||||||
* Fix memory when load database
|
* Fix memory when load database
|
||||||
* Fix small bugs
|
* Fix small bugs
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta18)
|
KeePassDX (2.5.0.0beta18)
|
||||||
* New recent databases views
|
* New recent databases views
|
||||||
* New information dialog
|
* New information dialog
|
||||||
* Custom fields for the Magikeyboard
|
* Custom fields for the Magikeyboard
|
||||||
@@ -85,10 +113,10 @@ KeepassDX (2.5.0.0beta18)
|
|||||||
* Fix memory when opening the database
|
* Fix memory when opening the database
|
||||||
* Memory management for attachments
|
* Memory management for attachments
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta17)
|
KeePassDX (2.5.0.0beta17)
|
||||||
* Fix font and search
|
* Fix font and search
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta16)
|
KeePassDX (2.5.0.0beta16)
|
||||||
* New search in a single fragment
|
* New search in a single fragment
|
||||||
* Search suggestions
|
* Search suggestions
|
||||||
* Added the display of usernames
|
* Added the display of usernames
|
||||||
@@ -96,20 +124,20 @@ KeepassDX (2.5.0.0beta16)
|
|||||||
* Fix read-only mode
|
* Fix read-only mode
|
||||||
* Fix parcelable / toolbar / back
|
* Fix parcelable / toolbar / back
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta15)
|
KeePassDX (2.5.0.0beta15)
|
||||||
* Read only mode
|
* Read only mode
|
||||||
* Best group recovery for the navigation fragment
|
* Best group recovery for the navigation fragment
|
||||||
* Fix copies in notifications
|
* Fix copies in notifications
|
||||||
* Fix orientation
|
* Fix orientation
|
||||||
* Added translations
|
* Added translations
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta14)
|
KeePassDX (2.5.0.0beta14)
|
||||||
* Optimize all the memory with parcelables / fix search
|
* Optimize all the memory with parcelables / fix search
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta13)
|
KeePassDX (2.5.0.0beta13)
|
||||||
* Fix memory issue with parcelable (crash in beta12 version)
|
* Fix memory issue with parcelable (crash in beta12 version)
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta12)
|
KeePassDX (2.5.0.0beta12)
|
||||||
* Added the Magikeyboard to fill the forms (settings still in development)
|
* Added the Magikeyboard to fill the forms (settings still in development)
|
||||||
* Added move and copy for groups and entries
|
* Added move and copy for groups and entries
|
||||||
* New navigation in a single screen / new animations between activities
|
* New navigation in a single screen / new animations between activities
|
||||||
@@ -122,10 +150,10 @@ KeepassDX (2.5.0.0beta12)
|
|||||||
* Fix the fingerprint recognition (WARNING : The keystore is reinit, you must delete the old keys)
|
* Fix the fingerprint recognition (WARNING : The keystore is reinit, you must delete the old keys)
|
||||||
* Fix small bugs
|
* Fix small bugs
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta11)
|
KeePassDX (2.5.0.0beta11)
|
||||||
* Fix crash in beta10 version
|
* Fix crash in beta10 version
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta10)
|
KeePassDX (2.5.0.0beta10)
|
||||||
* Dynamically change Algorithm and Key Derivation Function in settings
|
* Dynamically change Algorithm and Key Derivation Function in settings
|
||||||
* Upgrade translations
|
* Upgrade translations
|
||||||
* New red volcano theme, fix classic dark theme
|
* New red volcano theme, fix classic dark theme
|
||||||
@@ -133,7 +161,7 @@ KeepassDX (2.5.0.0beta10)
|
|||||||
* Update fingerprint state with checkbox
|
* Update fingerprint state with checkbox
|
||||||
* Fix bugs
|
* Fix bugs
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta9)
|
KeePassDX (2.5.0.0beta9)
|
||||||
* Education Screens to learn how to use the app
|
* Education Screens to learn how to use the app
|
||||||
* New designs
|
* New designs
|
||||||
* New custom font for character visibility
|
* New custom font for character visibility
|
||||||
@@ -142,9 +170,9 @@ KeepassDX (2.5.0.0beta9)
|
|||||||
* Change setting organisation
|
* Change setting organisation
|
||||||
* Pro version
|
* Pro version
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta8)
|
KeePassDX (2.5.0.0beta8)
|
||||||
* Hide custom entries protected
|
* Hide custom entries protected
|
||||||
* Best management of field references (https://keepass.info/help/base/fieldrefs.html)
|
* Best management of field references (https://KeePass.info/help/base/fieldrefs.html)
|
||||||
* Change database / default settings
|
* Change database / default settings
|
||||||
* Add Autofill for search
|
* Add Autofill for search
|
||||||
* Add sorting by last access and by creation time
|
* Add sorting by last access and by creation time
|
||||||
@@ -152,7 +180,7 @@ KeepassDX (2.5.0.0beta8)
|
|||||||
* Refactor old code
|
* Refactor old code
|
||||||
* Fix bugs
|
* Fix bugs
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta7)
|
KeePassDX (2.5.0.0beta7)
|
||||||
* Rebuild Notifications
|
* Rebuild Notifications
|
||||||
* Change links to https
|
* Change links to https
|
||||||
* Add extended Ascii (ñæËÌÂÝÜ...)
|
* Add extended Ascii (ñæËÌÂÝÜ...)
|
||||||
@@ -161,10 +189,10 @@ KeepassDX (2.5.0.0beta7)
|
|||||||
* Add setting to prevent the password copy
|
* Add setting to prevent the password copy
|
||||||
* Fix bugs
|
* Fix bugs
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta6)
|
KeePassDX (2.5.0.0beta6)
|
||||||
* Fix crash
|
* Fix crash
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta5)
|
KeePassDX (2.5.0.0beta5)
|
||||||
* Autofill (Android O)
|
* Autofill (Android O)
|
||||||
* Deletion for group
|
* Deletion for group
|
||||||
* New sorts with (Asc/Dsc, Groups before or after)
|
* New sorts with (Asc/Dsc, Groups before or after)
|
||||||
@@ -185,7 +213,7 @@ KeepassDX (2.5.0.0beta5)
|
|||||||
* Fix many small bugs
|
* Fix many small bugs
|
||||||
* Add recycle bin setting (not yet accessible)
|
* Add recycle bin setting (not yet accessible)
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta4)
|
KeePassDX (2.5.0.0beta4)
|
||||||
* Show only file name
|
* Show only file name
|
||||||
* Setting for full path
|
* Setting for full path
|
||||||
* Add information for each database file
|
* Add information for each database file
|
||||||
@@ -194,7 +222,7 @@ KeepassDX (2.5.0.0beta4)
|
|||||||
* Delete view assignment for fingerprint opening
|
* Delete view assignment for fingerprint opening
|
||||||
* Merge KeePassDroid 2.2.1
|
* Merge KeePassDroid 2.2.1
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta3)
|
KeePassDX (2.5.0.0beta3)
|
||||||
* New database workflow with new screens and folder selection
|
* New database workflow with new screens and folder selection
|
||||||
* Settings for default password generation
|
* Settings for default password generation
|
||||||
* Fingerprint dialog for explanations
|
* Fingerprint dialog for explanations
|
||||||
@@ -205,17 +233,17 @@ KeepassDX (2.5.0.0beta3)
|
|||||||
* Merge KeePassDroid 2.2.0.9
|
* Merge KeePassDroid 2.2.0.9
|
||||||
* Add corruption fix mode
|
* Add corruption fix mode
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta2)
|
KeePassDX (2.5.0.0beta2)
|
||||||
* Remove libs for F-Droid
|
* Remove libs for F-Droid
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta1)
|
KeePassDX (2.5.0.0beta1)
|
||||||
* Fork KeepassDroid
|
* Fork KeePassDroid
|
||||||
* Add Material Design
|
* Add Material Design
|
||||||
* Add Light and Night theme
|
* Add Light and Night theme
|
||||||
* Min API is 14
|
* Min API is 14
|
||||||
* Solve bug for fingerprint
|
* Solve bug for fingerprint
|
||||||
* Update French translation
|
* Update French translation
|
||||||
* Change donation (see KeepassDroid to contribute on both projects)
|
* Change donation (see KeePassDroid to contribute on both projects)
|
||||||
|
|
||||||
KeePassDroid (2.2.1)
|
KeePassDroid (2.2.1)
|
||||||
* Fix kdbx4 date corruption
|
* Fix kdbx4 date corruption
|
||||||
@@ -476,7 +504,7 @@ KeePassDroid (1.9.10)
|
|||||||
|
|
||||||
KeePassDroid (1.9.9)
|
KeePassDroid (1.9.9)
|
||||||
* Go back to explicitly storing blank fields in the database
|
* Go back to explicitly storing blank fields in the database
|
||||||
(works around bug in keepassx)
|
(works around bug in KeePassx)
|
||||||
* Add support for native code on MIPS architectures
|
* Add support for native code on MIPS architectures
|
||||||
* Adding Vibrate permission. On some devices notifications fail
|
* Adding Vibrate permission. On some devices notifications fail
|
||||||
without the vibrate permission.
|
without the vibrate permission.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ KeePassDX is a **free open source password manager for Android**, which helps yo
|
|||||||
|
|
||||||
Yes, KeePassDX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
|
Yes, KeePassDX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
|
||||||
|
|
||||||
*Note : If you access the application from a store, visual features may not be available to incentivize the contribution to the work of open source projects. These optional visuals are accessible after a donation (and a small congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
|
*Note : If you access the application from a store, visual styles may not be available to encourage contribution to the work of open source projects. These optional styles are accessible after a contribution (and a congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
@@ -53,9 +53,9 @@ You can contribute in different ways to help us on our work.
|
|||||||
alt="Get it on Google Play"
|
alt="Get it on Google Play"
|
||||||
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
||||||
|
|
||||||
## F.A.Q.
|
## Frequently Asked Questions
|
||||||
|
|
||||||
Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassDX/wiki/F.A.Q.)
|
Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
|
||||||
|
|
||||||
## Other devices
|
## Other devices
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,28 @@ apply plugin: 'kotlin-android-extensions'
|
|||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 29
|
||||||
buildToolsVersion '28.0.3'
|
buildToolsVersion '29.0.3'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 14
|
||||||
targetSdkVersion 28
|
targetSdkVersion 29
|
||||||
versionCode = 27
|
versionCode = 31
|
||||||
versionName = "2.5beta27"
|
versionName = "2.5RC1"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
||||||
|
|
||||||
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
arguments {
|
||||||
|
arg("room.incremental", "true")
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas".toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
@@ -79,7 +86,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def spongycastleVersion = "1.58.0.0"
|
def spongycastleVersion = "1.58.0.0"
|
||||||
def room_version = "2.2.1"
|
def room_version = "2.2.5"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
@@ -88,7 +95,10 @@ dependencies {
|
|||||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation 'androidx.biometric:biometric:1.0.0'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
|
implementation 'androidx.biometric:biometric:1.0.1'
|
||||||
|
implementation "androidx.core:core-ktx:1.2.0"
|
||||||
|
// To upgrade with style
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'com.google.android.material:material:1.0.0'
|
||||||
|
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "56438e5f7372ef3e36e33b782aed245d",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "file_database_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "databaseUri",
|
||||||
|
"columnName": "database_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "databaseAlias",
|
||||||
|
"columnName": "database_alias",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "keyFileUri",
|
||||||
|
"columnName": "keyfile_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updated",
|
||||||
|
"columnName": "updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"database_uri"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "cipher_database",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "databaseUri",
|
||||||
|
"columnName": "database_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "encryptedValue",
|
||||||
|
"columnName": "encrypted_value",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "specParameters",
|
||||||
|
"columnName": "specs_parameters",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"database_uri"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '56438e5f7372ef3e36e33b782aed245d')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.VIBRATE"/>
|
android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:maxSdkVersion="18"
|
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@@ -138,12 +137,12 @@
|
|||||||
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
<activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" />
|
<activity android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||||
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
|
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
|
||||||
android:label="@string/keyboard_name"
|
android:label="@string/keyboard_name"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.MagikIMESettings"
|
<activity android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||||
android:label="@string/keyboard_setting_label">
|
android:label="@string/keyboard_setting_label">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
|||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
|
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
@@ -73,6 +72,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
private var historyView: View? = null
|
private var historyView: View? = null
|
||||||
private var entryContentsView: EntryContentsView? = null
|
private var entryContentsView: EntryContentsView? = null
|
||||||
private var entryProgress: ProgressBar? = null
|
private var entryProgress: ProgressBar? = null
|
||||||
|
private var lockView: View? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
@@ -124,6 +124,11 @@ class EntryActivity : LockingActivity() {
|
|||||||
entryContentsView = findViewById(R.id.entry_contents)
|
entryContentsView = findViewById(R.id.entry_contents)
|
||||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||||
entryProgress = findViewById(R.id.entry_progress)
|
entryProgress = findViewById(R.id.entry_progress)
|
||||||
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
|
||||||
|
lockView?.setOnClickListener {
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
|
||||||
// Init the clipboard helper
|
// Init the clipboard helper
|
||||||
clipboardHelper = ClipboardHelper(this)
|
clipboardHelper = ClipboardHelper(this)
|
||||||
@@ -148,11 +153,20 @@ class EntryActivity : LockingActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
// Show the lock button
|
||||||
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
// Get Entry from UUID
|
// Get Entry from UUID
|
||||||
try {
|
try {
|
||||||
val keyEntry: NodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
|
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
|
||||||
mEntry = mDatabase?.getEntryById(keyEntry)
|
if (keyEntry != null) {
|
||||||
mEntryLastVersion = mEntry
|
mEntry = mDatabase?.getEntryById(keyEntry)
|
||||||
|
mEntryLastVersion = mEntry
|
||||||
|
}
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
Log.e(TAG, "Unable to retrieve the entry key")
|
Log.e(TAG, "Unable to retrieve the entry key")
|
||||||
}
|
}
|
||||||
@@ -460,8 +474,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
getString(R.string.entry_user_name)))
|
getString(R.string.entry_user_name)))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Launch autofill settings
|
performedNextEducation(entryActivityEducation, menu)
|
||||||
startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!entryCopyEducationPerformed) {
|
if (!entryCopyEducationPerformed) {
|
||||||
@@ -524,10 +537,6 @@ class EntryActivity : LockingActivity() {
|
|||||||
!mReadOnly && mAutoSaveEnable)
|
!mReadOnly && mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_lock -> {
|
|
||||||
lockAndExit()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
||||||
}
|
}
|
||||||
@@ -539,12 +548,10 @@ class EntryActivity : LockingActivity() {
|
|||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
// Transit data in previous Activity after an update
|
// Transit data in previous Activity after an update
|
||||||
/*
|
Intent().apply {
|
||||||
TODO Slowdown when add entry as result
|
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
|
||||||
Intent intent = new Intent();
|
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
|
||||||
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
|
}
|
||||||
onFinish(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
|
|
||||||
*/
|
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.DatePickerDialog
|
||||||
|
import android.app.TimePickerDialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
@@ -26,13 +28,15 @@ import android.util.Log
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ScrollView
|
import android.widget.DatePicker
|
||||||
|
import android.widget.TimePicker
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.widget.ActionMenuView
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.widget.NestedScrollView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.*
|
||||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
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
|
||||||
@@ -52,12 +56,15 @@ import com.kunzisoft.keepass.timeout.TimeoutHelper
|
|||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.view.EntryEditContentsView
|
import com.kunzisoft.keepass.view.EntryEditContentsView
|
||||||
import com.kunzisoft.keepass.view.showActionError
|
import com.kunzisoft.keepass.view.showActionError
|
||||||
|
import org.joda.time.DateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class EntryEditActivity : LockingActivity(),
|
class EntryEditActivity : LockingActivity(),
|
||||||
IconPickerDialogFragment.IconPickerListener,
|
IconPickerDialogFragment.IconPickerListener,
|
||||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||||
SetOTPDialogFragment.CreateOtpListener {
|
SetOTPDialogFragment.CreateOtpListener,
|
||||||
|
DatePickerDialog.OnDateSetListener,
|
||||||
|
TimePickerDialog.OnTimeSetListener {
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
@@ -70,9 +77,11 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var scrollView: ScrollView? = null
|
private var scrollView: NestedScrollView? = null
|
||||||
private var entryEditContentsView: EntryEditContentsView? = null
|
private var entryEditContentsView: EntryEditContentsView? = null
|
||||||
|
private var entryEditAddToolBar: ActionMenuView? = null
|
||||||
private var saveView: View? = null
|
private var saveView: View? = null
|
||||||
|
private var lockView: View? = null
|
||||||
|
|
||||||
// Education
|
// Education
|
||||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||||
@@ -94,6 +103,22 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
|
|
||||||
entryEditContentsView = findViewById(R.id.entry_edit_contents)
|
entryEditContentsView = findViewById(R.id.entry_edit_contents)
|
||||||
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||||
|
entryEditContentsView?.onDateClickListener = View.OnClickListener {
|
||||||
|
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||||
|
val dateTime = DateTime(expiresDate)
|
||||||
|
val defaultYear = dateTime.year
|
||||||
|
val defaultMonth = dateTime.monthOfYear-1
|
||||||
|
val defaultDay = dateTime.dayOfMonth
|
||||||
|
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
|
||||||
|
.show(supportFragmentManager, "DatePickerFragment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
lockView?.setOnClickListener {
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
|
||||||
// Focus view to reinitialize timeout
|
// Focus view to reinitialize timeout
|
||||||
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
|
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
|
||||||
|
|
||||||
@@ -167,17 +192,46 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
// Add listener to the icon
|
// Add listener to the icon
|
||||||
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
|
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
|
||||||
|
|
||||||
// Generate password button
|
// Bottom Bar
|
||||||
entryEditContentsView?.setOnPasswordGeneratorClickListener { openPasswordGenerator() }
|
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
||||||
|
entryEditAddToolBar?.apply {
|
||||||
|
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||||
|
|
||||||
|
menu.findItem(R.id.menu_add_field).apply {
|
||||||
|
val allowCustomField = mNewEntry?.allowCustomFields() == true
|
||||||
|
isEnabled = allowCustomField
|
||||||
|
isVisible = allowCustomField
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.findItem(R.id.menu_add_otp).apply {
|
||||||
|
val allowOTP = mDatabase?.allowOTP == true
|
||||||
|
isEnabled = allowOTP
|
||||||
|
isVisible = allowOTP
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnMenuItemClickListener { item ->
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.menu_generate_password -> {
|
||||||
|
openPasswordGenerator()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.menu_add_field -> {
|
||||||
|
addNewCustomField()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.menu_add_otp -> {
|
||||||
|
setupOTP()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save button
|
// Save button
|
||||||
saveView = findViewById(R.id.entry_edit_save)
|
saveView = findViewById(R.id.entry_edit_validate)
|
||||||
saveView?.setOnClickListener { saveEntry() }
|
saveView?.setOnClickListener { saveEntry() }
|
||||||
|
|
||||||
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) {
|
|
||||||
addNewCustomField()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the education views
|
// Verify the education views
|
||||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||||
|
|
||||||
@@ -194,6 +248,16 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun populateViewsWithEntry(newEntry: Entry) {
|
private fun populateViewsWithEntry(newEntry: Entry) {
|
||||||
// Don't start the field reference manager, we want to see the raw ref
|
// Don't start the field reference manager, we want to see the raw ref
|
||||||
mDatabase?.stopManageEntry(newEntry)
|
mDatabase?.stopManageEntry(newEntry)
|
||||||
@@ -207,6 +271,9 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
|
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
|
||||||
url = newEntry.url
|
url = newEntry.url
|
||||||
password = newEntry.password
|
password = newEntry.password
|
||||||
|
expires = newEntry.expires
|
||||||
|
if (expires)
|
||||||
|
expiresDate = newEntry.expiryTime
|
||||||
notes = newEntry.notes
|
notes = newEntry.notes
|
||||||
for (entry in newEntry.customFields.entries) {
|
for (entry in newEntry.customFields.entries) {
|
||||||
post {
|
post {
|
||||||
@@ -228,7 +295,11 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
username = entryView.username
|
username = entryView.username
|
||||||
url = entryView.url
|
url = entryView.url
|
||||||
password = entryView.password
|
password = entryView.password
|
||||||
notes = entryView.notes
|
expires = entryView.expires
|
||||||
|
if (entryView.expires) {
|
||||||
|
expiryTime = entryView.expiresDate
|
||||||
|
}
|
||||||
|
notes = entryView. notes
|
||||||
entryView.customFields.forEach { customField ->
|
entryView.customFields.forEach { customField ->
|
||||||
putExtraField(customField.name, customField.protectedValue)
|
putExtraField(customField.name, customField.protectedValue)
|
||||||
}
|
}
|
||||||
@@ -259,6 +330,13 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
entryEditContentsView?.addEmptyCustomField()
|
entryEditContentsView?.addEmptyCustomField()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupOTP() {
|
||||||
|
// Retrieve the current otpElement if exists
|
||||||
|
// and open the dialog to set up the OTP
|
||||||
|
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
|
||||||
|
.show(supportFragmentManager, "addOTPDialog")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the new entry or update an existing entry in the database
|
* Saves the new entry or update an existing entry in the database
|
||||||
*/
|
*/
|
||||||
@@ -307,8 +385,6 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
// Save database not needed here
|
// Save database not needed here
|
||||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||||
if (mDatabase?.allowOTP == true)
|
|
||||||
inflater.inflate(R.menu.entry_otp, menu)
|
|
||||||
|
|
||||||
entryEditActivityEducation?.let {
|
entryEditActivityEducation?.let {
|
||||||
Handler().post { performedNextEducation(it) }
|
Handler().post { performedNextEducation(it) }
|
||||||
@@ -318,12 +394,10 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||||
val passwordView = entryEditContentsView?.generatePasswordView
|
val passwordGeneratorView: View? = entryEditAddToolBar?.findViewById(R.id.menu_generate_password)
|
||||||
val addNewFieldView = entryEditContentsView?.addNewFieldButton
|
val generatePasswordEducationPerformed = passwordGeneratorView != null
|
||||||
|
|
||||||
val generatePasswordEducationPerformed = passwordView != null
|
|
||||||
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||||
passwordView,
|
passwordGeneratorView,
|
||||||
{
|
{
|
||||||
openPasswordGenerator()
|
openPasswordGenerator()
|
||||||
},
|
},
|
||||||
@@ -332,23 +406,33 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!generatePasswordEducationPerformed) {
|
if (!generatePasswordEducationPerformed) {
|
||||||
// entryNewFieldEducationPerformed
|
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
|
||||||
mNewEntry != null && mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
|
val addNewFieldEducationPerformed = mNewEntry != null
|
||||||
|
&& mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
|
||||||
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
|
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
|
||||||
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||||
addNewFieldView,
|
addNewFieldView,
|
||||||
{
|
{
|
||||||
addNewCustomField()
|
addNewCustomField()
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(entryEditActivityEducation)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!addNewFieldEducationPerformed) {
|
||||||
|
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
||||||
|
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
|
||||||
|
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||||
|
setupOtpView,
|
||||||
|
{
|
||||||
|
setupOTP()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_lock -> {
|
|
||||||
lockAndExit()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
||||||
}
|
}
|
||||||
@@ -356,14 +440,9 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
MenuUtil.onContributionItemSelected(this)
|
MenuUtil.onContributionItemSelected(this)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_add_otp -> {
|
android.R.id.home -> {
|
||||||
// Retrieve the current otpElement if exists
|
onBackPressed()
|
||||||
// and open the dialog to set up the OTP
|
|
||||||
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
|
|
||||||
.show(supportFragmentManager, "addOTPDialog")
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
android.R.id.home -> finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
@@ -383,6 +462,39 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
||||||
|
// To fix android 4.4 issue
|
||||||
|
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
||||||
|
if (datePicker?.isShown == true) {
|
||||||
|
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||||
|
// Save the date
|
||||||
|
entryEditContentsView?.expiresDate =
|
||||||
|
DateInstant(DateTime(expiresDate)
|
||||||
|
.withYear(year)
|
||||||
|
.withMonthOfYear(month + 1)
|
||||||
|
.withDayOfMonth(day)
|
||||||
|
.toDate())
|
||||||
|
// Launch the time picker
|
||||||
|
val dateTime = DateTime(expiresDate)
|
||||||
|
val defaultHour = dateTime.hourOfDay
|
||||||
|
val defaultMinute = dateTime.minuteOfHour
|
||||||
|
TimePickerFragment.getInstance(defaultHour, defaultMinute)
|
||||||
|
.show(supportFragmentManager, "TimePickerFragment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
|
||||||
|
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||||
|
// Save the date
|
||||||
|
entryEditContentsView?.expiresDate =
|
||||||
|
DateInstant(DateTime(expiresDate)
|
||||||
|
.withHourOfDay(hours)
|
||||||
|
.withMinuteOfHour(minutes)
|
||||||
|
.toDate())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
mNewEntry?.let {
|
mNewEntry?.let {
|
||||||
populateEntryWithViews(it)
|
populateEntryWithViews(it)
|
||||||
@@ -406,6 +518,15 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
// Do nothing here
|
// Do nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.discard_changes)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.discard) { _, _ ->
|
||||||
|
super@EntryEditActivity.onBackPressed()
|
||||||
|
}.create().show()
|
||||||
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
// Assign entry callback as a result in all case
|
// Assign entry callback as a result in all case
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -26,16 +26,14 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
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
|
||||||
@@ -48,9 +46,11 @@ import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
|||||||
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.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillHelper.KEY_SEARCH_INFO
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||||
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.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
@@ -62,7 +62,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var fileListContainer: View? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
|
private var fileManagerExplanationButton: View? = null
|
||||||
private var createButtonView: View? = null
|
private var createButtonView: View? = null
|
||||||
private var openDatabaseButtonView: View? = null
|
private var openDatabaseButtonView: View? = null
|
||||||
|
|
||||||
@@ -83,12 +84,17 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||||
|
|
||||||
setContentView(R.layout.activity_file_selection)
|
setContentView(R.layout.activity_file_selection)
|
||||||
fileListContainer = findViewById(R.id.container_file_list)
|
coordinatorLayout = findViewById(R.id.activity_file_selection_coordinator_layout)
|
||||||
|
|
||||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||||
toolbar.title = ""
|
toolbar.title = ""
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
|
fileManagerExplanationButton = findViewById(R.id.file_manager_explanation_button)
|
||||||
|
fileManagerExplanationButton?.setOnClickListener {
|
||||||
|
UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
||||||
|
}
|
||||||
|
|
||||||
// Create button
|
// Create button
|
||||||
createButtonView = findViewById(R.id.create_database_button)
|
createButtonView = findViewById(R.id.create_database_button)
|
||||||
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||||
@@ -103,8 +109,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
createButtonView?.setOnClickListener { createNewFile() }
|
createButtonView?.setOnClickListener { createNewFile() }
|
||||||
|
|
||||||
mOpenFileHelper = OpenFileHelper(this)
|
mOpenFileHelper = OpenFileHelper(this)
|
||||||
openDatabaseButtonView = findViewById(R.id.open_database_button)
|
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||||
openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
openDatabaseButtonView?.apply {
|
||||||
|
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
||||||
|
setOnClickListener(it)
|
||||||
|
setOnLongClickListener(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// History list
|
// History list
|
||||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||||
@@ -119,7 +130,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
|
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
|
||||||
}
|
}
|
||||||
updateFileListVisibility()
|
|
||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||||
// Remove from app database
|
// Remove from app database
|
||||||
@@ -128,7 +138,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
|
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
|
||||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
|
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
updateFileListVisibility()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -142,8 +151,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
if (!(savedInstanceState != null
|
if (!(savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(EXTRA_STAY)
|
&& savedInstanceState.containsKey(EXTRA_STAY)
|
||||||
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
|
||||||
val databasePath = prefs.getString(PasswordActivity.KEY_DEFAULT_DATABASE_PATH, "")
|
|
||||||
|
|
||||||
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
||||||
launchPasswordActivityWithPath(databaseFileUri)
|
launchPasswordActivityWithPath(databaseFileUri)
|
||||||
@@ -163,9 +171,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
onActionFinish = { actionTask, _ ->
|
onActionFinish = { actionTask, _ ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_TASK -> {
|
ACTION_DATABASE_CREATE_TASK -> {
|
||||||
// TODO Check
|
|
||||||
// mAdapterDatabaseHistory?.notifyDataSetChanged()
|
|
||||||
// updateFileListVisibility()
|
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity)
|
GroupActivity.launch(this@FileDatabaseSelectActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,7 +189,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
|
|
||||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||||
val error = getString(R.string.file_not_found_content)
|
val error = getString(R.string.file_not_found_content)
|
||||||
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
coordinatorLayout?.let {
|
||||||
|
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
}
|
||||||
Log.e(TAG, error, e)
|
Log.e(TAG, error, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +219,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
try {
|
try {
|
||||||
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||||
databaseUri, keyFile,
|
databaseUri, keyFile,
|
||||||
assistStructure)
|
assistStructure,
|
||||||
|
intent.getParcelableExtra(KEY_SEARCH_INFO))
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
fileNoFoundAction(e)
|
fileNoFoundAction(e)
|
||||||
}
|
}
|
||||||
@@ -224,16 +232,21 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
private fun launchGroupActivity(readOnly: Boolean) {
|
private fun launchGroupActivity(readOnly: Boolean) {
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
{
|
{
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity, readOnly)
|
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||||
|
readOnly)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity, readOnly)
|
GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity,
|
||||||
|
readOnly)
|
||||||
// Do not keep history
|
// Do not keep history
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{ assistStructure ->
|
{ assistStructure ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity, assistStructure, readOnly)
|
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||||
|
assistStructure,
|
||||||
|
intent.getParcelableExtra(KEY_SEARCH_INFO),
|
||||||
|
readOnly)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -245,25 +258,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
overridePendingTransition(0, 0)
|
overridePendingTransition(0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateExternalStorageWarning() {
|
|
||||||
// To show errors
|
|
||||||
var warning = -1
|
|
||||||
val state = Environment.getExternalStorageState()
|
|
||||||
if (state == Environment.MEDIA_MOUNTED_READ_ONLY) {
|
|
||||||
warning = R.string.read_only_warning
|
|
||||||
} else if (state != Environment.MEDIA_MOUNTED) {
|
|
||||||
warning = R.string.warning_unmounted
|
|
||||||
}
|
|
||||||
|
|
||||||
val labelWarningView = findViewById<TextView>(R.id.label_warning)
|
|
||||||
if (warning != -1) {
|
|
||||||
labelWarningView.setText(warning)
|
|
||||||
labelWarningView.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
labelWarningView.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
@@ -272,8 +266,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
updateExternalStorageWarning()
|
|
||||||
|
|
||||||
// Construct adapter with listeners
|
// Construct adapter with listeners
|
||||||
if (PreferencesUtil.showRecentFiles(this)) {
|
if (PreferencesUtil.showRecentFiles(this)) {
|
||||||
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
||||||
@@ -283,20 +275,17 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
// Show only uri accessible
|
// Show only uri accessible
|
||||||
historyList.filter {
|
historyList.filter {
|
||||||
if (hideBrokenLocations) {
|
if (hideBrokenLocations) {
|
||||||
UriUtil.parse(it.databaseUri)?.let { historyUri ->
|
FileDatabaseInfo(this@FileDatabaseSelectActivity,
|
||||||
UriUtil.isUriAccessible(contentResolver, historyUri)
|
it.databaseUri).exists
|
||||||
} ?: false
|
|
||||||
} else
|
} else
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
updateFileListVisibility()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
updateFileListVisibility()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register progress task
|
// Register progress task
|
||||||
@@ -318,13 +307,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFileListVisibility() {
|
|
||||||
if (mAdapterDatabaseHistory?.itemCount == 0)
|
|
||||||
fileListContainer?.visibility = View.INVISIBLE
|
|
||||||
else
|
|
||||||
fileListContainer?.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAssignKeyDialogPositiveClick(
|
override fun onAssignKeyDialogPositiveClick(
|
||||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
masterPasswordChecked: Boolean, masterPassword: String?,
|
||||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
keyFileChecked: Boolean, keyFile: Uri?) {
|
||||||
@@ -374,10 +356,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
if (mDatabaseFileUri != null) {
|
if (mDatabaseFileUri != null) {
|
||||||
AssignMasterKeyDialogFragment.getInstance(true)
|
AssignMasterKeyDialogFragment.getInstance(true)
|
||||||
.show(supportFragmentManager, "passwordDialog")
|
.show(supportFragmentManager, "passwordDialog")
|
||||||
|
} else {
|
||||||
|
val error = getString(R.string.error_create_database)
|
||||||
|
coordinatorLayout?.let {
|
||||||
|
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
}
|
||||||
|
Log.e(TAG, error)
|
||||||
}
|
}
|
||||||
// else {
|
|
||||||
// TODO Show error
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,10 +437,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure) {
|
fun launchForAutofillResult(activity: Activity,
|
||||||
|
assistStructure: AssistStructure,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
AutofillHelper.startActivityForAutofillResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
assistStructure)
|
assistStructure,
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import com.kunzisoft.keepass.database.element.node.Type
|
|||||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
@@ -89,6 +90,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
|
private var lockView: View? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var searchTitleView: View? = null
|
private var searchTitleView: View? = null
|
||||||
private var toolbarAction: ToolbarAction? = null
|
private var toolbarAction: ToolbarAction? = null
|
||||||
@@ -134,6 +136,11 @@ class GroupActivity : LockingActivity(),
|
|||||||
groupNameView = findViewById(R.id.group_name)
|
groupNameView = findViewById(R.id.group_name)
|
||||||
toolbarAction = findViewById(R.id.toolbar_action)
|
toolbarAction = findViewById(R.id.toolbar_action)
|
||||||
modeTitleView = findViewById(R.id.mode_title_view)
|
modeTitleView = findViewById(R.id.mode_title_view)
|
||||||
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
|
||||||
|
lockView?.setOnClickListener {
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
|
||||||
toolbar?.title = ""
|
toolbar?.title = ""
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
@@ -346,7 +353,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
// If it's a search
|
// If it's a search
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' })
|
val searchString = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
||||||
|
return mDatabase?.createVirtualGroupFromSearch(searchString)
|
||||||
}
|
}
|
||||||
// else a real group
|
// else a real group
|
||||||
else {
|
else {
|
||||||
@@ -438,8 +446,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
enableAddGroup(addGroupEnabled)
|
enableAddGroup(addGroupEnabled)
|
||||||
enableAddEntry(addEntryEnabled)
|
enableAddEntry(addEntryEnabled)
|
||||||
|
|
||||||
if (isEnable)
|
showButton()
|
||||||
showButton()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +493,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
// Build response with the entry selected
|
// Build response with the entry selected
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
AutofillHelper.buildResponseWhenEntrySelected(this@GroupActivity,
|
AutofillHelper.buildResponse(this@GroupActivity,
|
||||||
entryVersioned.getEntryInfo(database))
|
entryVersioned.getEntryInfo(database))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -503,6 +510,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
private fun finishNodeAction() {
|
private fun finishNodeAction() {
|
||||||
actionNodeMode?.finish()
|
actionNodeMode?.finish()
|
||||||
actionNodeMode = null
|
actionNodeMode = null
|
||||||
|
addNodeButtonView?.showButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNodeSelected(nodes: List<Node>): Boolean {
|
override fun onNodeSelected(nodes: List<Node>): Boolean {
|
||||||
@@ -514,6 +522,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
} else {
|
} else {
|
||||||
actionNodeMode?.invalidate()
|
actionNodeMode?.invalidate()
|
||||||
}
|
}
|
||||||
|
addNodeButtonView?.hideButton()
|
||||||
} else {
|
} else {
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
}
|
}
|
||||||
@@ -630,6 +639,13 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
// Show the lock button
|
||||||
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
// Refresh the elements
|
// Refresh the elements
|
||||||
assignGroupViewElements()
|
assignGroupViewElements()
|
||||||
// Refresh suggestions to change preferences
|
// Refresh suggestions to change preferences
|
||||||
@@ -663,13 +679,15 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the SearchView and set the searchable configuration
|
// Get the SearchView and set the searchable configuration
|
||||||
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
|
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
|
||||||
|
|
||||||
menu.findItem(R.id.menu_search)?.let {
|
menu.findItem(R.id.menu_search)?.let {
|
||||||
val searchView = it.actionView as SearchView?
|
val searchView = it.actionView as SearchView?
|
||||||
searchView?.apply {
|
searchView?.apply {
|
||||||
setSearchableInfo(searchManager.getSearchableInfo(
|
(searchManager?.getSearchableInfo(
|
||||||
ComponentName(this@GroupActivity, GroupActivity::class.java)))
|
ComponentName(this@GroupActivity, GroupActivity::class.java)))?.let { searchableInfo ->
|
||||||
|
setSearchableInfo(searchableInfo)
|
||||||
|
}
|
||||||
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
|
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
|
||||||
suggestionsAdapter = mSearchSuggestionAdapter
|
suggestionsAdapter = mSearchSuggestionAdapter
|
||||||
setOnSuggestionListener(object : SearchView.OnSuggestionListener {
|
setOnSuggestionListener(object : SearchView.OnSuggestionListener {
|
||||||
@@ -749,12 +767,11 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
if (!sortMenuEducationPerformed) {
|
if (!sortMenuEducationPerformed) {
|
||||||
// lockMenuEducationPerformed
|
// lockMenuEducationPerformed
|
||||||
toolbar != null
|
val lockButtonView = findViewById<View>(R.id.lock_button_icon)
|
||||||
&& toolbar!!.findViewById<View>(R.id.menu_lock) != null
|
lockButtonView != null
|
||||||
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
|
&& groupActivityEducation.checkAndPerformedLockMenuEducation(lockButtonView,
|
||||||
toolbar!!.findViewById(R.id.menu_lock),
|
|
||||||
{
|
{
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_lock))
|
lockAndExit()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(groupActivityEducation, menu)
|
performedNextEducation(groupActivityEducation, menu)
|
||||||
@@ -773,10 +790,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
R.id.menu_search ->
|
R.id.menu_search ->
|
||||||
//onSearchRequested();
|
//onSearchRequested();
|
||||||
return true
|
return true
|
||||||
R.id.menu_lock -> {
|
|
||||||
lockAndExit()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
||||||
return true
|
return true
|
||||||
@@ -859,8 +872,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
.iconPicked(bundle)
|
.iconPicked(bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
|
override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||||
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
|
mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startActivity(intent: Intent) {
|
override fun startActivity(intent: Intent) {
|
||||||
@@ -904,8 +917,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not directly get the entry from intent data but from database
|
// Directly used the onActivityResult in fragment
|
||||||
mListNodesFragment?.rebuildList()
|
mListNodesFragment?.onActivityResult(requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeSearchInIntent(intent: Intent) {
|
private fun removeSearchInIntent(intent: Intent) {
|
||||||
@@ -952,19 +965,41 @@ class GroupActivity : LockingActivity(),
|
|||||||
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
|
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
|
||||||
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
|
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
|
||||||
|
|
||||||
private fun buildAndLaunchIntent(context: Context, group: Group?, readOnly: Boolean,
|
private fun buildIntent(context: Context,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
group: Group?,
|
||||||
val checkTime = if (context is Activity)
|
searchInfo: SearchInfo?,
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeout(context)
|
readOnly: Boolean,
|
||||||
else
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
TimeoutHelper.checkTime(context)
|
val intent = Intent(context, GroupActivity::class.java)
|
||||||
if (checkTime) {
|
if (group != null) {
|
||||||
val intent = Intent(context, GroupActivity::class.java)
|
intent.putExtra(GROUP_ID_KEY, group.nodeId)
|
||||||
if (group != null) {
|
}
|
||||||
intent.putExtra(GROUP_ID_KEY, group.nodeId)
|
if (searchInfo != null) {
|
||||||
}
|
intent.action = Intent.ACTION_SEARCH
|
||||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
val searchQuery = searchInfo.webDomain ?: searchInfo.applicationId
|
||||||
intentBuildLauncher.invoke(intent)
|
intent.putExtra(SearchManager.QUERY, searchQuery)
|
||||||
|
}
|
||||||
|
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||||
|
intentBuildLauncher.invoke(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkTimeAndBuildIntent(activity: Activity,
|
||||||
|
group: Group?,
|
||||||
|
searchInfo: SearchInfo?,
|
||||||
|
readOnly: Boolean,
|
||||||
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
|
buildIntent(activity, group, searchInfo, readOnly, intentBuildLauncher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkTimeAndBuildIntent(context: Context,
|
||||||
|
group: Group?,
|
||||||
|
searchInfo: SearchInfo?,
|
||||||
|
readOnly: Boolean,
|
||||||
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
|
if (TimeoutHelper.checkTime(context)) {
|
||||||
|
buildIntent(context, group, searchInfo, readOnly, intentBuildLauncher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -973,11 +1008,9 @@ class GroupActivity : LockingActivity(),
|
|||||||
* Standard Launch
|
* Standard Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
fun launch(context: Context,
|
||||||
@JvmOverloads
|
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||||
fun launch(context: Context, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
checkTimeAndBuildIntent(context, null, null, readOnly) { intent ->
|
||||||
TimeoutHelper.recordTime(context)
|
|
||||||
buildAndLaunchIntent(context, null, readOnly) { intent ->
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -988,10 +1021,9 @@ class GroupActivity : LockingActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
// TODO implement pre search to directly open the direct group
|
// TODO implement pre search to directly open the direct group
|
||||||
|
fun launchForKeyboardSelection(context: Context,
|
||||||
fun launchForKeyboardSelection(context: Context, readOnly: Boolean) {
|
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||||
TimeoutHelper.recordTime(context)
|
checkTimeAndBuildIntent(context, null, null, readOnly) { intent ->
|
||||||
buildAndLaunchIntent(context, null, readOnly) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForEntrySelection(context, intent)
|
EntrySelectionHelper.startActivityForEntrySelection(context, intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1001,13 +1033,13 @@ class GroupActivity : LockingActivity(),
|
|||||||
* Autofill Launch
|
* Autofill Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
// TODO implement pre search to directly open the direct group
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure, readOnly: Boolean) {
|
fun launchForAutofillResult(activity: Activity,
|
||||||
TimeoutHelper.recordTime(activity)
|
assistStructure: AssistStructure,
|
||||||
buildAndLaunchIntent(activity, null, readOnly) { intent ->
|
searchInfo: SearchInfo? = null,
|
||||||
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure)
|
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
|
||||||
|
checkTimeAndBuildIntent(activity, null, searchInfo, readOnly) { intent ->
|
||||||
|
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure, searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ package com.kunzisoft.keepass.activities
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -69,8 +67,6 @@ 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
|
||||||
|
|
||||||
// Preferences for sorting
|
|
||||||
private var prefs: SharedPreferences? = null
|
|
||||||
|
|
||||||
private var readOnly: Boolean = false
|
private var readOnly: Boolean = false
|
||||||
get() {
|
get() {
|
||||||
@@ -155,7 +151,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
@@ -213,26 +208,26 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
fun rebuildList() {
|
fun rebuildList() {
|
||||||
// Add elements to the list
|
// Add elements to the list
|
||||||
mainGroup?.let { mainGroup ->
|
mainGroup?.let { mainGroup ->
|
||||||
mAdapter?.rebuildList(mainGroup)
|
mAdapter?.apply {
|
||||||
|
rebuildList(mainGroup)
|
||||||
|
// To visually change the elements
|
||||||
|
if (PreferencesUtil.APPEARANCE_CHANGED) {
|
||||||
|
notifyDataSetChanged()
|
||||||
|
PreferencesUtil.APPEARANCE_CHANGED = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||||
ascending: Boolean,
|
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||||
groupsBefore: Boolean,
|
// Save setting
|
||||||
recycleBinBottom: Boolean) {
|
context?.let {
|
||||||
// Toggle setting
|
PreferencesUtil.saveNodeSort(it, sortNodeEnum, sortNodeParameters)
|
||||||
prefs?.edit()?.apply {
|
|
||||||
putString(getString(R.string.sort_node_key), sortNodeEnum.name)
|
|
||||||
putBoolean(getString(R.string.sort_ascending_key), ascending)
|
|
||||||
putBoolean(getString(R.string.sort_group_before_key), groupsBefore)
|
|
||||||
putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom)
|
|
||||||
apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell the adapter to refresh it's list
|
// Tell the adapter to refresh it's list
|
||||||
mAdapter?.notifyChangeSort(sortNodeEnum,
|
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
|
||||||
SortNodeEnum.SortNodeParameters(ascending, groupsBefore, recycleBinBottom))
|
|
||||||
rebuildList()
|
rebuildList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,13 +369,11 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
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_ENTRY_RESULT_CODE
|
||||||
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||||
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode ->
|
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
|
||||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
||||||
mAdapter?.addNode(newNode)
|
addNode(changedNode)
|
||||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
|
||||||
//mAdapter.updateLastNodeRegister(newNode);
|
mAdapter?.notifyDataSetChanged()
|
||||||
rebuildList()
|
|
||||||
}
|
|
||||||
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,24 +23,21 @@ import android.app.Activity
|
|||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.app.backup.BackupManager
|
import android.app.backup.BackupManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import android.text.Editable
|
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.inputmethod.EditorInfo.IME_ACTION_DONE
|
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||||
import android.widget.Button
|
import android.widget.*
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import com.google.android.material.snackbar.Snackbar
|
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
|
||||||
@@ -52,11 +49,13 @@ import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
|||||||
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.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillHelper.KEY_SEARCH_INFO
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||||
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.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
@@ -68,6 +67,7 @@ import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
|||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import kotlinx.android.synthetic.main.activity_password.*
|
import kotlinx.android.synthetic.main.activity_password.*
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
@@ -79,7 +79,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
private var containerView: View? = null
|
private var containerView: View? = null
|
||||||
private var filenameView: TextView? = null
|
private var filenameView: TextView? = null
|
||||||
private var passwordView: EditText? = null
|
private var passwordView: EditText? = null
|
||||||
private var keyFileView: EditText? = null
|
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var checkboxPasswordView: CompoundButton? = null
|
private var checkboxPasswordView: CompoundButton? = null
|
||||||
private var checkboxKeyFileView: CompoundButton? = null
|
private var checkboxKeyFileView: CompoundButton? = null
|
||||||
@@ -91,11 +91,10 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
private var mDatabaseKeyFileUri: Uri? = null
|
private var mDatabaseKeyFileUri: Uri? = null
|
||||||
|
|
||||||
private var mSharedPreferences: SharedPreferences? = null
|
|
||||||
|
|
||||||
private var mRememberKeyFile: Boolean = false
|
private var mRememberKeyFile: Boolean = false
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mOpenFileHelper: OpenFileHelper? = null
|
||||||
|
|
||||||
|
private var mPermissionAsked = false
|
||||||
private var readOnly: Boolean = false
|
private var readOnly: Boolean = false
|
||||||
private var mForceReadOnly: Boolean = false
|
private var mForceReadOnly: Boolean = false
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -115,10 +114,6 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
|
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_password)
|
setContentView(R.layout.activity_password)
|
||||||
|
|
||||||
toolbar = findViewById(R.id.toolbar)
|
toolbar = findViewById(R.id.toolbar)
|
||||||
@@ -131,18 +126,23 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||||
filenameView = findViewById(R.id.filename)
|
filenameView = findViewById(R.id.filename)
|
||||||
passwordView = findViewById(R.id.password)
|
passwordView = findViewById(R.id.password)
|
||||||
keyFileView = findViewById(R.id.pass_keyfile)
|
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||||
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
||||||
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
|
|
||||||
|
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||||
|
|
||||||
val browseView = findViewById<View>(R.id.open_database_button)
|
|
||||||
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
||||||
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
|
keyFileSelectionView?.apply {
|
||||||
|
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
||||||
|
setOnClickListener(it)
|
||||||
|
setOnLongClickListener(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
passwordView?.addTextChangedListener(object : TextWatcher {
|
||||||
@@ -155,22 +155,17 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
checkboxPasswordView?.isChecked = true
|
checkboxPasswordView?.isChecked = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
keyFileView?.setOnEditorActionListener(onEditorActionListener)
|
|
||||||
keyFileView?.addTextChangedListener(object : TextWatcher {
|
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable) {
|
|
||||||
if (editable.toString().isNotEmpty() && checkboxKeyFileView?.isChecked != true)
|
|
||||||
checkboxKeyFileView?.isChecked = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
|
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||||
enableOrNotTheConfirmationButton()
|
enableOrNotTheConfirmationButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If is a view intent
|
||||||
|
getUriFromIntent(intent)
|
||||||
|
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||||
|
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
||||||
|
}
|
||||||
|
|
||||||
mProgressDialogThread = ProgressDialogThread(this).apply {
|
mProgressDialogThread = ProgressDialogThread(this).apply {
|
||||||
onActionFinish = { actionTask, result ->
|
onActionFinish = { actionTask, result ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
@@ -183,11 +178,9 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the password in view in all cases
|
|
||||||
removePassword()
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
setEmptyViews()
|
mDatabaseKeyFileUri = null
|
||||||
|
clearCredentialsViews(true)
|
||||||
launchGroupActivity()
|
launchGroupActivity()
|
||||||
} else {
|
} else {
|
||||||
var resultError = ""
|
var resultError = ""
|
||||||
@@ -243,19 +236,59 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getUriFromIntent(intent: Intent?) {
|
||||||
|
// If is a view intent
|
||||||
|
val action = intent?.action
|
||||||
|
if (action != null
|
||||||
|
&& action == VIEW_INTENT) {
|
||||||
|
mDatabaseFileUri = intent.data
|
||||||
|
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
||||||
|
} else {
|
||||||
|
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
||||||
|
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
getUriFromIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchGroupActivity() {
|
private fun launchGroupActivity() {
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
{
|
{
|
||||||
GroupActivity.launch(this@PasswordActivity, readOnly)
|
GroupActivity.launch(this@PasswordActivity,
|
||||||
|
readOnly)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly)
|
GroupActivity.launchForKeyboardSelection(this@PasswordActivity,
|
||||||
|
readOnly)
|
||||||
// Do not keep history
|
// Do not keep history
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{ assistStructure ->
|
{ assistStructure ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly)
|
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||||
|
AutofillHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Response is build
|
||||||
|
AutofillHelper.buildResponse(this, items)
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Here no search info found
|
||||||
|
GroupActivity.launchForAutofillResult(this@PasswordActivity,
|
||||||
|
assistStructure,
|
||||||
|
null,
|
||||||
|
readOnly)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -271,13 +304,16 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|
||||||
if (Database.getInstance().loaded)
|
if (Database.getInstance().loaded)
|
||||||
launchGroupActivity()
|
launchGroupActivity()
|
||||||
|
|
||||||
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
// If the database isn't accessible make sure to clear the password field, if it
|
// If the database isn't accessible make sure to clear the password field, if it
|
||||||
// was saved in the instance state
|
// was saved in the instance state
|
||||||
if (Database.getInstance().loaded) {
|
if (Database.getInstance().loaded) {
|
||||||
setEmptyViews()
|
clearCredentialsViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
// For check shutdown
|
// For check shutdown
|
||||||
@@ -289,46 +325,40 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
||||||
|
mDatabaseKeyFileUri?.let {
|
||||||
|
outState.putString(KEY_KEYFILE, it.toString())
|
||||||
|
}
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initUriFromIntent() {
|
private fun initUriFromIntent() {
|
||||||
|
/*
|
||||||
val databaseUri: Uri?
|
// "canXrite" doesn't work with Google Drive, don't really know why?
|
||||||
val keyFileUri: Uri?
|
mForceReadOnly = mDatabaseFileUri?.let {
|
||||||
|
!FileDatabaseInfo(this, it).canWrite
|
||||||
// If is a view intent
|
} ?: false
|
||||||
val action = intent.action
|
*/
|
||||||
if (action != null
|
mForceReadOnly = mDatabaseFileUri?.let {
|
||||||
&& action == VIEW_INTENT) {
|
!FileDatabaseInfo(this, it).exists
|
||||||
databaseUri = intent.data
|
} ?: true
|
||||||
keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
|
||||||
} else {
|
|
||||||
databaseUri = intent.getParcelableExtra(KEY_FILENAME)
|
|
||||||
keyFileUri = intent.getParcelableExtra(KEY_KEYFILE)
|
|
||||||
}
|
|
||||||
|
|
||||||
mForceReadOnly = UriUtil.isUriNotWritable(contentResolver, databaseUri)
|
|
||||||
|
|
||||||
// Post init uri with KeyFile if needed
|
// Post init uri with KeyFile if needed
|
||||||
if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) {
|
if (mRememberKeyFile && (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
||||||
// Retrieve KeyFile in a thread
|
// Retrieve KeyFile in a thread
|
||||||
databaseUri?.let { databaseUriNotNull ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
FileDatabaseHistoryAction.getInstance(applicationContext)
|
FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||||
.getKeyFileUriByDatabaseUri(databaseUriNotNull) {
|
.getKeyFileUriByDatabaseUri(databaseUri) {
|
||||||
onPostInitUri(databaseUri, it)
|
onPostInitUri(databaseUri, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onPostInitUri(databaseUri, keyFileUri)
|
onPostInitUri(mDatabaseFileUri, mDatabaseKeyFileUri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||||
mDatabaseFileUri = databaseFileUri
|
|
||||||
mDatabaseKeyFileUri = keyFileUri
|
|
||||||
|
|
||||||
// Define title
|
// Define title
|
||||||
databaseFileUri?.let {
|
databaseFileUri?.let {
|
||||||
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
|
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
|
||||||
@@ -337,26 +367,18 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Define Key File text
|
// Define Key File text
|
||||||
val keyUriString = keyFileUri?.toString() ?: ""
|
if (mRememberKeyFile) {
|
||||||
if (keyUriString.isNotEmpty() && mRememberKeyFile) { // Bug KeepassDX #18
|
populateKeyFileTextView(keyFileUri)
|
||||||
populateKeyFileTextView(keyUriString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define listeners for default database checkbox and validate button
|
// Define listeners for default database checkbox and validate button
|
||||||
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
|
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
|
||||||
var newDefaultFileName: Uri? = null
|
var newDefaultFileUri: Uri? = null
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
newDefaultFileName = databaseFileUri ?: newDefaultFileName
|
newDefaultFileUri = databaseFileUri ?: newDefaultFileUri
|
||||||
}
|
}
|
||||||
|
|
||||||
mSharedPreferences?.edit()?.apply {
|
PreferencesUtil.saveDefaultDatabasePath(this, newDefaultFileUri)
|
||||||
newDefaultFileName?.let {
|
|
||||||
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
|
|
||||||
} ?: kotlin.run {
|
|
||||||
remove(KEY_DEFAULT_DATABASE_PATH)
|
|
||||||
}
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
val backupManager = BackupManager(this@PasswordActivity)
|
val backupManager = BackupManager(this@PasswordActivity)
|
||||||
backupManager.dataChanged()
|
backupManager.dataChanged()
|
||||||
@@ -364,7 +386,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
||||||
|
|
||||||
// Retrieve settings for default database
|
// Retrieve settings for default database
|
||||||
val defaultFilename = mSharedPreferences?.getString(KEY_DEFAULT_DATABASE_PATH, "")
|
val defaultFilename = PreferencesUtil.getDefaultDatabasePath(this)
|
||||||
if (databaseFileUri != null
|
if (databaseFileUri != null
|
||||||
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
|
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
|
||||||
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
|
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
|
||||||
@@ -441,10 +463,9 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setEmptyViews() {
|
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||||
populatePasswordTextView(null)
|
populatePasswordTextView(null)
|
||||||
// Bug KeepassDX #18
|
if (clearKeyFile) {
|
||||||
if (!mRememberKeyFile) {
|
|
||||||
populateKeyFileTextView(null)
|
populateKeyFileTextView(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,13 +482,13 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateKeyFileTextView(text: String?) {
|
private fun populateKeyFileTextView(uri: Uri?) {
|
||||||
if (text == null || text.isEmpty()) {
|
if (uri == null || uri.toString().isEmpty()) {
|
||||||
keyFileView?.setText("")
|
keyFileSelectionView?.uri = null
|
||||||
if (checkboxKeyFileView?.isChecked == true)
|
if (checkboxKeyFileView?.isChecked == true)
|
||||||
checkboxKeyFileView?.isChecked = false
|
checkboxKeyFileView?.isChecked = false
|
||||||
} else {
|
} else {
|
||||||
keyFileView?.setText(text)
|
keyFileSelectionView?.uri = uri
|
||||||
if (checkboxKeyFileView?.isChecked != true)
|
if (checkboxKeyFileView?.isChecked != true)
|
||||||
checkboxKeyFileView?.isChecked = true
|
checkboxKeyFileView?.isChecked = true
|
||||||
}
|
}
|
||||||
@@ -488,7 +509,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
|
|
||||||
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||||
val password: String? = passwordView?.text?.toString()
|
val password: String? = passwordView?.text?.toString()
|
||||||
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
|
val keyFile: Uri? = keyFileSelectionView?.uri
|
||||||
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,7 +522,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
|
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
|
||||||
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
|
val keyFile: Uri? = keyFileSelectionView?.uri
|
||||||
verifyKeyFileCheckbox(keyFile)
|
verifyKeyFileCheckbox(keyFile)
|
||||||
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
|
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
|
||||||
}
|
}
|
||||||
@@ -510,18 +531,13 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removePassword() {
|
|
||||||
passwordView?.setText("")
|
|
||||||
checkboxPasswordView?.isChecked = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadDatabase(databaseFileUri: Uri?,
|
private fun loadDatabase(databaseFileUri: Uri?,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyFileUri: Uri?,
|
keyFileUri: Uri?,
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||||
|
|
||||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||||
removePassword()
|
clearCredentialsViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseFileUri?.let { databaseUri ->
|
databaseFileUri?.let { databaseUri ->
|
||||||
@@ -578,11 +594,42 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
|
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
launchEducation(menu)
|
launchEducation(menu) {
|
||||||
|
launchCheckPermission()
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
private fun launchCheckPermission() {
|
||||||
|
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
val permissions = arrayOf(writePermission)
|
||||||
|
if (Build.VERSION.SDK_INT >= 23
|
||||||
|
&& !readOnly
|
||||||
|
&& !mPermissionAsked) {
|
||||||
|
mPermissionAsked = true
|
||||||
|
// Check self permission to show or not the dialog
|
||||||
|
if (toolbar != null
|
||||||
|
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
|
||||||
|
when (requestCode) {
|
||||||
|
WRITE_EXTERNAL_STORAGE_REQUEST -> {
|
||||||
|
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
||||||
|
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// To fix multiple view education
|
// To fix multiple view education
|
||||||
private var performedEductionInProgress = false
|
private var performedEductionInProgress = false
|
||||||
private fun launchEducation(menu: Menu, onEducationFinished: (()-> Unit)? = null) {
|
private fun launchEducation(menu: Menu, onEducationFinished: (()-> Unit)? = null) {
|
||||||
@@ -684,7 +731,8 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||||
) { uri ->
|
) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
populateKeyFileTextView(uri.toString())
|
mDatabaseKeyFileUri = uri
|
||||||
|
populateKeyFileTextView(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -692,7 +740,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
// this block if not a key file response
|
// this block if not a key file response
|
||||||
when (resultCode) {
|
when (resultCode) {
|
||||||
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
|
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
|
||||||
setEmptyViews()
|
clearCredentialsViews()
|
||||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -703,14 +751,14 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
|
|
||||||
private val TAG = PasswordActivity::class.java.name
|
private val TAG = PasswordActivity::class.java.name
|
||||||
|
|
||||||
const val KEY_DEFAULT_DATABASE_PATH = "KEY_DEFAULT_DATABASE_PATH"
|
|
||||||
|
|
||||||
private const val KEY_FILENAME = "fileName"
|
private const val KEY_FILENAME = "fileName"
|
||||||
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_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 WRITE_EXTERNAL_STORAGE_REQUEST = 647
|
||||||
|
|
||||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
@@ -765,13 +813,15 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
activity: Activity,
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
assistStructure: AssistStructure?) {
|
assistStructure: AssistStructure?,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
if (assistStructure != null) {
|
if (assistStructure != null) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
assistStructure)
|
assistStructure,
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
launch(activity, databaseFile, keyFile)
|
launch(activity, databaseFile, keyFile)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import android.widget.CompoundButton
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
|
||||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -51,9 +51,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||||
private var passwordRepeatView: TextView? = null
|
private var passwordRepeatView: TextView? = null
|
||||||
|
|
||||||
private var keyFileTextInputLayout: TextInputLayout? = null
|
|
||||||
private var keyFileCheckBox: CompoundButton? = null
|
private var keyFileCheckBox: CompoundButton? = null
|
||||||
private var keyFileView: TextView? = null
|
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||||
|
|
||||||
private var mListener: AssignPasswordDialogListener? = null
|
private var mListener: AssignPasswordDialogListener? = null
|
||||||
|
|
||||||
@@ -69,16 +68,6 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val keyFileTextWatcher = object : TextWatcher {
|
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable) {
|
|
||||||
keyFileCheckBox?.isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AssignPasswordDialogListener {
|
interface AssignPasswordDialogListener {
|
||||||
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
||||||
keyFileChecked: Boolean, keyFile: Uri?)
|
keyFileChecked: Boolean, keyFile: Uri?)
|
||||||
@@ -121,13 +110,14 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||||
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
||||||
|
|
||||||
keyFileTextInputLayout = rootView?.findViewById(R.id.keyfile_input_layout)
|
|
||||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||||
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
|
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
mOpenFileHelper = OpenFileHelper(this)
|
mOpenFileHelper = OpenFileHelper(this)
|
||||||
rootView?.findViewById<View>(R.id.open_database_button)?.setOnClickListener { view ->
|
keyFileSelectionView?.apply {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
|
setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
||||||
|
setOnLongClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
||||||
|
}
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
|
|
||||||
@@ -176,14 +166,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
// To check checkboxes if a text is present
|
// To check checkboxes if a text is present
|
||||||
passwordView?.addTextChangedListener(passwordTextWatcher)
|
passwordView?.addTextChangedListener(passwordTextWatcher)
|
||||||
keyFileView?.addTextChangedListener(keyFileTextWatcher)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
passwordView?.removeTextChangedListener(passwordTextWatcher)
|
passwordView?.removeTextChangedListener(passwordTextWatcher)
|
||||||
keyFileView?.removeTextChangedListener(keyFileTextWatcher)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyPassword(): Boolean {
|
private fun verifyPassword(): Boolean {
|
||||||
@@ -216,11 +204,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
if (keyFileCheckBox != null
|
if (keyFileCheckBox != null
|
||||||
&& keyFileCheckBox!!.isChecked) {
|
&& keyFileCheckBox!!.isChecked) {
|
||||||
|
|
||||||
UriUtil.parse(keyFileView?.text?.toString())?.let { uri ->
|
keyFileSelectionView?.uri?.let { uri ->
|
||||||
mKeyFile = uri
|
mKeyFile = uri
|
||||||
} ?: run {
|
} ?: run {
|
||||||
error = true
|
error = true
|
||||||
keyFileTextInputLayout?.error = getString(R.string.error_nokeyfile)
|
keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return error
|
return error
|
||||||
@@ -265,8 +253,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
) { uri ->
|
) { uri ->
|
||||||
uri?.let { pathUri ->
|
uri?.let { pathUri ->
|
||||||
keyFileCheckBox?.isChecked = true
|
keyFileCheckBox?.isChecked = true
|
||||||
keyFileView?.text = pathUri.toString()
|
keyFileSelectionView?.uri = pathUri
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
|
||||||
|
class DatePickerFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mDefaultYear: Int = 2000
|
||||||
|
private var mDefaultMonth: Int = 1
|
||||||
|
private var mDefaultDay: Int = 1
|
||||||
|
|
||||||
|
private var mListener: DatePickerDialog.OnDateSetListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
mListener = context as DatePickerDialog.OnDateSetListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
// Create a new instance of DatePickerDialog and return it
|
||||||
|
return context?.let {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(DEFAULT_YEAR_BUNDLE_KEY))
|
||||||
|
mDefaultYear = getInt(DEFAULT_YEAR_BUNDLE_KEY)
|
||||||
|
if (containsKey(DEFAULT_MONTH_BUNDLE_KEY))
|
||||||
|
mDefaultMonth = getInt(DEFAULT_MONTH_BUNDLE_KEY)
|
||||||
|
if (containsKey(DEFAULT_DAY_BUNDLE_KEY))
|
||||||
|
mDefaultDay = getInt(DEFAULT_DAY_BUNDLE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
DatePickerDialog(it, mListener, mDefaultYear, mDefaultMonth, mDefaultDay)
|
||||||
|
} ?: super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val DEFAULT_YEAR_BUNDLE_KEY = "DEFAULT_YEAR_BUNDLE_KEY"
|
||||||
|
private const val DEFAULT_MONTH_BUNDLE_KEY = "DEFAULT_MONTH_BUNDLE_KEY"
|
||||||
|
private const val DEFAULT_DAY_BUNDLE_KEY = "DEFAULT_DAY_BUNDLE_KEY"
|
||||||
|
|
||||||
|
fun getInstance(defaultYear: Int,
|
||||||
|
defaultMonth: Int,
|
||||||
|
defaultDay: Int): DatePickerFragment {
|
||||||
|
return DatePickerFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putInt(DEFAULT_YEAR_BUNDLE_KEY, defaultYear)
|
||||||
|
putInt(DEFAULT_MONTH_BUNDLE_KEY, defaultMonth)
|
||||||
|
putInt(DEFAULT_DAY_BUNDLE_KEY, defaultDay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -112,7 +112,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
|
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
|
||||||
ta?.recycle()
|
ta.recycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ class SortDialogFragment : DialogFragment() {
|
|||||||
builder.setView(rootView)
|
builder.setView(rootView)
|
||||||
// Add action buttons
|
// Add action buttons
|
||||||
.setPositiveButton(android.R.string.ok
|
.setPositiveButton(android.R.string.ok
|
||||||
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
|
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum,
|
||||||
|
SortNodeEnum.SortNodeParameters(mAscending, mGroupsBefore, mRecycleBinBottom))
|
||||||
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
|
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
|
||||||
@@ -150,10 +152,7 @@ class SortDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SortSelectionListener {
|
interface SortSelectionListener {
|
||||||
fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters)
|
||||||
ascending: Boolean,
|
|
||||||
groupsBefore: Boolean,
|
|
||||||
recycleBinBottom: Boolean)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.app.TimePickerDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.format.DateFormat
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
|
||||||
|
class TimePickerFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var defaultHour: Int = 0
|
||||||
|
private var defaultMinute: Int = 0
|
||||||
|
|
||||||
|
private var mListener: TimePickerDialog.OnTimeSetListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
mListener = context as TimePickerDialog.OnTimeSetListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
// Create a new instance of DatePickerDialog and return it
|
||||||
|
return context?.let {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(DEFAULT_HOUR_BUNDLE_KEY))
|
||||||
|
defaultHour = getInt(DEFAULT_HOUR_BUNDLE_KEY)
|
||||||
|
if (containsKey(DEFAULT_MINUTE_BUNDLE_KEY))
|
||||||
|
defaultMinute = getInt(DEFAULT_MINUTE_BUNDLE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
TimePickerDialog(it, mListener, defaultHour, defaultMinute, DateFormat.is24HourFormat(activity))
|
||||||
|
} ?: super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val DEFAULT_HOUR_BUNDLE_KEY = "DEFAULT_HOUR_BUNDLE_KEY"
|
||||||
|
private const val DEFAULT_MINUTE_BUNDLE_KEY = "DEFAULT_MINUTE_BUNDLE_KEY"
|
||||||
|
|
||||||
|
fun getInstance(defaultHour: Int,
|
||||||
|
defaultMinute: Int): TimePickerFragment {
|
||||||
|
return TimePickerFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putInt(DEFAULT_HOUR_BUNDLE_KEY, defaultHour)
|
||||||
|
putInt(DEFAULT_MINUTE_BUNDLE_KEY, defaultMinute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,14 +52,22 @@ class OpenFileHelper {
|
|||||||
this.fragment = context
|
this.fragment = context
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class OpenFileOnClickViewListener : View.OnClickListener {
|
inner class OpenFileOnClickViewListener : View.OnClickListener, View.OnLongClickListener {
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
private fun onAbstractClick(longClick: Boolean = false) {
|
||||||
try {
|
try {
|
||||||
try {
|
if (longClick) {
|
||||||
openActivityWithActionOpenDocument()
|
try {
|
||||||
} catch(e: Exception) {
|
openActivityWithActionGetContent()
|
||||||
openActivityWithActionGetContent()
|
} catch (e: Exception) {
|
||||||
|
openActivityWithActionOpenDocument()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
openActivityWithActionOpenDocument()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
openActivityWithActionGetContent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Enable to start the file picker activity", e)
|
Log.e(TAG, "Enable to start the file picker activity", e)
|
||||||
@@ -68,6 +76,15 @@ class OpenFileHelper {
|
|||||||
showBrowserDialog()
|
showBrowserDialog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
onAbstractClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLongClick(v: View?): Boolean {
|
||||||
|
onAbstractClick(true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
|
|||||||
@@ -20,11 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.activities.lock
|
package com.kunzisoft.keepass.activities.lock
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -34,12 +30,9 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
|||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.*
|
||||||
|
|
||||||
abstract class LockingActivity : StylishActivity() {
|
abstract class LockingActivity : StylishActivity() {
|
||||||
|
|
||||||
@@ -81,12 +74,10 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mTimeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
mLockReceiver = LockReceiver()
|
mLockReceiver = LockReceiver {
|
||||||
val intentFilter = IntentFilter().apply {
|
lockAndExit()
|
||||||
addAction(Intent.ACTION_SCREEN_OFF)
|
|
||||||
addAction(LOCK_ACTION)
|
|
||||||
}
|
}
|
||||||
registerReceiver(mLockReceiver, intentFilter)
|
registerLockReceiver(mLockReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
mExitLock = false
|
mExitLock = false
|
||||||
@@ -151,29 +142,12 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
unregisterLockReceiver(mLockReceiver)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
if (mLockReceiver != null)
|
|
||||||
unregisterReceiver(mLockReceiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class LockReceiver : BroadcastReceiver() {
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
// If allowed, lock and exit
|
|
||||||
if (!TimeoutHelper.temporarilyDisableTimeout) {
|
|
||||||
intent.action?.let {
|
|
||||||
when (it) {
|
|
||||||
Intent.ACTION_SCREEN_OFF -> if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this@LockingActivity)) {
|
|
||||||
lockAndExit()
|
|
||||||
}
|
|
||||||
LOCK_ACTION -> lockAndExit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun lockAndExit() {
|
protected fun lockAndExit() {
|
||||||
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
lock()
|
lock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,20 +182,8 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun Activity.lock() {
|
fun Activity.lock() {
|
||||||
// Stop the Magikeyboard service
|
closeDatabase()
|
||||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
|
||||||
MagikIME.removeEntry(this)
|
|
||||||
|
|
||||||
// Stop the notification service
|
|
||||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
|
||||||
|
|
||||||
Log.i(Activity::class.java.name, "Shutdown " + localClassName +
|
|
||||||
" after inactivity or manual lock")
|
|
||||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
|
|
||||||
cancelAll()
|
|
||||||
}
|
|
||||||
// Clear data
|
|
||||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
|
||||||
// Add onActivityForResult response
|
// Add onActivityForResult response
|
||||||
setResult(LockingActivity.RESULT_EXIT_LOCK)
|
setResult(LockingActivity.RESULT_EXIT_LOCK)
|
||||||
finish()
|
finish()
|
||||||
|
|||||||
@@ -84,24 +84,25 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
// File path
|
// File path
|
||||||
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
|
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
|
||||||
|
|
||||||
if (fileDatabaseInfo.dataAccessible()) {
|
if (fileDatabaseInfo.exists) {
|
||||||
holder.fileInformation.clearColorFilter()
|
holder.fileInformation.clearColorFilter()
|
||||||
} else {
|
} else {
|
||||||
holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
|
holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modification
|
// Modification
|
||||||
if (fileDatabaseInfo.lastModificationAccessible()) {
|
fileDatabaseInfo.getModificationString()?.let {
|
||||||
holder.fileModification.text = fileDatabaseInfo.getModificationString()
|
holder.fileModification.text = it
|
||||||
holder.fileModification.visibility = View.VISIBLE
|
holder.fileModification.visibility = View.VISIBLE
|
||||||
} else {
|
} ?: run {
|
||||||
holder.fileModification.visibility = View.GONE
|
holder.fileModification.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size
|
// Size
|
||||||
if (fileDatabaseInfo.sizeAccessible()) {
|
fileDatabaseInfo.getSizeString()?.let {
|
||||||
holder.fileSize.text = fileDatabaseInfo.getSizeString()
|
holder.fileSize.text = it
|
||||||
holder.fileSize.visibility = View.VISIBLE
|
holder.fileSize.visibility = View.VISIBLE
|
||||||
} else {
|
} ?: run {
|
||||||
holder.fileSize.visibility = View.GONE
|
holder.fileSize.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,14 @@ import android.view.ViewGroup
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
|
||||||
|
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
|
||||||
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.Entry
|
||||||
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.strikeOut
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
@@ -38,8 +44,8 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
private val database: Database)
|
private val database: Database)
|
||||||
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||||
|
|
||||||
private val cursorInflater: LayoutInflater = context.getSystemService(
|
private val cursorInflater: LayoutInflater? = context.getSystemService(
|
||||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
private var displayUsername: Boolean = false
|
private var displayUsername: Boolean = false
|
||||||
private val iconColor: Int
|
private val iconColor: Int
|
||||||
|
|
||||||
@@ -58,7 +64,7 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
|
|
||||||
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
|
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
|
||||||
|
|
||||||
val view = cursorInflater.inflate(R.layout.item_search_entry, parent, false)
|
val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false)
|
||||||
val viewHolder = ViewHolder()
|
val viewHolder = ViewHolder()
|
||||||
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
|
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
|
||||||
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
|
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
|
||||||
@@ -69,8 +75,7 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun bindView(view: View, context: Context, cursor: Cursor) {
|
override fun bindView(view: View, context: Context, cursor: Cursor) {
|
||||||
|
getEntryFrom(cursor)?.let { currentEntry ->
|
||||||
database.getEntryFrom(cursor)?.let { currentEntry ->
|
|
||||||
val viewHolder = view.tag as ViewHolder
|
val viewHolder = view.tag as ViewHolder
|
||||||
|
|
||||||
// Assign image
|
// Assign image
|
||||||
@@ -98,14 +103,46 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ViewHolder {
|
private fun getEntryFrom(cursor: Cursor): Entry? {
|
||||||
internal var imageViewIcon: ImageView? = null
|
return database.createEntry()?.apply {
|
||||||
internal var textViewTitle: TextView? = null
|
database.startManageEntry(this)
|
||||||
internal var textViewSubTitle: TextView? = null
|
entryKDB?.let { entryKDB ->
|
||||||
|
(cursor as EntryCursorKDB).populateEntry(entryKDB, database.iconFactory)
|
||||||
|
}
|
||||||
|
entryKDBX?.let { entryKDBX ->
|
||||||
|
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, database.iconFactory)
|
||||||
|
}
|
||||||
|
database.stopManageEntry(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
|
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
|
||||||
return database.searchEntries(context, constraint.toString())
|
return searchEntries(context, constraint.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchEntries(context: Context, query: String): Cursor? {
|
||||||
|
var cursorKDB: EntryCursorKDB? = null
|
||||||
|
var cursorKDBX: EntryCursorKDBX? = null
|
||||||
|
|
||||||
|
if (database.type == DatabaseKDB.TYPE)
|
||||||
|
cursorKDB = EntryCursorKDB()
|
||||||
|
if (database.type == DatabaseKDBX.TYPE)
|
||||||
|
cursorKDBX = EntryCursorKDBX()
|
||||||
|
|
||||||
|
val searchGroup = database.createVirtualGroupFromSearch(query, SearchHelper.MAX_SEARCH_ENTRY)
|
||||||
|
if (searchGroup != null) {
|
||||||
|
// Search in hide entries but not meta-stream
|
||||||
|
for (entry in searchGroup.getFilteredChildEntries(*Group.ChildFilter.getDefaults(context))) {
|
||||||
|
entry.entryKDB?.let {
|
||||||
|
cursorKDB?.addEntry(it)
|
||||||
|
}
|
||||||
|
entry.entryKDBX?.let {
|
||||||
|
cursorKDBX?.addEntry(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursorKDB ?: cursorKDBX
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEntryFromPosition(position: Int): Entry? {
|
fun getEntryFromPosition(position: Int): Entry? {
|
||||||
@@ -113,9 +150,14 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
|
|
||||||
val cursor = this.cursor
|
val cursor = this.cursor
|
||||||
if (cursor.moveToFirst() && cursor.move(position)) {
|
if (cursor.moveToFirst() && cursor.move(position)) {
|
||||||
pwEntry = database.getEntryFrom(cursor)
|
pwEntry = getEntryFrom(cursor)
|
||||||
}
|
}
|
||||||
return pwEntry
|
return pwEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ViewHolder {
|
||||||
|
internal var imageViewIcon: ImageView? = null
|
||||||
|
internal var textViewTitle: TextView? = null
|
||||||
|
internal var textViewSubTitle: TextView? = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,14 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
||||||
|
ActionDatabaseAsyncTask(
|
||||||
|
{
|
||||||
|
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
fun deleteAllKeyFiles() {
|
fun deleteAllKeyFiles() {
|
||||||
ActionDatabaseAsyncTask(
|
ActionDatabaseAsyncTask(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ interface FileDatabaseHistoryDao {
|
|||||||
@Delete
|
@Delete
|
||||||
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
|
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
|
||||||
|
|
||||||
|
@Query("UPDATE file_database_history SET keyfile_uri=null WHERE database_uri = :databaseUriString")
|
||||||
|
fun deleteKeyFileByDatabaseUri(databaseUriString: String)
|
||||||
|
|
||||||
@Query("UPDATE file_database_history SET keyfile_uri=null")
|
@Query("UPDATE file_database_history SET keyfile_uri=null")
|
||||||
fun deleteAllKeyFiles()
|
fun deleteAllKeyFiles()
|
||||||
|
|
||||||
|
|||||||
@@ -26,15 +26,22 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.autofill.Dataset
|
import android.service.autofill.Dataset
|
||||||
import android.service.autofill.FillResponse
|
import android.service.autofill.FillResponse
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.autofill.AutofillManager
|
import android.view.autofill.AutofillManager
|
||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
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.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import java.util.*
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@@ -43,6 +50,7 @@ object AutofillHelper {
|
|||||||
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
||||||
|
|
||||||
private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
||||||
|
const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||||
|
|
||||||
fun retrieveAssistStructure(intent: Intent?): AssistStructure? {
|
fun retrieveAssistStructure(intent: Intent?): AssistStructure? {
|
||||||
intent?.let {
|
intent?.let {
|
||||||
@@ -56,27 +64,44 @@ object AutofillHelper {
|
|||||||
return String.format("%s (%s)", entryInfo.title, entryInfo.username)
|
return String.format("%s (%s)", entryInfo.title, entryInfo.username)
|
||||||
if (entryInfo.title.isNotEmpty())
|
if (entryInfo.title.isNotEmpty())
|
||||||
return entryInfo.title
|
return entryInfo.title
|
||||||
if (entryInfo.username.isNotEmpty())
|
|
||||||
return entryInfo.username
|
|
||||||
if (entryInfo.url.isNotEmpty())
|
if (entryInfo.url.isNotEmpty())
|
||||||
return entryInfo.url
|
return entryInfo.url
|
||||||
|
if (entryInfo.username.isNotEmpty())
|
||||||
|
return entryInfo.username
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDataset(context: Context,
|
internal fun addHeader(responseBuilder: FillResponse.Builder,
|
||||||
entryInfo: EntryInfo,
|
packageName: String,
|
||||||
struct: StructureParser.Result): Dataset? {
|
webDomain: String?,
|
||||||
|
applicationId: String?) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
if (webDomain != null) {
|
||||||
|
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply {
|
||||||
|
setTextViewText(R.id.autofill_web_domain_text, webDomain)
|
||||||
|
})
|
||||||
|
} else if (applicationId != null) {
|
||||||
|
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply {
|
||||||
|
setTextViewText(R.id.autofill_app_id_text, applicationId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun buildDataset(context: Context,
|
||||||
|
entryInfo: EntryInfo,
|
||||||
|
struct: StructureParser.Result): Dataset? {
|
||||||
val title = makeEntryTitle(entryInfo)
|
val title = makeEntryTitle(entryInfo)
|
||||||
val views = newRemoteViews(context.packageName, title)
|
val views = newRemoteViews(context, title, entryInfo.icon)
|
||||||
val builder = Dataset.Builder(views)
|
val builder = Dataset.Builder(views)
|
||||||
builder.setId(entryInfo.id)
|
builder.setId(entryInfo.id)
|
||||||
|
|
||||||
struct.password.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.password)) }
|
struct.usernameId?.let { usernameId ->
|
||||||
|
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
|
||||||
val ids = ArrayList(struct.username)
|
}
|
||||||
if (entryInfo.username.contains("@") || struct.username.isEmpty())
|
struct.passwordId?.let { password ->
|
||||||
ids.addAll(struct.email)
|
builder.setValue(password, AutofillValue.forText(entryInfo.password))
|
||||||
ids.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.username)) }
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
builder.build()
|
builder.build()
|
||||||
@@ -87,9 +112,16 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to hit when right key is selected
|
* Build the Autofill response for one entry
|
||||||
*/
|
*/
|
||||||
fun buildResponseWhenEntrySelected(activity: Activity, entryInfo: EntryInfo) {
|
fun buildResponse(activity: Activity, entryInfo: EntryInfo) {
|
||||||
|
buildResponse(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the Autofill response for many entry
|
||||||
|
*/
|
||||||
|
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) {
|
||||||
var setResultOk = false
|
var setResultOk = false
|
||||||
activity.intent?.extras?.let { extras ->
|
activity.intent?.extras?.let { extras ->
|
||||||
if (extras.containsKey(ASSIST_STRUCTURE)) {
|
if (extras.containsKey(ASSIST_STRUCTURE)) {
|
||||||
@@ -97,8 +129,9 @@ object AutofillHelper {
|
|||||||
StructureParser(structure).parse()?.let { result ->
|
StructureParser(structure).parse()?.let { result ->
|
||||||
// New Response
|
// New Response
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
val dataset = buildDataset(activity, entryInfo, result)
|
entriesInfo.forEach {
|
||||||
responseBuilder.addDataset(dataset)
|
responseBuilder.addDataset(buildDataset(activity, it, result))
|
||||||
|
}
|
||||||
val mReplyIntent = Intent()
|
val mReplyIntent = Intent()
|
||||||
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
||||||
mReplyIntent.putExtra(
|
mReplyIntent.putExtra(
|
||||||
@@ -116,12 +149,48 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to perform actions if item is found or not after an auto search in [database]
|
||||||
|
*/
|
||||||
|
fun checkAutoSearchInfo(context: Context,
|
||||||
|
database: Database,
|
||||||
|
searchInfo: SearchInfo?,
|
||||||
|
onItemsFound: (items: List<EntryInfo>) -> Unit,
|
||||||
|
onItemNotFound: () -> Unit,
|
||||||
|
onDatabaseClosed: () -> Unit) {
|
||||||
|
if (database.loaded && TimeoutHelper.checkTime(context)) {
|
||||||
|
var searchWithoutUI = false
|
||||||
|
if (PreferencesUtil.isAutofillAutoSearchEnable(context)
|
||||||
|
&& searchInfo != null) {
|
||||||
|
// If search provide results
|
||||||
|
database.createVirtualGroupFromSearch(searchInfo, SearchHelper.MAX_SEARCH_ENTRY)?.let { searchGroup ->
|
||||||
|
if (searchGroup.getNumberOfChildEntries() > 0) {
|
||||||
|
searchWithoutUI = true
|
||||||
|
onItemsFound.invoke(
|
||||||
|
searchGroup.getChildEntriesInfo(database))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!searchWithoutUI) {
|
||||||
|
onItemNotFound.invoke()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onDatabaseClosed.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method to start an activity with an Autofill for result
|
* Utility method to start an activity with an Autofill for result
|
||||||
*/
|
*/
|
||||||
fun startActivityForAutofillResult(activity: Activity, intent: Intent, assistStructure: AssistStructure) {
|
fun startActivityForAutofillResult(activity: Activity,
|
||||||
|
intent: Intent,
|
||||||
|
assistStructure: AssistStructure,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
|
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
|
||||||
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
|
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
|
||||||
|
searchInfo?.let {
|
||||||
|
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||||
|
}
|
||||||
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,9 +209,18 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newRemoteViews(packageName: String, remoteViewsText: String): RemoteViews {
|
private fun newRemoteViews(context: Context,
|
||||||
val presentation = RemoteViews(packageName, R.layout.item_autofill_service)
|
remoteViewsText: String,
|
||||||
presentation.setTextViewText(R.id.text, remoteViewsText)
|
remoteViewsIcon: IconImage? = null): RemoteViews {
|
||||||
|
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
||||||
|
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
||||||
|
if (remoteViewsIcon != null) {
|
||||||
|
presentation.assignDatabaseIcon(context,
|
||||||
|
R.id.autofill_entry_icon,
|
||||||
|
Database.getInstance().drawFactory,
|
||||||
|
remoteViewsIcon,
|
||||||
|
ContextCompat.getColor(context, R.color.green))
|
||||||
|
}
|
||||||
return presentation
|
return presentation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : AppCompatActivity() {
|
class AutofillLauncherActivity : AppCompatActivity() {
|
||||||
@@ -41,11 +40,31 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||||
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
|
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
|
||||||
if (assistStructure != null) {
|
if (assistStructure != null) {
|
||||||
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
|
// Build search param
|
||||||
GroupActivity.launchForAutofillResult(this, assistStructure, PreferencesUtil.enableReadOnlyDatabase(this))
|
val searchInfo = SearchInfo().apply {
|
||||||
else {
|
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(this, assistStructure)
|
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||||
}
|
}
|
||||||
|
// If database is open
|
||||||
|
AutofillHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Items found
|
||||||
|
AutofillHelper.buildResponse(this, items)
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
GroupActivity.launchForAutofillResult(this,
|
||||||
|
assistStructure)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If database not open
|
||||||
|
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||||
|
assistStructure, searchInfo)
|
||||||
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
@@ -61,10 +80,20 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun getAuthIntentSenderForResponse(context: Context): IntentSender {
|
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
||||||
val intent = Intent(context, AutofillLauncherActivity::class.java)
|
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
||||||
|
|
||||||
|
fun getAuthIntentSenderForResponse(context: Context,
|
||||||
|
searchInfo: SearchInfo? = null): IntentSender {
|
||||||
return PendingIntent.getActivity(context, 0,
|
return PendingIntent.getActivity(context, 0,
|
||||||
intent, PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
// Doesn't work with Parcelable (don't know why?)
|
||||||
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
|
searchInfo?.let {
|
||||||
|
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
||||||
|
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,31 +22,78 @@ package com.kunzisoft.keepass.autofill
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
import android.service.autofill.*
|
import android.service.autofill.*
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class KeeAutofillService : AutofillService() {
|
class KeeAutofillService : AutofillService() {
|
||||||
|
|
||||||
override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal,
|
override fun onFillRequest(request: FillRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
val fillContexts = request.fillContexts
|
val fillContexts = request.fillContexts
|
||||||
val latestStructure = fillContexts[fillContexts.size - 1].structure
|
val latestStructure = fillContexts[fillContexts.size - 1].structure
|
||||||
|
|
||||||
cancellationSignal.setOnCancelListener { Log.e(TAG, "Cancel autofill not implemented in this sample.") }
|
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||||
|
|
||||||
val responseBuilder = FillResponse.Builder()
|
|
||||||
// Check user's settings for authenticating Responses and Datasets.
|
// Check user's settings for authenticating Responses and Datasets.
|
||||||
val parseResult = StructureParser(latestStructure).parse()
|
StructureParser(latestStructure).parse()?.let { parseResult ->
|
||||||
parseResult?.allAutofillIds()?.let { autofillIds ->
|
|
||||||
if (listOf(*autofillIds).isNotEmpty()) {
|
val searchInfo = SearchInfo().apply {
|
||||||
|
applicationId = parseResult.applicationId
|
||||||
|
webDomain = parseResult.domain
|
||||||
|
}
|
||||||
|
|
||||||
|
AutofillHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
val responseBuilder = FillResponse.Builder()
|
||||||
|
AutofillHelper.addHeader(responseBuilder, packageName,
|
||||||
|
parseResult.domain, parseResult.applicationId)
|
||||||
|
items.forEach {
|
||||||
|
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
|
||||||
|
}
|
||||||
|
callback.onSuccess(responseBuilder.build())
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show UI if no search result
|
||||||
|
showUIForEntrySelection(parseResult, searchInfo, callback)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show UI if database not open
|
||||||
|
showUIForEntrySelection(parseResult, searchInfo, callback)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||||
|
searchInfo: SearchInfo,
|
||||||
|
callback: FillCallback) {
|
||||||
|
parseResult.allAutofillIds().let { autofillIds ->
|
||||||
|
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 sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this)
|
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this,
|
||||||
val presentation = RemoteViews(packageName, R.layout.item_autofill_service_unlock)
|
searchInfo)
|
||||||
responseBuilder.setAuthentication(autofillIds, sender, presentation)
|
val responseBuilder = FillResponse.Builder()
|
||||||
|
val remoteViewsUnlock: RemoteViews = if (!parseResult.domain.isNullOrEmpty()) {
|
||||||
|
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
|
||||||
|
setTextViewText(R.id.autofill_web_domain_text, parseResult.domain)
|
||||||
|
}
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
responseBuilder.setAuthentication(autofillIds, sender, remoteViewsUnlock)
|
||||||
callback.onSuccess(responseBuilder.build())
|
callback.onSuccess(responseBuilder.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ package com.kunzisoft.keepass.autofill
|
|||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
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
|
||||||
@@ -37,72 +37,196 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
private var usernameCandidate: AutofillId? = null
|
private var usernameCandidate: AutofillId? = null
|
||||||
|
|
||||||
fun parse(): Result? {
|
fun parse(): Result? {
|
||||||
result = Result()
|
try {
|
||||||
result?.apply {
|
result = Result()
|
||||||
usernameCandidate = null
|
result?.apply {
|
||||||
for (i in 0 until structure.windowNodeCount) {
|
usernameCandidate = null
|
||||||
val windowNode = structure.getWindowNodeAt(i)
|
mainLoop@ for (i in 0 until structure.windowNodeCount) {
|
||||||
title.add(windowNode.title)
|
val windowNode = structure.getWindowNodeAt(i)
|
||||||
windowNode.rootViewNode.webDomain?.let {
|
applicationId = windowNode.title.toString().split("/")[0]
|
||||||
webDomain.add(it)
|
Log.d(TAG, "Autofill applicationId: $applicationId")
|
||||||
}
|
|
||||||
parseViewNode(windowNode.rootViewNode)
|
|
||||||
}
|
|
||||||
// If not explicit username field found, add the field just before password field.
|
|
||||||
if (username.isEmpty() && email.isEmpty()
|
|
||||||
&& password.isNotEmpty() && usernameCandidate != null)
|
|
||||||
username.add(usernameCandidate!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
if (parseViewNode(windowNode.rootViewNode))
|
||||||
|
break@mainLoop
|
||||||
|
}
|
||||||
|
// If not explicit username field found, add the field just before password field.
|
||||||
|
if (usernameId == null && passwordId != null && usernameCandidate != null)
|
||||||
|
usernameId = usernameCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the result only if password field is retrieved
|
||||||
|
return if (result?.usernameId != null
|
||||||
|
&& result?.passwordId != null)
|
||||||
|
result
|
||||||
|
else
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseViewNode(node: AssistStructure.ViewNode) {
|
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
||||||
val hints = node.autofillHints
|
// Get the domain of a web app
|
||||||
|
node.webDomain?.let {
|
||||||
|
result?.domain = it
|
||||||
|
Log.d(TAG, "Autofill domain: $it")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only parse visible nodes
|
||||||
|
if (node.visibility == View.VISIBLE) {
|
||||||
|
if (node.autofillId != null
|
||||||
|
&& node.autofillType == View.AUTOFILL_TYPE_TEXT) {
|
||||||
|
// Parse methods
|
||||||
|
val hints = node.autofillHints
|
||||||
|
if (hints != null && hints.isNotEmpty()) {
|
||||||
|
if (parseNodeByAutofillHint(node))
|
||||||
|
return true
|
||||||
|
} else if (parseNodeByHtmlAttributes(node))
|
||||||
|
return true
|
||||||
|
else if (parseNodeByAndroidInput(node))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Recursive method to process each node
|
||||||
|
for (i in 0 until node.childCount) {
|
||||||
|
if (parseViewNode(node.getChildAt(i)))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
|
||||||
val autofillId = node.autofillId
|
val autofillId = node.autofillId
|
||||||
if (autofillId != null) {
|
node.autofillHints?.forEach {
|
||||||
if (hints != null && hints.isNotEmpty()) {
|
when {
|
||||||
when {
|
it.equals(View.AUTOFILL_HINT_USERNAME, true)
|
||||||
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_USERNAME == it } -> result?.username?.add(autofillId)
|
|| it.equals(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||||
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_EMAIL_ADDRESS == it } -> result?.email?.add(autofillId)
|
|| it.equals(View.AUTOFILL_HINT_PHONE, true)
|
||||||
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_PASSWORD == it } -> result?.password?.add(autofillId)
|
|| it.equals("usernameOrEmail", true)-> {
|
||||||
else -> Log.d(TAG, "unsupported hints")
|
result?.usernameId = autofillId
|
||||||
|
Log.d(TAG, "Autofill username hint")
|
||||||
}
|
}
|
||||||
} else if (node.autofillType == View.AUTOFILL_TYPE_TEXT) {
|
it.equals(View.AUTOFILL_HINT_PASSWORD, true)
|
||||||
val inputType = node.inputType
|
|| it.contains("password", true) -> {
|
||||||
when {
|
result?.passwordId = autofillId
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 -> result?.email?.add(autofillId)
|
Log.d(TAG, "Autofill password hint")
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD > 0 -> result?.password?.add(autofillId)
|
return true
|
||||||
result?.password?.isEmpty() == true -> usernameCandidate = autofillId
|
}
|
||||||
|
// Ignore autocomplete="off"
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
|
||||||
|
it.equals("off", true) ||
|
||||||
|
it.equals("on", true) -> {
|
||||||
|
Log.d(TAG, "Autofill web hint")
|
||||||
|
return parseNodeByHtmlAttributes(node)
|
||||||
|
}
|
||||||
|
else -> Log.d(TAG, "Autofill unsupported hint $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
|
||||||
|
val autofillId = node.autofillId
|
||||||
|
val nodHtml = node.htmlInfo
|
||||||
|
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
|
||||||
|
"input" -> {
|
||||||
|
nodHtml.attributes?.forEach { pairAttribute ->
|
||||||
|
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
|
||||||
|
"type" -> {
|
||||||
|
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
|
||||||
|
"tel", "email" -> {
|
||||||
|
result?.usernameId = autofillId
|
||||||
|
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
|
}
|
||||||
|
"text" -> {
|
||||||
|
usernameCandidate = autofillId
|
||||||
|
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
|
}
|
||||||
|
"password" -> {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
Log.d(TAG, "Autofill password web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
for (i in 0 until node.childCount)
|
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
|
||||||
parseViewNode(node.getChildAt(i))
|
val autofillId = node.autofillId
|
||||||
|
val inputType = node.inputType
|
||||||
|
if (inputType and InputType.TYPE_CLASS_TEXT != 0) {
|
||||||
|
when {
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS != 0 -> {
|
||||||
|
result?.usernameId = autofillId
|
||||||
|
Log.d(TAG, "Autofill username android type: $inputType")
|
||||||
|
}
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_NORMAL != 0 ||
|
||||||
|
inputType and InputType.TYPE_NUMBER_VARIATION_NORMAL != 0 ||
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_PERSON_NAME != 0 -> {
|
||||||
|
usernameCandidate = autofillId
|
||||||
|
Log.d(TAG, "Autofill username candidate android type: $inputType")
|
||||||
|
}
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD != 0 ||
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD != 0 ||
|
||||||
|
inputType and InputType.TYPE_NUMBER_VARIATION_PASSWORD != 0 -> {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
Log.d(TAG, "Autofill password android type: $inputType")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT != 0 ||
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_FILTER != 0 ||
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE != 0 ||
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_PHONETIC != 0 ||
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS != 0 ||
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_URI != 0 ||
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT != 0 ||
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS != 0 ||
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD != 0 -> {
|
||||||
|
// Type not used
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Autofill unknown android type: $inputType")
|
||||||
|
usernameCandidate = autofillId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
internal class Result {
|
internal class Result {
|
||||||
val title: MutableList<CharSequence>
|
var applicationId: String? = null
|
||||||
val webDomain: MutableList<String>
|
var domain: String? = null
|
||||||
val username: MutableList<AutofillId>
|
set(value) {
|
||||||
val email: MutableList<AutofillId>
|
if (field == null)
|
||||||
val password: MutableList<AutofillId>
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
var usernameId: AutofillId? = null
|
||||||
title = ArrayList()
|
set(value) {
|
||||||
webDomain = ArrayList()
|
if (field == null)
|
||||||
username = ArrayList()
|
field = value
|
||||||
email = ArrayList()
|
}
|
||||||
password = ArrayList()
|
|
||||||
}
|
var passwordId: 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>()
|
||||||
all.addAll(username)
|
usernameId?.let {
|
||||||
all.addAll(email)
|
all.add(it)
|
||||||
all.addAll(password)
|
}
|
||||||
|
passwordId?.let {
|
||||||
|
all.add(it)
|
||||||
|
}
|
||||||
return all.toTypedArray()
|
return all.toTypedArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,8 +313,9 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBiometricException(e: Exception) {
|
override fun onBiometricException(e: Exception) {
|
||||||
if (e.localizedMessage != null)
|
e.localizedMessage?.let {
|
||||||
setAdvancedUnlockedMessageView(e.localizedMessage)
|
setAdvancedUnlockedMessageView(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showFingerPrintViews(show: Boolean) {
|
private fun showFingerPrintViews(show: Boolean) {
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
// really not much to do when no fingerprint support found
|
// really not much to do when no fingerprint support found
|
||||||
isKeyManagerInit = false
|
isKeyManagerInit = false
|
||||||
} else {
|
} else {
|
||||||
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
|
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.action
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
@@ -70,6 +71,9 @@ open class AssignPasswordInDatabaseRunnable (
|
|||||||
// Erase the biometric
|
// Erase the biometric
|
||||||
CipherDatabaseAction.getInstance(context)
|
CipherDatabaseAction.getInstance(context)
|
||||||
.deleteByDatabaseUri(mDatabaseUri)
|
.deleteByDatabaseUri(mDatabaseUri)
|
||||||
|
// Erase the register keyfile
|
||||||
|
FileDatabaseHistoryAction.getInstance(context)
|
||||||
|
.deleteKeyFileByDatabaseUri(mDatabaseUri)
|
||||||
|
|
||||||
if (!result.isSuccess) {
|
if (!result.isSuccess) {
|
||||||
// Erase the current master key
|
// Erase the current master key
|
||||||
|
|||||||
@@ -86,8 +86,11 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called
|
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register the current time to init the lock timer
|
||||||
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
|
|
||||||
// Start the opening notification
|
// Start the opening notification
|
||||||
DatabaseOpenNotificationService.startIfAllowed(context)
|
DatabaseOpenNotificationService.start(context)
|
||||||
} else {
|
} else {
|
||||||
mDatabase.closeAndClear(cacheDirectory)
|
mDatabase.closeAndClear(cacheDirectory)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
@@ -35,6 +36,7 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||||
@@ -85,12 +87,17 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
|
|
||||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||||
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||||
TimeoutHelper.temporarilyDisableTimeout(activity)
|
TimeoutHelper.temporarilyDisableTimeout()
|
||||||
|
// Stop the opening notification
|
||||||
|
DatabaseOpenNotificationService.stop(activity)
|
||||||
startOrUpdateDialog(titleId, messageId, warningId)
|
startOrUpdateDialog(titleId, messageId, warningId)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||||
TimeoutHelper.temporarilyDisableTimeout(activity)
|
TimeoutHelper.temporarilyDisableTimeout()
|
||||||
|
// Stop the opening notification
|
||||||
|
DatabaseOpenNotificationService.stop(activity)
|
||||||
startOrUpdateDialog(titleId, messageId, warningId)
|
startOrUpdateDialog(titleId, messageId, warningId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +105,18 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
onActionFinish?.invoke(actionTask, result)
|
onActionFinish?.invoke(actionTask, result)
|
||||||
// Remove the progress task
|
// Remove the progress task
|
||||||
ProgressTaskDialogFragment.stop(activity)
|
ProgressTaskDialogFragment.stop(activity)
|
||||||
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity)
|
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
||||||
|
|
||||||
|
val inTime = if (activity is LockingActivity) {
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeout(activity)
|
||||||
|
} else {
|
||||||
|
TimeoutHelper.checkTime(activity)
|
||||||
|
}
|
||||||
|
// Start the opening notification if in time
|
||||||
|
// (databaseOpenService is open manually in Action Open Task)
|
||||||
|
if (actionTask != ACTION_DATABASE_LOAD_TASK && inTime) {
|
||||||
|
DatabaseOpenNotificationService.start(activity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +144,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
if (serviceConnection == null) {
|
if (serviceConnection == null) {
|
||||||
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)
|
addActionTaskListener(actionTaskListener)
|
||||||
getService().checkAction()
|
getService().checkAction()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,11 @@
|
|||||||
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.database.Cursor
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
|
|
||||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
|
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
@@ -49,6 +45,7 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
|||||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.stream.readBytes4ToInt
|
import com.kunzisoft.keepass.stream.readBytes4ToInt
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||||
@@ -137,6 +134,9 @@ class Database {
|
|||||||
val version: String
|
val version: String
|
||||||
get() = mDatabaseKDB?.version ?: mDatabaseKDBX?.version ?: "-"
|
get() = mDatabaseKDB?.version ?: mDatabaseKDBX?.version ?: "-"
|
||||||
|
|
||||||
|
val type: Class<*>?
|
||||||
|
get() = mDatabaseKDB?.javaClass ?: mDatabaseKDBX?.javaClass
|
||||||
|
|
||||||
val allowDataCompression: Boolean
|
val allowDataCompression: Boolean
|
||||||
get() = mDatabaseKDBX != null
|
get() = mDatabaseKDBX != null
|
||||||
|
|
||||||
@@ -397,54 +397,17 @@ class Database {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
fun createVirtualGroupFromSearch(searchQuery: String, max: Int = Integer.MAX_VALUE): Group? {
|
||||||
fun search(str: String, max: Int = Integer.MAX_VALUE): Group? {
|
return mSearchHelper?.createVirtualGroupWithSearchResult(this, searchQuery, max)
|
||||||
return mSearchHelper?.search(this, str, max)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchEntries(context: Context, query: String): Cursor? {
|
fun createVirtualGroupFromSearch(searchInfo: SearchInfo, max: Int = Integer.MAX_VALUE): Group? {
|
||||||
|
val query = (if (searchInfo.webDomain != null)
|
||||||
var cursorKDB: EntryCursorKDB? = null
|
searchInfo.webDomain
|
||||||
var cursorKDBX: EntryCursorKDBX? = null
|
else
|
||||||
|
searchInfo.applicationId)
|
||||||
if (mDatabaseKDB != null)
|
?: return null
|
||||||
cursorKDB = EntryCursorKDB()
|
return mSearchHelper?.createVirtualGroupWithSearchResult(this, query, max)
|
||||||
if (mDatabaseKDBX != null)
|
|
||||||
cursorKDBX = EntryCursorKDBX()
|
|
||||||
|
|
||||||
val searchResult = search(query, SearchHelper.MAX_SEARCH_ENTRY)
|
|
||||||
if (searchResult != null) {
|
|
||||||
// Search in hide entries but not meta-stream
|
|
||||||
for (entry in searchResult.getFilteredChildEntries(*Group.ChildFilter.getDefaults(context))) {
|
|
||||||
entry.entryKDB?.let {
|
|
||||||
cursorKDB?.addEntry(it)
|
|
||||||
}
|
|
||||||
entry.entryKDBX?.let {
|
|
||||||
cursorKDBX?.addEntry(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cursorKDB ?: cursorKDBX
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getEntryFrom(cursor: Cursor): Entry? {
|
|
||||||
val iconFactory = mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory()
|
|
||||||
|
|
||||||
return createEntry()?.apply {
|
|
||||||
startManageEntry(this)
|
|
||||||
mDatabaseKDB?.let {
|
|
||||||
entryKDB?.let { entryKDB ->
|
|
||||||
(cursor as EntryCursorKDB).populateEntry(entryKDB, iconFactory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mDatabaseKDBX?.let {
|
|
||||||
entryKDBX?.let { entryKDBX ->
|
|
||||||
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, iconFactory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stopManageEntry(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
@@ -493,10 +456,11 @@ class Database {
|
|||||||
var outputStream: OutputStream? = null
|
var outputStream: OutputStream? = null
|
||||||
try {
|
try {
|
||||||
outputStream = contentResolver.openOutputStream(uri)
|
outputStream = contentResolver.openOutputStream(uri)
|
||||||
val pmo =
|
outputStream?.let { definedOutputStream ->
|
||||||
mDatabaseKDB?.let { DatabaseOutputKDB(it, outputStream) }
|
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
||||||
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, outputStream) }
|
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
|
||||||
pmo?.output()
|
databaseOutput?.output()
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IOException(e)
|
throw IOException(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -723,7 +687,7 @@ class Database {
|
|||||||
fun canRecycle(entry: Entry): Boolean {
|
fun canRecycle(entry: Entry): Boolean {
|
||||||
var canRecycle: Boolean? = null
|
var canRecycle: Boolean? = null
|
||||||
entry.entryKDB?.let {
|
entry.entryKDB?.let {
|
||||||
canRecycle = mDatabaseKDB?.canRecycle(it)
|
canRecycle = mDatabaseKDB?.canRecycle()
|
||||||
}
|
}
|
||||||
entry.entryKDBX?.let {
|
entry.entryKDBX?.let {
|
||||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||||
@@ -734,7 +698,7 @@ class Database {
|
|||||||
fun canRecycle(group: Group): Boolean {
|
fun canRecycle(group: Group): Boolean {
|
||||||
var canRecycle: Boolean? = null
|
var canRecycle: Boolean? = null
|
||||||
group.groupKDB?.let {
|
group.groupKDB?.let {
|
||||||
canRecycle = mDatabaseKDB?.canRecycle(it)
|
canRecycle = mDatabaseKDB?.canRecycle()
|
||||||
}
|
}
|
||||||
group.groupKDBX?.let {
|
group.groupKDBX?.let {
|
||||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class DateInstant : Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(string: String) {
|
constructor(string: String) {
|
||||||
jDate = dateFormat.parse(string)
|
jDate = dateFormat.parse(string) ?: jDate
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -121,7 +121,7 @@ class DateInstant : Parcelable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isSameDate(d1: Date?, d2: Date?): Boolean {
|
private fun isSameDate(d1: Date, d2: Date): Boolean {
|
||||||
val cal1 = Calendar.getInstance()
|
val cal1 = Calendar.getInstance()
|
||||||
cal1.time = d1
|
cal1.time = d1
|
||||||
cal1.set(Calendar.MILLISECOND, 0)
|
cal1.set(Calendar.MILLISECOND, 0)
|
||||||
@@ -142,7 +142,7 @@ class DateInstant : Parcelable {
|
|||||||
fun getDateTimeString(resources: Resources, date: Date): String {
|
fun getDateTimeString(resources: Resources, date: Date): String {
|
||||||
return java.text.DateFormat.getDateTimeInstance(
|
return java.text.DateFormat.getDateTimeInstance(
|
||||||
java.text.DateFormat.MEDIUM,
|
java.text.DateFormat.MEDIUM,
|
||||||
java.text.DateFormat.MEDIUM,
|
java.text.DateFormat.SHORT,
|
||||||
ConfigurationCompat.getLocales(resources.configuration)[0])
|
ConfigurationCompat.getLocales(resources.configuration)[0])
|
||||||
.format(date)
|
.format(date)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,17 +26,25 @@ import java.util.UUID
|
|||||||
class DeletedObject {
|
class DeletedObject {
|
||||||
|
|
||||||
var uuid: UUID = DatabaseVersioned.UUID_ZERO
|
var uuid: UUID = DatabaseVersioned.UUID_ZERO
|
||||||
var deletionTime: Date? = null
|
private var mDeletionTime: Date? = null
|
||||||
get() = if (field == null) {
|
|
||||||
Date(System.currentTimeMillis())
|
fun getDeletionTime(): Date {
|
||||||
} else field
|
if (mDeletionTime == null) {
|
||||||
|
mDeletionTime = Date(System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
return mDeletionTime!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDeletionTime(deletionTime: Date) {
|
||||||
|
this.mDeletionTime = deletionTime
|
||||||
|
}
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
constructor(uuid: UUID, deletionTime: Date = Date()) {
|
constructor(uuid: UUID, deletionTime: Date = Date()) {
|
||||||
this.uuid = uuid
|
this.uuid = uuid
|
||||||
this.deletionTime = deletionTime
|
this.mDeletionTime = deletionTime
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|||||||
@@ -398,6 +398,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
database?.startManageEntry(this)
|
database?.startManageEntry(this)
|
||||||
entryInfo.id = nodeId.toString()
|
entryInfo.id = nodeId.toString()
|
||||||
entryInfo.title = title
|
entryInfo.title = title
|
||||||
|
entryInfo.icon = icon
|
||||||
entryInfo.username = username
|
entryInfo.username = username
|
||||||
entryInfo.password = password
|
entryInfo.password = password
|
||||||
entryInfo.url = url
|
entryInfo.url = url
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ 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 com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.node.*
|
import com.kunzisoft.keepass.database.element.node.*
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
@@ -251,6 +252,14 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
ArrayList()
|
ArrayList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getChildEntriesInfo(database: Database): List<EntryInfo> {
|
||||||
|
val entriesInfo = ArrayList<EntryInfo>()
|
||||||
|
getChildEntries().forEach { entry ->
|
||||||
|
entriesInfo.add(entry.getEntryInfo(database))
|
||||||
|
}
|
||||||
|
return entriesInfo
|
||||||
|
}
|
||||||
|
|
||||||
fun getFilteredChildEntries(vararg filter: ChildFilter): List<Entry> {
|
fun getFilteredChildEntries(vararg filter: ChildFilter): List<Entry> {
|
||||||
val withoutMetaStream = filter.contains(ChildFilter.META_STREAM)
|
val withoutMetaStream = filter.contains(ChildFilter.META_STREAM)
|
||||||
val showExpiredEntries = !filter.contains(ChildFilter.EXPIRED)
|
val showExpiredEntries = !filter.contains(ChildFilter.EXPIRED)
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
|||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
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.node.NodeVersioned
|
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -231,8 +230,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
* @param node Node to remove
|
* @param node Node to remove
|
||||||
* @return true if node can be recycle, false elsewhere
|
* @return true if node can be recycle, false elsewhere
|
||||||
*/
|
*/
|
||||||
fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
|
// TODO #394 Backup KDB
|
||||||
// TODO #394 Backup pw3
|
// fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
|
||||||
|
fun canRecycle(): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +261,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
val TYPE = DatabaseKDB::class.java
|
||||||
|
|
||||||
const val BACKUP_FOLDER_TITLE = "Backup"
|
const val BACKUP_FOLDER_TITLE = "Backup"
|
||||||
private const val BACKUP_FOLDER_UNDEFINED_ID = -1
|
private const val BACKUP_FOLDER_UNDEFINED_ID = -1
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
|||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
@@ -551,6 +552,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
val TYPE = DatabaseKDBX::class.java
|
||||||
private val TAG = DatabaseKDBX::class.java.name
|
private val TAG = DatabaseKDBX::class.java.name
|
||||||
|
|
||||||
private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited
|
private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ class FieldReferencesEngine {
|
|||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
val found = result.entry
|
val found = result.entry
|
||||||
|
found?.stopToManageFieldReferences()
|
||||||
val wanted = result.wanted
|
val wanted = result.wanted
|
||||||
|
|
||||||
var data: String? = null
|
var data: String? = null
|
||||||
@@ -145,22 +146,15 @@ class FieldReferencesEngine {
|
|||||||
searchParametersV4.setupNone()
|
searchParametersV4.setupNone()
|
||||||
|
|
||||||
searchParametersV4.searchString = ref.substring(4)
|
searchParametersV4.searchString = ref.substring(4)
|
||||||
if (scan == 'T') {
|
when (scan) {
|
||||||
searchParametersV4.searchInTitles = true
|
'T' -> searchParametersV4.searchInTitles = true
|
||||||
} else if (scan == 'U') {
|
'U' -> searchParametersV4.searchInUserNames = true
|
||||||
searchParametersV4.searchInUserNames = true
|
'A' -> searchParametersV4.searchInUrls = true
|
||||||
} else if (scan == 'A') {
|
'P' -> searchParametersV4.searchInPasswords = true
|
||||||
searchParametersV4.searchInUrls = true
|
'N' -> searchParametersV4.searchInNotes = true
|
||||||
} else if (scan == 'P') {
|
'I' -> searchParametersV4.searchInUUIDs = true
|
||||||
searchParametersV4.searchInPasswords = true
|
'O' -> searchParametersV4.searchInOther = true
|
||||||
} else if (scan == 'N') {
|
else -> return null
|
||||||
searchParametersV4.searchInNotes = true
|
|
||||||
} else if (scan == 'I') {
|
|
||||||
searchParametersV4.searchInUUIDs = true
|
|
||||||
} else if (scan == 'O') {
|
|
||||||
searchParametersV4.searchInOther = true
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val list = ArrayList<EntryKDBX>()
|
val list = ArrayList<EntryKDBX>()
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
|
|||||||
|
|
||||||
fun allowAddEntryIfIsRoot(): Boolean
|
fun allowAddEntryIfIsRoot(): Boolean
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun doForEachChildAndForIt(entryHandler: NodeHandler<Entry>,
|
fun doForEachChildAndForIt(entryHandler: NodeHandler<Entry>,
|
||||||
groupHandler: NodeHandler<Group>) {
|
groupHandler: NodeHandler<Group>) {
|
||||||
doForEachChild(entryHandler, groupHandler)
|
doForEachChild(entryHandler, groupHandler)
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ package com.kunzisoft.keepass.database.element.node
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import java.util.*
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
class NodeIdUUID : NodeId<UUID> {
|
class NodeIdUUID : NodeId<UUID> {
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ class BinaryAttachment : Parcelable {
|
|||||||
val compressedByte = parcel.readByte().toInt()
|
val compressedByte = parcel.readByte().toInt()
|
||||||
isCompressed = if (compressedByte == 2) null else compressedByte != 0
|
isCompressed = if (compressedByte == 2) null else compressedByte != 0
|
||||||
isProtected = parcel.readByte().toInt() != 0
|
isProtected = parcel.readByte().toInt() != 0
|
||||||
dataFile = File(parcel.readString())
|
parcel.readString()?.let {
|
||||||
|
dataFile = File(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@@ -74,10 +76,10 @@ class BinaryAttachment : Parcelable {
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||||
if (dataFile != null) {
|
dataFile?.let { concreteDataFile ->
|
||||||
// To compress, create a new binary with file
|
// To compress, create a new binary with file
|
||||||
if (isCompressed != true) {
|
if (isCompressed != true) {
|
||||||
val fileBinaryCompress = File(dataFile!!.parent, dataFile!!.name + "_temp")
|
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||||
var outputStream: GZIPOutputStream? = null
|
var outputStream: GZIPOutputStream? = null
|
||||||
var inputStream: InputStream? = null
|
var inputStream: InputStream? = null
|
||||||
try {
|
try {
|
||||||
@@ -91,8 +93,8 @@ class BinaryAttachment : Parcelable {
|
|||||||
outputStream?.close()
|
outputStream?.close()
|
||||||
|
|
||||||
// Remove unGzip file
|
// Remove unGzip file
|
||||||
if (dataFile!!.delete()) {
|
if (concreteDataFile.delete()) {
|
||||||
if (fileBinaryCompress.renameTo(dataFile)) {
|
if (fileBinaryCompress.renameTo(concreteDataFile)) {
|
||||||
// Harmonize with database compression
|
// Harmonize with database compression
|
||||||
isCompressed = true
|
isCompressed = true
|
||||||
}
|
}
|
||||||
@@ -104,9 +106,9 @@ class BinaryAttachment : Parcelable {
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||||
if (dataFile != null) {
|
dataFile?.let { concreteDataFile ->
|
||||||
if (isCompressed != false) {
|
if (isCompressed != false) {
|
||||||
val fileBinaryDecompress = File(dataFile!!.parent, dataFile!!.name + "_temp")
|
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||||
var outputStream: FileOutputStream? = null
|
var outputStream: FileOutputStream? = null
|
||||||
var inputStream: GZIPInputStream? = null
|
var inputStream: GZIPInputStream? = null
|
||||||
try {
|
try {
|
||||||
@@ -120,8 +122,8 @@ class BinaryAttachment : Parcelable {
|
|||||||
outputStream?.close()
|
outputStream?.close()
|
||||||
|
|
||||||
// Remove gzip file
|
// Remove gzip file
|
||||||
if (dataFile!!.delete()) {
|
if (concreteDataFile.delete()) {
|
||||||
if (fileBinaryDecompress.renameTo(dataFile)) {
|
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
|
||||||
// Harmonize with database compression
|
// Harmonize with database compression
|
||||||
isCompressed = false
|
isCompressed = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,11 +127,7 @@ object DatabaseKDBXXML {
|
|||||||
const val ElemCustomData = "CustomData"
|
const val ElemCustomData = "CustomData"
|
||||||
const val ElemStringDictExItem = "Item"
|
const val ElemStringDictExItem = "Item"
|
||||||
|
|
||||||
val dateFormatter: ThreadLocal<SimpleDateFormat> = object : ThreadLocal<SimpleDateFormat>() {
|
val DateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).apply {
|
||||||
override fun initialValue(): SimpleDateFormat {
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
|
||||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
|
||||||
return dateFormat
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -660,7 +660,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
|
KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
|
||||||
ctxDeletedObject?.uuid = readUuid(xpp)
|
ctxDeletedObject?.uuid = readUuid(xpp)
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
|
||||||
ctxDeletedObject?.deletionTime = readTime(xpp)
|
ctxDeletedObject?.setDeletionTime(readTime(xpp))
|
||||||
} else {
|
} else {
|
||||||
readUnknown(xpp)
|
readUnknown(xpp)
|
||||||
}
|
}
|
||||||
@@ -829,7 +829,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
} else {
|
} else {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
utcDate = DatabaseKDBXXML.dateFormatter.get()?.parse(sDate)
|
utcDate = DatabaseKDBXXML.DateFormatter.parse(sDate)
|
||||||
} catch (e: ParseException) {
|
} catch (e: ParseException) {
|
||||||
// Catch with null test below
|
// Catch with null test below
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -384,9 +384,9 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
private fun writeObject(name: String, value: Date?) {
|
private fun writeObject(name: String, value: Date) {
|
||||||
if (header!!.version < DatabaseHeaderKDBX.FILE_VERSION_32_4) {
|
if (header!!.version < DatabaseHeaderKDBX.FILE_VERSION_32_4) {
|
||||||
writeObject(name, DatabaseKDBXXML.dateFormatter.get().format(value))
|
writeObject(name, DatabaseKDBXXML.DateFormatter.format(value))
|
||||||
} else {
|
} else {
|
||||||
val dt = DateTime(value)
|
val dt = DateTime(value)
|
||||||
val seconds = DateKDBXUtil.convertDateToKDBX4Time(dt)
|
val seconds = DateKDBXUtil.convertDateToKDBX4Time(dt)
|
||||||
@@ -553,7 +553,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
||||||
|
|
||||||
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
|
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
|
||||||
writeObject(DatabaseKDBXXML.ElemDeletionTime, value.deletionTime)
|
writeObject(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime())
|
||||||
|
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import com.kunzisoft.keepass.database.element.Group
|
|||||||
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIterator
|
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIterator
|
||||||
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDB
|
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDB
|
||||||
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
|
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class SearchHelper(private val isOmitBackup: Boolean) {
|
class SearchHelper(private val isOmitBackup: Boolean) {
|
||||||
|
|
||||||
@@ -36,22 +35,19 @@ class SearchHelper(private val isOmitBackup: Boolean) {
|
|||||||
|
|
||||||
private var incrementEntry = 0
|
private var incrementEntry = 0
|
||||||
|
|
||||||
fun search(database: Database, qStr: String, max: Int): Group? {
|
fun createVirtualGroupWithSearchResult(database: Database, searchQuery: String, max: Int): Group? {
|
||||||
|
|
||||||
val searchGroup = database.createGroup()
|
val searchGroup = database.createGroup()
|
||||||
searchGroup?.title = "\"" + qStr + "\""
|
searchGroup?.title = "\"" + searchQuery + "\""
|
||||||
|
|
||||||
// Search all entries
|
// Search all entries
|
||||||
val loc = Locale.getDefault()
|
|
||||||
val finalQStr = qStr.toLowerCase(loc)
|
|
||||||
|
|
||||||
incrementEntry = 0
|
incrementEntry = 0
|
||||||
database.rootGroup?.doForEachChild(
|
database.rootGroup?.doForEachChild(
|
||||||
object : NodeHandler<Entry>() {
|
object : NodeHandler<Entry>() {
|
||||||
override fun operate(node: Entry): Boolean {
|
override fun operate(node: Entry): Boolean {
|
||||||
if (incrementEntry >= max)
|
if (incrementEntry >= max)
|
||||||
return false
|
return false
|
||||||
if (entryContainsString(node, finalQStr, loc)) {
|
if (entryContainsString(node, searchQuery)) {
|
||||||
searchGroup?.addChildEntry(node)
|
searchGroup?.addChildEntry(node)
|
||||||
incrementEntry++
|
incrementEntry++
|
||||||
}
|
}
|
||||||
@@ -73,7 +69,7 @@ class SearchHelper(private val isOmitBackup: Boolean) {
|
|||||||
return searchGroup
|
return searchGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entryContainsString(entry: Entry, searchString: String, locale: Locale): Boolean {
|
private fun entryContainsString(entry: Entry, searchString: String): Boolean {
|
||||||
|
|
||||||
// Entry don't contains string if the search string is empty
|
// Entry don't contains string if the search string is empty
|
||||||
if (searchString.isEmpty())
|
if (searchString.isEmpty())
|
||||||
@@ -90,11 +86,10 @@ class SearchHelper(private val isOmitBackup: Boolean) {
|
|||||||
|
|
||||||
iterator?.let {
|
iterator?.let {
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
val str = it.next()
|
val currentString = it.next()
|
||||||
if (str.isNotEmpty()) {
|
if (currentString.isNotEmpty()
|
||||||
if (str.toLowerCase(locale).contains(searchString)) {
|
&& currentString.contains(searchString, true)) {
|
||||||
return true
|
return true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ constructor(private val mEntry: EntryKDB,
|
|||||||
title -> mEntry.title
|
title -> mEntry.title
|
||||||
url -> mEntry.url
|
url -> mEntry.url
|
||||||
username -> mEntry.username
|
username -> mEntry.username
|
||||||
comment -> mEntry.notes
|
notes -> mEntry.notes
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ constructor(private val mEntry: EntryKDB,
|
|||||||
title -> mSearchParameters.searchInTitles
|
title -> mSearchParameters.searchInTitles
|
||||||
url -> mSearchParameters.searchInUrls
|
url -> mSearchParameters.searchInUrls
|
||||||
username -> mSearchParameters.searchInUserNames
|
username -> mSearchParameters.searchInUserNames
|
||||||
comment -> mSearchParameters.searchInNotes
|
notes -> mSearchParameters.searchInNotes
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ constructor(private val mEntry: EntryKDB,
|
|||||||
private const val title = 0
|
private const val title = 0
|
||||||
private const val url = 1
|
private const val url = 1
|
||||||
private const val username = 2
|
private const val username = 2
|
||||||
private const val comment = 3
|
private const val notes = 3
|
||||||
private const val maxEntries = 4
|
private const val maxEntries = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ package com.kunzisoft.keepass.education
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import com.getkeepsafe.taptargetview.TapTarget
|
import com.getkeepsafe.taptargetview.TapTarget
|
||||||
import com.getkeepsafe.taptargetview.TapTargetView
|
import com.getkeepsafe.taptargetview.TapTargetView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -94,7 +94,8 @@ open class Education(val activity: Activity) {
|
|||||||
R.string.education_copy_username_key,
|
R.string.education_copy_username_key,
|
||||||
R.string.education_entry_edit_key,
|
R.string.education_entry_edit_key,
|
||||||
R.string.education_password_generator_key,
|
R.string.education_password_generator_key,
|
||||||
R.string.education_entry_new_field_key)
|
R.string.education_entry_new_field_key,
|
||||||
|
R.string.education_setup_OTP_key)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -271,6 +272,18 @@ open class Education(val activity: Activity) {
|
|||||||
context.resources.getBoolean(R.bool.education_entry_new_field_default))
|
context.resources.getBoolean(R.bool.education_entry_new_field_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the explanatory view to setup OTP has already been displayed.
|
||||||
|
*
|
||||||
|
* @param context The context to open the SharedPreferences
|
||||||
|
* @return boolean value of education_setup_OTP_key key
|
||||||
|
*/
|
||||||
|
fun isEducationSetupOTPPerformed(context: Context): Boolean {
|
||||||
|
val prefs = getEducationSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.education_setup_OTP_key),
|
||||||
|
context.resources.getBoolean(R.bool.education_setup_OTP_default))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines if the reset education preference has been reclicked
|
* Defines if the reset education preference has been reclicked
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class EntryEditActivityEducation(activity: Activity)
|
|||||||
activity.getString(R.string.education_generate_password_title),
|
activity.getString(R.string.education_generate_password_title),
|
||||||
activity.getString(R.string.education_generate_password_summary))
|
activity.getString(R.string.education_generate_password_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.textColorInt(Color.WHITE)
|
||||||
.tintTarget(false)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
override fun onTargetClick(view: TapTargetView) {
|
override fun onTargetClick(view: TapTargetView) {
|
||||||
@@ -66,7 +66,7 @@ class EntryEditActivityEducation(activity: Activity)
|
|||||||
activity.getString(R.string.education_entry_new_field_title),
|
activity.getString(R.string.education_entry_new_field_title),
|
||||||
activity.getString(R.string.education_entry_new_field_summary))
|
activity.getString(R.string.education_entry_new_field_summary))
|
||||||
.textColorInt(Color.WHITE)
|
.textColorInt(Color.WHITE)
|
||||||
.tintTarget(false)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
override fun onTargetClick(view: TapTargetView) {
|
override fun onTargetClick(view: TapTargetView) {
|
||||||
@@ -82,4 +82,33 @@ class EntryEditActivityEducation(activity: Activity)
|
|||||||
},
|
},
|
||||||
R.string.education_entry_new_field_key)
|
R.string.education_entry_new_field_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and display learning views
|
||||||
|
* Displays the explanation to setup OTP
|
||||||
|
*/
|
||||||
|
fun checkAndPerformedSetUpOTPEducation(educationView: View,
|
||||||
|
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||||
|
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
||||||
|
return checkAndPerformedEducation(!isEducationSetupOTPPerformed(activity),
|
||||||
|
TapTarget.forView(educationView,
|
||||||
|
activity.getString(R.string.education_setup_OTP_title),
|
||||||
|
activity.getString(R.string.education_setup_OTP_summary))
|
||||||
|
.textColorInt(Color.WHITE)
|
||||||
|
.tintTarget(true)
|
||||||
|
.cancelable(true),
|
||||||
|
object : TapTargetView.Listener() {
|
||||||
|
override fun onTargetClick(view: TapTargetView) {
|
||||||
|
super.onTargetClick(view)
|
||||||
|
onEducationViewClick?.invoke(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOuterCircleClick(view: TapTargetView?) {
|
||||||
|
super.onOuterCircleClick(view)
|
||||||
|
view?.dismiss(false)
|
||||||
|
onOuterViewClick?.invoke(view)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
R.string.education_setup_OTP_key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -22,16 +22,16 @@ package com.kunzisoft.keepass.icons
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Bitmap
|
import android.graphics.*
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import androidx.core.widget.ImageViewCompat
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import androidx.core.widget.ImageViewCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
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
|
||||||
@@ -70,6 +70,23 @@ class IconDrawableFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to assign a drawable to a RemoteView and tint it
|
||||||
|
*/
|
||||||
|
fun assignDrawableToRemoteViews(superDrawable: SuperDrawable,
|
||||||
|
remoteViews: RemoteViews,
|
||||||
|
imageId: Int,
|
||||||
|
tintColor: Int = Color.BLACK) {
|
||||||
|
val bitmap = superDrawable.drawable.toBitmap()
|
||||||
|
// Tint bitmap if it's not a custom icon
|
||||||
|
if (!superDrawable.custom) {
|
||||||
|
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply {
|
||||||
|
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
remoteViews.setImageViewBitmap(imageId, bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [SuperDrawable] [icon] (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] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed
|
||||||
*/
|
*/
|
||||||
@@ -233,7 +250,6 @@ class IconDrawableFactory {
|
|||||||
*/
|
*/
|
||||||
fun ImageView.assignDefaultDatabaseIcon(iconFactory: IconDrawableFactory, tintColor: Int = Color.WHITE) {
|
fun ImageView.assignDefaultDatabaseIcon(iconFactory: IconDrawableFactory, tintColor: Int = Color.WHITE) {
|
||||||
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
|
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
|
||||||
|
|
||||||
iconFactory.assignDrawableToImageView(
|
iconFactory.assignDrawableToImageView(
|
||||||
iconFactory.getIconSuperDrawable(context,
|
iconFactory.getIconSuperDrawable(context,
|
||||||
selectedIconPack.defaultIconId,
|
selectedIconPack.defaultIconId,
|
||||||
@@ -249,9 +265,10 @@ fun ImageView.assignDefaultDatabaseIcon(iconFactory: IconDrawableFactory, tintCo
|
|||||||
/**
|
/**
|
||||||
* Assign a database [icon] to an ImageView and tint it with [tintColor] if needed
|
* Assign a database [icon] to an ImageView and tint it with [tintColor] if needed
|
||||||
*/
|
*/
|
||||||
fun ImageView.assignDatabaseIcon(iconFactory: IconDrawableFactory, icon: IconImage, tintColor: Int = Color.WHITE) {
|
fun ImageView.assignDatabaseIcon(iconFactory: IconDrawableFactory,
|
||||||
|
icon: IconImage,
|
||||||
|
tintColor: Int = Color.WHITE) {
|
||||||
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
|
IconPackChooser.getSelectedIconPack(context)?.let { selectedIconPack ->
|
||||||
|
|
||||||
iconFactory.assignDrawableToImageView(
|
iconFactory.assignDrawableToImageView(
|
||||||
iconFactory.getIconSuperDrawable(context,
|
iconFactory.getIconSuperDrawable(context,
|
||||||
icon,
|
icon,
|
||||||
@@ -263,3 +280,19 @@ fun ImageView.assignDatabaseIcon(iconFactory: IconDrawableFactory, icon: IconIma
|
|||||||
tintColor)
|
tintColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun RemoteViews.assignDatabaseIcon(context: Context,
|
||||||
|
imageId: Int,
|
||||||
|
iconFactory: IconDrawableFactory,
|
||||||
|
icon: IconImage,
|
||||||
|
tintColor: Int = Color.BLACK) {
|
||||||
|
iconFactory.assignDrawableToRemoteViews(
|
||||||
|
iconFactory.getIconSuperDrawable(context,
|
||||||
|
icon,
|
||||||
|
24,
|
||||||
|
true,
|
||||||
|
tintColor),
|
||||||
|
this,
|
||||||
|
imageId,
|
||||||
|
tintColor)
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,14 +24,13 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
|
||||||
class KeyboardLauncherActivity : AppCompatActivity() {
|
class KeyboardLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
|
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
|
||||||
GroupActivity.launchForKeyboardSelection(this, PreferencesUtil.enableReadOnlyDatabase(this))
|
GroupActivity.launchForKeyboardSelection(this)
|
||||||
else {
|
else {
|
||||||
// Pass extra to get entry
|
// Pass extra to get entry
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelection(this)
|
FileDatabaseSelectActivity.launchForKeyboardSelection(this)
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard
|
package com.kunzisoft.keepass.magikeyboard
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.inputmethodservice.InputMethodService
|
import android.inputmethodservice.InputMethodService
|
||||||
import android.inputmethodservice.Keyboard
|
import android.inputmethodservice.Keyboard
|
||||||
import android.inputmethodservice.KeyboardView
|
import android.inputmethodservice.KeyboardView
|
||||||
@@ -42,8 +42,7 @@ import com.kunzisoft.keepass.model.EntryInfo
|
|||||||
import com.kunzisoft.keepass.model.Field
|
import com.kunzisoft.keepass.model.Field
|
||||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.utils.REMOVE_ENTRY_MAGIKEYBOARD_ACTION
|
|
||||||
|
|
||||||
class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||||
|
|
||||||
@@ -55,29 +54,18 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
private var fieldsAdapter: FieldsAdapter? = null
|
private var fieldsAdapter: FieldsAdapter? = null
|
||||||
private var playSoundDuringCLick: Boolean = false
|
private var playSoundDuringCLick: Boolean = false
|
||||||
|
|
||||||
private var lockBroadcastReceiver: BroadcastReceiver? = null
|
private var lockReceiver: LockReceiver? = null
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
// 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
|
||||||
lockBroadcastReceiver = object : BroadcastReceiver() {
|
lockReceiver = LockReceiver {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
when (intent?.action) {
|
|
||||||
REMOVE_ENTRY_MAGIKEYBOARD_ACTION, LOCK_ACTION -> {
|
|
||||||
removeEntryInfo()
|
removeEntryInfo()
|
||||||
assignKeyboardView()
|
assignKeyboardView()
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerReceiver(lockBroadcastReceiver,
|
registerLockReceiver(lockReceiver, true)
|
||||||
IntentFilter().apply {
|
|
||||||
addAction(LOCK_ACTION)
|
|
||||||
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateInputView(): View {
|
override fun onCreateInputView(): View {
|
||||||
@@ -187,12 +175,12 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
private fun switchToPreviousKeyboard() {
|
private fun switchToPreviousKeyboard() {
|
||||||
var imeManager: InputMethodManager? = null
|
var imeManager: InputMethodManager? = null
|
||||||
try {
|
try {
|
||||||
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
switchToPreviousInputMethod()
|
switchToPreviousInputMethod()
|
||||||
} else {
|
} else {
|
||||||
window.window?.let { window ->
|
window.window?.let { window ->
|
||||||
imeManager.switchToLastInputMethod(window.attributes.token)
|
imeManager?.switchToLastInputMethod(window.attributes.token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -214,8 +202,8 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
KEY_BACK_KEYBOARD -> switchToPreviousKeyboard()
|
KEY_BACK_KEYBOARD -> switchToPreviousKeyboard()
|
||||||
|
|
||||||
KEY_CHANGE_KEYBOARD -> {
|
KEY_CHANGE_KEYBOARD -> {
|
||||||
val imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?)
|
||||||
imeManager.showInputMethodPicker()
|
?.showInputMethodPicker()
|
||||||
}
|
}
|
||||||
KEY_UNLOCK -> {
|
KEY_UNLOCK -> {
|
||||||
}
|
}
|
||||||
@@ -301,7 +289,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
dismissCustomKeys()
|
dismissCustomKeys()
|
||||||
unregisterReceiver(lockBroadcastReceiver)
|
unregisterLockReceiver(lockReceiver)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,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.icon.IconImage
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -29,6 +30,7 @@ class EntryInfo : Parcelable {
|
|||||||
|
|
||||||
var id: String = ""
|
var id: String = ""
|
||||||
var title: String = ""
|
var title: String = ""
|
||||||
|
var icon: IconImage? = null
|
||||||
var username: String = ""
|
var username: String = ""
|
||||||
var password: String = ""
|
var password: String = ""
|
||||||
var url: String = ""
|
var url: String = ""
|
||||||
@@ -41,11 +43,12 @@ class EntryInfo : Parcelable {
|
|||||||
private constructor(parcel: Parcel) {
|
private constructor(parcel: Parcel) {
|
||||||
id = parcel.readString() ?: id
|
id = parcel.readString() ?: id
|
||||||
title = parcel.readString() ?: title
|
title = parcel.readString() ?: title
|
||||||
|
icon = parcel.readParcelable(IconImage::class.java.classLoader)
|
||||||
username = parcel.readString() ?: username
|
username = parcel.readString() ?: username
|
||||||
password = parcel.readString() ?: password
|
password = parcel.readString() ?: password
|
||||||
url = parcel.readString() ?: url
|
url = parcel.readString() ?: url
|
||||||
notes = parcel.readString() ?: notes
|
notes = parcel.readString() ?: notes
|
||||||
parcel.readList(customFields, Field::class.java.classLoader)
|
parcel.readList(customFields as List<Field>, Field::class.java.classLoader)
|
||||||
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
|
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +59,7 @@ class EntryInfo : Parcelable {
|
|||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeString(id)
|
parcel.writeString(id)
|
||||||
parcel.writeString(title)
|
parcel.writeString(title)
|
||||||
|
parcel.writeParcelable(icon, flags)
|
||||||
parcel.writeString(username)
|
parcel.writeString(username)
|
||||||
parcel.writeString(password)
|
parcel.writeString(password)
|
||||||
parcel.writeString(url)
|
parcel.writeString(url)
|
||||||
|
|||||||
42
app/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt
Normal file
42
app/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
class SearchInfo : Parcelable {
|
||||||
|
|
||||||
|
var applicationId: String? = null
|
||||||
|
var webDomain: String? = null
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
|
||||||
|
private constructor(parcel: Parcel) {
|
||||||
|
val readAppId = parcel.readString()
|
||||||
|
applicationId = if (readAppId.isNullOrEmpty()) null else readAppId
|
||||||
|
val readDomain = parcel.readString()
|
||||||
|
webDomain = if (readDomain.isNullOrEmpty()) null else readDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(applicationId ?: "")
|
||||||
|
parcel.writeString(webDomain ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATOR: Parcelable.Creator<SearchInfo> = object : Parcelable.Creator<SearchInfo> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): SearchInfo {
|
||||||
|
return SearchInfo(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<SearchInfo?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,20 +94,21 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
val nextNotificationId = (downloadFileUris.values.maxBy { it.notificationId }
|
val nextNotificationId = (downloadFileUris.values.maxBy { it.notificationId }
|
||||||
?.notificationId ?: notificationId) + 1
|
?.notificationId ?: notificationId) + 1
|
||||||
|
|
||||||
val entryAttachment: EntryAttachment = intent.getParcelableExtra(ATTACHMENT_KEY)
|
|
||||||
val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
|
|
||||||
downloadFileUris[downloadFileUri] = attachmentNotification
|
|
||||||
try {
|
try {
|
||||||
AttachmentFileAsyncTask(downloadFileUri,
|
intent.getParcelableExtra<EntryAttachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
|
||||||
attachmentNotification,
|
val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
|
||||||
contentResolver).apply {
|
downloadFileUris[downloadFileUri] = attachmentNotification
|
||||||
onUpdate = { uri, attachment, notificationIdAttach ->
|
AttachmentFileAsyncTask(downloadFileUri,
|
||||||
newNotification(uri, attachment, notificationIdAttach)
|
attachmentNotification,
|
||||||
mActionTaskListeners.forEach { actionListener ->
|
contentResolver).apply {
|
||||||
actionListener.onAttachmentProgress(downloadFileUri, attachment)
|
onUpdate = { uri, attachment, notificationIdAttach ->
|
||||||
|
newNotification(uri, attachment, notificationIdAttach)
|
||||||
|
mActionTaskListeners.forEach { actionListener ->
|
||||||
|
actionListener.onAttachmentProgress(downloadFileUri, attachment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}.execute()
|
||||||
}.execute()
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to download $downloadFileUri", e)
|
Log.e(TAG, "Unable to download $downloadFileUri", e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,14 +22,11 @@ package com.kunzisoft.keepass.notifications
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.exception.ClipboardException
|
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
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.timeout.TimeoutHelper
|
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER
|
import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -62,9 +59,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
|
mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
|
||||||
|
|
||||||
//Get settings
|
//Get settings
|
||||||
notificationTimeoutMilliSecs = PreferenceManager.getDefaultSharedPreferences(this)
|
notificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
|
||||||
.getString(getString(R.string.clipboard_timeout_key),
|
|
||||||
getString(R.string.clipboard_timeout_default))?.toLong() ?: TimeoutHelper.DEFAULT_TIMEOUT
|
|
||||||
|
|
||||||
when {
|
when {
|
||||||
intent == null -> Log.w(TAG, "null intent")
|
intent == null -> Log.w(TAG, "null intent")
|
||||||
@@ -78,12 +73,14 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
}
|
}
|
||||||
else -> for (actionKey in ClipboardEntryNotificationField.allActionKeys) {
|
else -> for (actionKey in ClipboardEntryNotificationField.allActionKeys) {
|
||||||
if (actionKey == intent.action) {
|
if (actionKey == intent.action) {
|
||||||
val fieldToCopy = intent.getParcelableExtra<ClipboardEntryNotificationField>(
|
intent.getParcelableExtra<ClipboardEntryNotificationField>(
|
||||||
ClipboardEntryNotificationField.getExtraKeyLinkToActionKey(actionKey))
|
ClipboardEntryNotificationField.getExtraKeyLinkToActionKey(actionKey))?.let {
|
||||||
val nextFields = constructListOfField(intent)
|
fieldToCopy ->
|
||||||
// Remove the current field from the next fields
|
val nextFields = constructListOfField(intent)
|
||||||
nextFields.remove(fieldToCopy)
|
// Remove the current field from the next fields
|
||||||
copyField(fieldToCopy, nextFields)
|
nextFields.remove(fieldToCopy)
|
||||||
|
copyField(fieldToCopy, nextFields)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,10 +88,12 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun constructListOfField(intent: Intent?): ArrayList<ClipboardEntryNotificationField> {
|
private fun constructListOfField(intent: Intent?): ArrayList<ClipboardEntryNotificationField> {
|
||||||
var fieldList = ArrayList<ClipboardEntryNotificationField>()
|
val fieldList = ArrayList<ClipboardEntryNotificationField>()
|
||||||
if (intent != null && intent.extras != null) {
|
if (intent?.extras?.containsKey(EXTRA_CLIPBOARD_FIELDS) == true) {
|
||||||
if (intent.extras!!.containsKey(EXTRA_CLIPBOARD_FIELDS))
|
intent.getParcelableArrayListExtra<ClipboardEntryNotificationField>(EXTRA_CLIPBOARD_FIELDS)?.let { retrieveFields ->
|
||||||
fieldList = intent.getParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS)
|
fieldList.clear()
|
||||||
|
fieldList.addAll(retrieveFields)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return fieldList
|
return fieldList
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ import android.os.Build
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.closeDatabase
|
||||||
|
|
||||||
class DatabaseOpenNotificationService: LockNotificationService() {
|
class DatabaseOpenNotificationService: LockNotificationService() {
|
||||||
|
|
||||||
@@ -36,8 +37,14 @@ class DatabaseOpenNotificationService: LockNotificationService() {
|
|||||||
private fun stopNotificationAndSendLock() {
|
private fun stopNotificationAndSendLock() {
|
||||||
// Send lock action
|
// Send lock action
|
||||||
sendBroadcast(Intent(LOCK_ACTION))
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
// Stop the service
|
}
|
||||||
stopSelf()
|
|
||||||
|
override fun actionOnLock() {
|
||||||
|
closeDatabase()
|
||||||
|
// Remove the lock timer (no more needed if it exists)
|
||||||
|
TimeoutHelper.cancelLockTimer(this)
|
||||||
|
// Service is stopped after receive the broadcast
|
||||||
|
super.actionOnLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
@@ -60,13 +67,16 @@ class DatabaseOpenNotificationService: LockNotificationService() {
|
|||||||
|
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
notificationManager?.notify(notificationId, buildNewNotification().apply {
|
startForeground(notificationId, buildNewNotification().apply {
|
||||||
setSmallIcon(R.drawable.notification_ic_database_open)
|
setSmallIcon(R.drawable.notification_ic_database_open)
|
||||||
setContentTitle(getString(R.string.database_opened))
|
setContentTitle(getString(R.string.database_opened))
|
||||||
setContentText(database.name + " (" + database.version + ")")
|
setContentText(database.name + " (" + database.version + ")")
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setContentIntent(pendingDatabaseIntent)
|
setContentIntent(pendingDatabaseIntent)
|
||||||
|
// Unfortunately swipe is disabled in lollipop+
|
||||||
setDeleteIntent(pendingDeleteIntent)
|
setDeleteIntent(pendingDeleteIntent)
|
||||||
|
addAction(R.drawable.ic_lock_white_24dp, getString(R.string.lock),
|
||||||
|
pendingDeleteIntent)
|
||||||
}.build())
|
}.build())
|
||||||
} else {
|
} else {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
@@ -80,9 +90,11 @@ class DatabaseOpenNotificationService: LockNotificationService() {
|
|||||||
companion object {
|
companion object {
|
||||||
const val ACTION_CLOSE_DATABASE = "ACTION_CLOSE_DATABASE"
|
const val ACTION_CLOSE_DATABASE = "ACTION_CLOSE_DATABASE"
|
||||||
|
|
||||||
fun startIfAllowed(context: Context) {
|
fun start(context: Context) {
|
||||||
if (PreferencesUtil.isPersistentNotificationEnable(context)) {
|
// Start the opening notification, keep it active to receive lock
|
||||||
// Start the opening notification
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.startForegroundService(Intent(context, DatabaseOpenNotificationService::class.java))
|
||||||
|
} else {
|
||||||
context.startService(Intent(context, DatabaseOpenNotificationService::class.java))
|
context.startService(Intent(context, DatabaseOpenNotificationService::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseR
|
|||||||
import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable
|
import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable
|
||||||
import com.kunzisoft.keepass.database.action.node.*
|
import com.kunzisoft.keepass.database.action.node.*
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
@@ -209,8 +210,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
|
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
|
||||||
&& intent.hasExtra(KEY_FILE_KEY)
|
&& intent.hasExtra(KEY_FILE_KEY)
|
||||||
) {
|
) {
|
||||||
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY)
|
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
|
||||||
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
|
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
|
||||||
|
|
||||||
|
if (databaseUri == null)
|
||||||
|
return null
|
||||||
|
|
||||||
return CreateDatabaseRunnable(this,
|
return CreateDatabaseRunnable(this,
|
||||||
Database.getInstance(),
|
Database.getInstance(),
|
||||||
databaseUri,
|
databaseUri,
|
||||||
@@ -236,12 +241,15 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
|
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY)
|
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
|
||||||
val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY)
|
val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY)
|
||||||
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
|
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
|
||||||
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
|
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
|
||||||
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
|
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
|
||||||
|
|
||||||
|
if (databaseUri == null)
|
||||||
|
return null
|
||||||
|
|
||||||
return LoadDatabaseRunnable(
|
return LoadDatabaseRunnable(
|
||||||
this,
|
this,
|
||||||
database,
|
database,
|
||||||
@@ -275,9 +283,10 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
|
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
|
||||||
&& intent.hasExtra(KEY_FILE_KEY)
|
&& intent.hasExtra(KEY_FILE_KEY)
|
||||||
) {
|
) {
|
||||||
|
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
|
||||||
AssignPasswordInDatabaseRunnable(this,
|
AssignPasswordInDatabaseRunnable(this,
|
||||||
Database.getInstance(),
|
Database.getInstance(),
|
||||||
intent.getParcelableExtra(DATABASE_URI_KEY),
|
databaseUri,
|
||||||
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
|
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
|
||||||
intent.getStringExtra(MASTER_PASSWORD_KEY),
|
intent.getStringExtra(MASTER_PASSWORD_KEY),
|
||||||
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
|
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
|
||||||
@@ -304,10 +313,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { parent ->
|
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
|
||||||
|
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
|
||||||
|
|
||||||
|
if (parentId == null
|
||||||
|
|| newGroup == null)
|
||||||
|
return null
|
||||||
|
|
||||||
|
database.getGroupById(parentId)?.let { parent ->
|
||||||
AddGroupRunnable(this,
|
AddGroupRunnable(this,
|
||||||
database,
|
database,
|
||||||
intent.getParcelableExtra(GROUP_KEY),
|
newGroup,
|
||||||
parent,
|
parent,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
AfterActionNodesRunnable())
|
AfterActionNodesRunnable())
|
||||||
@@ -323,8 +339,14 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
database.getGroupById(intent.getParcelableExtra(GROUP_ID_KEY))?.let { oldGroup ->
|
val groupId: NodeId<*>? = intent.getParcelableExtra(GROUP_ID_KEY)
|
||||||
val newGroup: Group = intent.getParcelableExtra(GROUP_KEY)
|
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
|
||||||
|
|
||||||
|
if (groupId == null
|
||||||
|
|| newGroup == null)
|
||||||
|
return null
|
||||||
|
|
||||||
|
database.getGroupById(groupId)?.let { oldGroup ->
|
||||||
UpdateGroupRunnable(this,
|
UpdateGroupRunnable(this,
|
||||||
database,
|
database,
|
||||||
oldGroup,
|
oldGroup,
|
||||||
@@ -343,10 +365,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { parent ->
|
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
|
||||||
|
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
|
||||||
|
|
||||||
|
if (parentId == null
|
||||||
|
|| newEntry == null)
|
||||||
|
return null
|
||||||
|
|
||||||
|
database.getGroupById(parentId)?.let { parent ->
|
||||||
AddEntryRunnable(this,
|
AddEntryRunnable(this,
|
||||||
database,
|
database,
|
||||||
intent.getParcelableExtra(ENTRY_KEY),
|
newEntry,
|
||||||
parent,
|
parent,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
AfterActionNodesRunnable())
|
AfterActionNodesRunnable())
|
||||||
@@ -362,8 +391,14 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { oldEntry ->
|
val entryId: NodeId<UUID>? = intent.getParcelableExtra(ENTRY_ID_KEY)
|
||||||
val newEntry: Entry = intent.getParcelableExtra(ENTRY_KEY)
|
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
|
||||||
|
|
||||||
|
if (entryId == null
|
||||||
|
|| newEntry == null)
|
||||||
|
return null
|
||||||
|
|
||||||
|
database.getEntryById(entryId)?.let { oldEntry ->
|
||||||
UpdateEntryRunnable(this,
|
UpdateEntryRunnable(this,
|
||||||
database,
|
database,
|
||||||
oldEntry,
|
oldEntry,
|
||||||
@@ -383,7 +418,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { newParent ->
|
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
|
||||||
|
|
||||||
|
database.getGroupById(parentId)?.let { newParent ->
|
||||||
CopyNodesRunnable(this,
|
CopyNodesRunnable(this,
|
||||||
database,
|
database,
|
||||||
getListNodesFromBundle(database, intent.extras!!),
|
getListNodesFromBundle(database, intent.extras!!),
|
||||||
@@ -403,7 +440,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { newParent ->
|
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
|
||||||
|
|
||||||
|
database.getGroupById(parentId)?.let { newParent ->
|
||||||
MoveNodesRunnable(this,
|
MoveNodesRunnable(this,
|
||||||
database,
|
database,
|
||||||
getListNodesFromBundle(database, intent.extras!!),
|
getListNodesFromBundle(database, intent.extras!!),
|
||||||
@@ -438,7 +477,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { mainEntry ->
|
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
|
||||||
|
|
||||||
|
database.getEntryById(entryId)?.let { mainEntry ->
|
||||||
RestoreEntryHistoryDatabaseRunnable(this,
|
RestoreEntryHistoryDatabaseRunnable(this,
|
||||||
database,
|
database,
|
||||||
mainEntry,
|
mainEntry,
|
||||||
@@ -456,7 +497,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { mainEntry ->
|
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
|
||||||
|
|
||||||
|
database.getEntryById(entryId)?.let { mainEntry ->
|
||||||
DeleteEntryHistoryDatabaseRunnable(this,
|
DeleteEntryHistoryDatabaseRunnable(this,
|
||||||
database,
|
database,
|
||||||
mainEntry,
|
mainEntry,
|
||||||
@@ -472,10 +515,18 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
return if (intent.hasExtra(OLD_ELEMENT_KEY)
|
return if (intent.hasExtra(OLD_ELEMENT_KEY)
|
||||||
&& intent.hasExtra(NEW_ELEMENT_KEY)
|
&& intent.hasExtra(NEW_ELEMENT_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)) {
|
&& intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||||
|
|
||||||
|
val oldElement: CompressionAlgorithm? = intent.getParcelableExtra(OLD_ELEMENT_KEY)
|
||||||
|
val newElement: CompressionAlgorithm? = intent.getParcelableExtra(NEW_ELEMENT_KEY)
|
||||||
|
|
||||||
|
if (oldElement == null
|
||||||
|
|| newElement == null)
|
||||||
|
return null
|
||||||
|
|
||||||
return UpdateCompressionBinariesDatabaseRunnable(this,
|
return UpdateCompressionBinariesDatabaseRunnable(this,
|
||||||
Database.getInstance(),
|
Database.getInstance(),
|
||||||
intent.getParcelableExtra(OLD_ELEMENT_KEY),
|
oldElement,
|
||||||
intent.getParcelableExtra(NEW_ELEMENT_KEY),
|
newElement,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
).apply {
|
).apply {
|
||||||
mAfterSaveDatabase = { result ->
|
mAfterSaveDatabase = { result ->
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
|||||||
else -> {
|
else -> {
|
||||||
notificationManager?.cancel(notificationId)
|
notificationManager?.cancel(notificationId)
|
||||||
if (intent.hasExtra(ENTRY_INFO_KEY)) {
|
if (intent.hasExtra(ENTRY_INFO_KEY)) {
|
||||||
newNotification(intent.getParcelableExtra(ENTRY_INFO_KEY))
|
intent.getParcelableExtra<EntryInfo>(ENTRY_INFO_KEY)?.let {
|
||||||
|
newNotification(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,31 +19,28 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.notifications
|
package com.kunzisoft.keepass.notifications
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import com.kunzisoft.keepass.utils.LockReceiver
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.registerLockReceiver
|
||||||
|
import com.kunzisoft.keepass.utils.unregisterLockReceiver
|
||||||
|
|
||||||
abstract class LockNotificationService : NotificationService() {
|
abstract class LockNotificationService : NotificationService() {
|
||||||
|
|
||||||
private var lockBroadcastReceiver: BroadcastReceiver? = null
|
private var mLockReceiver: LockReceiver? = null
|
||||||
|
|
||||||
|
protected open fun actionOnLock() {
|
||||||
|
// Stop the service in all cases
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
// Register a lock receiver to stop notification service when lock on keyboard is performed
|
// Register a lock receiver to stop notification service when lock on keyboard is performed
|
||||||
lockBroadcastReceiver = object : BroadcastReceiver() {
|
mLockReceiver = LockReceiver {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
actionOnLock()
|
||||||
// Stop the service in all cases
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
registerReceiver(lockBroadcastReceiver,
|
registerLockReceiver(mLockReceiver)
|
||||||
IntentFilter().apply {
|
|
||||||
addAction(LOCK_ACTION)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun stopTask(task: Thread?) {
|
protected fun stopTask(task: Thread?) {
|
||||||
@@ -59,7 +56,7 @@ abstract class LockNotificationService : NotificationService() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|
||||||
unregisterReceiver(lockBroadcastReceiver)
|
unregisterLockReceiver(mLockReceiver)
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ object OtpEntryFields {
|
|||||||
// [^&=\s]+=[^&=\s]+(&[^&=\s]+=[^&=\s]+)*
|
// [^&=\s]+=[^&=\s]+(&[^&=\s]+=[^&=\s]+)*
|
||||||
private const val validKeyValue = "[^&=\\s]+"
|
private const val validKeyValue = "[^&=\\s]+"
|
||||||
private const val validKeyValuePair = "$validKeyValue=$validKeyValue"
|
private const val validKeyValuePair = "$validKeyValue=$validKeyValue"
|
||||||
private const val validKeyValueRegex = "$validKeyValuePair&($validKeyValuePair)*"
|
private const val validKeyValueRegex = "$validKeyValuePair(&$validKeyValuePair)*"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse fields of an entry to retrieve an OtpElement
|
* Parse fields of an entry to retrieve an OtpElement
|
||||||
@@ -243,21 +243,18 @@ object OtpEntryFields {
|
|||||||
val plainText = getField(OTP_FIELD)
|
val plainText = getField(OTP_FIELD)
|
||||||
if (plainText != null && plainText.isNotEmpty()) {
|
if (plainText != null && plainText.isNotEmpty()) {
|
||||||
if (Pattern.matches(validKeyValueRegex, plainText)) {
|
if (Pattern.matches(validKeyValueRegex, plainText)) {
|
||||||
try {
|
return try {
|
||||||
// KeeOtp string format
|
// KeeOtp string format
|
||||||
val query = breakDownKeyValuePairs(plainText)
|
val query = breakDownKeyValuePairs(plainText)
|
||||||
|
|
||||||
var secretString = query[SEED_KEY]
|
otpElement.setBase32Secret(query[SEED_KEY] ?: "")
|
||||||
if (secretString == null)
|
otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS
|
||||||
secretString = ""
|
otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
||||||
otpElement.setBase32Secret(secretString)
|
|
||||||
otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS
|
|
||||||
otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
|
||||||
|
|
||||||
otpElement.type = OtpType.TOTP
|
otpElement.type = OtpType.TOTP
|
||||||
return true
|
true
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
return false
|
false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Malformed
|
// Malformed
|
||||||
@@ -281,8 +278,10 @@ object OtpEntryFields {
|
|||||||
// malformed
|
// malformed
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
otpElement.period = matcher.group(1).toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
||||||
otpElement.tokenType = OtpTokenType.getFromString(matcher.group(2))
|
otpElement.tokenType = matcher.group(2)?.let {
|
||||||
|
OtpTokenType.getFromString(it)
|
||||||
|
} ?: OtpTokenType.RFC6238
|
||||||
}
|
}
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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.settings
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
|
||||||
|
class AutofillSettingsActivity : StylishActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_toolbar)
|
||||||
|
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||||
|
toolbar.setTitle(R.string.autofill_preference_title)
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.fragment_container, AutofillSettingsFragment())
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -20,16 +20,14 @@
|
|||||||
package com.kunzisoft.keepass.settings
|
package com.kunzisoft.keepass.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
|
||||||
class SettingsAutofillActivity : SettingsActivity() {
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
mTimeoutEnable = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun retrieveMainFragment(): Fragment {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
return NestedSettingsFragment.newInstance(NestedSettingsFragment.Screen.FORM_FILLING)
|
// Load the preferences from an XML resource
|
||||||
|
setPreferencesFromResource(R.xml.preferences_autofill, rootKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ import android.view.MenuItem
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
|
||||||
class MagikIMESettings : StylishActivity() {
|
class MagikeyboardSettingsActivity : StylishActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -39,7 +39,7 @@ class MagikIMESettings : StylishActivity() {
|
|||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.fragment_container, MagikIMESettingsFragment())
|
.replace(R.id.fragment_container, MagikeyboardSettingsFragment())
|
||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ import androidx.preference.PreferenceFragmentCompat
|
|||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
class MagikIMESettingsFragment : PreferenceFragmentCompat() {
|
class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
// Load the preferences from an XML resource
|
// Load the preferences from an XML resource
|
||||||
@@ -24,7 +24,6 @@ import android.os.Bundle
|
|||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
|
||||||
class MainPreferenceFragment : PreferenceFragmentCompat() {
|
class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||||
|
|||||||
@@ -160,12 +160,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
findPreference<Preference>(getString(R.string.magic_keyboard_preference_key))?.setOnPreferenceClickListener {
|
findPreference<Preference>(getString(R.string.magic_keyboard_preference_key))?.setOnPreferenceClickListener {
|
||||||
startActivity(Intent(context, MagikIMESettings::class.java))
|
startActivity(Intent(context, MagikeyboardSettingsActivity::class.java))
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
findPreference<Preference>(getString(R.string.clipboard_explanation_key))?.setOnPreferenceClickListener {
|
|
||||||
UriUtil.gotoUrl(context!!, R.string.clipboard_explanation_url)
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +169,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findPreference<Preference>(getString(R.string.settings_autofill_key))?.setOnPreferenceClickListener {
|
||||||
|
startActivity(Intent(context, AutofillSettingsActivity::class.java))
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
findPreference<Preference>(getString(R.string.clipboard_explanation_key))?.setOnPreferenceClickListener {
|
||||||
|
UriUtil.gotoUrl(context!!, R.string.clipboard_explanation_url)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
// Present in two places
|
// Present in two places
|
||||||
allowCopyPassword()
|
allowCopyPassword()
|
||||||
}
|
}
|
||||||
@@ -248,13 +253,19 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric(
|
BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric(
|
||||||
activity,
|
activity,
|
||||||
object : BiometricUnlockDatabaseHelper.BiometricUnlockErrorCallback {
|
object : BiometricUnlockDatabaseHelper.BiometricUnlockErrorCallback {
|
||||||
override fun onInvalidKeyException(e: Exception) {}
|
fun showException(e: Exception) {
|
||||||
|
|
||||||
override fun onBiometricException(e: Exception) {
|
|
||||||
Toast.makeText(context,
|
Toast.makeText(context,
|
||||||
getString(R.string.biometric_scanning_error, e.localizedMessage),
|
getString(R.string.biometric_scanning_error, e.localizedMessage),
|
||||||
Toast.LENGTH_SHORT).show()
|
Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onInvalidKeyException(e: Exception) {
|
||||||
|
showException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBiometricException(e: Exception) {
|
||||||
|
showException(e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
CipherDatabaseAction.getInstance(context.applicationContext).deleteAll()
|
CipherDatabaseAction.getInstance(context.applicationContext).deleteAll()
|
||||||
@@ -276,6 +287,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
private fun onCreateAppearancePreferences(rootKey: String?) {
|
private fun onCreateAppearancePreferences(rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.preferences_appearance, rootKey)
|
setPreferencesFromResource(R.xml.preferences_appearance, rootKey)
|
||||||
|
|
||||||
|
// To change list items appearance
|
||||||
|
PreferencesUtil.APPEARANCE_CHANGED = true
|
||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
findPreference<ListPreference>(getString(R.string.setting_style_key))?.setOnPreferenceChangeListener { _, newValue ->
|
findPreference<ListPreference>(getString(R.string.setting_style_key))?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
var styleEnabled = true
|
var styleEnabled = true
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import com.kunzisoft.androidclearchroma.ChromaUtil
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.lock
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
@@ -551,14 +550,10 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
|
|
||||||
val settingActivity = activity as SettingsActivity?
|
val settingActivity = activity as SettingsActivity?
|
||||||
|
|
||||||
when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.menu_lock -> {
|
|
||||||
settingActivity?.lock()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
settingActivity?.mProgressDialogThread?.startDatabaseSave(!mDatabaseReadOnly)
|
settingActivity?.mProgressDialogThread?.startDatabaseSave(!mDatabaseReadOnly)
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@@ -566,7 +561,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
settingActivity?.let {
|
settingActivity?.let {
|
||||||
MenuUtil.onDefaultMenuOptionsItemSelected(it, item, mDatabaseReadOnly, true)
|
MenuUtil.onDefaultMenuOptionsItemSelected(it, item, mDatabaseReadOnly, true)
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
package com.kunzisoft.keepass.settings
|
package com.kunzisoft.keepass.settings
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.preference.PreferenceManager
|
import android.net.Uri
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
@@ -28,6 +29,38 @@ import java.util.*
|
|||||||
|
|
||||||
object PreferencesUtil {
|
object PreferencesUtil {
|
||||||
|
|
||||||
|
var APPEARANCE_CHANGED = false
|
||||||
|
|
||||||
|
fun saveDefaultDatabasePath(context: Context, defaultDatabaseUri: Uri?) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
prefs?.edit()?.apply {
|
||||||
|
defaultDatabaseUri?.let {
|
||||||
|
putString(context.getString(R.string.default_database_path_key), it.toString())
|
||||||
|
} ?: kotlin.run {
|
||||||
|
remove(context.getString(R.string.default_database_path_key))
|
||||||
|
}
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultDatabasePath(context: Context): String? {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getString(context.getString(R.string.default_database_path_key), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveNodeSort(context: Context,
|
||||||
|
sortNodeEnum: SortNodeEnum,
|
||||||
|
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
prefs?.edit()?.apply {
|
||||||
|
putString(context.getString(R.string.sort_node_key), sortNodeEnum.name)
|
||||||
|
putBoolean(context.getString(R.string.sort_ascending_key), sortNodeParameters.ascending)
|
||||||
|
putBoolean(context.getString(R.string.sort_group_before_key), sortNodeParameters.groupsBefore)
|
||||||
|
putBoolean(context.getString(R.string.sort_recycle_bin_bottom_key), sortNodeParameters.recycleBinBottom)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun rememberDatabaseLocations(context: Context): Boolean {
|
fun rememberDatabaseLocations(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.remember_database_locations_key),
|
return prefs.getBoolean(context.getString(R.string.remember_database_locations_key),
|
||||||
@@ -147,6 +180,13 @@ object PreferencesUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getClipboardTimeout(context: Context): Long {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getString(context.getString(R.string.clipboard_timeout_key),
|
||||||
|
context.getString(R.string.clipboard_timeout_default))?.toLong()
|
||||||
|
?: TimeoutHelper.DEFAULT_TIMEOUT
|
||||||
|
}
|
||||||
|
|
||||||
fun isLockDatabaseWhenScreenShutOffEnable(context: Context): Boolean {
|
fun isLockDatabaseWhenScreenShutOffEnable(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.lock_database_screen_off_key),
|
return prefs.getBoolean(context.getString(R.string.lock_database_screen_off_key),
|
||||||
@@ -159,18 +199,18 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.lock_database_back_root_default))
|
context.resources.getBoolean(R.bool.lock_database_back_root_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showLockDatabaseButton(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.lock_database_show_button_key),
|
||||||
|
context.resources.getBoolean(R.bool.lock_database_show_button_default))
|
||||||
|
}
|
||||||
|
|
||||||
fun isAutoSaveDatabaseEnabled(context: Context): Boolean {
|
fun isAutoSaveDatabaseEnabled(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.enable_auto_save_database_key),
|
return prefs.getBoolean(context.getString(R.string.enable_auto_save_database_key),
|
||||||
context.resources.getBoolean(R.bool.enable_auto_save_database_default))
|
context.resources.getBoolean(R.bool.enable_auto_save_database_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isPersistentNotificationEnable(context: Context): Boolean {
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
return prefs.getBoolean(context.getString(R.string.persistent_notification_key),
|
|
||||||
context.resources.getBoolean(R.bool.persistent_notification_default))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isBiometricUnlockEnable(context: Context): Boolean {
|
fun isBiometricUnlockEnable(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
|
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
|
||||||
@@ -314,4 +354,10 @@ object PreferencesUtil {
|
|||||||
return prefs.getBoolean(context.getString(R.string.keyboard_key_sound_key),
|
return prefs.getBoolean(context.getString(R.string.keyboard_key_sound_key),
|
||||||
context.resources.getBoolean(R.bool.keyboard_key_sound_default))
|
context.resources.getBoolean(R.bool.keyboard_key_sound_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isAutofillAutoSearchEnable(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.autofill_auto_search_key),
|
||||||
|
context.resources.getBoolean(R.bool.autofill_auto_search_default))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
@@ -47,6 +49,7 @@ open class SettingsActivity
|
|||||||
|
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
|
private var lockView: View? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@@ -84,6 +87,11 @@ open class SettingsActivity
|
|||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
lockView?.setOnClickListener {
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.add(R.id.fragment_container, retrieveMainFragment())
|
.add(R.id.fragment_container, retrieveMainFragment())
|
||||||
@@ -154,6 +162,23 @@ open class SettingsActivity
|
|||||||
keyFile: Uri?) {
|
keyFile: Uri?) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hideOrShowLockButton(key: NestedSettingsFragment.Screen) {
|
||||||
|
if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
|
when (key) {
|
||||||
|
NestedSettingsFragment.Screen.DATABASE,
|
||||||
|
NestedSettingsFragment.Screen.DATABASE_MASTER_KEY,
|
||||||
|
NestedSettingsFragment.Screen.DATABASE_SECURITY -> {
|
||||||
|
lockView?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
lockView?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lockView?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
// this if statement is necessary to navigate through nested and main fragments
|
// this if statement is necessary to navigate through nested and main fragments
|
||||||
if (supportFragmentManager.backStackEntryCount == 0) {
|
if (supportFragmentManager.backStackEntryCount == 0) {
|
||||||
@@ -162,6 +187,7 @@ open class SettingsActivity
|
|||||||
supportFragmentManager.popBackStack()
|
supportFragmentManager.popBackStack()
|
||||||
}
|
}
|
||||||
toolbar?.setTitle(R.string.settings)
|
toolbar?.setTitle(R.string.settings)
|
||||||
|
hideOrShowLockButton(NestedSettingsFragment.Screen.APPLICATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun replaceFragment(key: NestedSettingsFragment.Screen) {
|
private fun replaceFragment(key: NestedSettingsFragment.Screen) {
|
||||||
@@ -173,6 +199,7 @@ open class SettingsActivity
|
|||||||
.commit()
|
.commit()
|
||||||
|
|
||||||
toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key)
|
toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key)
|
||||||
|
hideOrShowLockButton(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) {
|
override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class HashedBlockInputStream(inputStream: InputStream) : InputStream() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val computedHash = messageDigest.digest(buffer)
|
val computedHash = messageDigest.digest(buffer)
|
||||||
if (computedHash == null || computedHash.size != HASH_SIZE) {
|
if (computedHash.size != HASH_SIZE) {
|
||||||
throw IOException("Hash wrong size")
|
throw IOException("Hash wrong size")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import java.io.IOException
|
|||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
class HashedBlockOutputStream : OutputStream {
|
class HashedBlockOutputStream : OutputStream {
|
||||||
|
|
||||||
@@ -61,11 +62,11 @@ class HashedBlockOutputStream : OutputStream {
|
|||||||
override fun close() {
|
override fun close() {
|
||||||
if (bufferPos != 0) {
|
if (bufferPos != 0) {
|
||||||
// Write remaining buffered amount
|
// Write remaining buffered amount
|
||||||
WriteHashedBlock()
|
writeHashedBlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write terminating block
|
// Write terminating block
|
||||||
WriteHashedBlock()
|
writeHashedBlock()
|
||||||
|
|
||||||
flush()
|
flush()
|
||||||
baseStream!!.close()
|
baseStream!!.close()
|
||||||
@@ -82,12 +83,12 @@ class HashedBlockOutputStream : OutputStream {
|
|||||||
var counter = count
|
var counter = count
|
||||||
while (counter > 0) {
|
while (counter > 0) {
|
||||||
if (bufferPos == buffer!!.size) {
|
if (bufferPos == buffer!!.size) {
|
||||||
WriteHashedBlock()
|
writeHashedBlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
val copyLen = Math.min(buffer!!.size - bufferPos, counter)
|
val copyLen = min(buffer!!.size - bufferPos, counter)
|
||||||
|
|
||||||
System.arraycopy(b, currentOffset, buffer, bufferPos, copyLen)
|
System.arraycopy(b, currentOffset, buffer!!, bufferPos, copyLen)
|
||||||
|
|
||||||
currentOffset += copyLen
|
currentOffset += copyLen
|
||||||
bufferPos += copyLen
|
bufferPos += copyLen
|
||||||
@@ -97,21 +98,21 @@ class HashedBlockOutputStream : OutputStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun WriteHashedBlock() {
|
private fun writeHashedBlock() {
|
||||||
baseStream!!.writeUInt(bufferIndex)
|
baseStream!!.writeUInt(bufferIndex)
|
||||||
bufferIndex++
|
bufferIndex++
|
||||||
|
|
||||||
if (bufferPos > 0) {
|
if (bufferPos > 0) {
|
||||||
var md: MessageDigest? = null
|
val messageDigest: MessageDigest
|
||||||
try {
|
try {
|
||||||
md = MessageDigest.getInstance("SHA-256")
|
messageDigest = MessageDigest.getInstance("SHA-256")
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
throw IOException("SHA-256 not implemented here.")
|
throw IOException("SHA-256 not implemented here.")
|
||||||
}
|
}
|
||||||
|
|
||||||
val hash: ByteArray
|
val hash: ByteArray
|
||||||
md!!.update(buffer, 0, bufferPos)
|
messageDigest.update(buffer!!, 0, bufferPos)
|
||||||
hash = md.digest()
|
hash = messageDigest.digest()
|
||||||
/*
|
/*
|
||||||
if ( bufferPos == buffer.length) {
|
if ( bufferPos == buffer.length) {
|
||||||
hash = md.digest(buffer);
|
hash = md.digest(buffer);
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import android.content.ClipData
|
|||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.text.util.Linkify
|
import android.text.util.Linkify
|
||||||
@@ -33,6 +32,7 @@ import android.widget.TextView
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.exception.ClipboardException
|
import com.kunzisoft.keepass.database.exception.ClipboardException
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class ClipboardHelper(private val context: Context) {
|
class ClipboardHelper(private val context: Context) {
|
||||||
@@ -58,13 +58,9 @@ class ClipboardHelper(private val context: Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val clipboardTimeout = PreferencesUtil.getClipboardTimeout(context)
|
||||||
val sClipClear = prefs.getString(context.getString(R.string.clipboard_timeout_key),
|
if (clipboardTimeout > 0) {
|
||||||
context.getString(R.string.clipboard_timeout_default))
|
mTimer.schedule(ClearClipboardTask(context, text), clipboardTimeout)
|
||||||
|
|
||||||
val clipClearTime = (sClipClear ?: "300000").toLong()
|
|
||||||
if (clipClearTime > 0) {
|
|
||||||
mTimer.schedule(ClearClipboardTask(context, text), clipClearTime)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +85,7 @@ class ClipboardHelper(private val context: Context) {
|
|||||||
@Throws(ClipboardException::class)
|
@Throws(ClipboardException::class)
|
||||||
fun copyToClipboard(label: String, value: String) {
|
fun copyToClipboard(label: String, value: String) {
|
||||||
try {
|
try {
|
||||||
getClipboardManager()?.primaryClip = ClipData.newPlainText(label, value)
|
getClipboardManager()?.setPrimaryClip(ClipData.newPlainText(label, value))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw ClipboardException(e)
|
throw ClipboardException(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,53 +46,63 @@ object TimeoutHelper {
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
private fun getLockPendingIntent(context: Context): PendingIntent {
|
private fun getLockPendingIntent(context: Context): PendingIntent {
|
||||||
return PendingIntent.getBroadcast(context,
|
return PendingIntent.getBroadcast(context.applicationContext,
|
||||||
REQUEST_ID,
|
REQUEST_ID,
|
||||||
Intent(LOCK_ACTION),
|
Intent(LOCK_ACTION),
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record the current time to check it later with checkTime
|
* Start the lock timer by creating an alarm,
|
||||||
|
* if the method is recalled with a previous lock timer pending, the previous one is deleted
|
||||||
*/
|
*/
|
||||||
fun recordTime(context: Context) {
|
private fun startLockTimer(context: Context) {
|
||||||
// Record timeout time in case timeout service is killed
|
|
||||||
PreferencesUtil.saveCurrentTime(context)
|
|
||||||
|
|
||||||
if (Database.getInstance().loaded) {
|
if (Database.getInstance().loaded) {
|
||||||
val timeout = PreferencesUtil.getAppTimeout(context)
|
val timeout = PreferencesUtil.getAppTimeout(context)
|
||||||
|
|
||||||
// No timeout don't start timeout service
|
|
||||||
if (timeout != NEVER) {
|
if (timeout != NEVER) {
|
||||||
val triggerTime = System.currentTimeMillis() + timeout
|
// No timeout don't start timeout service
|
||||||
val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
(context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||||
Log.d(TAG, "TimeoutHelper start")
|
val triggerTime = System.currentTimeMillis() + timeout
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
Log.d(TAG, "TimeoutHelper start")
|
||||||
am.setExact(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
} else {
|
alarmManager.setExact(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
||||||
am.set(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
} else {
|
||||||
|
alarmManager.set(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the lock timer currently pending, useful if lock was triggered by another way
|
||||||
|
*/
|
||||||
|
fun cancelLockTimer(context: Context) {
|
||||||
|
(context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||||
|
Log.d(TAG, "TimeoutHelper cancel")
|
||||||
|
alarmManager.cancel(getLockPendingIntent(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record the current time, to check it later with checkTime and start a new lock timer
|
||||||
|
*/
|
||||||
|
fun recordTime(context: Context) {
|
||||||
|
// Record timeout time in case timeout service is killed
|
||||||
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
|
startLockTimer(context)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the time previously record with recordTime and do the [timeoutAction] if timeout
|
* Check the time previously record with recordTime and do the [timeoutAction] if timeout
|
||||||
* if temporarilyDisableTimeout() is called, the function as no effect until releaseTemporarilyDisableTimeoutAndCheckTime() is called
|
* if temporarilyDisableTimeout() is called, the function as no effect until releaseTemporarilyDisableTimeoutAndCheckTime() is called
|
||||||
* return 'false' if timeout, 'true' if in time
|
* return 'false' and send broadcast lock action if timeout, 'true' if in time
|
||||||
*/
|
*/
|
||||||
fun checkTime(context: Context, timeoutAction: (() -> Unit)? = null): Boolean {
|
fun checkTime(context: Context, timeoutAction: (() -> Unit)? = null): Boolean {
|
||||||
// No effect if temporarily disable
|
// No effect if temporarily disable
|
||||||
if (temporarilyDisableTimeout)
|
if (temporarilyDisableTimeout)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
// Cancel the lock PendingIntent
|
|
||||||
if (Database.getInstance().loaded) {
|
|
||||||
val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
|
||||||
Log.d(TAG, "TimeoutHelper cancel")
|
|
||||||
am.cancel(getLockPendingIntent(context))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the timeout has expired
|
// Check whether the timeout has expired
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
|
|
||||||
@@ -115,6 +125,7 @@ object TimeoutHelper {
|
|||||||
if (diff >= appTimeout) {
|
if (diff >= appTimeout) {
|
||||||
// We have timed out
|
// We have timed out
|
||||||
timeoutAction?.invoke()
|
timeoutAction?.invoke()
|
||||||
|
context.sendBroadcast(Intent(LOCK_ACTION))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -142,27 +153,14 @@ object TimeoutHelper {
|
|||||||
/**
|
/**
|
||||||
* Temporarily disable timeout, checkTime() function always return true
|
* Temporarily disable timeout, checkTime() function always return true
|
||||||
*/
|
*/
|
||||||
fun temporarilyDisableTimeout(context: Context) {
|
fun temporarilyDisableTimeout() {
|
||||||
temporarilyDisableTimeout = true
|
temporarilyDisableTimeout = true
|
||||||
|
|
||||||
// Stop the opening notification
|
|
||||||
DatabaseOpenNotificationService.stop(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release the temporarily disable timeout and directly call checkTime()
|
* Release the temporarily disable timeout
|
||||||
*/
|
*/
|
||||||
fun releaseTemporarilyDisableTimeoutAndLockIfTimeout(context: Context): Boolean {
|
fun releaseTemporarilyDisableTimeout() {
|
||||||
temporarilyDisableTimeout = false
|
temporarilyDisableTimeout = false
|
||||||
val inTime = if (context is LockingActivity) {
|
|
||||||
checkTimeAndLockIfTimeout(context)
|
|
||||||
} else {
|
|
||||||
checkTime(context)
|
|
||||||
}
|
|
||||||
if (inTime) {
|
|
||||||
// Start the opening notification
|
|
||||||
DatabaseOpenNotificationService.startIfAllowed(context)
|
|
||||||
}
|
|
||||||
return inTime
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,10 +19,110 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.utils
|
package com.kunzisoft.keepass.utils
|
||||||
|
|
||||||
|
import android.app.AlarmManager
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.ALARM_SERVICE
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
|
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||||
|
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
|
||||||
const val DATABASE_START_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_START_TASK_ACTION"
|
const val DATABASE_START_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_START_TASK_ACTION"
|
||||||
const val DATABASE_STOP_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_STOP_TASK_ACTION"
|
const val DATABASE_STOP_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_STOP_TASK_ACTION"
|
||||||
|
|
||||||
const val LOCK_ACTION = "com.kunzisoft.keepass.LOCK"
|
const val LOCK_ACTION = "com.kunzisoft.keepass.LOCK"
|
||||||
|
|
||||||
const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY_MAGIKEYBOARD"
|
const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY_MAGIKEYBOARD"
|
||||||
|
|
||||||
|
class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
|
||||||
|
|
||||||
|
var mLockPendingIntent: PendingIntent? = null
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
// If allowed, lock and exit
|
||||||
|
if (!TimeoutHelper.temporarilyDisableTimeout) {
|
||||||
|
intent.action?.let {
|
||||||
|
when (it) {
|
||||||
|
Intent.ACTION_SCREEN_ON -> {
|
||||||
|
cancelLockPendingIntent(context)
|
||||||
|
}
|
||||||
|
Intent.ACTION_SCREEN_OFF -> {
|
||||||
|
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) {
|
||||||
|
mLockPendingIntent = PendingIntent.getBroadcast(context,
|
||||||
|
4575,
|
||||||
|
Intent(intent).apply {
|
||||||
|
action = LOCK_ACTION
|
||||||
|
},
|
||||||
|
0)
|
||||||
|
// Launch the effective action after a small time
|
||||||
|
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
|
||||||
|
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
alarmManager?.setExact(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent)
|
||||||
|
} else {
|
||||||
|
alarmManager?.set(AlarmManager.RTC_WAKEUP, first, mLockPendingIntent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cancelLockPendingIntent(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOCK_ACTION,
|
||||||
|
REMOVE_ENTRY_MAGIKEYBOARD_ACTION -> lockAction.invoke()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelLockPendingIntent(context: Context) {
|
||||||
|
mLockPendingIntent?.let {
|
||||||
|
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?
|
||||||
|
alarmManager?.cancel(mLockPendingIntent)
|
||||||
|
mLockPendingIntent = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.registerLockReceiver(lockReceiver: LockReceiver?,
|
||||||
|
registerRemoveEntryMagikeyboard: Boolean = false) {
|
||||||
|
lockReceiver?.let {
|
||||||
|
registerReceiver(it, IntentFilter().apply {
|
||||||
|
addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
|
addAction(Intent.ACTION_SCREEN_ON)
|
||||||
|
addAction(LOCK_ACTION)
|
||||||
|
if (registerRemoveEntryMagikeyboard)
|
||||||
|
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.unregisterLockReceiver(lockReceiver: LockReceiver?) {
|
||||||
|
lockReceiver?.let {
|
||||||
|
unregisterReceiver(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.closeDatabase() {
|
||||||
|
// Stop the Magikeyboard service
|
||||||
|
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||||
|
MagikIME.removeEntry(this)
|
||||||
|
|
||||||
|
// Stop the notification service
|
||||||
|
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||||
|
|
||||||
|
Log.i(Context::class.java.name, "Shutdown after inactivity or manual lock")
|
||||||
|
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?)?.apply {
|
||||||
|
cancelAll()
|
||||||
|
}
|
||||||
|
// Clear data
|
||||||
|
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||||
|
}
|
||||||
@@ -21,25 +21,77 @@ package com.kunzisoft.keepass.utils
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class FileDatabaseInfo : FileInfo {
|
class FileDatabaseInfo : Serializable {
|
||||||
|
|
||||||
constructor(context: Context, fileUri: Uri): super(context, fileUri)
|
private var context: Context
|
||||||
|
private var documentFile: DocumentFile? = null
|
||||||
|
var fileUri: Uri?
|
||||||
|
private set
|
||||||
|
|
||||||
constructor(context: Context, filePath: String): super(context, filePath)
|
constructor(context: Context, fileUri: Uri) {
|
||||||
|
this.context = context
|
||||||
|
this.fileUri = fileUri
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context, filePath: String) {
|
||||||
|
this.context = context
|
||||||
|
this.fileUri = UriUtil.parse(filePath)
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
documentFile = UriUtil.getFileData(context, fileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
var exists: Boolean = false
|
||||||
|
get() {
|
||||||
|
return documentFile?.exists() ?: field
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
|
||||||
|
var canRead: Boolean = false
|
||||||
|
get() {
|
||||||
|
return documentFile?.canRead() ?: field
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
|
||||||
|
var canWrite: Boolean = false
|
||||||
|
get() {
|
||||||
|
return documentFile?.canWrite() ?: field
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun getModificationString(): String? {
|
||||||
|
return documentFile?.lastModified()?.let {
|
||||||
|
DateFormat.getDateTimeInstance()
|
||||||
|
.format(Date(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSizeString(): String? {
|
||||||
|
return documentFile?.let {
|
||||||
|
Formatter.formatFileSize(context, it.length())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun retrieveDatabaseAlias(alias: String): String {
|
fun retrieveDatabaseAlias(alias: String): String {
|
||||||
return when {
|
return when {
|
||||||
alias.isNotEmpty() -> alias
|
alias.isNotEmpty() -> alias
|
||||||
PreferencesUtil.isFullFilePathEnable(context) -> filePath ?: ""
|
PreferencesUtil.isFullFilePathEnable(context) -> fileUri?.path ?: ""
|
||||||
else -> fileName ?: ""
|
else -> if (exists) documentFile?.name ?: "" else fileUri?.path ?: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveDatabaseTitle(titleCallback: (String)->Unit) {
|
fun retrieveDatabaseTitle(titleCallback: (String)->Unit) {
|
||||||
|
|
||||||
fileUri?.let { fileUri ->
|
fileUri?.let { fileUri ->
|
||||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||||
.getFileDatabaseHistory(fileUri) { fileDatabaseHistoryEntity ->
|
.getFileDatabaseHistory(fileUri) { fileDatabaseHistoryEntity ->
|
||||||
@@ -48,5 +100,4 @@ class FileDatabaseInfo : FileInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,84 +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.utils
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.text.format.Formatter
|
|
||||||
import java.io.Serializable
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
open class FileInfo : Serializable {
|
|
||||||
|
|
||||||
var context: Context
|
|
||||||
var fileUri: Uri?
|
|
||||||
var filePath: String? = null
|
|
||||||
var fileName: String? = ""
|
|
||||||
var lastModification = Date(0L)
|
|
||||||
var size: Long = 0L
|
|
||||||
|
|
||||||
constructor(context: Context, fileUri: Uri) {
|
|
||||||
this.context = context
|
|
||||||
this.fileUri = fileUri
|
|
||||||
init()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context, filePath: String) {
|
|
||||||
this.context = context
|
|
||||||
this.fileUri = UriUtil.parse(filePath)
|
|
||||||
init()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun init() {
|
|
||||||
this.filePath = fileUri?.path
|
|
||||||
|
|
||||||
UriUtil.getFileData(context, fileUri)?.let { file ->
|
|
||||||
size = file.length()
|
|
||||||
fileName = file.name
|
|
||||||
lastModification = Date(file.lastModified())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileName == null || fileName!!.isEmpty()) {
|
|
||||||
fileName = filePath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun lastModificationAccessible(): Boolean {
|
|
||||||
return lastModification.after(Date(0L))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sizeAccessible(): Boolean {
|
|
||||||
return size != 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dataAccessible(): Boolean {
|
|
||||||
return UriUtil.isUriAccessible(context.contentResolver, fileUri)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getModificationString(): String {
|
|
||||||
return DateFormat.getDateTimeInstance()
|
|
||||||
.format(lastModification)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSizeString(): String {
|
|
||||||
return Formatter.formatFileSize(context, size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -100,5 +100,5 @@ object ParcelableUtil {
|
|||||||
inline fun <reified T : Enum<T>> Parcel.readEnum() =
|
inline fun <reified T : Enum<T>> Parcel.readEnum() =
|
||||||
readString()?.let { enumValueOf<T>(it) }
|
readString()?.let { enumValueOf<T>(it) }
|
||||||
|
|
||||||
inline fun <T : Enum<T>> Parcel.writeEnum(value: T?) =
|
fun <T : Enum<T>> Parcel.writeEnum(value: T?) =
|
||||||
writeString(value?.name)
|
writeString(value?.name)
|
||||||
|
|||||||
@@ -19,8 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.utils
|
package com.kunzisoft.keepass.utils
|
||||||
|
|
||||||
import java.util.ArrayList
|
import java.util.*
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
object StringUtil {
|
object StringUtil {
|
||||||
|
|
||||||
@@ -85,5 +84,17 @@ object StringUtil {
|
|||||||
|
|
||||||
return currentText
|
return currentText
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UUID.toKeePassRefString(): String {
|
||||||
|
val tempString = toString().replace("-", "").toUpperCase(Locale.ENGLISH)
|
||||||
|
return StringBuffer(reverseString2(tempString.substring(12, 16)))
|
||||||
|
.append(reverseString2(tempString.substring(8, 12)))
|
||||||
|
.append(reverseString2(tempString.substring(0, 8)))
|
||||||
|
.append(reverseString2(tempString.substring(20, 32)))
|
||||||
|
.append(reverseString2(tempString.substring(16, 20))).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reverseString2(string: String): String {
|
||||||
|
return string.chunked(2).reversed().joinToString("")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -37,31 +36,6 @@ import java.util.*
|
|||||||
|
|
||||||
object UriUtil {
|
object UriUtil {
|
||||||
|
|
||||||
fun isUriAccessible(contentResolver: ContentResolver, fileUri: Uri?): Boolean {
|
|
||||||
if (fileUri == null)
|
|
||||||
return false
|
|
||||||
return try {
|
|
||||||
//https://developer.android.com/reference/android/content/res/AssetFileDescriptor
|
|
||||||
contentResolver.openAssetFileDescriptor(fileUri, "r")?.close()
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(UriUtil.javaClass.name, "Unable to access uri $fileUri : ${e.message}")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isUriNotWritable(contentResolver: ContentResolver, fileUri: Uri?): Boolean {
|
|
||||||
if (fileUri == null)
|
|
||||||
return true
|
|
||||||
return try {
|
|
||||||
contentResolver.openAssetFileDescriptor(fileUri, "wa")?.close()
|
|
||||||
false
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(UriUtil.javaClass.name, "Unable to access uri $fileUri : ${e.message}")
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFileData(context: Context, fileUri: Uri?): DocumentFile? {
|
fun getFileData(context: Context, fileUri: Uri?): DocumentFile? {
|
||||||
if (fileUri == null)
|
if (fileUri == null)
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun inflate(context: Context) {
|
private fun inflate(context: Context) {
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
inflater.inflate(R.layout.view_button_add_node, this)
|
inflater?.inflate(R.layout.view_button_add_node, this)
|
||||||
|
|
||||||
addEntryEnable = true
|
addEntryEnable = true
|
||||||
addGroupEnable = true
|
addGroupEnable = true
|
||||||
@@ -132,7 +132,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun showButton() {
|
fun showButton() {
|
||||||
if (addButtonView?.visibility != VISIBLE)
|
if (isEnable && addButtonView?.visibility != VISIBLE)
|
||||||
addButtonView?.show(onAddButtonVisibilityChangedListener)
|
addButtonView?.show(onAddButtonVisibilityChangedListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
inflater.inflate(R.layout.view_advanced_unlock, this)
|
inflater?.inflate(R.layout.view_advanced_unlock, this)
|
||||||
|
|
||||||
unlockContainerView = findViewById(R.id.fingerprint_container)
|
unlockContainerView = findViewById(R.id.fingerprint_container)
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package com.kunzisoft.keepass.view
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.text.util.Linkify
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -31,6 +32,7 @@ import android.widget.TextView
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
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 com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsAdapter
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsAdapter
|
||||||
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
||||||
@@ -40,8 +42,8 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
|
|||||||
import com.kunzisoft.keepass.model.EntryAttachment
|
import com.kunzisoft.keepass.model.EntryAttachment
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
|
import com.kunzisoft.keepass.utils.toKeePassRefString
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
|
||||||
|
|
||||||
|
|
||||||
class EntryContentsView @JvmOverloads constructor(context: Context,
|
class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||||
@@ -70,8 +72,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
private val urlContainerView: View
|
private val urlContainerView: View
|
||||||
private val urlView: TextView
|
private val urlView: TextView
|
||||||
|
|
||||||
private val commentContainerView: View
|
private val notesContainerView: View
|
||||||
private val commentView: TextView
|
private val notesView: TextView
|
||||||
|
|
||||||
private val extrasContainerView: View
|
private val extrasContainerView: View
|
||||||
private val extrasView: ViewGroup
|
private val extrasView: ViewGroup
|
||||||
@@ -91,6 +93,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
private val historyAdapter = EntryHistoryAdapter(context)
|
private val historyAdapter = EntryHistoryAdapter(context)
|
||||||
|
|
||||||
private val uuidView: TextView
|
private val uuidView: TextView
|
||||||
|
private val uuidReferenceView: TextView
|
||||||
|
|
||||||
val isUserNamePresent: Boolean
|
val isUserNamePresent: Boolean
|
||||||
get() = userNameContainerView.visibility == View.VISIBLE
|
get() = userNameContainerView.visibility == View.VISIBLE
|
||||||
@@ -99,8 +102,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
get() = passwordContainerView.visibility == View.VISIBLE
|
get() = passwordContainerView.visibility == View.VISIBLE
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
inflater.inflate(R.layout.view_entry_contents, this)
|
inflater?.inflate(R.layout.view_entry_contents, this)
|
||||||
|
|
||||||
userNameContainerView = findViewById(R.id.entry_user_name_container)
|
userNameContainerView = findViewById(R.id.entry_user_name_container)
|
||||||
userNameView = findViewById(R.id.entry_user_name)
|
userNameView = findViewById(R.id.entry_user_name)
|
||||||
@@ -118,8 +121,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
urlContainerView = findViewById(R.id.entry_url_container)
|
urlContainerView = findViewById(R.id.entry_url_container)
|
||||||
urlView = findViewById(R.id.entry_url)
|
urlView = findViewById(R.id.entry_url)
|
||||||
|
|
||||||
commentContainerView = findViewById(R.id.entry_notes_container)
|
notesContainerView = findViewById(R.id.entry_notes_container)
|
||||||
commentView = findViewById(R.id.entry_notes)
|
notesView = findViewById(R.id.entry_notes)
|
||||||
|
|
||||||
extrasContainerView = findViewById(R.id.extra_strings_container)
|
extrasContainerView = findViewById(R.id.extra_strings_container)
|
||||||
extrasView = findViewById(R.id.extra_strings)
|
extrasView = findViewById(R.id.extra_strings)
|
||||||
@@ -146,6 +149,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
uuidView = findViewById(R.id.entry_UUID)
|
uuidView = findViewById(R.id.entry_UUID)
|
||||||
|
uuidReferenceView = findViewById(R.id.entry_UUID_reference)
|
||||||
|
|
||||||
val attrColorAccent = intArrayOf(R.attr.colorAccent)
|
val attrColorAccent = intArrayOf(R.attr.colorAccent)
|
||||||
val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent)
|
val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent)
|
||||||
@@ -286,15 +290,17 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
fun assignComment(comment: String?) {
|
fun assignComment(comment: String?) {
|
||||||
if (comment != null && comment.isNotEmpty()) {
|
if (comment != null && comment.isNotEmpty()) {
|
||||||
commentContainerView.visibility = View.VISIBLE
|
notesContainerView.visibility = View.VISIBLE
|
||||||
commentView.apply {
|
notesView.apply {
|
||||||
text = comment
|
text = comment
|
||||||
if (fontInVisibility)
|
if (fontInVisibility)
|
||||||
applyFontVisibility()
|
applyFontVisibility()
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
Linkify.addLinks(notesView, Linkify.ALL)
|
||||||
|
} catch (e: Exception) {}
|
||||||
} else {
|
} else {
|
||||||
commentContainerView.visibility = View.GONE
|
notesContainerView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,6 +352,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
fun assignUUID(uuid: UUID) {
|
fun assignUUID(uuid: UUID) {
|
||||||
uuidView.text = uuid.toString()
|
uuidView.text = uuid.toString()
|
||||||
|
uuidReferenceView.text = uuid.toKeePassRefString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------
|
/* -------------
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ open class EntryCustomField @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
inflater.inflate(R.layout.item_entry_new_field, this)
|
inflater?.inflate(R.layout.item_entry_new_field, this)
|
||||||
|
|
||||||
labelView = findViewById(R.id.title)
|
labelView = findViewById(R.id.title)
|
||||||
valueView = findViewById(R.id.value)
|
valueView = findViewById(R.id.value)
|
||||||
|
|||||||
@@ -21,21 +21,21 @@ package com.kunzisoft.keepass.view
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.EditText
|
import android.widget.*
|
||||||
import android.widget.ImageView
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import android.widget.LinearLayout
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
|
||||||
import com.kunzisoft.keepass.model.Field
|
import com.kunzisoft.keepass.model.Field
|
||||||
|
import org.joda.time.Duration
|
||||||
|
import org.joda.time.Instant
|
||||||
|
|
||||||
class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
@@ -52,16 +52,26 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
|||||||
private val entryPasswordLayoutView: TextInputLayout
|
private val entryPasswordLayoutView: TextInputLayout
|
||||||
private val entryPasswordView: EditText
|
private val entryPasswordView: EditText
|
||||||
private val entryConfirmationPasswordView: EditText
|
private val entryConfirmationPasswordView: EditText
|
||||||
val generatePasswordView: View
|
private val entryExpiresCheckBox: CompoundButton
|
||||||
private val entryCommentView: EditText
|
private val entryExpiresTextView: TextView
|
||||||
|
private val entryNotesView: EditText
|
||||||
private val entryExtraFieldsContainer: ViewGroup
|
private val entryExtraFieldsContainer: ViewGroup
|
||||||
val addNewFieldButton: View
|
|
||||||
|
|
||||||
private var iconColor: Int = 0
|
private var iconColor: Int = 0
|
||||||
|
private var expiresInstant: DateInstant = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
|
||||||
|
|
||||||
|
var onDateClickListener: OnClickListener? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
if (entryExpiresCheckBox.isChecked)
|
||||||
|
entryExpiresTextView.setOnClickListener(value)
|
||||||
|
else
|
||||||
|
entryExpiresTextView.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
inflater.inflate(R.layout.view_entry_edit_contents, this)
|
inflater?.inflate(R.layout.view_entry_edit_contents, this)
|
||||||
|
|
||||||
entryTitleLayoutView = findViewById(R.id.entry_edit_container_title)
|
entryTitleLayoutView = findViewById(R.id.entry_edit_container_title)
|
||||||
entryTitleView = findViewById(R.id.entry_edit_title)
|
entryTitleView = findViewById(R.id.entry_edit_title)
|
||||||
@@ -71,10 +81,14 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
|||||||
entryPasswordLayoutView = findViewById(R.id.entry_edit_container_password)
|
entryPasswordLayoutView = findViewById(R.id.entry_edit_container_password)
|
||||||
entryPasswordView = findViewById(R.id.entry_edit_password)
|
entryPasswordView = findViewById(R.id.entry_edit_password)
|
||||||
entryConfirmationPasswordView = findViewById(R.id.entry_edit_confirmation_password)
|
entryConfirmationPasswordView = findViewById(R.id.entry_edit_confirmation_password)
|
||||||
generatePasswordView = findViewById(R.id.entry_edit_generate_button)
|
entryExpiresCheckBox = findViewById(R.id.entry_edit_expires_checkbox)
|
||||||
entryCommentView = findViewById(R.id.entry_edit_notes)
|
entryExpiresTextView = findViewById(R.id.entry_edit_expires_text)
|
||||||
|
entryNotesView = findViewById(R.id.entry_edit_notes)
|
||||||
entryExtraFieldsContainer = findViewById(R.id.entry_edit_advanced_container)
|
entryExtraFieldsContainer = findViewById(R.id.entry_edit_advanced_container)
|
||||||
addNewFieldButton = findViewById(R.id.entry_edit_add_new_field)
|
|
||||||
|
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
@@ -141,43 +155,61 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnPasswordGeneratorClickListener(clickListener: () -> Unit) {
|
private fun assignExpiresDateText() {
|
||||||
generatePasswordView.setOnClickListener { clickListener.invoke() }
|
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
||||||
|
entryExpiresTextView.setOnClickListener(onDateClickListener)
|
||||||
|
expiresInstant.getDateTimeString(resources)
|
||||||
|
} else {
|
||||||
|
entryExpiresTextView.setOnClickListener(null)
|
||||||
|
resources.getString(R.string.never)
|
||||||
|
}
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryExpiresTextView.applyFontVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var expires: Boolean
|
||||||
|
get() {
|
||||||
|
return entryExpiresCheckBox.isChecked
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryExpiresCheckBox.isChecked = value
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiresDate: DateInstant
|
||||||
|
get() {
|
||||||
|
return expiresInstant
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
expiresInstant = value
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
var notes: String
|
var notes: String
|
||||||
get() {
|
get() {
|
||||||
return entryCommentView.text.toString()
|
return entryNotesView.text.toString()
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
entryCommentView.setText(value)
|
entryNotesView.setText(value)
|
||||||
if (fontInVisibility)
|
if (fontInVisibility)
|
||||||
entryCommentView.applyFontVisibility()
|
entryNotesView.applyFontVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun allowCustomField(allow: Boolean, action: () -> Unit) {
|
|
||||||
addNewFieldButton.apply {
|
|
||||||
if (allow) {
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
setOnClickListener { action.invoke() }
|
|
||||||
} else {
|
|
||||||
visibility = View.GONE
|
|
||||||
setOnClickListener(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val customFields: MutableList<Field>
|
val customFields: MutableList<Field>
|
||||||
get() {
|
get() {
|
||||||
val customFieldsArray = ArrayList<Field>()
|
val customFieldsArray = ArrayList<Field>()
|
||||||
// Add extra fields from views
|
// Add extra fields from views
|
||||||
entryExtraFieldsContainer.let {
|
entryExtraFieldsContainer.let {
|
||||||
for (i in 0 until it.childCount) {
|
try {
|
||||||
val view = it.getChildAt(i) as EntryEditCustomField
|
for (i in 0 until it.childCount) {
|
||||||
val key = view.label
|
val view = it.getChildAt(i) as EntryEditCustomField
|
||||||
val value = view.value
|
val key = view.label
|
||||||
val protect = view.isProtected
|
val value = view.value
|
||||||
customFieldsArray.add(Field(key, ProtectedString(protect, value)))
|
val protect = view.isProtected
|
||||||
|
customFieldsArray.add(Field(key, ProtectedString(protect, value)))
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
// Extra field container contains another type of view
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return customFieldsArray
|
return customFieldsArray
|
||||||
@@ -187,11 +219,14 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
|||||||
* Add a new view to fill in the information of the customized field and focus it
|
* Add a new view to fill in the information of the customized field and focus it
|
||||||
*/
|
*/
|
||||||
fun addEmptyCustomField() {
|
fun addEmptyCustomField() {
|
||||||
val entryEditCustomField = EntryEditCustomField(context).apply {
|
// Fix current custom field before adding a new one
|
||||||
setFontVisibility(fontInVisibility)
|
if (isValid()) {
|
||||||
requestFocus()
|
val entryEditCustomField = EntryEditCustomField(context).apply {
|
||||||
|
setFontVisibility(fontInVisibility)
|
||||||
|
requestFocus()
|
||||||
|
}
|
||||||
|
entryExtraFieldsContainer.addView(entryEditCustomField)
|
||||||
}
|
}
|
||||||
entryExtraFieldsContainer.addView(entryEditCustomField)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -226,34 +261,34 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
|||||||
* @return ErrorValidation An error with a message or a validation without message
|
* @return ErrorValidation An error with a message or a validation without message
|
||||||
*/
|
*/
|
||||||
fun isValid(): Boolean {
|
fun isValid(): Boolean {
|
||||||
var isValid = true
|
|
||||||
|
|
||||||
// Require title
|
|
||||||
if (entryTitleView.text.toString().isEmpty()) {
|
|
||||||
entryTitleLayoutView.error = context.getString(R.string.error_title_required)
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
entryTitleLayoutView.error = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate password
|
// Validate password
|
||||||
if (entryPasswordView.text.toString() != entryConfirmationPasswordView.text.toString()) {
|
if (entryPasswordView.text.toString() != entryConfirmationPasswordView.text.toString()) {
|
||||||
entryPasswordLayoutView.error = context.getString(R.string.error_pass_match)
|
entryPasswordLayoutView.error = context.getString(R.string.error_pass_match)
|
||||||
isValid = false
|
return false
|
||||||
} else {
|
} else {
|
||||||
entryPasswordLayoutView.error = null
|
entryPasswordLayoutView.error = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate extra fields
|
// Validate extra fields
|
||||||
entryExtraFieldsContainer.let {
|
entryExtraFieldsContainer.let {
|
||||||
for (i in 0 until it.childCount) {
|
try {
|
||||||
val entryEditCustomField = it.getChildAt(i) as EntryEditCustomField
|
val customFieldLabelSet = HashSet<String>()
|
||||||
if (!entryEditCustomField.isValid()) {
|
for (i in 0 until it.childCount) {
|
||||||
isValid = false
|
val entryEditCustomField = it.getChildAt(i) as EntryEditCustomField
|
||||||
|
if (customFieldLabelSet.contains(entryEditCustomField.label)) {
|
||||||
|
entryEditCustomField.setError(R.string.error_label_exists)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
customFieldLabelSet.add(entryEditCustomField.label)
|
||||||
|
if (!entryEditCustomField.isValid()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return isValid
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,7 @@ import android.widget.CompoundButton
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
|
|
||||||
@@ -54,8 +55,8 @@ class EntryEditCustomField @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
inflater.inflate(R.layout.view_entry_new_field, this)
|
inflater?.inflate(R.layout.view_entry_new_field, this)
|
||||||
|
|
||||||
val deleteView = findViewById<View>(R.id.entry_new_field_delete)
|
val deleteView = findViewById<View>(R.id.entry_new_field_delete)
|
||||||
deleteView.setOnClickListener { deleteViewFromParent() }
|
deleteView.setOnClickListener { deleteViewFromParent() }
|
||||||
@@ -84,14 +85,20 @@ class EntryEditCustomField @JvmOverloads constructor(context: Context,
|
|||||||
fun isValid(): Boolean {
|
fun isValid(): Boolean {
|
||||||
// Validate extra field
|
// Validate extra field
|
||||||
if (label.isEmpty()) {
|
if (label.isEmpty()) {
|
||||||
labelLayoutView.error = context.getString(R.string.error_string_key)
|
setError(R.string.error_string_key)
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
labelLayoutView.error = null
|
setError(null)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setError(@StringRes errorId: Int?) {
|
||||||
|
labelLayoutView.error = if (errorId == null) null else {
|
||||||
|
context.getString(errorId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setFontVisibility(applyFontVisibility: Boolean) {
|
fun setFontVisibility(applyFontVisibility: Boolean) {
|
||||||
if (applyFontVisibility)
|
if (applyFontVisibility)
|
||||||
valueView.applyFontVisibility()
|
valueView.applyFontVisibility()
|
||||||
@@ -103,7 +110,7 @@ class EntryEditCustomField @JvmOverloads constructor(context: Context,
|
|||||||
parent.removeView(this)
|
parent.removeView(this)
|
||||||
parent.invalidate()
|
parent.invalidate()
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
Log.e(javaClass.name, e.message)
|
Log.e(javaClass.name, "Unable to delete view", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.kunzisoft.keepass.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
class KeyFileSelectionView @JvmOverloads constructor(context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyle: Int = 0)
|
||||||
|
: ConstraintLayout(context, attrs, defStyle) {
|
||||||
|
|
||||||
|
private var mUri: Uri? = null
|
||||||
|
|
||||||
|
private val keyFileNameInputLayout: TextInputLayout
|
||||||
|
private val keyFileNameView: TextView
|
||||||
|
private val keyFileOpenView: ImageView
|
||||||
|
|
||||||
|
init {
|
||||||
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
|
inflater?.inflate(R.layout.view_keyfile_selection, this)
|
||||||
|
|
||||||
|
keyFileNameInputLayout = findViewById(R.id.input_entry_keyfile)
|
||||||
|
keyFileNameView = findViewById(R.id.keyfile_name)
|
||||||
|
keyFileOpenView = findViewById(R.id.keyfile_open_button)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setOnClickListener(l: OnClickListener?) {
|
||||||
|
super.setOnClickListener(l)
|
||||||
|
keyFileNameView.setOnClickListener(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setOnLongClickListener(l: OnLongClickListener?) {
|
||||||
|
super.setOnLongClickListener(l)
|
||||||
|
keyFileNameView.setOnLongClickListener(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
var error: CharSequence?
|
||||||
|
get() = keyFileNameInputLayout.error
|
||||||
|
set(value) {
|
||||||
|
keyFileNameInputLayout.error = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var uri: Uri?
|
||||||
|
get() {
|
||||||
|
return mUri
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
mUri = value
|
||||||
|
keyFileNameView.text = value?.let {
|
||||||
|
DocumentFile.fromSingleUri(context, value)?.name ?: ""
|
||||||
|
} ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
package com.kunzisoft.keepass.view
|
package com.kunzisoft.keepass.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace font by monospace, must be called after seText()
|
* Replace font by monospace, must be called after seText()
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ typedef struct _master_key {
|
|||||||
} master_key;
|
} master_key;
|
||||||
|
|
||||||
|
|
||||||
void *generate_key_material(void *arg) {
|
uint32_t generate_key_material(void *arg) {
|
||||||
#if defined(KPD_PROFILE)
|
#if defined(KPD_PROFILE)
|
||||||
struct timespec start, end;
|
struct timespec start, end;
|
||||||
#endif
|
#endif
|
||||||
@@ -435,7 +435,7 @@ void *generate_key_material(void *arg) {
|
|||||||
pthread_mutex_unlock(&mk->lock2);
|
pthread_mutex_unlock(&mk->lock2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (void *)flip;
|
return flip;
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jbyteArray JNICALL Java_com_kunzisoft_keepass_crypto_finalkey_NativeFinalKey_nTransformMasterKey(JNIEnv *env, jobject this, jbyteArray seed, jbyteArray key, jlong rounds) {
|
JNIEXPORT jbyteArray JNICALL Java_com_kunzisoft_keepass_crypto_finalkey_NativeFinalKey_nTransformMasterKey(JNIEnv *env, jobject this, jbyteArray seed, jbyteArray key, jlong rounds) {
|
||||||
@@ -474,12 +474,12 @@ JNIEXPORT jbyteArray JNICALL Java_com_kunzisoft_keepass_crypto_finalkey_NativeFi
|
|||||||
(*env)->GetByteArrayRegion(env, key, 0, MASTER_KEY_SIZE, (jbyte *)mk.key1);
|
(*env)->GetByteArrayRegion(env, key, 0, MASTER_KEY_SIZE, (jbyte *)mk.key1);
|
||||||
|
|
||||||
// step 2: encrypt the hash "rounds" (default: 6000) times
|
// step 2: encrypt the hash "rounds" (default: 6000) times
|
||||||
iret = pthread_create( &t1, NULL, generate_key_material, (void*)&mk );
|
iret = pthread_create( &t1, NULL, (void*)generate_key_material, (void*)&mk );
|
||||||
if( iret != 0 ) {
|
if( iret != 0 ) {
|
||||||
(*env)->ThrowNew(env, bad_arg, "TransformMasterKey: failed to launch thread 1"); // FIXME: get a better exception class for this...
|
(*env)->ThrowNew(env, bad_arg, "TransformMasterKey: failed to launch thread 1"); // FIXME: get a better exception class for this...
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
iret = pthread_create( &t2, NULL, generate_key_material, (void*)&mk );
|
iret = pthread_create( &t2, NULL, (void*)generate_key_material, (void*)&mk );
|
||||||
if( iret != 0 ) {
|
if( iret != 0 ) {
|
||||||
(*env)->ThrowNew(env, bad_arg, "TransformMasterKey: failed to launch thread 2"); // FIXME: get a better exception class for this...
|
(*env)->ThrowNew(env, bad_arg, "TransformMasterKey: failed to launch thread 2"); // FIXME: get a better exception class for this...
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="?attr/colorPrimaryDark" android:state_pressed="true" />
|
||||||
|
<item android:color="@color/grey_dark" android:state_enabled="false" />
|
||||||
|
<item android:color="?android:windowBackground" android:state_enabled="true" />
|
||||||
|
</selector>
|
||||||
@@ -5,8 +5,12 @@
|
|||||||
tools:targetApi="lollipop">
|
tools:targetApi="lollipop">
|
||||||
<item>
|
<item>
|
||||||
<shape>
|
<shape>
|
||||||
<stroke android:color="?attr/colorAccent" android:width="1dp"/>
|
<corners
|
||||||
<solid android:color="@color/transparent"/>
|
android:topLeftRadius="0dp"
|
||||||
|
android:topRightRadius="40dp"
|
||||||
|
android:bottomLeftRadius="0dp"
|
||||||
|
android:bottomRightRadius="0dp"/>
|
||||||
|
<solid android:color="?android:attr/windowBackground"/>
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
</ripple>
|
</ripple>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:color="@color/white"
|
||||||
|
tools:targetApi="lollipop">
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="?android:attr/textColorPrimary" />
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="0dp"
|
||||||
|
android:topRightRadius="40dp"
|
||||||
|
android:bottomLeftRadius="0dp"
|
||||||
|
android:bottomRightRadius="0dp"/>
|
||||||
|
<solid
|
||||||
|
android:color="@color/transparent"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item>
|
|
||||||
<shape>
|
|
||||||
<stroke android:color="@color/grey" android:width="1dp"/>
|
|
||||||
<solid android:color="@color/transparent"/>
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
</layer-list>
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:state_pressed="true">
|
||||||
|
<shape>
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="0dp"
|
||||||
|
android:topRightRadius="40dp"
|
||||||
|
android:bottomLeftRadius="0dp"
|
||||||
|
android:bottomRightRadius="0dp"/>
|
||||||
|
<padding
|
||||||
|
android:left="4dp"
|
||||||
|
android:right="12dp"
|
||||||
|
android:top="18dp"
|
||||||
|
android:bottom="8dp"/>
|
||||||
|
<solid android:color="@color/grey_dark"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="0dp"
|
||||||
|
android:topRightRadius="40dp"
|
||||||
|
android:bottomLeftRadius="0dp"
|
||||||
|
android:bottomRightRadius="0dp"/>
|
||||||
|
<padding
|
||||||
|
android:left="4dp"
|
||||||
|
android:right="12dp"
|
||||||
|
android:top="18dp"
|
||||||
|
android:bottom="8dp"/>
|
||||||
|
<solid android:color="@color/grey"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="@color/white">
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="@color/green" />
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="0dp"
|
||||||
|
android:topRightRadius="40dp"
|
||||||
|
android:bottomLeftRadius="0dp"
|
||||||
|
android:bottomRightRadius="0dp"/>
|
||||||
|
<solid
|
||||||
|
android:color="@color/transparent"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
5
app/src/main/res/drawable/ic_apps_white_24dp.xml
Normal file
5
app/src/main/res/drawable/ic_apps_white_24dp.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_attach_file_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_attach_file_white_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M16.5,6v11.5c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4V5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5v10.5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V6H10v9.5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5V5c0,-2.21 -1.79,-4 -4,-4S7,2.79 7,5v12.5c0,3.04 2.46,5.5 5.5,5.5s5.5,-2.46 5.5,-5.5V6h-1.5z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="24.0"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="M11,17c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1zM11,3v4h2L13,5.08c3.39,0.49 6,3.39 6,6.92 0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-1.68 0.59,-3.22 1.58,-4.42L12,13l1.41,-1.41 -6.8,-6.8v0.02C4.42,6.45 3,9.05 3,12c0,4.97 4.02,9 9,9 4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9h-1zM18,12c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1 0.45,1 1,1 1,-0.45 1,-1zM6,12c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1z"/>
|
|
||||||
</vector>
|
|
||||||
5
app/src/main/res/drawable/ic_check_white_24dp.xml
Normal file
5
app/src/main/res/drawable/ic_check_white_24dp.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||||
|
</vector>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user