Compare commits

...

445 Commits

Author SHA1 Message Date
J-Jamet
977782a9f7 Merge branch 'release/2.5.0.0beta19' 2019-08-14 19:25:54 +02:00
J-Jamet
f100dda20b Fix icon resolution 2019-08-14 18:34:01 +02:00
J-Jamet
9c0a140a17 Change small code 2019-08-14 14:35:26 +02:00
J-Jamet
b4bcaf54ad Change small code 2019-08-14 14:32:57 +02:00
J-Jamet
82c0ca0f3c Kotlinized KDF 2019-08-14 14:08:03 +02:00
J-Jamet
bfbd81e3ee Better key retrieve code 2019-08-14 14:00:08 +02:00
J-Jamet
67fecf3fef Remove sound and vibration for notification 2019-08-14 10:41:12 +02:00
J-Jamet
c65d96802e Add clipboard clear at closing dependency 2019-08-14 09:23:29 +02:00
J-Jamet
64af8ddc2e Remove Request Focus 2019-08-13 19:49:14 +02:00
J-Jamet
732f472146 Fix warnings 2019-08-13 19:33:57 +02:00
J-Jamet
6dc46604a4 Add execution to gradlew 2019-08-13 19:18:46 +02:00
J-Jamet
f0c3071de1 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translation 2019-08-13 19:17:43 +02:00
J-Jamet
b250ad2f8b Upgrade CHANGELOG 2019-08-13 17:41:46 +02:00
J-Jamet
69390a81ab Fix lock with back button 2019-08-13 17:22:57 +02:00
J-Jamet
c398b92eb1 Encapsulate DatabaseTaskNotification 2019-08-13 17:12:35 +02:00
J-Jamet
3018206e2f New icons for notifications 2019-08-13 16:54:36 +02:00
J-Jamet
811d0f2534 Change default value for timeout 2019-08-13 16:17:10 +02:00
J-Jamet
2b33d785ac Add clear database option when close clipboard notification 2019-08-13 16:06:30 +02:00
J-Jamet
c7a8322c5d Change inputText importance for accessibility and autofill 2019-08-13 15:26:46 +02:00
J-Jamet
37e722847a Fix color style and remove colorAccentCompat 2019-08-13 14:19:13 +02:00
J-Jamet
4aa8d892a4 Fix small button color in API 21 2019-08-13 13:41:05 +02:00
J-Jamet
e164062cf3 Remove menu in selection mode 2019-08-13 13:27:09 +02:00
J-Jamet
935d3e1c4b Better notification implementation 2019-08-13 12:36:58 +02:00
J-Jamet
17eadcee2b Better notification implementation 2019-08-13 11:33:42 +02:00
J-Jamet
5ffbe0e9ee New Magikeyboard notification flow #182 2019-08-12 19:37:34 +02:00
J-Jamet
4c3da45141 Fix lock with magikeyboard 2019-08-12 15:28:07 +02:00
J-Jamet
c516ef7c28 Fix remove entry in Magikeyboard when lock in database performed 2019-08-12 14:55:51 +02:00
J-Jamet
dcf654cf0a Add entry title in Magikeyboard 2019-08-12 14:05:23 +02:00
J-Jamet
23f2e5decc Better Magikeyboard buttons width 2019-08-12 13:23:45 +02:00
J-Jamet
ddbf03bc91 Better button style 2019-08-12 12:33:35 +02:00
J-Jamet
94070ea5e0 Fix destroy Magikeyboard service when lock activity 2019-08-12 11:24:21 +02:00
J-Jamet
725d39626f Fix clear data when opening database #285 2019-08-12 11:01:38 +02:00
Zhai2333
25655abbbb Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2019-08-11 16:23:55 +02:00
Zhai2333
60bee79bc5 Translated using Weblate (Chinese (Traditional))
Currently translated at 23.8% (82 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2019-08-11 16:23:55 +02:00
C. Rüdinger
8f9d278d2f Translated using Weblate (German)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-08-11 16:23:54 +02:00
CurlingTongs
dc120135b1 Translated using Weblate (German)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-08-11 16:23:53 +02:00
J-Jamet
d2f56e7472 Retrieve database with a singleton 2019-08-09 23:29:28 +02:00
J-Jamet
3877e1aa22 Update description 2019-08-09 16:27:12 +02:00
J-Jamet
e08e5eca3a Fix entries search 2019-08-09 16:18:56 +02:00
J-Jamet
7321cc067f Fix app:theme deprecation 2019-08-07 14:00:56 +02:00
J-Jamet
43f14e1474 Fix style for pre lollipop devices 2019-08-07 13:50:42 +02:00
J-Jamet
ced6a77819 Add handler to fix password freeze #226 2019-08-07 11:27:11 +02:00
J-Jamet
bca777a97e Rename FingerPrintViewsManager 2019-08-07 11:05:16 +02:00
J-Jamet
5b98129da9 Fix fingerprint 2019-08-07 11:00:13 +02:00
J-Jamet
dc37ab74c1 Fix fingerprint 2019-08-07 10:57:35 +02:00
J-Jamet
ee76a35728 Fix fingerprint 2019-08-07 10:56:04 +02:00
J-Jamet
e31ea8c916 Better fingerprint version code 2019-08-06 20:22:49 +02:00
J-Jamet
41561cb7b6 Add comment fingerprint 2019-08-06 19:41:08 +02:00
J-Jamet
0c8fdca6f3 Fix assist structure for old device 2019-08-06 19:23:20 +02:00
J-Jamet
a50626dafb Encapsulate Fingerprint methods 2019-08-06 19:07:44 +02:00
J-Jamet
ca55081437 Better focus and revert hide credential view 2019-08-06 14:11:52 +02:00
J-Jamet
5cd66d074e Fix layout scroll and landscape for tablet 2019-08-06 12:55:52 +02:00
J-Jamet
9529e3a7ba Better entry top view 2019-08-06 12:14:29 +02:00
J-Jamet
d3e2668d85 Add Hindi source file 2019-08-06 11:54:18 +02:00
J-Jamet
b3b2bb90e1 Setting to delete the password entered after a connection attempt #214 2019-08-06 11:49:06 +02:00
J-Jamet
9c597665bf Hide credentials when opening database #250 2019-08-06 11:00:35 +02:00
J-Jamet
a845436af4 Fix small visual bug 2019-08-05 18:23:13 +02:00
J-Jamet
cabd487fa5 Adjust resize to save form 2019-08-05 18:18:55 +02:00
J-Jamet
9c7c43e7da Move change master key menu in Settings 2019-08-05 18:03:58 +02:00
J-Jamet
0c421c4906 Close button in edit activity 2019-08-05 17:27:03 +02:00
J-Jamet
4df8880b0d Fix classic icon view 2019-08-05 17:21:30 +02:00
J-Jamet
45046ee01a Fix icon cache density issue 2019-08-05 16:43:32 +02:00
J-Jamet
2ff6522b60 Fix visual bug 2019-08-05 16:05:53 +02:00
J-Jamet
ee8c589ea3 Fix extra card view 2019-08-05 13:30:14 +02:00
J-Jamet
b31d40dbb6 Better entry views 2019-08-05 13:24:04 +02:00
J-Jamet
970b966bcb Collapsing Toolbar for entry activity 2019-08-05 12:25:48 +02:00
J-Jamet
6f1dc14bdc Rename layout for better visibility 2019-08-05 09:49:58 +02:00
J-Jamet
949e6f247d Change copying notification priority #281 and fix never timeout 2019-08-03 14:17:58 +02:00
J-Jamet
77a848cf0e Fira mono font #223 2019-08-03 12:00:49 +02:00
J-Jamet
21e268b8c2 Merge branch 'feature/Refactor_Kotlin' into develop 2019-08-02 20:55:47 +02:00
J-Jamet
580c761fa0 New icon selection style and fix views 2019-08-02 20:51:17 +02:00
Ldm Public
e13b12550c Translated using Weblate (French)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-08-02 19:12:28 +02:00
J-Jamet
05dc421ea8 Fix orientation change entry with many bugs #228 2019-08-02 14:59:59 +02:00
Allan Nordhøy
423957d023 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-08-01 07:14:11 +02:00
bluepencil
c3b584973b Translated using Weblate (Korean)
Currently translated at 43.8% (151 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2019-08-01 07:14:10 +02:00
Marco Scaglioni
74e1805970 Translated using Weblate (Italian)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2019-08-01 07:14:02 +02:00
Wilker Santana da Silva
e2743e2c61 Translated using Weblate (English)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-08-01 07:14:02 +02:00
mondstern
59f0b90c72 Translated using Weblate (German)
Currently translated at 99.7% (344 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-08-01 07:14:01 +02:00
Wilker Santana da Silva
c962096fd5 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2019-08-01 07:14:01 +02:00
Mesut Akcan
d9aa9f6cb3 Translated using Weblate (Turkish)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2019-08-01 07:13:53 +02:00
J-Jamet
874e422dbc Add vertical padding to autofill label #262 2019-07-31 15:31:39 +02:00
J-Jamet
63c825c056 Fix modify entry with no parent 2019-07-31 15:20:42 +02:00
J-Jamet
b507fa2a09 Fix long press magikeyboard 2019-07-31 14:54:20 +02:00
J-Jamet
eb2040edbd Fix ref autofill #274 2019-07-31 14:49:16 +02:00
J-Jamet
02ba1dabe7 Fix ref keyboard form field #274 2019-07-31 14:34:18 +02:00
J-Jamet
03494d78a5 Fix dialog preference description 2019-07-31 13:49:30 +02:00
J-Jamet
b598945903 Fix orientation change dimension 2019-07-31 12:19:01 +02:00
J-Jamet
8bf017e14e Allow no password default false 2019-07-30 09:51:57 +02:00
J-Jamet
00e4d77503 Fix crash during swipe keyboard notification #258 2019-07-29 21:55:05 +02:00
J-Jamet
e6efdadb6f Fix Autofill layout background #238 2019-07-29 21:41:18 +02:00
J-Jamet
02a8e7ea75 Fix Autofill setting 2019-07-29 21:02:43 +02:00
J-Jamet
dc9b7591a0 Update CHANGELOG 2019-07-29 20:26:53 +02:00
J-Jamet
7ae0d329e1 Fix bad base64 decoder implementation #222 #140 2019-07-29 20:23:52 +02:00
J-Jamet
bff9ec86ff Change layout to better visibility in landscape mode 2019-07-29 17:35:56 +02:00
J-Jamet
f445fbca3d Merge branch 'feature/Refactor_Kotlin' of github.com:Kunzisoft/KeePassDX into feature/Refactor_Kotlin 2019-07-27 09:35:42 +02:00
J-Jamet
9079ce30a0 Fix preference 2019-07-26 18:21:54 +02:00
J-Jamet
2202f718b9 Add database task notification 2019-07-26 17:08:05 +02:00
J-Jamet
63b80634c2 Fix lock timeout 2019-07-26 11:22:23 +02:00
J-Jamet
9bbfeaba72 Better database creation runnable 2019-07-25 15:23:04 +02:00
J-Jamet
2d640dbe62 Fix database creation index 2019-07-25 14:50:51 +02:00
J-Jamet
2ce3fc3bd6 Fix first Recycle Bin creation 2019-07-24 21:23:43 +02:00
J-Jamet
009710f854 Upgrade CHANGELOG 2019-07-24 16:53:21 +02:00
J-Jamet
7eeff1b50a Add default Recycle Bin name as string resource 2019-07-24 16:49:18 +02:00
J-Jamet
6f736d6415 Better index action implementation 2019-07-24 16:37:24 +02:00
J-Jamet
3dc988dd08 Fix recycleBin 2019-07-24 15:55:52 +02:00
Deleted User
ca292fbb85 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-07-23 19:31:06 +02:00
J-Jamet
53a3fe83cb Fix small code 2019-07-22 13:57:32 +02:00
J-Jamet
d667940157 Fix password encoding 2019-07-22 13:54:58 +02:00
J-Jamet
404fb9b39f Change licence in XML and .kt files 2019-07-22 11:53:12 +02:00
J-Jamet
d1c4bdb8eb Kotlinized V4 XML class 2019-07-22 11:49:49 +02:00
J-Jamet
7b67e4e811 Fix search 2019-07-22 11:26:47 +02:00
J-Jamet
2b387c8613 Fix list rebuild and add button animation 2019-07-20 01:25:19 +02:00
J-Jamet
c7f6cb6747 Fix remove crash 2019-07-20 01:20:19 +02:00
J-Jamet
5e5dc58482 Fix warning 2019-07-20 00:18:13 +02:00
J-Jamet
80d0510b86 Refactor util 2019-07-20 00:03:03 +02:00
J-Jamet
fcd3f7c3fe Refactorize uri util 2019-07-19 23:36:36 +02:00
J-Jamet
38abb9ca49 Refactorize string util 2019-07-19 21:57:50 +02:00
J-Jamet
c47834f470 Fix small crypto code 2019-07-19 19:15:33 +02:00
J-Jamet
b899212d14 Fix small crypto code 2019-07-19 19:06:34 +02:00
J-Jamet
5a11e47653 Fix small crypto code 2019-07-19 18:46:11 +02:00
J-Jamet
d0faf0f1b6 Fix small dialog code 2019-07-19 18:41:53 +02:00
J-Jamet
27de6b456b Kotlinized Stylish 2019-07-19 17:59:29 +02:00
J-Jamet
92f6684ca9 Fix education add button 2019-07-19 16:10:45 +02:00
J-Jamet
59c45f5627 Kotlinized icons 2019-07-19 16:03:40 +02:00
Ldm Public
35c2075f73 Translated using Weblate (French)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-07-18 23:05:52 +02:00
J-Jamet
4b659248f7 Kotlinized Magikeyboard 2019-07-18 21:05:29 +02:00
J-Jamet
3d01e09198 Kotlinized Notification 2019-07-18 19:44:13 +02:00
J-Jamet
a7805d9d4c Kotlinized Password Generator 2019-07-18 18:40:01 +02:00
J-Jamet
2439c0e7cd Kotlinized Preference 2019-07-18 18:19:03 +02:00
J-Jamet
67f99a4430 Kotlinized Fingerprint 2019-07-18 18:00:59 +02:00
J-Jamet
0a8c7700dc Kotlinized views 2019-07-18 16:55:02 +02:00
Mattéo Rossillol‑‑Laruelle
68ca17a0da Translated using Weblate (French)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-07-17 22:28:41 +02:00
J-Jamet
dcc7c4d39e Kotlinized preference 2019-07-17 22:20:07 +02:00
J-Jamet
06fcbf8995 Kotlinized preference dialog fragment and fix result action runnable 2019-07-17 20:46:22 +02:00
J-Jamet
a939d06f77 Kotlinized crypto engine 2019-07-17 16:19:24 +02:00
bluepencil
11e342ae19 Translated using Weblate (Korean)
Currently translated at 0.9% (3 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ko/
2019-07-16 18:25:04 +02:00
J-Jamet
5ac40d13a5 Change Readme.md year 2019-07-16 16:36:21 +02:00
J-Jamet
1dea366cec Fix translation 2019-07-16 15:49:15 +02:00
J-Jamet
7e5ae5a8af Fix small code 2019-07-16 12:22:49 +02:00
bluepencil
4e430b2fd4 Added translation using Weblate (Korean) 2019-07-16 04:20:19 +02:00
Júlio Costa
d61c345630 Translated using Weblate (Portuguese (Brazil))
Currently translated at 45.2% (156 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2019-07-15 20:02:57 +02:00
J-Jamet
0c7aa3fad2 Merge branch 'feature/Refactor_Kotlin' of github.com:Kunzisoft/KeePassDX into feature/Refactor_Kotlin 2019-07-14 17:03:44 +02:00
J-Jamet
8de7e4a326 Upgrade CHANGELOG 2019-07-14 14:02:53 +02:00
J-Jamet
4fd5d72d97 Better ProgressDialog implementation 2019-07-13 19:51:52 +02:00
J-Jamet
d08d2552aa Update Gradle 2019-07-13 16:38:47 +02:00
J-Jamet
6b6968cbbe Update Gradle 2019-07-13 15:38:47 +02:00
Thomas johansen
49157a38ae Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-07-12 21:01:58 +02:00
J-Jamet
402a0d3e43 Kotlinized Fragments 2019-07-11 15:45:05 +02:00
J-Jamet
5cd8cbd514 Fix PwGroupV3 and PwEntryV3 2019-07-11 14:51:20 +02:00
J-Jamet
32537e85a2 Move classes 2019-07-11 13:54:18 +02:00
J-Jamet
d937ca85a2 Kotlinized fragments 2019-07-10 23:37:40 +02:00
J-Jamet
6a5ed7f460 Fix kotlin null crash 2019-07-10 19:10:59 +02:00
J-Jamet
a158e96ba6 Refactor BasicViewHolder 2019-07-10 17:59:59 +02:00
J-Jamet
7ad43dbed7 Kotlinized Activities 2019-07-10 17:47:06 +02:00
J-Jamet
63f261f169 Fix add new entry 2019-07-09 17:12:00 +02:00
J-Jamet
f36849e815 Kotlinized Autofill and BackupAgent 2019-07-07 21:39:27 +02:00
J-Jamet
b0bc0f9d85 Kotlinized Adapters and fix StyledAttributes 2019-07-07 20:53:08 +02:00
J-Jamet
cd271e6f04 Kotlinized ListNodesFragment 2019-07-07 20:22:06 +02:00
J-Jamet
d6bac74e1b Kotlinized GroupActivity 2019-07-07 20:01:17 +02:00
J-Jamet
eda7fa0fe7 Fix NodeHandler parameter 2019-07-07 18:49:37 +02:00
J-Jamet
191e0cc654 Kotlinized EntryEditActivity 2019-07-07 18:48:56 +02:00
J-Jamet
cc35a1e8aa Fix deprecated buttons 2019-07-07 18:47:32 +02:00
J-Jamet
7dfe85450d Kotlinized EntryActivity 2019-07-07 16:37:57 +02:00
J-Jamet
e8b341c69f Refactor Education 2019-07-07 15:23:47 +02:00
J-Jamet
77c397e0d1 Fix small bugs 2019-07-04 19:10:33 +02:00
J-Jamet
15794f09c3 Move database history in singleton 2019-07-04 19:04:42 +02:00
J-Jamet
cdf8baeb10 Kotlinized PwDate 2019-07-04 15:25:47 +02:00
J-Jamet
e4b550afc1 4 spaces files harmonisation #184 2019-07-04 14:34:06 +02:00
WaldiS
49bb34b742 Translated using Weblate (Polish)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-07-04 12:02:05 +02:00
C. Rüdinger
ab01d01fce Translated using Weblate (German)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-06-29 20:02:47 +02:00
Allan Nordhøy
4905c262ba Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.7% (344 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-06-28 15:02:12 +02:00
Allan Nordhøy
ce05bb467e Translated using Weblate (English)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-06-28 15:02:12 +02:00
CurlingTongs
8f0fe89579 Translated using Weblate (German)
Currently translated at 99.7% (344 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-06-28 15:02:11 +02:00
Noel
23bd6e20a4 Translated using Weblate (Spanish)
Currently translated at 49.0% (169 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2019-06-24 17:01:43 +02:00
Yaron Shahrabani
3f0071ac58 Translated using Weblate (Hebrew)
Currently translated at 21.2% (73 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2019-06-18 21:05:56 +02:00
GiulioEl
776660923b Translated using Weblate (Italian)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2019-06-15 09:40:31 +02:00
J-Jamet
32f170f644 Remove unused import 2019-06-10 14:40:08 +02:00
J-Jamet
93222990b5 Kotlinized database package 2019-06-02 16:10:57 +02:00
J-Jamet
8890296beb Kotlinized importers 2019-06-02 14:04:36 +02:00
J-Jamet
9907af8135 Kotlinized many classes 2019-06-02 13:36:05 +02:00
J-Jamet
17ec357c1b Kotlinized PwNode PwEntry PwGroup 2019-06-02 02:41:51 +02:00
J-Jamet
575e967627 Kotlinized PwNode 2019-06-01 15:01:02 +02:00
J-Jamet
0b53e84761 Kotlinized small code 2019-06-01 13:58:41 +02:00
J-Jamet
fadf78aabd Remove PwVersion 2019-06-01 13:51:03 +02:00
J-Jamet
47fce14d16 Kotlinized PwIcon 2019-06-01 13:47:44 +02:00
J-Jamet
149b6fbcd4 Kotlinized PwNodeId 2019-06-01 13:12:51 +02:00
J-Jamet
e649be230e Remove clone 2019-06-01 12:58:28 +02:00
J-Jamet
6ba45c3d87 Better search implementation, kotlinize, start remove clone 2019-06-01 12:34:06 +02:00
J-Jamet
0e701189a3 Fix search 2019-06-01 11:10:42 +02:00
J-Jamet
af316607e1 Fix copy and clone 2019-05-30 21:11:33 +02:00
Allan Nordhøy
548039c66f Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.1% (342 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-05-16 17:49:10 +02:00
J-Jamet
e75a2502d1 Better index implementation 2019-05-10 00:39:31 +02:00
J-Jamet
6be2148951 Fix opening bugs 2019-05-09 23:41:26 +02:00
ButterflyOfFire
f149688da6 Translated using Weblate (Arabic)
Currently translated at 71.0% (245 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2019-05-07 12:49:02 +02:00
J-Jamet
8f77e5874e Refactor database import 2019-05-03 18:33:23 +02:00
J-Jamet
eabd2d3e4c Refactor 2019-05-03 17:15:53 +02:00
J-Jamet
e460a4fe4f Refactor database import 2019-04-24 21:40:28 +02:00
J-Jamet
9e79da0efc Refactor database elements 2019-04-24 20:38:13 +02:00
Tobias Johansson
6851d4a1d3 Translated using Weblate (Swedish)
Currently translated at 21.2% (73 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sv/
2019-04-18 15:05:18 +02:00
J-Jamet
5705d367ed Fix UI thread for ProgressDialogFragment 2019-04-13 18:03:31 +02:00
J-Jamet
d1f88112ce Finish database encapsulation 2019-04-12 22:22:17 +02:00
J-Jamet
eba1dbc8fa Move touch, fix tree construction, refactor getPwDatabase 2019-04-12 21:46:40 +02:00
J-Jamet
dcb819b087 Fix display message Toast bug 2019-04-12 19:19:13 +02:00
J-Jamet
25ced826c4 Refactor tree perform (to debug) 2019-04-12 17:56:07 +02:00
Noel
5a30c3219e Translated using Weblate (Spanish)
Currently translated at 48.4% (167 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2019-04-04 22:04:43 +02:00
Noel
6a5ac3729d Translated using Weblate (German)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-04-04 22:04:39 +02:00
Ldm Public
31326224ac Translated using Weblate (French)
Currently translated at 99.7% (344 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-04-04 22:04:38 +02:00
Semen Turchikhin
811a44574a Translated using Weblate (Russian)
Currently translated at 64.1% (221 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2019-03-26 05:34:41 +01:00
aasami
6782442d8e Translated using Weblate (Slovak)
Currently translated at 25.2% (87 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/sk/
2019-03-25 15:38:42 +01:00
J-Jamet
a376d2f286 Refactor Entry and Group 2019-03-21 17:58:08 +01:00
Rui Mendes
ce3e1c86bc Translated using Weblate (Portuguese (Portugal))
Currently translated at 45.5% (157 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2019-03-21 15:03:56 +01:00
J-Jamet
20f7675135 Refactor database elements (In PROGRESS) 2019-03-15 16:48:59 +01:00
J-Jamet
dad3edee5e Refactor database elements 2019-03-15 10:58:04 +01:00
J-Jamet
1bfdae53c6 Add a warning message in save progress dialog 2019-03-14 18:16:49 +01:00
J-Jamet
e5902b9cf6 First refactoring pass 2019-03-14 18:00:09 +01:00
George
88829ca91a Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2019-03-12 13:03:54 +01:00
abidin toumi
4bdf99b2f4 Translated using Weblate (Arabic)
Currently translated at 66.1% (228 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2019-03-12 13:03:51 +01:00
George
54883bb043 Translated using Weblate (Chinese (Simplified))
Currently translated at 84.6% (292 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2019-03-10 15:03:36 +01:00
abidin toumi
6db55b5eaa Translated using Weblate (Arabic)
Currently translated at 49.9% (172 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2019-03-09 12:03:40 +01:00
Ldm Public
821dc6df63 Translated using Weblate (French)
Currently translated at 99.7% (344 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2019-03-07 17:03:35 +01:00
George
8de86e26d6 Translated using Weblate (Chinese (Simplified))
Currently translated at 83.8% (289 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2019-03-07 17:03:35 +01:00
WaldiS
1cf8b1fc7d Translated using Weblate (Polish)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2019-03-04 15:58:02 +01:00
Nils B
f469fa224e Translated using Weblate (German)
Currently translated at 99.7% (344 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2019-03-04 15:58:01 +01:00
George
56912ddaf7 Translated using Weblate (Chinese (Simplified))
Currently translated at 71.3% (246 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2019-03-04 15:58:00 +01:00
J-Jamet
620b1b4f02 Refactor database runnable and prevent activity to be closed during saving 2019-02-28 16:34:25 +01:00
J-Jamet
2208c67b62 Fix renaming groups #194 2019-02-28 10:46:35 +01:00
abvgeej
6f6b53ff73 Translated using Weblate (Russian)
Currently translated at 64.1% (221 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2019-02-26 11:18:17 +01:00
jan madsen
529197fb7d Translated using Weblate (Danish)
Currently translated at 98.6% (340 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2019-02-26 11:18:14 +01:00
J-Jamet
953e63f55d Add selection mode and fix read only 2019-02-23 14:39:03 +01:00
Mesut Akcan
783cbc634e Translated using Weblate (Turkish)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2019-02-23 08:18:07 +01:00
Casiyre
9ac25b0d74 Translated using Weblate (English)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2019-02-23 08:18:06 +01:00
J-Jamet
5f051ed2a9 Merge branch 'feature/50-Back_Lock' into develop 2019-02-22 17:39:50 +01:00
J-Jamet
b40d2f3b9a Fix keyboard settings 2019-02-22 17:29:17 +01:00
J-Jamet
3f90276ce0 Refactor Timeout 2019-02-22 16:52:02 +01:00
J-Jamet
66e077f8f1 Fix keyboard entry disappear 2019-02-22 16:47:24 +01:00
J-Jamet
8426b1d91f Fix opening activity 2019-02-22 16:13:40 +01:00
J-Jamet
3b55937085 Fix keyboard entry selection 2019-02-22 15:10:07 +01:00
J-Jamet
da86a971b5 Refactor Autofill and Keyboard selection 2019-02-22 13:34:07 +01:00
J-Jamet
ff7f0e0a60 Add option for back lock 2019-02-21 16:25:00 +01:00
J-Jamet
9233b610c2 Refactor keys of preferences 2019-02-21 15:52:00 +01:00
J-Jamet
9dc183b9b7 Update version and Changelog 2019-02-21 14:21:22 +01:00
J-Jamet
d000b6f884 Add lock button always visible 2019-02-21 14:17:26 +01:00
J-Jamet
e0b0dd134f Refactor launch method 2019-02-21 13:50:07 +01:00
J-Jamet
e3d09bff36 Merge branch 'feature/WorkFlow_Connection' into develop 2019-02-21 13:40:26 +01:00
naofum
5e901812ad Translated using Weblate (Japanese)
Currently translated at 32.8% (113 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2019-02-17 10:22:32 +01:00
Balázs Meskó
b7f9ff8c98 Translated using Weblate (Hungarian)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2019-02-12 16:10:26 +01:00
Balázs Meskó
76ed603bf1 Translated using Weblate (Hungarian)
Currently translated at 93.3% (322 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2019-02-11 14:30:17 +01:00
Balázs Meskó
337816b7f2 Translated using Weblate (Hungarian)
Currently translated at 92.2% (318 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2019-02-09 06:09:05 +01:00
Balázs Meskó
37b1566535 Translated using Weblate (Hungarian)
Currently translated at 39.1% (135 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/hu/
2019-02-06 20:55:59 +01:00
J-Jamet
9864d1e62d Fix setting opening 2019-01-24 18:21:44 +01:00
naofum
e3a07377e3 Translated using Weblate (Japanese)
Currently translated at 31.3% (108 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ja/
2019-01-18 08:20:49 +01:00
Hadrián Candela
f54b908dbb Translated using Weblate (Galician)
Currently translated at 8.7% (30 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2019-01-12 11:06:53 +01:00
Allan Nordhøy
e3f3a5faf7 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.6% (340 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2019-01-09 04:06:57 +01:00
Osoitz
2046e15f65 Translated using Weblate (Basque)
Currently translated at 21.4% (74 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/eu/
2018-12-31 18:09:25 +01:00
Ldm Public
4e8f554ae6 Translated using Weblate (French)
Currently translated at 99.7% (344 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2018-12-29 00:09:26 +01:00
Éfrit
0f21040888 Translated using Weblate (French)
Currently translated at 99.7% (344 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2018-12-27 22:32:55 +01:00
J-Jamet
0d86fa0d95 Fix infinite time with keyboard and autofill 2018-12-19 22:38:43 +01:00
J-Jamet
0ad8826b51 New lock implementation 2018-12-19 22:27:06 +01:00
J-Jamet
a535f123ff Fix settings timeout 2018-12-19 19:04:44 +01:00
J-Jamet
4fcb2de2bb Rename lock or reset timeout method 2018-12-19 17:20:37 +01:00
random r
8885192db1 Translated using Weblate (Italian)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2018-12-19 16:09:17 +01:00
J-Jamet
7ebf64939b Merge branch 'feature/Unexpected_Lock' into develop 2018-12-19 12:07:28 +01:00
J-Jamet
6712b0927d Refactor entry views and check focus to reset timeout 2018-12-19 11:58:39 +01:00
J-Jamet
8853bb7618 Move LockingActivity 2018-12-17 19:07:24 +01:00
J-Jamet
cbc8ec9880 Refactor Lock with Kotlin 2018-12-17 19:00:08 +01:00
J-Jamet
6372099cb2 Add Kotlin dependency 2018-12-17 16:04:35 +01:00
Mesut Akcan
15fa114742 Translated using Weblate (Turkish)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2018-12-11 11:08:59 +01:00
wellinkstein
c16ded1377 Translated using Weblate (French)
Currently translated at 98.6% (340 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2018-12-11 05:42:13 +01:00
Mesut Akcan
8ecd0e0eb1 Translated using Weblate (Turkish)
Currently translated at 69.3% (239 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2018-12-08 15:08:40 +01:00
Mesut Akcan
c5895ed61b Translated using Weblate (Turkish)
Currently translated at 69.0% (238 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2018-12-07 14:08:36 +01:00
ssantos
35751844b0 Translated using Weblate (German)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-12-07 14:08:33 +01:00
Mesut Akcan
fd66e9acb8 Translated using Weblate (Turkish)
Currently translated at 56.8% (196 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2018-12-04 10:11:57 +01:00
Mesut Akcan
310ae58388 Translated using Weblate (Turkish)
Currently translated at 51.9% (179 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2018-12-02 12:08:30 +01:00
Bruno Guerreiro
d4a31567cd Translated using Weblate (Portuguese (Portugal))
Currently translated at 36.8% (127 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_PT/
2018-11-30 20:45:31 +01:00
Mesut Akcan
d54abf66fe Translated using Weblate (Turkish)
Currently translated at 48.7% (168 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2018-11-30 11:08:25 +01:00
MESUT AKCAN
792b3b7a81 Translated using Weblate (Turkish)
Currently translated at 33.0% (114 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2018-11-24 11:09:17 +01:00
mohammadA
0345606903 Translated using Weblate (Arabic)
Currently translated at 35.1% (121 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2018-11-24 11:09:13 +01:00
MESUT AKCAN
06aec23c8a Translated using Weblate (Turkish)
Currently translated at 27.2% (94 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2018-11-22 17:07:43 +01:00
liushuyu011
71a77250d0 Translated using Weblate (Chinese (Simplified))
Currently translated at 58.6% (202 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2018-11-20 07:08:32 +01:00
MESUT AKCAN
af32ef24db Translated using Weblate (Turkish)
Currently translated at 20.3% (70 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2018-11-19 01:22:15 +01:00
GoodMirek
214dca1d4e Translated using Weblate (Czech)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2018-11-17 23:07:38 +01:00
C. Rüdinger
af8ca15e29 Translated using Weblate (German)
Currently translated at 98.6% (340 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-11-15 14:07:39 +01:00
MESUT AKCAN
ac71aec39d Translated using Weblate (Turkish)
Currently translated at 13.6% (47 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/tr/
2018-11-13 19:07:48 +01:00
wellinkstein
9b7b363b49 Translated using Weblate (French)
Currently translated at 98.0% (338 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2018-11-13 19:07:44 +01:00
xanadus1
948b54dc58 Translated using Weblate (Chinese (Simplified))
Currently translated at 37.4% (129 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2018-11-13 19:07:38 +01:00
MESUT AKCAN
20553713a8 Added translation using Weblate (Turkish) 2018-11-11 14:14:56 +01:00
TasogareRiiku
1424001ffd Translated using Weblate (Portuguese (Brazil))
Currently translated at 41.7% (144 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2018-11-09 15:07:50 +01:00
Allan Nordhøy
fa1c49aaf2 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.1% (342 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2018-11-09 15:07:50 +01:00
Krombel
316369f117 Translated using Weblate (German)
Currently translated at 97.4% (336 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-11-09 15:07:49 +01:00
ssantos
2253d3c2cc Translated using Weblate (German)
Currently translated at 97.4% (336 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-11-09 15:07:48 +01:00
Kunzisoft
b09bf68b7c Translated using Weblate (French)
Currently translated at 50.4% (174 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2018-11-09 15:07:47 +01:00
jan madsen
526111af98 Translated using Weblate (Danish)
Currently translated at 98.6% (340 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2018-11-09 15:07:47 +01:00
WaldiS
5942a33f42 Translated using Weblate (Polish)
Currently translated at 100.0% (345 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2018-11-04 17:24:15 +01:00
Allan Nordhøy
62b077c4b2 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.1% (342 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb/
2018-11-04 17:24:11 +01:00
C. Rüdinger
89996f09f1 Translated using Weblate (German)
Currently translated at 90.4% (312 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-11-04 17:24:10 +01:00
Kunzisoft
4b760c6361 Translated using Weblate (French)
Currently translated at 50.1% (173 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2018-11-04 17:24:07 +01:00
Heimen Stoffels
293b7e3a05 Translated using Weblate (Dutch)
Currently translated at 77.9% (269 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2018-11-04 17:24:06 +01:00
jan madsen
12686ecb83 Translated using Weblate (Danish)
Currently translated at 79.4% (274 of 345 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2018-11-04 17:24:04 +01:00
J-Jamet
456912b134 Merge tag '2.5.0.0beta18' into develop
2.5.0.0beta18
2018-11-01 17:46:17 +01:00
J-Jamet
88ac2ecf98 Merge branch 'release/2.5.0.0beta18' 2018-11-01 17:45:56 +01:00
J-Jamet
227cb078a5 Fix theme warning 2018-11-01 17:37:37 +01:00
J-Jamet
a17582bf6d Merge branch 'translations' into develop 2018-11-01 17:25:25 +01:00
J-Jamet
e8ba5dc6e2 Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2018-11-01 17:20:33 +01:00
J-Jamet
f37d3324e6 Upgrade app to version 18 2018-10-27 21:57:32 +02:00
J-Jamet
fcb06c8c22 Upgrade changelog 2018-10-27 21:42:00 +02:00
J-Jamet
d8bdf4500a Merge branch 'feature/Attachments' into develop 2018-10-27 21:38:57 +02:00
J-Jamet
69ea4d5414 Fix views 2018-10-27 20:46:17 +02:00
J-Jamet
7e9f99e5c8 Change the default icon pack 2018-10-27 20:30:31 +02:00
J-Jamet
95541d99c4 Replace a password sentence 2018-10-27 20:30:08 +02:00
J-Jamet
05ca7e6d69 Merge branch 'comradekingu-patch-2' into develop 2018-10-27 20:13:53 +02:00
J-Jamet
644a4a4886 Update strings 2018-10-27 20:13:15 +02:00
J-Jamet
7eb82ddbea Merge branch 'patch-2' of git://github.com/comradekingu/KeePassDX into comradekingu-patch-2 with corrections 2018-10-27 19:42:42 +02:00
J-Jamet
e721d4ca9e Fix keyboard clear and optimize views 2018-10-27 16:59:16 +02:00
J-Jamet
c72582b679 Update file selection view, title and Changelog 2018-10-27 16:30:36 +02:00
J-Jamet
217f0a82b5 Merge branch 'feature/Magikeyboard' into develop 2018-10-27 14:25:41 +02:00
J-Jamet
1efd172b42 Fix image 2018-10-27 14:17:59 +02:00
J-Jamet
e8d0d03ad9 Add keyboard Timeout 2018-10-27 13:56:08 +02:00
J-Jamet
6cb97de793 Remove keyboard sound and vibration duration settings 2018-10-20 12:57:26 +02:00
J-Jamet
77dbdc235a Add alpha channel to capture_keyboard images 2018-10-20 12:48:44 +02:00
J-Jamet
a9c87ea2b8 Add custom fields selection in keyboard 2018-10-13 16:10:45 +02:00
J-Jamet
232829526c Upgrade grdale 2018-10-13 10:52:04 +02:00
אלקנה בירדוגו
b5d8b2f14c Translated using Weblate (Hebrew)
Currently translated at 33.7% (110 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/he/
2018-09-27 15:31:03 +02:00
C. Rüdinger
c2673a627c Translated using Weblate (German)
Currently translated at 100.0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-09-24 15:27:48 +02:00
Tatiana Canales
bb49256188 Translated using Weblate (Spanish)
Currently translated at 66.5% (217 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/es/
2018-09-22 11:36:53 +02:00
Allan Nordhøy
1b6a284dd6 "Database name" 2018-09-21 02:42:26 +02:00
Allan Nordhøy
36b35b907e Requested changes implemented 2018-09-21 02:41:40 +02:00
Allan Nordhøy
f61b310ff2 Language rework 2
Transformation rounds
2018-09-19 03:39:04 +02:00
J-Jamet
c1e84b3bf1 Add popup view for custom fields 2018-09-18 21:27:36 +02:00
Pavel Borecki
9971d27ef3 Translated using Weblate (Czech)
Currently translated at 100.0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/cs/
2018-09-18 20:22:34 +02:00
J-Jamet
3c0835f725 Fix keyboard settings 2018-09-18 15:52:38 +02:00
J-Jamet
023aba2b60 Remove Magikeyboard and Model modules for easier integration 2018-09-18 11:46:37 +02:00
J-Jamet
abc0a14dda Update gradle 2018-09-18 10:14:15 +02:00
J-Jamet
dc23c6a445 Optimize memory pool and revert algo 2018-09-18 10:11:44 +02:00
J-Jamet
161a0ffc59 Change links 2018-09-17 22:38:48 +02:00
J-Jamet
b96436c906 Change small code 2018-09-16 11:40:22 +02:00
J-Jamet
cee7419208 Fix equals and hashcode for ProtectedBinary 2018-09-15 17:36:14 +02:00
J-Jamet
9ff6e7a080 Clear the cache to keep a small memory when the app is down 2018-09-15 17:22:42 +02:00
Lieven Blancke
9112a568d9 Translated using Weblate (Dutch)
Currently translated at 100.0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2018-09-09 15:16:12 +02:00
Heimen Stoffels
8d0c9bc894 Translated using Weblate (Dutch)
Currently translated at 100.0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2018-09-08 12:16:14 +02:00
RainSlide
e7b4e4501e Translated using Weblate (Chinese (Simplified))
Currently translated at 63.8% (208 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hans/
2018-09-08 12:14:45 +02:00
J-Jamet
df80084822 Fix attachment recovery 2018-09-06 22:05:31 +02:00
Heimen Stoffels
4df2891b1a Translated using Weblate (Dutch)
Currently translated at 46.9% (153 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nl/
2018-09-06 13:15:42 +02:00
J-Jamet
d0560677fa Temp commit 2018-09-05 22:02:24 +02:00
J-Jamet
9130a3851f Fix Out of memory for large attachment #115 2018-09-03 20:06:50 +02:00
J-Jamet
bb3fb26847 Update gradle 2018-09-03 12:00:57 +02:00
BO41
8406264138 Translated using Weblate (German)
Currently translated at 100.0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-08-29 15:30:49 +02:00
ButterflyOfFire
e51f48ebe0 Translated using Weblate (Arabic)
Currently translated at 41.4% (135 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2018-08-29 10:34:27 +02:00
Daniel
a0a2aa4c5d Translated using Weblate (German)
Currently translated at 99.3% (324 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-08-24 12:38:48 +02:00
Claus Rüdinger
65cea7b5ec Translated using Weblate (German)
Currently translated at 99.3% (324 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-08-24 12:38:48 +02:00
WaldiS
fb589419de Translated using Weblate (Polish)
Currently translated at 100.0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2018-08-23 18:50:11 +02:00
Allan Nordhøy
ae9844d5a1 Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.1% (294 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2018-08-23 08:41:23 +02:00
J-Jamet
645ea21581 Remove warning_read_only 2018-08-23 00:24:18 +02:00
Allan Nordhøy
60fd675dff Translated using Weblate (Norwegian Bokmål)
Currently translated at 79.4% (259 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb_NO/
2018-08-22 05:41:35 +02:00
Allan Nordhøy
48903da446 Translated using Weblate (Danish)
Currently translated at 99.6% (325 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2018-08-22 04:36:29 +02:00
Claus Rüdinger
5399df134c Translated using Weblate (German)
Currently translated at 97.5% (318 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-08-21 21:37:54 +02:00
WaldiS
a858040ace Translated using Weblate (Polish)
Currently translated at 73.9% (241 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2018-08-21 20:41:36 +02:00
Allan Nordhøy
d792b7d5ae Added translation using Weblate (Norwegian Bokmål) 2018-08-21 03:27:23 +02:00
Claus Rüdinger
8b3ed1a7dc Translated using Weblate (German)
Currently translated at 97.5% (318 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-08-19 01:38:34 +02:00
jan madsen
47004857ea Translated using Weblate (Danish)
Currently translated at 99.6% (325 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2018-08-16 18:35:31 +02:00
J-Jamet
82a6b8d7ef Merge branch 'master' into develop 2018-08-15 09:32:13 +02:00
J-Jamet
a30bcf1fa7 Merge branch 'mohammadnaseri-master' into develop 2018-08-15 09:30:55 +02:00
Hosted Weblate
febe978fa1 Merge branch 'origin/master' into Weblate 2018-08-15 09:28:48 +02:00
jan madsen
1eeefcf495 Translated using Weblate (Danish)
Currently translated at 96.6% (315 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/da/
2018-08-15 09:28:48 +02:00
Jérémy JAMET
ff6b0eee6c Merge pull request #167 from tuxayo/patch-1
CHANGELOG: Fix forgotten version bump
2018-08-15 09:28:33 +02:00
Claus Rüdinger
a7c8eaa937 Translated using Weblate (German)
Currently translated at 92.3% (301 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-08-14 23:37:23 +02:00
Telugu Speaker
55545e31e8 Translated using Weblate (Telugu)
Currently translated at 3.3% (11 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/te/
2018-08-14 04:44:56 +02:00
Tobirium
34132c6da7 Translated using Weblate (German)
Currently translated at 91,4% (298 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-08-13 22:53:31 +02:00
Claus Rüdinger
fbafa99a3b Translated using Weblate (German)
Currently translated at 91,4% (298 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-08-13 22:53:31 +02:00
random r
bd63805611 Translated using Weblate (Italian)
Currently translated at 100.0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2018-08-13 11:39:07 +02:00
lnxw
ae8398a434 Added translation using Weblate (Telugu) 2018-08-13 03:30:16 +02:00
عادل حسين المطهر
0b9318de88 Translated using Weblate (Arabic)
Currently translated at 37.4% (122 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ar/
2018-08-12 16:37:49 +02:00
WaldiS
55e4bbf9db Translated using Weblate (Polish)
Currently translated at 53.3% (174 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2018-08-11 19:42:59 +02:00
tuxayo
f33a572751 CHANGELOG: Fix forgotten version bump 2018-08-11 14:22:06 +02:00
K
2849cedbd8 Translated using Weblate (Latvian)
Currently translated at 34.6% (113 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lv/
2018-08-11 09:40:28 +02:00
Mohammad Naseri
041d1b29d1 Passwords should be invisible to the accessibility services 2018-08-10 15:43:49 +02:00
K
cdcdc2fee1 Translated using Weblate (Latvian)
Currently translated at 34.0% (111 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/lv/
2018-08-09 18:39:53 +02:00
Tobirium
8e35d5bc8d Translated using Weblate (German)
Currently translated at 90.4% (295 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-08-08 18:38:37 +02:00
Daniel
f12e368707 Translated using Weblate (German)
Currently translated at 90.4% (295 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-08-08 18:38:36 +02:00
J-Jamet
f01895c7a7 Remove build version from app version (commit TAG = app name) 2018-08-08 12:35:15 +02:00
Aidar Gilmutdinov
4ce0c067e1 Translated using Weblate (Russian)
Currently translated at 100.0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2018-08-07 22:41:35 +02:00
WaldiS
728a2cbfae Translated using Weblate (Polish)
Currently translated at 50.9% (166 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pl/
2018-08-07 17:41:48 +02:00
Tiago A. Reul
a4aab3b933 Translated using Weblate (Portuguese (Brazil))
Currently translated at 73.6% (240 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2018-08-07 16:43:32 +02:00
Aidar Gilmutdinov
11ee0d6544 Translated using Weblate (Russian)
Currently translated at 100.0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/ru/
2018-08-06 21:52:24 +02:00
Allan Nordhøy
2b80dad47c Translated using Weblate (Norwegian Bokmål)
Currently translated at 2.1% (7 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb/
2018-08-06 19:41:56 +02:00
random r
7bb13ec1c2 Translated using Weblate (Italian)
Currently translated at 100,0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2018-08-06 11:19:27 +02:00
Ty Moss
344010d2bb Translated using Weblate (Norwegian Bokmål)
Currently translated at 2,1% (7 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/nb/
2018-08-05 18:42:40 +02:00
Ty Moss
ef37ecddc6 Added translation using Weblate (Norwegian Bokmål) 2018-08-05 18:05:54 +02:00
Kunzisoft
91a38e205e Translated using Weblate (French)
Currently translated at 100.0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2018-08-04 11:38:13 +02:00
Kunzisoft
bd4de382e4 Translated using Weblate (English)
Currently translated at 100.0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/en/
2018-08-04 10:36:42 +02:00
random r
623244c3bd Translated using Weblate (Italian)
Currently translated at 99.6% (325 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2018-08-03 14:39:19 +02:00
Kunzisoft
7a06ff6dec Translated using Weblate (French)
Currently translated at 100.0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2018-08-03 11:23:15 +02:00
random r
6d735e24ff Translated using Weblate (Italian)
Currently translated at 100,0% (326 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2018-08-02 14:23:10 +02:00
Hosted Weblate
5c30d544fa Merge branch 'origin/master' into Weblate 2018-08-02 13:11:19 +02:00
random r
54a3876288 Translated using Weblate (Italian)
Currently translated at 88.0% (287 of 326 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2018-08-02 13:11:19 +02:00
J-Jamet
57f71afb98 Fix font and upgrade version 2018-08-02 13:02:18 +02:00
J-Jamet
d8ec198f6f Fix null pointer 2018-08-02 11:58:15 +02:00
Hosted Weblate
faa6d7c9f6 Merge branch 'origin/master' into Weblate 2018-08-02 11:21:06 +02:00
Kunzisoft
d79a5b17a5 Translated using Weblate (French)
Currently translated at 100.0% (321 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2018-08-02 11:21:05 +02:00
J-Jamet
34deea5d32 Merge tag '2.5.0.0beta16' into develop
2.5.0.0bet16
2018-08-02 11:19:58 +02:00
J-Jamet
edc7324c1d Merge branch 'release/2.5.0.0beta16' 2018-08-02 11:19:41 +02:00
J-Jamet
b9190e8254 Fix translations 2018-08-02 11:18:59 +02:00
J-Jamet
534878ae99 Merge branch 'translations' into develop 2018-08-02 11:04:39 +02:00
J-Jamet
99bfd21ecc Merge branch 'master' of https://hosted.weblate.org/projects/keepass-dx/strings into translations 2018-08-02 11:03:37 +02:00
J-Jamet
cb5c324c9d Fix search / Fix icon - back #138 / Upgrade changelogs 2018-08-02 10:34:04 +02:00
J-Jamet
55dc504f26 Fix visual elements 2018-08-01 15:45:49 +02:00
J-Jamet
ecb0138c90 Add display username settings and rearrange form filling 2018-08-01 12:00:15 +02:00
J-Jamet
0860eeb87f Show website domains if entries given no title #124 and add username 2018-07-31 22:04:41 +02:00
J-Jamet
f08bff61cf Update changelogs 2018-07-31 08:52:58 +02:00
J-Jamet
432aca6465 Fix transparent toolbar 2018-07-30 22:17:23 +02:00
J-Jamet
7cdb8db146 New error message #154 2018-07-30 21:10:11 +02:00
random r
6ba9dedcb8 Translated using Weblate (Italian)
Currently translated at 100,0% (321 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2018-07-30 11:59:13 +02:00
random r
6d603608f4 Translated using Weblate (Italian)
Currently translated at 99.3% (319 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/it/
2018-07-30 11:58:11 +02:00
Kunzisoft
64f4d9fb84 Translated using Weblate (French)
Currently translated at 99.6% (320 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/fr/
2018-07-29 14:37:40 +02:00
J-Jamet
60ccc450ae Fix read-only education screen 2018-07-29 09:44:01 +02:00
J-Jamet
ddb5f327a3 Add warning dialog on password copy button and sort settings 2018-07-28 18:00:57 +02:00
J-Jamet
df90ea42eb Deleting points in parameter explanations 2018-07-28 16:18:36 +02:00
Jérémy JAMET
a062d648b3 Merge pull request #158 from comradekingu/patch-1
Spelling: Language rework
2018-07-28 15:56:37 +02:00
J-Jamet
a59ae820b5 Add gray copy icon when copy not available 2018-07-28 15:18:34 +02:00
J-Jamet
228831acdd Upgrade version and add changelogs 2018-07-28 14:29:03 +02:00
J-Jamet
bf15ee43da Populate custom fields in search entry 2018-07-28 14:00:49 +02:00
J-Jamet
608f45677c Fix quick search with magikeyboard 2018-07-28 09:37:26 +02:00
Daniel
1cb15f214b Translated using Weblate (German)
Currently translated at 100.0% (321 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-07-28 00:37:41 +02:00
Claus Rüdinger
cd4a9e9b03 Translated using Weblate (German)
Currently translated at 100.0% (321 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-07-28 00:37:40 +02:00
J-Jamet
d6eae56d4f Merge branch 'feature/Search' into develop #26 #147 #153 2018-07-27 22:19:43 +02:00
J-Jamet
b5a87a63dc Fix search fragment 2018-07-27 21:39:26 +02:00
Allan Nordhøy
1eb17c4f34 Language rework 2018-07-27 15:11:56 +02:00
Hadrián Candela
8a6ce1f711 Translated using Weblate (Galician)
Currently translated at 7.4% (24 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/gl/
2018-07-27 13:38:02 +02:00
J-Jamet
0f22f8af45 Add search title 2018-07-27 10:54:58 +02:00
Tobirium
b2e81e6fd9 Translated using Weblate (German)
Currently translated at 100,0% (321 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-07-26 23:52:55 +02:00
Daniel
f82eab942d Translated using Weblate (German)
Currently translated at 100,0% (321 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-07-26 23:52:55 +02:00
J-Jamet
aa29aec40f Add search suggestions 2018-07-26 14:08:48 +02:00
Hadrián Candela
181def52ab Added translation using Weblate (Galician) 2018-07-26 13:01:37 +02:00
HybridGlucose
96d2bd63cc Translated using Weblate (Chinese (Traditional))
Currently translated at 44.5% (143 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/zh_Hant/
2018-07-25 20:36:13 +02:00
Claus Rüdinger
194021a957 Translated using Weblate (German)
Currently translated at 100,0% (321 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-07-25 18:38:46 +02:00
Daniel
9e307f94ea Translated using Weblate (German)
Currently translated at 100,0% (321 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-07-25 18:38:45 +02:00
Tobirium
155b2de138 Translated using Weblate (German)
Currently translated at 100,0% (321 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/de/
2018-07-25 18:38:45 +02:00
J-Jamet
c76c3fd2be Fix fragment duplication 2018-07-25 11:50:18 +02:00
Matheus Gritz
01c5554944 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (321 of 321 strings)

Translation: KeePass DX/Strings
Translate-URL: https://hosted.weblate.org/projects/keepass-dx/strings/pt_BR/
2018-07-24 19:11:13 +02:00
J-Jamet
542cf65b41 Fix timeout and new animation 2018-07-24 18:32:19 +02:00
J-Jamet
f037561a67 Search in fragment 2018-07-24 18:02:08 +02:00
J-Jamet
379263a6d3 Search and Group in one activity 2018-07-24 14:48:03 +02:00
J-Jamet
c420ca01f6 Update Readme 2018-07-23 14:21:46 +02:00
J-Jamet
a1f9db6eee Try to solve the date crash 2018-07-23 14:12:07 +02:00
J-Jamet
c9212174c4 Change text by reset 2018-07-22 12:13:02 +02:00
J-Jamet
0065336377 Merge tag '2.5.0.0beta15' into develop
2.5.0.0beta15
2018-07-19 11:27:47 +02:00
691 changed files with 38174 additions and 37659 deletions

View File

@@ -1,3 +1,34 @@
KeepassDX (2.5.0.0beta19)
* Add lock button always visible
* New connection workflow
* Code refactored in Kotlin
* Better notification implementation
* Better views for large screen
* Magikeyboard enhancement
* Fix Recycle Bin
* Fix memory when load database
* Fix small bugs
KeepassDX (2.5.0.0beta18)
* New recent databases views
* New information dialog
* Custom fields for the Magikeyboard
* Timeout for the Magikeyboard
* Long press for keyboard selection
* Fix memory when opening the database
* Memory management for attachments
KeepassDX (2.5.0.0beta17)
* Fix font and search
KeepassDX (2.5.0.0beta16)
* New search in a single fragment
* Search suggestions
* Added the display of usernames
* Added translations
* Fix read-only mode
* 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

55
FAQ.md
View File

@@ -1,55 +0,0 @@
# F.A.Q.
## Why KeePass DX?
KeePass DX is an **Android password manager** implemented from Keepass password manager.
KeePass DX was created to meet the security and usability needs of a KeePass application on Android :
- To be easy to use with **secure password management and form filling tools**.
- To use only tools under **open source license** to guarantee the security of the application (With [open source store](https://f-droid.org/en/) and no closed API).
- To be in a **native langage** (java) for weight, security and a better integration of the application.
- To respect **Android design, architecture and ergonomic**.
## What makes KeePass DX stand out from other password managers?
- We **do not recover your sensitive data** on a private server or a closed cloud, you have control of your passwords.
- We respect **KeePass file standards** to maintain compatibility and data porting on different devices (computers and portable devices with different operating system).
- The code is **open source**, which implies increased **security**, you can check how the encryption algorithms are implemented.
- We remain attentive to **your needs** and we can even integrate the features that you have defined.
- We **do not put advertising** even in the free version.
## How am I sure my passwords are safely stored on the application?
- We allow users to save and use passwords, keys and digital identities in a secure way by **integrating the last encryption algorithms** and **Android architecture standards**.
- You can increase the security of your database by increasing the rounds of encryption keys. *(In Settings -> Database Settings when your database is open)* **Warning**: *Increase the number of rounds sparingly to have a reasonable opening time.*
## Can I store my data on a cloud storage?
**Yes** this is possible. Otherwise, we **recommend using cloud with personal server and open source license**, like [NextCloud](https://f-droid.org/en/packages/com.nextcloud.client/) to be sure how your databases are stored.
## Can I recover my passwords on another device if I loose my main device?
**Yes** you can, but you **must first save the .kdb or .kdbx file from your database to an external storage** *(like a hardrive or a cloud)*.
We recommend you save your data after each modification so incase you loose your android device you could retrieve the data and import it into the new KeePass DX installed on the new android device.
## Why are updates not available at the same time on all stores?
- **PlayStore** only needs an APK generated and manually signed to be available on the store, it usually takes **20 minutes** to be available because it is deployed with fastlane. But the management of the APK and its data by the google servers is obscure.
- **F-Droid**, to **ensure that the code is open source**, checks the sources directly on git repository (by checking the presence of new tags) and builds itself the APK that the server signs during the compilation of code and dependencies. Updating the project will take **1-10 days** for F-Droid to analyze all available repositories, build sources and deploy the generated APK. So F-Droid is slower for deployment but it is run by **volunteers** and guaranteed a **clean APK**. :)
## Why not an online version?
The offline and online client concepts only exists with Keepass2Android because the file access network tools are directly integrated into the code of the main application. Which is a very dubious choice knowing that **it is not normally the purpose of a password management application to take care of external file synchronization on clouds** (which can be under closed licensed and recover your data base), it is rather the purpose of the [file management application](https://developer.android.com/guide/topics/providers/document-provider).
## Can I open my database easily other than with a password?
**Yes**, we have integrated a secure openning option of fingerprint for android devices that support this feature, so no one can access the application without scanning his/her fingerprint or fill a master key.
## Can I open my database without my master key (master password and/or key file)?
**No**, you can not open a database file without the master password (and / or) the associated key file. Be sure to remember your master password and save the key file in a safe place.
## Can I suggest features and report bugs for the application?
**Yes**, we welcome this you could go ahead and do that on our github:
https://github.com/Kunzisoft/KeePassDX

View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,93 @@
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -15,6 +15,7 @@
* **AutoFill** and Integration * **AutoFill** and Integration
* Field filling **keyboard** * Field filling **keyboard**
* Precise management of **settings** * Precise management of **settings**
* Code written in **native language** *(Kotlin / Java / JNI / C)*
Keepass DX is **open source** and **ad-free**. Keepass DX is **open source** and **ad-free**.
@@ -28,7 +29,7 @@ KeePass DX is a **free open source password manager for Android**, which helps y
Yes, KeePass DX 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, KeePass DX 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.* *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.*
## Contributions ## Contributions
@@ -53,7 +54,7 @@ You can contribute in different ways to help us on our work.
## F.A.Q. ## F.A.Q.
Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassDX/blob/master/FAQ.md) Other questions? You can read the [F.A.Q.](https://www.keepassdx.com/FAQ)
## Other devices ## Other devices
@@ -63,11 +64,11 @@ Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassD
## License ## License
Copyright (c) 2017 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com). Copyright (c) 2019 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
This file is part of KeePass DX. This file is part of KeePass DX.
KeePass DX is free software: you can redistribute it and/or modify [KeePass DX](https://www.keepassdx.com) is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.

View File

@@ -1,15 +1,18 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion 27 compileSdkVersion 27
buildToolsVersion '27.0.3' buildToolsVersion '28.0.3'
defaultConfig { defaultConfig {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 14 minSdkVersion 14
targetSdkVersion 27 targetSdkVersion 27
versionCode = 15 versionCode = 19
versionName = "2.5.0.0beta15" versionName = "2.5.0.0beta19"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
@@ -39,7 +42,7 @@ android {
productFlavors { productFlavors {
libre { libre {
applicationIdSuffix = ".libre" applicationIdSuffix = ".libre"
versionNameSuffix "-libre" buildConfigField "String", "BUILD_VERSION", "\"libre\""
buildConfigField "boolean", "FULL_VERSION", "true" buildConfigField "boolean", "FULL_VERSION", "true"
buildConfigField "boolean", "CLOSED_STORE", "false" buildConfigField "boolean", "CLOSED_STORE", "false"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}" buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
@@ -48,7 +51,7 @@ android {
} }
pro { pro {
applicationIdSuffix = ".pro" applicationIdSuffix = ".pro"
versionNameSuffix "-pro" buildConfigField "String", "BUILD_VERSION", "\"pro\""
buildConfigField "boolean", "FULL_VERSION", "true" buildConfigField "boolean", "FULL_VERSION", "true"
buildConfigField "boolean", "CLOSED_STORE", "true" buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED", "{}" buildConfigField "String[]", "STYLES_DISABLED", "{}"
@@ -56,7 +59,7 @@ android {
} }
free { free {
applicationIdSuffix = ".free" applicationIdSuffix = ".free"
versionNameSuffix "-free" buildConfigField "String", "BUILD_VERSION", "\"free\""
buildConfigField "boolean", "FULL_VERSION", "false" buildConfigField "boolean", "FULL_VERSION", "false"
buildConfigField "boolean", "CLOSED_STORE", "true" buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}" buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
@@ -78,14 +81,16 @@ android {
def supportVersion = "27.1.1" def supportVersion = "27.1.1"
def spongycastleVersion = "1.58.0.0" def spongycastleVersion = "1.58.0.0"
def permissionDispatcherVersion = "3.1.0" def permissionDispatcherVersion = "3.3.1"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.android.support:appcompat-v7:$supportVersion" implementation "com.android.support:appcompat-v7:$supportVersion"
implementation "com.android.support:design:$supportVersion" implementation "com.android.support:design:$supportVersion"
implementation "com.android.support:preference-v7:$supportVersion" implementation "com.android.support:preference-v7:$supportVersion"
implementation "com.android.support:preference-v14:$supportVersion" implementation "com.android.support:preference-v14:$supportVersion"
implementation "com.android.support:cardview-v7:$supportVersion" implementation "com.android.support:cardview-v7:$supportVersion"
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation "com.madgag.spongycastle:core:$spongycastleVersion" implementation "com.madgag.spongycastle:core:$spongycastleVersion"
implementation "com.madgag.spongycastle:prov:$spongycastleVersion" implementation "com.madgag.spongycastle:prov:$spongycastleVersion"
// Expandable view // Expandable view
@@ -94,22 +99,23 @@ dependencies {
implementation 'joda-time:joda-time:2.9.9' implementation 'joda-time:joda-time:2.9.9'
implementation 'org.sufficientlysecure:html-textview:3.5' implementation 'org.sufficientlysecure:html-textview:3.5'
implementation 'com.nononsenseapps:filepicker:4.1.0' implementation 'com.nononsenseapps:filepicker:4.1.0'
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.11.0' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0'
// Permissions // Permissions
implementation("com.github.hotchemi:permissionsdispatcher:$permissionDispatcherVersion") { implementation("com.github.hotchemi:permissionsdispatcher:$permissionDispatcherVersion") {
// if you don't use android.app.Fragment you can exclude support for them // if you don't use android.app.Fragment you can exclude support for them
exclude module: "support-v13" exclude module: "support-v13"
} }
annotationProcessor "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcherVersion" kapt "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcherVersion"
// Apache Commons Collections // Apache Commons Collections
implementation 'commons-collections:commons-collections:3.2.1' implementation 'commons-collections:commons-collections:3.2.1'
implementation 'org.apache.commons:commons-io:1.3.2'
// Base64 // Base64
implementation 'biz.source_code:base64coder:2010-12-19' implementation 'biz.source_code:base64coder:2010-12-19'
// IO-Extras
implementation 'com.github.davidmoten:io-extras:0.1'
implementation 'com.google.code.gson:gson:2.8.4' implementation 'com.google.code.gson:gson:2.8.4'
implementation 'com.google.guava:guava:23.0-android' implementation 'com.google.guava:guava:23.0-android'
// Icon pack, classic for all, material for libre and pro // Icon pack
implementation project(path: ':icon-pack-classic') implementation project(path: ':icon-pack-classic')
implementation project(path: ':icon-pack-material') implementation project(path: ':icon-pack-material')
implementation project(path: ':magikeyboard')
implementation project(path: ':keepass-model')
} }

View File

@@ -1,22 +1,22 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* KeePass DX is distributed in the hope that it will be useful, * KeePass DX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>. * along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.tests; package com.kunzisoft.keepass.tests;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
@@ -24,19 +24,21 @@ import android.test.AndroidTestCase;
import com.kunzisoft.keepass.tests.database.TestData; import com.kunzisoft.keepass.tests.database.TestData;
public class AccentTest extends AndroidTestCase { public class AccentTest extends AndroidTestCase {
private static final String KEYFILE = "";
private static final String PASSWORD = "é";
private static final String ASSET = "accent.kdb";
private static final String FILENAME = "/sdcard/accent.kdb";
public void testOpen() {
try { private static final String KEYFILE = "";
TestData.GetDb(getContext(), ASSET, PASSWORD, KEYFILE, FILENAME); private static final String PASSWORD = "é";
} catch (Exception e) { private static final String ASSET = "accent.kdb";
assertTrue("Failed to open database", false); private static final String FILENAME = "/sdcard/accent.kdb";
}
} public void testOpen() {
/*
try {
TestData.GetDb(getContext(), ASSET, PASSWORD, KEYFILE, FILENAME);
} catch (Exception e) {
assertTrue("Failed to open database", false);
}
*/
}
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -26,10 +26,10 @@ import android.test.suitebuilder.TestSuiteBuilder;
public class OutputTests extends TestSuite { public class OutputTests extends TestSuite {
public static Test suite() { public static Test suite() {
return new TestSuiteBuilder(AllTests.class) return new TestSuiteBuilder(AllTests.class)
.includePackages("com.kunzisoft.keepass.tests.output") .includePackages("com.kunzisoft.keepass.tests.output")
.build(); .build();
} }
} }

View File

@@ -1,38 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.tests;
import junit.framework.TestCase;
import com.kunzisoft.keepass.database.PwDate;
public class PwDateTest extends TestCase {
public void testDate() {
PwDate jDate = new PwDate(System.currentTimeMillis());
PwDate intermediate = (PwDate) jDate.clone();
PwDate cDate = new PwDate(intermediate.getCDate(), 0);
assertTrue("jDate and intermediate not equal", jDate.equals(intermediate));
assertTrue("jDate and cDate not equal", cDate.equals(jDate));
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.tests
import junit.framework.TestCase
import com.kunzisoft.keepass.database.element.PwDate
import org.junit.Assert
class PwDateTest : TestCase() {
fun testDate() {
val jDate = PwDate(System.currentTimeMillis())
val intermediate = PwDate(jDate)
val cDate = PwDate(intermediate.byteArrayDate!!, 0)
Assert.assertTrue("jDate and intermediate not equal", jDate == intermediate)
Assert.assertTrue("jDate and cDate not equal", cDate == jDate)
}
}

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -27,37 +27,37 @@ import java.util.Calendar;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.PwEntryV3; import com.kunzisoft.keepass.database.element.PwEntryV3;
import com.kunzisoft.keepass.tests.database.TestData; import com.kunzisoft.keepass.tests.database.TestData;
public class PwEntryTestV3 extends AndroidTestCase { public class PwEntryTestV3 extends AndroidTestCase {
PwEntryV3 mPE; PwEntryV3 mPE;
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
mPE = (PwEntryV3) TestData.GetTest1(getContext()).getEntryAt(0); // mPE = (PwEntryV3) TestData.GetTest1(getContext()).getEntryAt(0);
} }
public void testName() { public void testName() {
assertTrue("Name was " + mPE.getTitle(), mPE.getTitle().equals("Amazon")); assertTrue("Name was " + mPE.getTitle(), mPE.getTitle().equals("Amazon"));
} }
public void testPassword() throws UnsupportedEncodingException { public void testPassword() throws UnsupportedEncodingException {
String sPass = "12345"; String sPass = "12345";
byte[] password = sPass.getBytes("UTF-8"); byte[] password = sPass.getBytes("UTF-8");
assertArrayEquals(password, mPE.getPasswordBytes()); assertArrayEquals(password, mPE.getPasswordBytes());
} }
public void testCreation() { public void testCreation() {
Calendar cal = Calendar.getInstance(); Calendar cal = Calendar.getInstance();
cal.setTime(mPE.getCreationTime().getDate()); cal.setTime(mPE.getCreationTime().getDate());
assertEquals("Incorrect year.", cal.get(Calendar.YEAR), 2009); assertEquals("Incorrect year.", cal.get(Calendar.YEAR), 2009);
assertEquals("Incorrect month.", cal.get(Calendar.MONTH), 3); assertEquals("Incorrect month.", cal.get(Calendar.MONTH), 3);
assertEquals("Incorrect day.", cal.get(Calendar.DAY_OF_MONTH), 23); assertEquals("Incorrect day.", cal.get(Calendar.DAY_OF_MONTH), 23);
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -19,20 +19,12 @@
*/ */
package com.kunzisoft.keepass.tests; package com.kunzisoft.keepass.tests;
import com.kunzisoft.keepass.database.AutoType;
import com.kunzisoft.keepass.database.PwEntryV4;
import com.kunzisoft.keepass.database.PwGroupV4;
import com.kunzisoft.keepass.database.PwIconCustom;
import com.kunzisoft.keepass.database.PwIconStandard;
import com.kunzisoft.keepass.database.security.ProtectedBinary;
import com.kunzisoft.keepass.database.security.ProtectedString;
import junit.framework.TestCase; import junit.framework.TestCase;
import java.util.UUID;
public class PwEntryTestV4 extends TestCase { public class PwEntryTestV4 extends TestCase {
public void testAssign() { public void testAssign() {
/*
TODO Test
PwEntryV4 entry = new PwEntryV4(); PwEntryV4 entry = new PwEntryV4();
entry.setAdditional("test223"); entry.setAdditional("test223");
@@ -43,17 +35,17 @@ public class PwEntryTestV4 extends TestCase {
entry.getAutoType().obfuscationOptions = 123412432109L; entry.getAutoType().obfuscationOptions = 123412432109L;
entry.getAutoType().put("key", "value"); entry.getAutoType().put("key", "value");
entry.setBackgroupColor("blue"); entry.setBackgroundColor("blue");
entry.putProtectedBinary("key1", new ProtectedBinary(false, new byte[] {0,1})); entry.putProtectedBinary("key1", new ProtectedBinary(false, new byte[] {0,1}));
entry.setCustomIcon(new PwIconCustom(UUID.randomUUID(), new byte[0])); entry.setIconCustom(new PwIconCustom(UUID.randomUUID(), new byte[0]));
entry.setForegroundColor("red"); entry.setForegroundColor("red");
entry.addToHistory(new PwEntryV4()); entry.addToHistory(new PwEntryV4());
entry.setIcon(new PwIconStandard(5)); entry.setIconStandard(new PwIconStandard(5));
entry.setOverrideURL("override"); entry.setOverrideURL("override");
entry.setParent(new PwGroupV4()); entry.setParent(new PwGroupV4());
entry.addExtraField("key2", new ProtectedString(false, "value2")); entry.addExtraField("key2", new ProtectedString(false, "value2"));
entry.setUrl("http://localhost"); entry.setUrl("http://localhost");
entry.setUUID(UUID.randomUUID()); entry.setNodeId(UUID.randomUUID());
PwEntryV4 target = new PwEntryV4(); PwEntryV4 target = new PwEntryV4();
target.updateWith(entry); target.updateWith(entry);
@@ -61,7 +53,7 @@ public class PwEntryTestV4 extends TestCase {
/* This test is not so useful now that I am not implementing value equality for Entries /* This test is not so useful now that I am not implementing value equality for Entries
assertTrue("Entries do not match.", entry.equals(target)); assertTrue("Entries do not match.", entry.equals(target));
*/ */
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -22,23 +22,23 @@ package com.kunzisoft.keepass.tests;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.PwGroupV3; import com.kunzisoft.keepass.database.element.PwGroupV3;
import com.kunzisoft.keepass.tests.database.TestData; import com.kunzisoft.keepass.tests.database.TestData;
public class PwGroupTest extends AndroidTestCase { public class PwGroupTest extends AndroidTestCase {
PwGroupV3 mPG; PwGroupV3 mPG;
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
mPG = (PwGroupV3) TestData.GetTest1(getContext()).getGroups().get(0); //mPG = (PwGroupV3) TestData.GetTest1(getContext()).getGroups().get(0);
} }
public void testGroupName() { public void testGroupName() {
assertTrue("Name was " + mPG.getName(), mPG.getName().equals("Internet")); //assertTrue("Name was " + mPG.getTitle(), mPG.getTitle().equals("Internet"));
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -19,53 +19,38 @@
*/ */
package com.kunzisoft.keepass.tests; package com.kunzisoft.keepass.tests;
import android.content.Context;
import android.content.res.AssetManager;
import android.os.Environment;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
import android.content.Context;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.Environment;
import com.kunzisoft.keepass.utils.EmptyUtils;
import com.kunzisoft.keepass.utils.UriUtil;
public class TestUtil { public class TestUtil {
private static final File sdcard = Environment.getExternalStorageDirectory(); private static final File sdcard = Environment.getExternalStorageDirectory();
public static void extractKey(Context ctx, String asset, String target) throws Exception { public static void extractKey(Context ctx, String asset, String target) throws Exception {
InputStream key = ctx.getAssets().open(asset, AssetManager.ACCESS_STREAMING);
FileOutputStream keyFile = new FileOutputStream(target);
while (true) {
byte[] buf = new byte[1024];
int read = key.read(buf);
if ( read == -1 ) {
break;
} else {
keyFile.write(buf, 0, read);
}
}
keyFile.close();
} InputStream key = ctx.getAssets().open(asset, AssetManager.ACCESS_STREAMING);
public static InputStream getKeyFileInputStream(Context ctx, String keyfile) throws FileNotFoundException { FileOutputStream keyFile = new FileOutputStream(target);
InputStream keyIs = null; while (true) {
if (!EmptyUtils.isNullOrEmpty(keyfile)) { byte[] buf = new byte[1024];
Uri uri = UriUtil.parseDefaultFile(keyfile); int read = key.read(buf);
keyIs = UriUtil.getUriInputStream(ctx, uri); if ( read == -1 ) {
} break;
} else {
keyFile.write(buf, 0, read);
}
}
return keyIs; keyFile.close();
}
public static String getSdPath(String filename) { }
File file = new File(sdcard, filename);
return file.getAbsolutePath(); public static String getSdPath(String filename) {
} File file = new File(sdcard, filename);
return file.getAbsolutePath();
}
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -28,184 +28,184 @@ import java.util.UUID;
import junit.framework.TestCase; import junit.framework.TestCase;
import com.kunzisoft.keepass.database.PwDate; import com.kunzisoft.keepass.database.element.PwDate;
import com.kunzisoft.keepass.stream.LEDataInputStream; import com.kunzisoft.keepass.stream.LEDataInputStream;
import com.kunzisoft.keepass.stream.LEDataOutputStream; import com.kunzisoft.keepass.stream.LEDataOutputStream;
import com.kunzisoft.keepass.utils.Types; import com.kunzisoft.keepass.utils.Types;
public class TypesTest extends TestCase { public class TypesTest extends TestCase {
public void testReadWriteLongZero() { public void testReadWriteLongZero() {
testReadWriteLong((byte) 0); testReadWriteLong((byte) 0);
} }
public void testReadWriteLongMax() {
testReadWriteLong(Byte.MAX_VALUE);
}
public void testReadWriteLongMin() {
testReadWriteLong(Byte.MIN_VALUE);
}
public void testReadWriteLongRnd() {
Random rnd = new Random();
byte[] buf = new byte[1];
rnd.nextBytes(buf);
testReadWriteLong(buf[0]);
}
private void testReadWriteLong(byte value) {
byte[] orig = new byte[8];
byte[] dest = new byte[8];
setArray(orig, value, 0, 8);
long one = LEDataInputStream.readLong(orig, 0);
LEDataOutputStream.writeLong(one, dest, 0);
assertArrayEquals(orig, dest);
} public void testReadWriteLongMax() {
testReadWriteLong(Byte.MAX_VALUE);
public void testReadWriteIntZero() { }
testReadWriteInt((byte) 0);
}
public void testReadWriteIntMin() {
testReadWriteInt(Byte.MIN_VALUE);
}
public void testReadWriteIntMax() {
testReadWriteInt(Byte.MAX_VALUE);
}
private void testReadWriteInt(byte value) {
byte[] orig = new byte[4];
byte[] dest = new byte[4];
for (int i = 0; i < 4; i++ ) {
orig[i] = 0;
}
setArray(orig, value, 0, 4);
int one = LEDataInputStream.readInt(orig, 0);
LEDataOutputStream.writeInt(one, dest, 0);
assertArrayEquals(orig, dest); public void testReadWriteLongMin() {
testReadWriteLong(Byte.MIN_VALUE);
} }
private void setArray(byte[] buf, byte value, int offset, int size) {
for (int i = offset; i < offset + size; i++) {
buf[i] = value;
}
}
public void testReadWriteShortOne() {
byte[] orig = new byte[2];
byte[] dest = new byte[2];
orig[0] = 0;
orig[1] = 1;
int one = LEDataInputStream.readUShort(orig, 0);
dest = LEDataOutputStream.writeUShortBuf(one);
assertArrayEquals(orig, dest);
}
public void testReadWriteShortMin() {
testReadWriteShort(Byte.MIN_VALUE);
}
public void testReadWriteShortMax() {
testReadWriteShort(Byte.MAX_VALUE);
}
private void testReadWriteShort(byte value) {
byte[] orig = new byte[2];
byte[] dest = new byte[2];
setArray(orig, value, 0, 2);
int one = LEDataInputStream.readUShort(orig, 0);
LEDataOutputStream.writeUShort(one, dest, 0);
assertArrayEquals(orig, dest);
} public void testReadWriteLongRnd() {
Random rnd = new Random();
byte[] buf = new byte[1];
rnd.nextBytes(buf);
public void testReadWriteByteZero() { testReadWriteLong(buf[0]);
testReadWriteByte((byte) 0); }
}
public void testReadWriteByteMin() {
testReadWriteByte(Byte.MIN_VALUE);
}
public void testReadWriteByteMax() {
testReadWriteShort(Byte.MAX_VALUE);
}
private void testReadWriteByte(byte value) {
byte[] orig = new byte[1];
byte[] dest = new byte[1];
setArray(orig, value, 0, 1);
int one = Types.readUByte(orig, 0);
Types.writeUByte(one, dest, 0);
assertArrayEquals(orig, dest);
}
public void testDate() {
Calendar cal = Calendar.getInstance();
Calendar expected = Calendar.getInstance();
expected.set(2008, 1, 2, 3, 4, 5);
byte[] buf = PwDate.writeTime(expected.getTime(), cal);
Calendar actual = Calendar.getInstance();
actual.setTime(PwDate.readTime(buf, 0, cal));
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR));
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH));
assertEquals("Day mismatch: ", 1, actual.get(Calendar.DAY_OF_MONTH));
assertEquals("Hour mismatch: ", 3, actual.get(Calendar.HOUR_OF_DAY));
assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE));
assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND));
}
public void testUUID() {
Random rnd = new Random();
byte[] bUUID = new byte[16];
rnd.nextBytes(bUUID);
UUID uuid = Types.bytestoUUID(bUUID);
byte[] eUUID = Types.UUIDtoBytes(uuid);
assertArrayEquals("UUID match failed", bUUID, eUUID);
}
public void testULongMax() throws Exception { private void testReadWriteLong(byte value) {
byte[] ulongBytes = new byte[8]; byte[] orig = new byte[8];
for (int i = 0; i < ulongBytes.length; i++) { byte[] dest = new byte[8];
ulongBytes[i] = -1;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream(); setArray(orig, value, 0, 8);
LEDataOutputStream leos = new LEDataOutputStream(bos);
leos.writeLong(Types.ULONG_MAX_VALUE);
leos.close();
byte[] uLongMax = bos.toByteArray(); long one = LEDataInputStream.readLong(orig, 0);
LEDataOutputStream.writeLong(one, dest, 0);
assertArrayEquals(ulongBytes, uLongMax); assertArrayEquals(orig, dest);
}
}
public void testReadWriteIntZero() {
testReadWriteInt((byte) 0);
}
public void testReadWriteIntMin() {
testReadWriteInt(Byte.MIN_VALUE);
}
public void testReadWriteIntMax() {
testReadWriteInt(Byte.MAX_VALUE);
}
private void testReadWriteInt(byte value) {
byte[] orig = new byte[4];
byte[] dest = new byte[4];
for (int i = 0; i < 4; i++ ) {
orig[i] = 0;
}
setArray(orig, value, 0, 4);
int one = LEDataInputStream.readInt(orig, 0);
LEDataOutputStream.writeInt(one, dest, 0);
assertArrayEquals(orig, dest);
}
private void setArray(byte[] buf, byte value, int offset, int size) {
for (int i = offset; i < offset + size; i++) {
buf[i] = value;
}
}
public void testReadWriteShortOne() {
byte[] orig = new byte[2];
byte[] dest = new byte[2];
orig[0] = 0;
orig[1] = 1;
int one = LEDataInputStream.readUShort(orig, 0);
dest = LEDataOutputStream.writeUShortBuf(one);
assertArrayEquals(orig, dest);
}
public void testReadWriteShortMin() {
testReadWriteShort(Byte.MIN_VALUE);
}
public void testReadWriteShortMax() {
testReadWriteShort(Byte.MAX_VALUE);
}
private void testReadWriteShort(byte value) {
byte[] orig = new byte[2];
byte[] dest = new byte[2];
setArray(orig, value, 0, 2);
int one = LEDataInputStream.readUShort(orig, 0);
LEDataOutputStream.writeUShort(one, dest, 0);
assertArrayEquals(orig, dest);
}
public void testReadWriteByteZero() {
testReadWriteByte((byte) 0);
}
public void testReadWriteByteMin() {
testReadWriteByte(Byte.MIN_VALUE);
}
public void testReadWriteByteMax() {
testReadWriteShort(Byte.MAX_VALUE);
}
private void testReadWriteByte(byte value) {
byte[] orig = new byte[1];
byte[] dest = new byte[1];
setArray(orig, value, 0, 1);
int one = Types.readUByte(orig, 0);
Types.writeUByte(one, dest, 0);
assertArrayEquals(orig, dest);
}
public void testDate() {
Calendar cal = Calendar.getInstance();
Calendar expected = Calendar.getInstance();
expected.set(2008, 1, 2, 3, 4, 5);
byte[] buf = PwDate.Companion.writeTime(expected.getTime(), cal);
Calendar actual = Calendar.getInstance();
actual.setTime(PwDate.Companion.readTime(buf, 0, cal));
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR));
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH));
assertEquals("Day mismatch: ", 1, actual.get(Calendar.DAY_OF_MONTH));
assertEquals("Hour mismatch: ", 3, actual.get(Calendar.HOUR_OF_DAY));
assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE));
assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND));
}
public void testUUID() {
Random rnd = new Random();
byte[] bUUID = new byte[16];
rnd.nextBytes(bUUID);
UUID uuid = Types.bytestoUUID(bUUID);
byte[] eUUID = Types.UUIDtoBytes(uuid);
assertArrayEquals("UUID match failed", bUUID, eUUID);
}
public void testULongMax() throws Exception {
byte[] ulongBytes = new byte[8];
for (int i = 0; i < ulongBytes.length; i++) {
ulongBytes[i] = -1;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
LEDataOutputStream leos = new LEDataOutputStream(bos);
leos.writeLong(Types.ULONG_MAX_VALUE);
leos.close();
byte[] uLongMax = bos.toByteArray();
assertArrayEquals(ulongBytes, uLongMax);
}
} }

View File

@@ -1,22 +1,22 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* KeePass DX is distributed in the hope that it will be useful, * KeePass DX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>. * along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.tests.crypto; package com.kunzisoft.keepass.tests.crypto;
import com.kunzisoft.keepass.crypto.CipherFactory; import com.kunzisoft.keepass.crypto.CipherFactory;
@@ -38,46 +38,46 @@ import javax.crypto.spec.SecretKeySpec;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
public class AESTest extends TestCase { public class AESTest extends TestCase {
private Random mRand = new Random(); private Random mRand = new Random();
public void testEncrypt() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { public void testEncrypt() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
// Test above below and at the blocksize // Test above below and at the blocksize
testFinal(15); testFinal(15);
testFinal(16); testFinal(16);
testFinal(17); testFinal(17);
// Test random larger sizes // Test random larger sizes
int size = mRand.nextInt(494) + 18; int size = mRand.nextInt(494) + 18;
testFinal(size); testFinal(size);
} }
private void testFinal(int dataSize) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { private void testFinal(int dataSize) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
// Generate some input // Generate some input
byte[] input = new byte[dataSize]; byte[] input = new byte[dataSize];
mRand.nextBytes(input); mRand.nextBytes(input);
// Generate key // Generate key
byte[] keyArray = new byte[32]; byte[] keyArray = new byte[32];
mRand.nextBytes(keyArray); mRand.nextBytes(keyArray);
SecretKeySpec key = new SecretKeySpec(keyArray, "AES"); SecretKeySpec key = new SecretKeySpec(keyArray, "AES");
// Generate IV // Generate IV
byte[] ivArray = new byte[16]; byte[] ivArray = new byte[16];
mRand.nextBytes(ivArray); mRand.nextBytes(ivArray);
IvParameterSpec iv = new IvParameterSpec(ivArray); IvParameterSpec iv = new IvParameterSpec(ivArray);
Cipher android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true); Cipher android = CipherFactory.INSTANCE.getInstance("AES/CBC/PKCS5Padding", true);
android.init(Cipher.ENCRYPT_MODE, key, iv); android.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] outAndroid = android.doFinal(input, 0, dataSize); byte[] outAndroid = android.doFinal(input, 0, dataSize);
Cipher nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding"); Cipher nat = CipherFactory.INSTANCE.getInstance("AES/CBC/PKCS5Padding");
nat.init(Cipher.ENCRYPT_MODE, key, iv); nat.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] outNative = nat.doFinal(input, 0, dataSize); byte[] outNative = nat.doFinal(input, 0, dataSize);
assertArrayEquals("Arrays differ on size: " + dataSize, outAndroid, outNative); assertArrayEquals("Arrays differ on size: " + dataSize, outAndroid, outNative);
} }
} }

View File

@@ -1,22 +1,22 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* KeePass DX is distributed in the hope that it will be useful, * KeePass DX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>. * along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.tests.crypto; package com.kunzisoft.keepass.tests.crypto;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
@@ -44,57 +44,57 @@ import com.kunzisoft.keepass.stream.BetterCipherInputStream;
import com.kunzisoft.keepass.stream.LEDataInputStream; import com.kunzisoft.keepass.stream.LEDataInputStream;
public class CipherTest extends TestCase { public class CipherTest extends TestCase {
private Random rand = new Random(); private Random rand = new Random();
public void testCipherFactory() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
byte[] key = new byte[32];
byte[] iv = new byte[16];
byte[] plaintext = new byte[1024];
rand.nextBytes(key);
rand.nextBytes(iv);
rand.nextBytes(plaintext);
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID);
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
byte[] secrettext = encrypt.doFinal(plaintext); public void testCipherFactory() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
byte[] decrypttext = decrypt.doFinal(secrettext); byte[] key = new byte[32];
byte[] iv = new byte[16];
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
}
public void testCipherStreams() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException { byte[] plaintext = new byte[1024];
final int MESSAGE_LENGTH = 1024;
byte[] key = new byte[32];
byte[] iv = new byte[16];
byte[] plaintext = new byte[MESSAGE_LENGTH];
rand.nextBytes(key);
rand.nextBytes(iv);
rand.nextBytes(plaintext);
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID); rand.nextBytes(key);
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv); rand.nextBytes(iv);
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv); rand.nextBytes(plaintext);
ByteArrayOutputStream bos = new ByteArrayOutputStream(); CipherEngine aes = CipherFactory.INSTANCE.getInstance(AesEngine.CIPHER_UUID);
CipherOutputStream cos = new CipherOutputStream(bos, encrypt); Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
cos.write(plaintext); Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
cos.close();
byte[] secrettext = encrypt.doFinal(plaintext);
byte[] secrettext = bos.toByteArray(); byte[] decrypttext = decrypt.doFinal(secrettext);
ByteArrayInputStream bis = new ByteArrayInputStream(secrettext); assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
BetterCipherInputStream cis = new BetterCipherInputStream(bis, decrypt); }
LEDataInputStream lis = new LEDataInputStream(cis);
public void testCipherStreams() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException {
byte[] decrypttext = lis.readBytes(MESSAGE_LENGTH); final int MESSAGE_LENGTH = 1024;
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext); byte[] key = new byte[32];
} byte[] iv = new byte[16];
byte[] plaintext = new byte[MESSAGE_LENGTH];
rand.nextBytes(key);
rand.nextBytes(iv);
rand.nextBytes(plaintext);
CipherEngine aes = CipherFactory.INSTANCE.getInstance(AesEngine.CIPHER_UUID);
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
CipherOutputStream cos = new CipherOutputStream(bos, encrypt);
cos.write(plaintext);
cos.close();
byte[] secrettext = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(secrettext);
BetterCipherInputStream cis = new BetterCipherInputStream(bis, decrypt);
LEDataInputStream lis = new LEDataInputStream(cis);
byte[] decrypttext = lis.readBytes(MESSAGE_LENGTH);
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
}
} }

View File

@@ -1,22 +1,22 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* KeePass DX is distributed in the hope that it will be useful, * KeePass DX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>. * along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.tests.crypto; package com.kunzisoft.keepass.tests.crypto;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
@@ -30,37 +30,37 @@ import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey;
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey; import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey;
public class FinalKeyTest extends TestCase { public class FinalKeyTest extends TestCase {
private Random mRand; private Random mRand;
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
mRand = new Random(); mRand = new Random();
} }
public void testNativeAndroid() throws IOException { public void testNativeAndroid() throws IOException {
// Test both an old and an even number to test my flip variable // Test both an old and an even number to test my flip variable
testNativeFinalKey(5); testNativeFinalKey(5);
testNativeFinalKey(6); testNativeFinalKey(6);
} }
private void testNativeFinalKey(int rounds) throws IOException { private void testNativeFinalKey(int rounds) throws IOException {
byte[] seed = new byte[32]; byte[] seed = new byte[32];
byte[] key = new byte[32]; byte[] key = new byte[32];
byte[] nativeKey; byte[] nativeKey;
byte[] androidKey; byte[] androidKey;
mRand.nextBytes(seed); mRand.nextBytes(seed);
mRand.nextBytes(key); mRand.nextBytes(key);
AndroidFinalKey aKey = new AndroidFinalKey(); AndroidFinalKey aKey = new AndroidFinalKey();
androidKey = aKey.transformMasterKey(seed, key, rounds); androidKey = aKey.transformMasterKey(seed, key, rounds);
NativeFinalKey nKey = new NativeFinalKey(); NativeFinalKey nKey = new NativeFinalKey();
nativeKey = nKey.transformMasterKey(seed, key, rounds); nativeKey = nKey.transformMasterKey(seed, key, rounds);
assertArrayEquals("Does not match", androidKey, nativeKey); assertArrayEquals("Does not match", androidKey, nativeKey);
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -19,31 +19,24 @@
*/ */
package com.kunzisoft.keepass.tests.database; package com.kunzisoft.keepass.tests.database;
import android.content.Context;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.element.GroupVersioned;
import com.kunzisoft.keepass.database.Database; import com.kunzisoft.keepass.database.element.PwDatabase;
import com.kunzisoft.keepass.database.PwDatabase; import com.kunzisoft.keepass.database.element.PwDatabaseV3;
import com.kunzisoft.keepass.database.PwDatabaseV3; import com.kunzisoft.keepass.database.element.PwEntryV3;
import com.kunzisoft.keepass.database.PwEntry;
import com.kunzisoft.keepass.database.PwEntryV3;
import com.kunzisoft.keepass.database.PwGroup;
import com.kunzisoft.keepass.database.action.node.DeleteGroupRunnable;
import com.kunzisoft.keepass.search.SearchDbHelper;
import java.util.List;
public class DeleteEntry extends AndroidTestCase { public class DeleteEntry extends AndroidTestCase {
private static final String GROUP1_NAME = "Group1"; private static final String GROUP1_NAME = "Group1";
private static final String ENTRY1_NAME = "Test1"; private static final String ENTRY1_NAME = "Test1";
private static final String ENTRY2_NAME = "Test2"; private static final String ENTRY2_NAME = "Test2";
private static final String KEYFILE = ""; private static final String KEYFILE = "";
private static final String PASSWORD = "12345"; private static final String PASSWORD = "12345";
private static final String ASSET = "delete.kdb"; private static final String ASSET = "delete.kdb";
private static final String FILENAME = "/sdcard/delete.kdb"; private static final String FILENAME = "/sdcard/delete.kdb";
public void testDelete() { public void testDelete() {
/*
Database db; Database db;
Context ctx = getContext(); Context ctx = getContext();
@@ -56,7 +49,7 @@ public class DeleteEntry extends AndroidTestCase {
} }
PwDatabaseV3 pm = (PwDatabaseV3) db.getPwDatabase(); PwDatabaseV3 pm = (PwDatabaseV3) db.getPwDatabase();
PwGroup group1 = getGroup(pm, GROUP1_NAME); GroupVersioned group1 = getGroup(pm, GROUP1_NAME);
assertNotNull("Could not find group1", group1); assertNotNull("Could not find group1", group1);
// Delete the group // Delete the group
@@ -64,16 +57,16 @@ public class DeleteEntry extends AndroidTestCase {
task.run(); task.run();
// Verify the entries were deleted // Verify the entries were deleted
PwEntry entry1 = getEntry(pm, ENTRY1_NAME); PwEntryInterface entry1 = getEntry(pm, ENTRY1_NAME);
assertNull("Entry 1 was not removed", entry1); assertNull("Entry 1 was not removed", entry1);
PwEntry entry2 = getEntry(pm, ENTRY2_NAME); PwEntryInterface entry2 = getEntry(pm, ENTRY2_NAME);
assertNull("Entry 2 was not removed", entry2); assertNull("Entry 2 was not removed", entry2);
// Verify the entries were removed from the search index // Verify the entries were removed from the search index
SearchDbHelper dbHelp = new SearchDbHelper(ctx); SearchDbHelper dbHelp = new SearchDbHelper(ctx);
PwGroup results1 = dbHelp.search(db.getPwDatabase(), ENTRY1_NAME); GroupVersioned results1 = dbHelp.search(db.getPwDatabase(), ENTRY1_NAME, 100);
PwGroup results2 = dbHelp.search(db.getPwDatabase(), ENTRY2_NAME); GroupVersioned results2 = dbHelp.search(db.getPwDatabase(), ENTRY2_NAME, 100);
assertEquals("Entry1 was not removed from the search results", 0, results1.numbersOfChildEntries()); assertEquals("Entry1 was not removed from the search results", 0, results1.numbersOfChildEntries());
assertEquals("Entry2 was not removed from the search results", 0, results2.numbersOfChildEntries()); assertEquals("Entry2 was not removed from the search results", 0, results2.numbersOfChildEntries());
@@ -81,10 +74,13 @@ public class DeleteEntry extends AndroidTestCase {
// Verify the group was deleted // Verify the group was deleted
group1 = getGroup(pm, GROUP1_NAME); group1 = getGroup(pm, GROUP1_NAME);
assertNull("Group 1 was not removed.", group1); assertNull("Group 1 was not removed.", group1);
*/
} }
private PwEntryV3 getEntry(PwDatabaseV3 pm, String name) { private PwEntryV3 getEntry(PwDatabaseV3 pm, String name) {
/*
TODO test
List<PwEntryV3> entries = pm.getEntries(); List<PwEntryV3> entries = pm.getEntries();
for ( int i = 0; i < entries.size(); i++ ) { for ( int i = 0; i < entries.size(); i++ ) {
PwEntryV3 entry = entries.get(i); PwEntryV3 entry = entries.get(i);
@@ -92,22 +88,24 @@ public class DeleteEntry extends AndroidTestCase {
return entry; return entry;
} }
} }
*/
return null; return null;
} }
private PwGroup getGroup(PwDatabase pm, String name) { private GroupVersioned getGroup(PwDatabase pm, String name) {
List<PwGroup> groups = pm.getGroups(); /*
List<GroupVersioned> groups = pm.getGroups();
for ( int i = 0; i < groups.size(); i++ ) { for ( int i = 0; i < groups.size(); i++ ) {
PwGroup group = groups.get(i); GroupVersioned group = groups.get(i);
if ( group.getName().equals(name) ) { if ( group.getTitle().equals(name) ) {
return group; return group;
} }
} }
*/
return null;
} return null;
}
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -19,14 +19,15 @@
*/ */
package com.kunzisoft.keepass.tests.database; package com.kunzisoft.keepass.tests.database;
import junit.framework.TestCase; import com.kunzisoft.keepass.database.element.PwDatabaseV4;
import com.kunzisoft.keepass.database.element.PwEntryV4;
import com.kunzisoft.keepass.database.PwDatabaseV4; import junit.framework.TestCase;
import com.kunzisoft.keepass.database.PwEntryV4;
public class EntryV4 extends TestCase { public class EntryV4 extends TestCase {
public void testBackup() { public void testBackup() {
/*
PwDatabaseV4 db = new PwDatabaseV4(); PwDatabaseV4 db = new PwDatabaseV4();
db.setHistoryMaxItems(2); db.setHistoryMaxItems(2);
@@ -46,9 +47,10 @@ public class EntryV4 extends TestCase {
entry.createBackup(db); entry.createBackup(db);
PwEntryV4 backup = entry.getHistory().get(0); PwEntryV4 backup = entry.getHistory().get(0);
entry.endToManageFieldReferences(); entry.stopToManageFieldReferences();
assertEquals("Title2", backup.getTitle()); assertEquals("Title2", backup.getTitle());
assertEquals("User2", backup.getUsername()); assertEquals("User2", backup.getUsername());
} */
}
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -19,22 +19,12 @@
*/ */
package com.kunzisoft.keepass.tests.database; package com.kunzisoft.keepass.tests.database;
import android.content.Context;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.Environment;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.load.ImporterV3;
import com.kunzisoft.keepass.tests.TestUtil;
import com.kunzisoft.keepass.utils.UriUtil;
import java.io.InputStream;
import java.io.File;
public class Kdb3 extends AndroidTestCase { public class Kdb3 extends AndroidTestCase {
private void testKeyfile(String dbAsset, String keyAsset, String password) throws Exception { private void testKeyfile(String dbAsset, String keyAsset, String password) throws Exception {
/*
Context ctx = getContext(); Context ctx = getContext();
File sdcard = Environment.getExternalStorageDirectory(); File sdcard = Environment.getExternalStorageDirectory();
@@ -49,14 +39,15 @@ public class Kdb3 extends AndroidTestCase {
importer.openDatabase(is, password, TestUtil.getKeyFileInputStream(ctx, keyPath)); importer.openDatabase(is, password, TestUtil.getKeyFileInputStream(ctx, keyPath));
is.close(); is.close();
} */
}
public void testXMLKeyFile() throws Exception {
testKeyfile("kdb_with_xml_keyfile.kdb", "keyfile.key", "12345"); public void testXMLKeyFile() throws Exception {
} testKeyfile("kdb_with_xml_keyfile.kdb", "keyfile.key", "12345");
}
public void testBinary64KeyFile() throws Exception {
testKeyfile("binary-key.kdb", "binary.key", "12345"); public void testBinary64KeyFile() throws Exception {
} testKeyfile("binary-key.kdb", "binary.key", "12345");
}
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -19,18 +19,11 @@
*/ */
package com.kunzisoft.keepass.tests.database; package com.kunzisoft.keepass.tests.database;
import java.io.InputStream;
import android.content.Context;
import android.content.res.AssetManager;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.PwDatabaseV3;
import com.kunzisoft.keepass.database.PwEncryptionAlgorithm;
import com.kunzisoft.keepass.database.load.ImporterV3;
public class Kdb3Twofish extends AndroidTestCase { public class Kdb3Twofish extends AndroidTestCase {
public void testReadTwofish() throws Exception { public void testReadTwofish() throws Exception {
/*
Context ctx = getContext(); Context ctx = getContext();
AssetManager am = ctx.getAssets(); AssetManager am = ctx.getAssets();
@@ -43,6 +36,6 @@ public class Kdb3Twofish extends AndroidTestCase {
assertTrue(db.getEncryptionAlgorithm() == PwEncryptionAlgorithm.Twofish); assertTrue(db.getEncryptionAlgorithm() == PwEncryptionAlgorithm.Twofish);
is.close(); is.close();
*/
} }
} }

View File

@@ -19,27 +19,17 @@
*/ */
package com.kunzisoft.keepass.tests.database; package com.kunzisoft.keepass.tests.database;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import android.content.Context; import android.content.Context;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.PwDatabaseV4;
import com.kunzisoft.keepass.database.exception.InvalidDBException; import com.kunzisoft.keepass.database.exception.InvalidDBException;
import com.kunzisoft.keepass.database.exception.PwDbOutputException; import com.kunzisoft.keepass.database.exception.PwDbOutputException;
import com.kunzisoft.keepass.database.load.Importer;
import com.kunzisoft.keepass.database.load.ImporterFactory;
import com.kunzisoft.keepass.database.load.ImporterV4;
import com.kunzisoft.keepass.database.save.PwDbOutput;
import com.kunzisoft.keepass.database.save.PwDbV4Output;
import com.kunzisoft.keepass.stream.CopyInputStream;
import com.kunzisoft.keepass.tests.TestUtil; import com.kunzisoft.keepass.tests.TestUtil;
import java.io.IOException;
import java.io.InputStream;
public class Kdb4 extends AndroidTestCase { public class Kdb4 extends AndroidTestCase {
public void testDetection() throws IOException, InvalidDBException { public void testDetection() throws IOException, InvalidDBException {
@@ -48,11 +38,13 @@ public class Kdb4 extends AndroidTestCase {
AssetManager am = ctx.getAssets(); AssetManager am = ctx.getAssets();
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING); InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
Importer importer = ImporterFactory.createImporter(is); Importer importer = ImporterFactory.createImporter(is);
assertTrue(importer instanceof ImporterV4); assertTrue(importer instanceof ImporterV4);
is.close(); is.close();
*/
} }
public void testParsing() throws IOException, InvalidDBException { public void testParsing() throws IOException, InvalidDBException {
@@ -61,12 +53,13 @@ public class Kdb4 extends AndroidTestCase {
AssetManager am = ctx.getAssets(); AssetManager am = ctx.getAssets();
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING); InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4(); ImporterV4 importer = new ImporterV4();
importer.openDatabase(is, "12345", null); importer.openDatabase(is, "12345", null);
is.close(); is.close();
*/
} }
public void testSavingKDBXV3() throws IOException, InvalidDBException, PwDbOutputException { public void testSavingKDBXV3() throws IOException, InvalidDBException, PwDbOutputException {
@@ -83,6 +76,8 @@ public class Kdb4 extends AndroidTestCase {
AssetManager am = ctx.getAssets(); AssetManager am = ctx.getAssets();
InputStream is = am.open(inputFile, AssetManager.ACCESS_STREAMING); InputStream is = am.open(inputFile, AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4(); ImporterV4 importer = new ImporterV4();
PwDatabaseV4 db = importer.openDatabase(is, password, null); PwDatabaseV4 db = importer.openDatabase(is, password, null);
is.close(); is.close();
@@ -103,7 +98,7 @@ public class Kdb4 extends AndroidTestCase {
bis.close(); bis.close();
fos.close(); fos.close();
*/
} }
@Override @Override
@@ -120,10 +115,13 @@ public class Kdb4 extends AndroidTestCase {
AssetManager am = ctx.getAssets(); AssetManager am = ctx.getAssets();
InputStream is = am.open("keyfile.kdbx", AssetManager.ACCESS_STREAMING); InputStream is = am.open("keyfile.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4(); ImporterV4 importer = new ImporterV4();
importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key"))); importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key")));
is.close(); is.close();
*/
} }
@@ -133,11 +131,13 @@ public class Kdb4 extends AndroidTestCase {
AssetManager am = ctx.getAssets(); AssetManager am = ctx.getAssets();
InputStream is = am.open("keyfile-binary.kdbx", AssetManager.ACCESS_STREAMING); InputStream is = am.open("keyfile-binary.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4(); ImporterV4 importer = new ImporterV4();
importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx,TestUtil.getSdPath("key-binary"))); importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx,TestUtil.getSdPath("key-binary")));
is.close(); is.close();
*/
} }
public void testKeyfile() throws IOException, InvalidDBException { public void testKeyfile() throws IOException, InvalidDBException {
@@ -145,13 +145,13 @@ public class Kdb4 extends AndroidTestCase {
AssetManager am = ctx.getAssets(); AssetManager am = ctx.getAssets();
InputStream is = am.open("key-only.kdbx", AssetManager.ACCESS_STREAMING); InputStream is = am.open("key-only.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4(); ImporterV4 importer = new ImporterV4();
importer.openDatabase(is, "", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key"))); importer.openDatabase(is, "", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key")));
is.close(); is.close();
*/
} }
public void testNoGzip() throws IOException, InvalidDBException { public void testNoGzip() throws IOException, InvalidDBException {
@@ -159,13 +159,13 @@ public class Kdb4 extends AndroidTestCase {
AssetManager am = ctx.getAssets(); AssetManager am = ctx.getAssets();
InputStream is = am.open("no-encrypt.kdbx", AssetManager.ACCESS_STREAMING); InputStream is = am.open("no-encrypt.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4(); ImporterV4 importer = new ImporterV4();
importer.openDatabase(is, "12345", null); importer.openDatabase(is, "12345", null);
is.close(); is.close();
*/
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -23,19 +23,17 @@ import android.content.Context;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import com.kunzisoft.keepass.crypto.engine.AesEngine;
import com.kunzisoft.keepass.database.PwDatabaseV4;
import com.kunzisoft.keepass.database.load.ImporterV4;
import java.io.InputStream; import java.io.InputStream;
public class Kdb4Header extends AndroidTestCase { public class Kdb4Header extends AndroidTestCase {
public void testReadHeader() throws Exception { public void testReadHeader() throws Exception {
Context ctx = getContext(); Context ctx = getContext();
AssetManager am = ctx.getAssets(); AssetManager am = ctx.getAssets();
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING); InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4(); ImporterV4 importer = new ImporterV4();
PwDatabaseV4 db = importer.openDatabase(is, "12345", null); PwDatabaseV4 db = importer.openDatabase(is, "12345", null);
@@ -45,6 +43,7 @@ public class Kdb4Header extends AndroidTestCase {
assertTrue(db.getDataCipher().equals(AesEngine.CIPHER_UUID)); assertTrue(db.getDataCipher().equals(AesEngine.CIPHER_UUID));
is.close(); is.close();
*/
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -19,62 +19,52 @@
*/ */
package com.kunzisoft.keepass.tests.database; package com.kunzisoft.keepass.tests.database;
import java.io.InputStream;
import java.util.UUID;
import android.content.Context; import android.content.Context;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import biz.source_code.base64Coder.Base64Coder;
import com.kunzisoft.keepass.database.PwDatabase; import com.kunzisoft.keepass.database.element.PwDatabaseV4;
import com.kunzisoft.keepass.database.PwDatabaseV4; import com.kunzisoft.keepass.database.element.SprEngineV4;
import com.kunzisoft.keepass.database.PwEntryV4;
import com.kunzisoft.keepass.database.load.ImporterV4; import java.io.InputStream;
import com.kunzisoft.keepass.utils.SprEngineV4;
import com.kunzisoft.keepass.utils.Types;
public class SprEngineTest extends AndroidTestCase { public class SprEngineTest extends AndroidTestCase {
private PwDatabaseV4 db; private PwDatabaseV4 db;
private SprEngineV4 spr; private SprEngineV4 spr;
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
Context ctx = getContext(); Context ctx = getContext();
AssetManager am = ctx.getAssets(); AssetManager am = ctx.getAssets();
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING); InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
/*
TODO Test
ImporterV4 importer = new ImporterV4(); ImporterV4 importer = new ImporterV4();
db = importer.openDatabase(is, "12345", null); db = importer.openDatabase(is, "12345", null);
is.close(); is.close();
spr = new SprEngineV4(); spr = new SprEngineV4();
} */
}
private final String REF = "{REF:P@I:2B1D56590D961F48A8CE8C392CE6CD35}";
private final String ENCODE_UUID = "IN7RkON49Ui1UZ2ddqmLcw=="; private final String REF = "{REF:P@I:2B1D56590D961F48A8CE8C392CE6CD35}";
private final String RESULT = "Password"; private final String ENCODE_UUID = "IN7RkON49Ui1UZ2ddqmLcw==";
public void testRefReplace() { private final String RESULT = "Password";
public void testRefReplace() {
/*
TODO TEST
UUID entryUUID = decodeUUID(ENCODE_UUID); UUID entryUUID = decodeUUID(ENCODE_UUID);
PwEntryV4 entry = (PwEntryV4) db.getEntryByUUIDId(entryUUID); PwEntryV4 entry = (PwEntryV4) db.getEntryById(entryUUID);
assertEquals(RESULT, spr.compile(REF, entry, db)); assertEquals(RESULT, spr.compile(REF, entry, db));
*/
}
private UUID decodeUUID(String encoded) {
if (encoded == null || encoded.length() == 0 ) {
return PwDatabase.UUID_ZERO;
}
byte[] buf = Base64Coder.decode(encoded);
return Types.bytestoUUID(buf);
}
}
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -19,24 +19,16 @@
*/ */
package com.kunzisoft.keepass.tests.database; package com.kunzisoft.keepass.tests.database;
import java.io.InputStream; import com.kunzisoft.keepass.database.element.Database;
import android.content.Context;
import android.content.res.AssetManager;
import android.net.Uri;
import com.kunzisoft.keepass.database.Database;
import com.kunzisoft.keepass.database.PwDatabaseV3Debug;
import com.kunzisoft.keepass.database.load.Importer;
import com.kunzisoft.keepass.tests.TestUtil;
public class TestData { public class TestData {
private static final String TEST1_KEYFILE = ""; private static final String TEST1_KEYFILE = "";
private static final String TEST1_KDB = "test1.kdb"; private static final String TEST1_KDB = "test1.kdb";
private static final String TEST1_PASSWORD = "12345"; private static final String TEST1_PASSWORD = "12345";
private static Database mDb1; private static Database mDb1;
/*
public static Database GetDb1(Context ctx) throws Exception { public static Database GetDb1(Context ctx) throws Exception {
return GetDb1(ctx, false); return GetDb1(ctx, false);
@@ -72,6 +64,8 @@ public class TestData {
GetDb1(ctx); GetDb1(ctx);
} }
return (PwDatabaseV3Debug) mDb1.getPwDatabase(); //return (PwDatabaseV3Debug) mDb1.getPwDatabase();
return null;
} }
*/
} }

View File

@@ -19,33 +19,12 @@
*/ */
package com.kunzisoft.keepass.tests.output; package com.kunzisoft.keepass.tests.output;
import static org.junit.Assert.assertArrayEquals;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import android.content.res.AssetManager;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.PwDatabaseV3Debug;
import com.kunzisoft.keepass.database.PwDbHeader;
import com.kunzisoft.keepass.database.PwDbHeaderV3;
import com.kunzisoft.keepass.database.exception.PwDbOutputException;
import com.kunzisoft.keepass.database.save.PwDbHeaderOutputV3;
import com.kunzisoft.keepass.database.save.PwDbV3Output;
import com.kunzisoft.keepass.database.save.PwDbV3OutputDebug;
import com.kunzisoft.keepass.stream.NullOutputStream;
import com.kunzisoft.keepass.tests.TestUtil;
import com.kunzisoft.keepass.tests.database.TestData;
public class PwManagerOutputTest extends AndroidTestCase { public class PwManagerOutputTest extends AndroidTestCase {
PwDatabaseV3Debug mPM; // PwDatabaseV3Debug mPM;
/*
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
@@ -143,4 +122,5 @@ public class PwManagerOutputTest extends AndroidTestCase {
assertArrayEquals("Databases do not match.", bExpected.toByteArray(), bActual.toByteArray()); assertArrayEquals("Databases do not match.", bExpected.toByteArray(), bActual.toByteArray());
} }
*/
} }

View File

@@ -1,22 +1,22 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* KeePass DX is distributed in the hope that it will be useful, * KeePass DX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>. * along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.tests.search; package com.kunzisoft.keepass.tests.search;
@@ -24,49 +24,47 @@ import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.element.Database;
import com.kunzisoft.keepass.database.Database; import com.kunzisoft.keepass.database.element.GroupVersioned;
import com.kunzisoft.keepass.database.PwGroup;
import com.kunzisoft.keepass.tests.database.TestData;
public class SearchTest extends AndroidTestCase { public class SearchTest extends AndroidTestCase {
private Database mDb; private Database mDb;
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
mDb = TestData.GetDb1(getContext(), true); //mDb = TestData.GetDb1(getContext(), true);
} }
public void testSearch() { public void testSearch() {
PwGroup results = mDb.search("Amazon"); GroupVersioned results = mDb.search("Amazon");
assertTrue("Search result not found.", results.numbersOfChildEntries() > 0); //assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
} }
public void testBackupIncluded() { public void testBackupIncluded() {
updateOmitSetting(false); updateOmitSetting(false);
PwGroup results = mDb.search("BackupOnly"); GroupVersioned results = mDb.search("BackupOnly");
assertTrue("Search result not found.", results.numbersOfChildEntries() > 0); //assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
} }
public void testBackupExcluded() { public void testBackupExcluded() {
updateOmitSetting(true); updateOmitSetting(true);
PwGroup results = mDb.search("BackupOnly"); GroupVersioned results = mDb.search("BackupOnly");
assertFalse("Search result found, but should not have been.", results.numbersOfChildEntries() > 0); //assertFalse("Search result found, but should not have been.", results.numbersOfChildEntries() > 0);
} }
private void updateOmitSetting(boolean setting) { private void updateOmitSetting(boolean setting) {
Context ctx = getContext(); Context ctx = getContext();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
SharedPreferences.Editor editor = prefs.edit(); SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("settings_omitbackup_key", setting); editor.putBoolean("settings_omitbackup_key", setting);
editor.commit(); editor.commit();
} }
} }

View File

@@ -1,22 +1,22 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* KeePass DX is distributed in the hope that it will be useful, * KeePass DX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>. * along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.tests.stream; package com.kunzisoft.keepass.tests.stream;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
@@ -34,77 +34,77 @@ import com.kunzisoft.keepass.stream.HashedBlockInputStream;
import com.kunzisoft.keepass.stream.HashedBlockOutputStream; import com.kunzisoft.keepass.stream.HashedBlockOutputStream;
public class HashedBlock extends TestCase { public class HashedBlock extends TestCase {
private static Random rand = new Random();
public void testBlockAligned() throws IOException { private static Random rand = new Random();
testSize(1024, 1024);
}
public void testOffset() throws IOException {
testSize(1500, 1024);
}
private void testSize(int blockSize, int bufferSize) throws IOException {
byte[] orig = new byte[blockSize];
rand.nextBytes(orig);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HashedBlockOutputStream output = new HashedBlockOutputStream(bos, bufferSize);
output.write(orig);
output.close();
byte[] encoded = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
HashedBlockInputStream input = new HashedBlockInputStream(bis);
ByteArrayOutputStream decoded = new ByteArrayOutputStream(); public void testBlockAligned() throws IOException {
while ( true ) { testSize(1024, 1024);
byte[] buf = new byte[1024]; }
int read = input.read(buf);
if ( read == -1 ) { public void testOffset() throws IOException {
break; testSize(1500, 1024);
} }
decoded.write(buf, 0, read); private void testSize(int blockSize, int bufferSize) throws IOException {
} byte[] orig = new byte[blockSize];
byte[] out = decoded.toByteArray(); rand.nextBytes(orig);
assertArrayEquals(orig, out); ByteArrayOutputStream bos = new ByteArrayOutputStream();
HashedBlockOutputStream output = new HashedBlockOutputStream(bos, bufferSize);
} output.write(orig);
output.close();
public void testGZIPStream() throws IOException {
final int testLength = 32000; byte[] encoded = bos.toByteArray();
byte[] orig = new byte[testLength]; ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
rand.nextBytes(orig); HashedBlockInputStream input = new HashedBlockInputStream(bis);
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream decoded = new ByteArrayOutputStream();
HashedBlockOutputStream hos = new HashedBlockOutputStream(bos); while ( true ) {
GZIPOutputStream zos = new GZIPOutputStream(hos); byte[] buf = new byte[1024];
int read = input.read(buf);
zos.write(orig); if ( read == -1 ) {
zos.close(); break;
}
byte[] compressed = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(compressed); decoded.write(buf, 0, read);
HashedBlockInputStream his = new HashedBlockInputStream(bis); }
GZIPInputStream zis = new GZIPInputStream(his);
byte[] out = decoded.toByteArray();
byte[] uncompressed = new byte[testLength];
assertArrayEquals(orig, out);
int read = 0;
while (read != -1 && testLength - read > 0) { }
read += zis.read(uncompressed, read, testLength - read);
public void testGZIPStream() throws IOException {
} final int testLength = 32000;
assertArrayEquals("Output not equal to input", orig, uncompressed); byte[] orig = new byte[testLength];
rand.nextBytes(orig);
} ByteArrayOutputStream bos = new ByteArrayOutputStream();
HashedBlockOutputStream hos = new HashedBlockOutputStream(bos);
GZIPOutputStream zos = new GZIPOutputStream(hos);
zos.write(orig);
zos.close();
byte[] compressed = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(compressed);
HashedBlockInputStream his = new HashedBlockInputStream(bis);
GZIPInputStream zis = new GZIPInputStream(his);
byte[] uncompressed = new byte[testLength];
int read = 0;
while (read != -1 && testLength - read > 0) {
read += zis.read(uncompressed, read, testLength - read);
}
assertArrayEquals("Output not equal to input", orig, uncompressed);
}
} }

View File

@@ -1,57 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.tests.utils;
import java.util.Locale;
import com.kunzisoft.keepass.utils.StrUtil;
import junit.framework.TestCase;
public class StrUtilTest extends TestCase {
private final String text = "AbCdEfGhIj";
private final String search = "BcDe";
private final String badSearch = "Ed";
public void testIndexOfIgnoreCase1() {
assertEquals(1, StrUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH));
}
public void testIndexOfIgnoreCase2() {
assertEquals(-1, StrUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH), 2);
}
public void testIndexOfIgnoreCase3() {
assertEquals(-1, StrUtil.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH));
}
private final String repText = "AbCtestingaBc";
private final String repSearch = "ABc";
private final String repSearchBad = "CCCCCC";
private final String repNew = "12345";
private final String repResult = "12345testing12345";
public void testReplaceAllIgnoresCase1() {
assertEquals(repResult, StrUtil.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH));
}
public void testReplaceAllIgnoresCase2() {
assertEquals(repText, StrUtil.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH));
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.tests.utils;
import java.util.Locale;
import com.kunzisoft.keepass.utils.StringUtil;
import junit.framework.TestCase;
public class StringUtilTest extends TestCase {
private final String text = "AbCdEfGhIj";
private final String search = "BcDe";
private final String badSearch = "Ed";
public void testIndexOfIgnoreCase1() {
assertEquals(1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH));
}
public void testIndexOfIgnoreCase2() {
assertEquals(-1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH), 2);
}
public void testIndexOfIgnoreCase3() {
assertEquals(-1, StringUtil.INSTANCE.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH));
}
private final String repText = "AbCtestingaBc";
private final String repSearch = "ABc";
private final String repSearchBad = "CCCCCC";
private final String repNew = "12345";
private final String repResult = "12345testing12345";
public void testReplaceAllIgnoresCase1() {
assertEquals(repResult, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH));
}
public void testReplaceAllIgnoresCase2() {
assertEquals(repText, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH));
}
}

View File

@@ -20,19 +20,18 @@
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup" android:fullBackupContent="@xml/backup"
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent" android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
android:theme="@style/KeepassDXStyle.Night" android:theme="@style/KeepassDXStyle.Night">
tools:replace="android:theme">
<!-- TODO backup API Key --> <!-- TODO backup API Key -->
<meta-data <meta-data
android:name="com.google.android.backup.api_key" android:name="com.google.android.backup.api_key"
android:value="" /> android:value="" />
<activity <activity
android:name="com.kunzisoft.keepass.fileselect.FileSelectActivity" android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
android:theme="@style/KeepassDXStyle.SplashScreen" android:theme="@style/KeepassDXStyle.SplashScreen"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTop" android:launchMode="singleTop"
android:configChanges="orientation|keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="stateHidden" > android:windowSoftInputMode="stateHidden" >
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -40,12 +39,8 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name="com.kunzisoft.keepass.activities.AboutActivity" android:name="com.kunzisoft.keepass.activities.PasswordActivity"
android:launchMode="singleTask" android:configChanges="keyboardHidden"
android:label="@string/menu_about" />
<activity
android:name="com.kunzisoft.keepass.password.PasswordActivity"
android:configChanges="orientation|keyboardHidden"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@@ -94,50 +89,64 @@
android:resource="@xml/nnf_provider_paths" /> android:resource="@xml/nnf_provider_paths" />
</provider> </provider>
<activity <activity
android:name="com.kunzisoft.keepass.fileselect.FilePickerStylishActivity" android:name=".activities.stylish.FilePickerStylishActivity"
android:label="@string/app_name"> android:label="@string/app_name">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.GET_CONTENT" /> <action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Main Activity -->
<activity <activity
android:name="com.kunzisoft.keepass.activities.GroupActivity" android:name="com.kunzisoft.keepass.activities.GroupActivity"
android:configChanges="orientation|keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustPan"> android:windowSoftInputMode="adjustPan"
android:launchMode="singleTask">
<meta-data <meta-data
android:name="android.app.default_searchable" android:name="android.app.default_searchable"
android:value="com.kunzisoft.keepass.search.SearchResults" android:value="com.kunzisoft.keepass.search.SearchResults"
android:exported="false"/> android:exported="false"/>
</activity>
<activity
android:name="com.kunzisoft.keepass.activities.EntryActivity"
android:configChanges="orientation|keyboardHidden"
android:windowSoftInputMode="stateHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
android:configChanges="orientation|keyboardHidden"
android:windowSoftInputMode="stateHidden" />
<activity
android:name="com.kunzisoft.keepass.search.SearchResultsActivity"
android:launchMode="standard">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEARCH" /> <action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.app.searchable" android:name="android.app.searchable"
android:resource="@xml/searchable" /> android:resource="@xml/searchable" />
</activity> </activity>
<activity
android:name="com.kunzisoft.keepass.activities.EntryActivity"
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustResize" />
<!-- About and Settings -->
<activity
android:name="com.kunzisoft.keepass.activities.AboutActivity"
android:launchMode="singleTask"
android:label="@string/menu_about" />
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" /> <activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
<activity android:name="com.kunzisoft.keepass.autofill.AutoFillAuthActivity" <activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
android:configChanges="orientation|keyboardHidden" /> android:configChanges="keyboardHidden" />
<activity android:name="com.kunzisoft.keepass.selection.EntrySelectionAuthActivity"
android:configChanges="orientation|keyboardHidden" />
<activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" /> <activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" />
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
android:label="@string/keyboard_name">
</activity>
<activity android:name="com.kunzisoft.keepass.settings.MagikIMESettings"
android:label="@string/keyboard_setting_label">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
<service <service
android:name="com.kunzisoft.keepass.notifications.NotificationCopyingService" android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<!-- Receiver for Autofill --> <!-- Receiver for Autofill -->
@@ -152,6 +161,20 @@
<action android:name="android.service.autofill.AutofillService" /> <action android:name="android.service.autofill.AutofillService" />
</intent-filter> </intent-filter>
</service> </service>
<service
android:name="com.kunzisoft.keepass.magikeyboard.MagikIME"
android:label="@string/keyboard_label"
android:permission="android.permission.BIND_INPUT_METHOD" >
<meta-data android:name="android.view.im"
android:resource="@xml/keyboard_method"/>
<intent-filter>
<action android:name="android.view.InputMethod" />
</intent-filter>
</service>
<service
android:name="com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService"
android:enabled="true"
android:exported="false" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" /> <meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
</application> </application>

Binary file not shown.

View File

@@ -1,81 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.MenuItem;
import android.widget.TextView;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.stylish.StylishActivity;
import org.joda.time.DateTime;
public class AboutActivity extends StylishActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.about);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar.setTitle(getString(R.string.menu_about));
setSupportActionBar(toolbar);
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
String version;
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
version = packageInfo.versionName;
} catch (NameNotFoundException e) {
Log.w(getClass().getSimpleName(), "Unable to get application version", e);
version = "Unable to get application version.";
}
version = getString(R.string.version_label) + " " + version;
TextView versionText = (TextView) findViewById(R.id.activity_about_version);
versionText.setText(version);
TextView disclaimerText = (TextView) findViewById(R.id.disclaimer);
disclaimerText.setText(getString(R.string.disclaimer_formal, new DateTime().getYear()));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
switch (id) {
case android.R.id.home:
finish();
break;
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
import android.content.pm.PackageManager.NameNotFoundException
import android.os.Bundle
import android.support.v7.widget.Toolbar
import android.util.Log
import android.view.MenuItem
import android.widget.TextView
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import org.joda.time.DateTime
class AboutActivity : StylishActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_about)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.title = getString(R.string.menu_about)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
var version: String
var build: String
try {
version = packageManager.getPackageInfo(packageName, 0).versionName
build = BuildConfig.BUILD_VERSION
} catch (e: NameNotFoundException) {
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
version = "Unable to get the app version"
build = "Unable to get the build version"
}
version = getString(R.string.version_label, version)
val versionTextView = findViewById<TextView>(R.id.activity_about_version)
versionTextView.text = version
build = getString(R.string.build_label, build)
val buildTextView = findViewById<TextView>(R.id.activity_about_build)
buildTextView.text = build
val disclaimerText = findViewById<TextView>(R.id.disclaimer)
disclaimerText.text = getString(R.string.disclaimer_formal, DateTime().year)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
when (item.itemId) {
android.R.id.home -> finish()
}
return super.onOptionsItemSelected(item)
}
}

View File

@@ -1,495 +0,0 @@
/*
*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.getkeepsafe.taptargetview.TapTarget;
import com.getkeepsafe.taptargetview.TapTargetView;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.app.App;
import com.kunzisoft.keepass.database.Database;
import com.kunzisoft.keepass.database.ExtraFields;
import com.kunzisoft.keepass.database.PwDatabase;
import com.kunzisoft.keepass.database.PwEntry;
import com.kunzisoft.keepass.database.security.ProtectedString;
import com.kunzisoft.keepass.icons.IconPackChooser;
import com.kunzisoft.keepass.lock.LockingActivity;
import com.kunzisoft.keepass.lock.LockingHideActivity;
import com.kunzisoft.keepass.notifications.NotificationCopyingService;
import com.kunzisoft.keepass.notifications.NotificationField;
import com.kunzisoft.keepass.settings.PreferencesUtil;
import com.kunzisoft.keepass.settings.SettingsAutofillActivity;
import com.kunzisoft.keepass.timeout.ClipboardHelper;
import com.kunzisoft.keepass.utils.EmptyUtils;
import com.kunzisoft.keepass.utils.MenuUtil;
import com.kunzisoft.keepass.utils.Types;
import com.kunzisoft.keepass.utils.Util;
import com.kunzisoft.keepass.view.EntryContentsView;
import java.util.ArrayList;
import java.util.Date;
import java.util.UUID;
import static com.kunzisoft.keepass.settings.PreferencesUtil.isClipboardNotificationsEnable;
public class EntryActivity extends LockingHideActivity {
private final static String TAG = EntryActivity.class.getName();
public static final String KEY_ENTRY = "entry";
private ImageView titleIconView;
private TextView titleView;
private EntryContentsView entryContentsView;
private Toolbar toolbar;
protected PwEntry mEntry;
private boolean mShowPassword;
private ClipboardHelper clipboardHelper;
private boolean firstLaunchOfActivity;
public static void launch(Activity act, PwEntry pw, boolean readOnly) {
if (LockingActivity.checkTimeIsAllowedOrFinish(act)) {
Intent intent = new Intent(act, EntryActivity.class);
intent.putExtra(KEY_ENTRY, Types.UUIDtoBytes(pw.getUUID()));
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly);
act.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.entry_view);
toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
assert getSupportActionBar() != null;
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
Database db = App.getDB();
// Likely the app has been killed exit the activity
if ( ! db.getLoaded() ) {
finish();
return;
}
readOnly = db.isReadOnly() || readOnly;
mShowPassword = !PreferencesUtil.isPasswordMask(this);
// Get Entry from UUID
Intent i = getIntent();
UUID uuid = Types.bytestoUUID(i.getByteArrayExtra(KEY_ENTRY));
mEntry = db.getPwDatabase().getEntryByUUIDId(uuid);
if (mEntry == null) {
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show();
finish();
return;
}
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
invalidateOptionsMenu();
// Update last access time.
mEntry.touch(false, false);
// Get views
titleIconView = findViewById(R.id.entry_icon);
titleView = findViewById(R.id.entry_title);
entryContentsView = findViewById(R.id.entry_contents);
entryContentsView.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this));
// Init the clipboard helper
clipboardHelper = new ClipboardHelper(this);
firstLaunchOfActivity = true;
}
@Override
protected void onResume() {
super.onResume();
// Fill data in resume to update from EntryEditActivity
fillData();
invalidateOptionsMenu();
// Start to manage field reference to copy a value from ref
mEntry.startToManageFieldReferences(App.getDB().getPwDatabase());
boolean containsUsernameToCopy =
mEntry.getUsername().length() > 0;
boolean containsPasswordToCopy =
(mEntry.getPassword().length() > 0
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(this));
boolean containsExtraFieldToCopy =
(mEntry.allowExtraFields()
&& ((mEntry.containsCustomFields()
&& mEntry.containsCustomFieldsNotProtected())
|| (mEntry.containsCustomFields()
&& mEntry.containsCustomFieldsProtected()
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(this))
)
);
// If notifications enabled in settings
// Don't if application timeout
if (firstLaunchOfActivity && !App.isShutdown() && isClipboardNotificationsEnable(getApplicationContext())) {
if (containsUsernameToCopy
|| containsPasswordToCopy
|| containsExtraFieldToCopy
) {
// username already copied, waiting for user's action before copy password.
Intent intent = new Intent(this, NotificationCopyingService.class);
intent.setAction(NotificationCopyingService.ACTION_NEW_NOTIFICATION);
if (mEntry.getTitle() != null)
intent.putExtra(NotificationCopyingService.EXTRA_ENTRY_TITLE, mEntry.getTitle());
// Construct notification fields
ArrayList<NotificationField> notificationFields = new ArrayList<>();
// Add username if exists to notifications
if (containsUsernameToCopy)
notificationFields.add(
new NotificationField(
NotificationField.NotificationFieldId.USERNAME,
mEntry.getUsername(),
getResources()));
// Add password to notifications
if (containsPasswordToCopy) {
notificationFields.add(
new NotificationField(
NotificationField.NotificationFieldId.PASSWORD,
mEntry.getPassword(),
getResources()));
}
// Add extra fields
if (containsExtraFieldToCopy) {
try {
mEntry.getFields().doActionToAllCustomProtectedField(new ExtraFields.ActionProtected() {
private int anonymousFieldNumber = 0;
@Override
public void doAction(String key, ProtectedString value) {
//If value is not protected or allowed
if (!value.isProtected() || PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this)) {
notificationFields.add(
new NotificationField(
NotificationField.NotificationFieldId.getAnonymousFieldId()[anonymousFieldNumber],
value.toString(),
key,
getResources()));
anonymousFieldNumber++;
}
}
});
} catch (ArrayIndexOutOfBoundsException e) {
Log.w(TAG, "Only " + NotificationField.NotificationFieldId.getAnonymousFieldId().length +
" anonymous notifications are available");
}
}
// Add notifications
intent.putParcelableArrayListExtra(NotificationCopyingService.EXTRA_FIELDS, notificationFields);
startService(intent);
}
mEntry.endToManageFieldReferences();
}
firstLaunchOfActivity = false;
}
/**
* Check and display learning views
* Displays the explanation for copying a field and editing an entry
*/
private void checkAndPerformedEducation(Menu menu) {
if (PreferencesUtil.isEducationScreensEnabled(this)) {
if (entryContentsView != null && entryContentsView.isUserNamePresent()
&& !PreferencesUtil.isEducationCopyUsernamePerformed(this)) {
TapTargetView.showFor(this,
TapTarget.forView(findViewById(R.id.entry_user_name_action_image),
getString(R.string.education_field_copy_title),
getString(R.string.education_field_copy_summary))
.textColorInt(Color.WHITE)
.tintTarget(false)
.cancelable(true),
new TapTargetView.Listener() {
@Override
public void onTargetClick(TapTargetView view) {
super.onTargetClick(view);
clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(),
getString(R.string.copy_field, getString(R.string.entry_user_name)));
}
@Override
public void onOuterCircleClick(TapTargetView view) {
super.onOuterCircleClick(view);
view.dismiss(false);
// Launch autofill settings
startActivity(new Intent(EntryActivity.this, SettingsAutofillActivity.class));
}
});
PreferencesUtil.saveEducationPreference(this,
R.string.education_copy_username_key);
} else if (!PreferencesUtil.isEducationEntryEditPerformed(this)) {
try {
TapTargetView.showFor(this,
TapTarget.forToolbarMenuItem(toolbar, R.id.menu_edit,
getString(R.string.education_entry_edit_title),
getString(R.string.education_entry_edit_summary))
.textColorInt(Color.WHITE)
.tintTarget(true)
.cancelable(true),
new TapTargetView.Listener() {
@Override
public void onTargetClick(TapTargetView view) {
super.onTargetClick(view);
MenuItem editItem = menu.findItem(R.id.menu_edit);
onOptionsItemSelected(editItem);
}
@Override
public void onOuterCircleClick(TapTargetView view) {
super.onOuterCircleClick(view);
view.dismiss(false);
// Open Keepass doc to create field references
Intent browserIntent = new Intent(Intent.ACTION_VIEW,
Uri.parse(getString(R.string.field_references_url)));
startActivity(browserIntent);
}
});
PreferencesUtil.saveEducationPreference(this,
R.string.education_entry_edit_key);
} catch (Exception e) {
// If icon not visible
Log.w(TAG, "Can't performed education for entry's edition");
}
}
}
}
protected void fillData() {
Database db = App.getDB();
PwDatabase pm = db.getPwDatabase();
mEntry.startToManageFieldReferences(pm);
// Assign title icon
if (IconPackChooser.getSelectedIconPack(this).tintable()) {
// Retrieve the textColor to tint the icon
int[] attrs = {R.attr.textColorInverse};
TypedArray ta = getTheme().obtainStyledAttributes(attrs);
int iconColor = ta.getColor(0, Color.WHITE);
App.getDB().getDrawFactory().assignDatabaseIconTo(this, titleIconView, mEntry.getIcon(), true, iconColor);
} else {
App.getDB().getDrawFactory().assignDatabaseIconTo(this, titleIconView, mEntry.getIcon());
}
// Assign title text
titleView.setText(mEntry.getTitle());
// Assign basic fields
entryContentsView.assignUserName(mEntry.getUsername());
entryContentsView.assignUserNameCopyListener(view ->
clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(),
getString(R.string.copy_field, getString(R.string.entry_user_name)))
);
entryContentsView.assignPassword(mEntry.getPassword());
if (PreferencesUtil.allowCopyPasswordAndProtectedFields(this)) {
entryContentsView.assignPasswordCopyListener(view ->
clipboardHelper.timeoutCopyToClipboard(mEntry.getPassword(),
getString(R.string.copy_field, getString(R.string.entry_password)))
);
}
entryContentsView.assignURL(mEntry.getUrl());
entryContentsView.setHiddenPasswordStyle(!mShowPassword);
entryContentsView.assignComment(mEntry.getNotes());
// Assign custom fields
if (mEntry.allowExtraFields()) {
entryContentsView.clearExtraFields();
mEntry.getFields().doActionToAllCustomProtectedField((label, value) -> {
boolean showAction = (!value.isProtected() || PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this));
entryContentsView.addExtraField(label, value, showAction, view ->
clipboardHelper.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
)
);
});
}
// Assign dates
entryContentsView.assignCreationDate(mEntry.getCreationTime().getDate());
entryContentsView.assignModificationDate(mEntry.getLastModificationTime().getDate());
entryContentsView.assignLastAccessDate(mEntry.getLastAccessTime().getDate());
Date expires = mEntry.getExpiryTime().getDate();
if ( mEntry.isExpires() ) {
entryContentsView.assignExpiresDate(expires);
} else {
entryContentsView.assignExpiresDate(getString(R.string.never));
}
mEntry.endToManageFieldReferences();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE:
fillData();
break;
}
}
private void changeShowPasswordIcon(MenuItem togglePassword) {
if ( mShowPassword ) {
togglePassword.setTitle(R.string.menu_hide_password);
togglePassword.setIcon(R.drawable.ic_visibility_off_white_24dp);
} else {
togglePassword.setTitle(R.string.menu_showpass);
togglePassword.setIcon(R.drawable.ic_visibility_white_24dp);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
MenuUtil.contributionMenuInflater(inflater, menu);
inflater.inflate(R.menu.entry, menu);
inflater.inflate(R.menu.database_lock, menu);
if (readOnly) {
MenuItem edit = menu.findItem(R.id.menu_edit);
if (edit != null)
edit.setVisible(false);
}
MenuItem togglePassword = menu.findItem(R.id.menu_toggle_pass);
if (entryContentsView != null && togglePassword != null) {
if (entryContentsView.isPasswordPresent() || entryContentsView.atLeastOneFieldProtectedPresent()) {
changeShowPasswordIcon(togglePassword);
} else {
togglePassword.setVisible(false);
}
}
MenuItem gotoUrl = menu.findItem(R.id.menu_goto_url);
if (gotoUrl != null) {
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
// so mEntry may not be set
if (mEntry == null) {
gotoUrl.setVisible(false);
} else {
String url = mEntry.getUrl();
if (EmptyUtils.isNullOrEmpty(url)) {
// disable button if url is not available
gotoUrl.setVisible(false);
}
}
}
// Show education views
new Handler().post(() -> checkAndPerformedEducation(menu));
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch ( item.getItemId() ) {
case R.id.menu_contribute:
return MenuUtil.onContributionItemSelected(this);
case R.id.menu_toggle_pass:
mShowPassword = !mShowPassword;
changeShowPasswordIcon(item);
entryContentsView.setHiddenPasswordStyle(!mShowPassword);
return true;
case R.id.menu_edit:
EntryEditActivity.launch(EntryActivity.this, mEntry);
return true;
case R.id.menu_goto_url:
String url;
url = mEntry.getUrl();
// Default http:// if no protocol specified
if ( ! url.contains("://") ) {
url = "http://" + url;
}
try {
Util.gotoUrl(this, url);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show();
}
return true;
case R.id.menu_lock:
lockAndExit();
return true;
case android.R.id.home :
finish(); // close this activity and return to preview activity (if there is any)
}
return super.onOptionsItemSelected(item);
}
@Override
public void finish() {
// Transit data in previous Activity after an update
/*
TODO Slowdown when add entry as result
Intent intent = new Intent();
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
*/
super.finish();
}
}

View File

@@ -0,0 +1,403 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.support.design.widget.CollapsingToolbarLayout
import android.support.v7.app.AlertDialog
import android.support.v7.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.EntryVersioned
import com.kunzisoft.keepass.database.element.PwNodeId
import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.Util
import com.kunzisoft.keepass.view.EntryContentsView
class EntryActivity : LockingHideActivity() {
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var titleIconView: ImageView? = null
private var entryContentsView: EntryContentsView? = null
private var toolbar: Toolbar? = null
private var mEntry: EntryVersioned? = null
private var mShowPassword: Boolean = false
private var clipboardHelper: ClipboardHelper? = null
private var firstLaunchOfActivity: Boolean = false
private var iconColor: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entry)
toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
val currentDatabase = Database.getInstance()
readOnly = currentDatabase.isReadOnly || readOnly
mShowPassword = !PreferencesUtil.isPasswordMask(this)
// Get Entry from UUID
try {
val keyEntry: PwNodeId<*> = intent.getParcelableExtra(KEY_ENTRY)
mEntry = currentDatabase.getEntryById(keyEntry)
} catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key")
}
if (mEntry == null) {
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
finish()
return
}
// Update last access time.
mEntry?.touch(modified = false, touchParents = false)
// Retrieve the textColor to tint the icon
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
iconColor = taIconColor.getColor(0, Color.WHITE)
taIconColor.recycle()
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
invalidateOptionsMenu()
// Get views
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
titleIconView = findViewById(R.id.entry_icon)
entryContentsView = findViewById(R.id.entry_contents)
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
// Init the clipboard helper
clipboardHelper = ClipboardHelper(this)
firstLaunchOfActivity = true
}
override fun onResume() {
super.onResume()
mEntry?.let { entry ->
// Fill data in resume to update from EntryEditActivity
fillEntryDataInContentsView(entry)
// Refresh Menu
invalidateOptionsMenu()
val entryInfo = entry.getEntryInfo(Database.getInstance())
// Manage entry copy to start notification if allowed
if (firstLaunchOfActivity) {
// Manage entry to launch copying notification if allowed
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
}
}
}
firstLaunchOfActivity = false
}
private fun fillEntryDataInContentsView(entry: EntryVersioned) {
val database = Database.getInstance()
database.startManageEntry(entry)
// Assign title icon
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor)
// Assign title text
val entryTitle = entry.getVisualTitle()
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
// Assign basic fields
entryContentsView?.assignUserName(entry.username)
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
clipboardHelper?.timeoutCopyToClipboard(entry.username,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
})
val allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
entryContentsView?.assignPassword(entry.password, allowCopyPassword)
if (allowCopyPassword) {
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
clipboardHelper?.timeoutCopyToClipboard(entry.password,
getString(R.string.copy_field,
getString(R.string.entry_password)))
})
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)) {
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
val message = getString(R.string.allow_copy_password_warning) +
"\n\n" +
getString(R.string.clipboard_warning)
val warningDialog = AlertDialog.Builder(this@EntryActivity)
.setMessage(message).create()
warningDialog.setButton(AlertDialog.BUTTON_POSITIVE, getText(android.R.string.ok)
) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
warningDialog.setButton(AlertDialog.BUTTON_NEGATIVE, getText(android.R.string.cancel)
) { dialog, _ ->
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
dialog.dismiss()
fillEntryDataInContentsView(entry)
}
warningDialog.show()
})
} else {
entryContentsView?.assignPasswordCopyListener(null)
}
}
entryContentsView?.assignURL(entry.url)
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
entryContentsView?.assignComment(entry.notes)
// Assign custom fields
if (entry.allowExtraFields()) {
entryContentsView?.clearExtraFields()
entry.fields.doActionToAllCustomProtectedField { label, value ->
val showAction = !value.isProtected || PreferencesUtil.allowCopyPasswordAndProtectedFields(this@EntryActivity)
entryContentsView?.addExtraField(label, value, showAction, View.OnClickListener {
clipboardHelper?.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
)
})
}
}
// Assign dates
entry.creationTime.date?.let {
entryContentsView?.assignCreationDate(it)
}
entry.lastModificationTime.date?.let {
entryContentsView?.assignModificationDate(it)
}
entry.lastAccessTime.date?.let {
entryContentsView?.assignLastAccessDate(it)
}
val expires = entry.expiryTime.date
if (entry.isExpires && expires != null) {
entryContentsView?.assignExpiresDate(expires)
} else {
entryContentsView?.assignExpiresDate(getString(R.string.never))
}
database.stopManageEntry(entry)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
// Not directly get the entry from intent data but from database
mEntry?.let {
fillEntryDataInContentsView(it)
}
}
}
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
if (mShowPassword) {
togglePassword?.setTitle(R.string.menu_hide_password)
togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp)
} else {
togglePassword?.setTitle(R.string.menu_showpass)
togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
val inflater = menuInflater
MenuUtil.contributionMenuInflater(inflater, menu)
inflater.inflate(R.menu.entry, menu)
inflater.inflate(R.menu.database_lock, menu)
if (readOnly) {
menu.findItem(R.id.menu_edit)?.isVisible = false
}
val togglePassword = menu.findItem(R.id.menu_toggle_pass)
entryContentsView?.let {
if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) {
changeShowPasswordIcon(togglePassword)
} else {
togglePassword?.isVisible = false
}
}
val gotoUrl = menu.findItem(R.id.menu_goto_url)
gotoUrl?.apply {
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
// so mEntry may not be set
if (mEntry == null) {
isVisible = false
} else {
if (mEntry?.url?.isEmpty() != false) {
// disable button if url is not available
isVisible = false
}
}
}
// Show education views
Handler().post { performedNextEducation(EntryActivityEducation(this), menu) }
return true
}
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
menu: Menu) {
if (entryContentsView?.isUserNamePresent == true
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
findViewById(R.id.entry_user_name_action_image),
{
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
},
{
// Launch autofill settings
startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java))
}))
else if (toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
toolbar!!.findViewById(R.id.menu_edit),
{
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
},
{
// Open Keepass doc to create field references
startActivity(Intent(Intent.ACTION_VIEW,
Uri.parse(getString(R.string.field_references_url))))
}))
;
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this)
R.id.menu_toggle_pass -> {
mShowPassword = !mShowPassword
changeShowPasswordIcon(item)
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
return true
}
R.id.menu_edit -> {
mEntry?.let {
EntryEditActivity.launch(this@EntryActivity, it)
}
return true
}
R.id.menu_goto_url -> {
var url: String = mEntry?.url ?: ""
// Default http:// if no protocol specified
if (!url.contains("://")) {
url = "http://$url"
}
try {
Util.gotoUrl(this, url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show()
}
return true
}
R.id.menu_lock -> {
lockAndExit()
return true
}
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
}
return super.onOptionsItemSelected(item)
}
override fun finish() {
// Transit data in previous Activity after an update
/*
TODO Slowdown when add entry as result
Intent intent = new Intent();
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
*/
super.finish()
}
companion object {
private val TAG = EntryActivity::class.java.name
const val KEY_ENTRY = "entry"
fun launch(activity: Activity, pw: EntryVersioned, readOnly: Boolean) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, pw.nodeId)
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
}
}
}
}

View File

@@ -1,582 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities;
import android.app.Activity;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.Toast;
import com.getkeepsafe.taptargetview.TapTarget;
import com.getkeepsafe.taptargetview.TapTargetView;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.app.App;
import com.kunzisoft.keepass.database.Database;
import com.kunzisoft.keepass.database.PwDatabase;
import com.kunzisoft.keepass.database.PwDate;
import com.kunzisoft.keepass.database.PwEntry;
import com.kunzisoft.keepass.database.PwGroup;
import com.kunzisoft.keepass.database.PwGroupId;
import com.kunzisoft.keepass.database.PwIconStandard;
import com.kunzisoft.keepass.database.PwNode;
import com.kunzisoft.keepass.database.action.RunnableOnFinish;
import com.kunzisoft.keepass.database.action.node.AddEntryRunnable;
import com.kunzisoft.keepass.database.action.node.AfterActionNodeOnFinish;
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable;
import com.kunzisoft.keepass.database.security.ProtectedString;
import com.kunzisoft.keepass.dialogs.GeneratePasswordDialogFragment;
import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment;
import com.kunzisoft.keepass.icons.IconPackChooser;
import com.kunzisoft.keepass.lock.LockingActivity;
import com.kunzisoft.keepass.lock.LockingHideActivity;
import com.kunzisoft.keepass.settings.PreferencesUtil;
import com.kunzisoft.keepass.tasks.SaveDatabaseProgressTaskDialogFragment;
import com.kunzisoft.keepass.tasks.UpdateProgressTaskStatus;
import com.kunzisoft.keepass.utils.MenuUtil;
import com.kunzisoft.keepass.utils.Types;
import com.kunzisoft.keepass.utils.Util;
import com.kunzisoft.keepass.view.EntryEditCustomField;
import java.util.UUID;
import javax.annotation.Nullable;
import static com.kunzisoft.keepass.dialogs.IconPickerDialogFragment.UNDEFINED_ICON_ID;
public class EntryEditActivity extends LockingHideActivity
implements IconPickerDialogFragment.IconPickerListener,
GeneratePasswordDialogFragment.GeneratePasswordListener {
private static final String TAG = EntryEditActivity.class.getName();
// Keys for current Activity
public static final String KEY_ENTRY = "entry";
public static final String KEY_PARENT = "parent";
// Keys for callback
public static final int ADD_ENTRY_RESULT_CODE = 31;
public static final int UPDATE_ENTRY_RESULT_CODE = 32;
public static final int ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129;
public static final String ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY";
protected PwEntry mEntry;
protected PwEntry mCallbackNewEntry;
protected boolean mIsNew;
protected int mSelectedIconID = UNDEFINED_ICON_ID;
// Views
private ScrollView scrollView;
private EditText entryTitleView;
private ImageView entryIconView;
private EditText entryUserNameView;
private EditText entryUrlView;
private EditText entryPasswordView;
private EditText entryConfirmationPasswordView;
private View generatePasswordView;
private EditText entryCommentView;
private ViewGroup entryExtraFieldsContainer;
private View addNewFieldView;
private View saveView;
private int iconColor;
/**
* Launch EntryEditActivity to update an existing entry
*
* @param act from activity
* @param pw Entry to update
*/
public static void launch(Activity act, PwEntry pw) {
if (LockingActivity.checkTimeIsAllowedOrFinish(act)) {
Intent intent = new Intent(act, EntryEditActivity.class);
intent.putExtra(KEY_ENTRY, Types.UUIDtoBytes(pw.getUUID()));
act.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE);
}
}
/**
* Launch EntryEditActivity to add a new entry
*
* @param act from activity
* @param pwGroup Group who will contains new entry
*/
public static void launch(Activity act, PwGroup pwGroup) {
if (LockingActivity.checkTimeIsAllowedOrFinish(act)) {
Intent intent = new Intent(act, EntryEditActivity.class);
intent.putExtra(KEY_PARENT, pwGroup.getId());
act.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.entry_edit);
Toolbar toolbar = findViewById(R.id.toolbar);
toolbar.setTitle(getString(R.string.app_name));
setSupportActionBar(toolbar);
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
scrollView = findViewById(R.id.entry_scroll);
scrollView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
entryTitleView = findViewById(R.id.entry_title);
entryIconView = findViewById(R.id.icon_button);
entryUserNameView = findViewById(R.id.entry_user_name);
entryUrlView = findViewById(R.id.entry_url);
entryPasswordView = findViewById(R.id.entry_password);
entryConfirmationPasswordView = findViewById(R.id.entry_confpassword);
entryCommentView = findViewById(R.id.entry_comment);
entryExtraFieldsContainer = findViewById(R.id.advanced_container);
// Likely the app has been killed exit the activity
Database db = App.getDB();
if ( ! db.getLoaded() ) {
finish();
return;
}
Intent intent = getIntent();
byte[] uuidBytes = intent.getByteArrayExtra(KEY_ENTRY);
// Retrieve the textColor to tint the icon
int[] attrs = {android.R.attr.textColorPrimary};
TypedArray ta = getTheme().obtainStyledAttributes(attrs);
iconColor = ta.getColor(0, Color.WHITE);
PwDatabase pm = db.getPwDatabase();
if ( uuidBytes == null ) {
PwGroupId parentId = intent.getParcelableExtra(KEY_PARENT);
PwGroup parent = pm.getGroupByGroupId(parentId);
mEntry = db.createEntry(parent);
mIsNew = true;
// Add the default icon
if (IconPackChooser.getSelectedIconPack(this).tintable()) {
App.getDB().getDrawFactory().assignDefaultDatabaseIconTo(this, entryIconView, true, iconColor);
} else {
App.getDB().getDrawFactory().assignDefaultDatabaseIconTo(this, entryIconView);
}
} else {
UUID uuid = Types.bytestoUUID(uuidBytes);
mEntry = pm.getEntryByUUIDId(uuid);
mIsNew = false;
fillData();
}
// Retrieve the icon after an orientation change
if (savedInstanceState != null && savedInstanceState.containsKey(IconPickerDialogFragment.KEY_ICON_ID)) {
iconPicked(savedInstanceState);
}
// Add listener to the icon
entryIconView.setOnClickListener(v ->
IconPickerDialogFragment.launch(EntryEditActivity.this));
// Generate password button
generatePasswordView = findViewById(R.id.generate_button);
generatePasswordView.setOnClickListener(v -> openPasswordGenerator());
// Save button
saveView = findViewById(R.id.entry_save);
saveView.setOnClickListener(v -> saveEntry());
if (mEntry.allowExtraFields()) {
addNewFieldView = findViewById(R.id.add_new_field);
addNewFieldView.setVisibility(View.VISIBLE);
addNewFieldView.setOnClickListener(v -> addNewCustomField());
}
// Verify the education views
checkAndPerformedEducation();
}
/**
* Open the password generator fragment
*/
private void openPasswordGenerator() {
GeneratePasswordDialogFragment generatePasswordDialogFragment = new GeneratePasswordDialogFragment();
generatePasswordDialogFragment.show(getSupportFragmentManager(), "PasswordGeneratorFragment");
}
/**
* Add a new view to fill in the information of the customized field
*/
private void addNewCustomField() {
EntryEditCustomField entryEditCustomField = new EntryEditCustomField(EntryEditActivity.this);
entryEditCustomField.setData("", new ProtectedString(false, ""));
boolean visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this);
entryEditCustomField.setFontVisibility(visibilityFontActivated);
entryExtraFieldsContainer.addView(entryEditCustomField);
// Scroll bottom
scrollView.post(() -> scrollView.fullScroll(ScrollView.FOCUS_DOWN));
}
/**
* Saves the new entry or update an existing entry in the database
*/
private void saveEntry() {
if (!validateBeforeSaving()) {
return;
}
mCallbackNewEntry = populateNewEntry();
// Open a progress dialog and save entry
AfterActionNodeOnFinish onFinish = new AfterSave();
EntryEditActivity act = EntryEditActivity.this;
RunnableOnFinish task;
if ( mIsNew ) {
task = new AddEntryRunnable(act, App.getDB(), mCallbackNewEntry, onFinish);
} else {
task = new UpdateEntryRunnable(act, App.getDB(), mEntry, mCallbackNewEntry, onFinish);
}
task.setUpdateProgressTaskStatus(
new UpdateProgressTaskStatus(this,
SaveDatabaseProgressTaskDialogFragment.start(
getSupportFragmentManager())
));
new Thread(task).start();
}
/**
* Check and display learning views
* Displays the explanation for the icon selection, the password generator and for a new field
*/
private void checkAndPerformedEducation() {
if (PreferencesUtil.isEducationScreensEnabled(this)) {
// TODO Show icon
if (!PreferencesUtil.isEducationPasswordGeneratorPerformed(this)) {
TapTargetView.showFor(this,
TapTarget.forView(generatePasswordView,
getString(R.string.education_generate_password_title),
getString(R.string.education_generate_password_summary))
.textColorInt(Color.WHITE)
.tintTarget(false)
.cancelable(true),
new TapTargetView.Listener() {
@Override
public void onTargetClick(TapTargetView view) {
super.onTargetClick(view);
openPasswordGenerator();
}
@Override
public void onOuterCircleClick(TapTargetView view) {
super.onOuterCircleClick(view);
view.dismiss(false);
}
});
PreferencesUtil.saveEducationPreference(this,
R.string.education_password_generator_key);
} else if (mEntry.allowExtraFields()
&& !mEntry.containsCustomFields()
&& !PreferencesUtil.isEducationEntryNewFieldPerformed(this)) {
TapTargetView.showFor(this,
TapTarget.forView(addNewFieldView,
getString(R.string.education_entry_new_field_title),
getString(R.string.education_entry_new_field_summary))
.textColorInt(Color.WHITE)
.tintTarget(false)
.cancelable(true),
new TapTargetView.Listener() {
@Override
public void onTargetClick(TapTargetView view) {
super.onTargetClick(view);
addNewCustomField();
}
@Override
public void onOuterCircleClick(TapTargetView view) {
super.onOuterCircleClick(view);
view.dismiss(false);
}
});
PreferencesUtil.saveEducationPreference(this,
R.string.education_entry_new_field_key);
}
}
}
/**
* Utility class to retrieve a validation or an error with a message
*/
private class ErrorValidation {
static final int unknownMessage = -1;
boolean isValidate = false;
int messageId = unknownMessage;
void showValidationErrorIfNeeded() {
if (!isValidate && messageId != unknownMessage)
Toast.makeText(EntryEditActivity.this, messageId, Toast.LENGTH_LONG).show();
}
}
/**
* Validate or not the entry form
*
* @return ErrorValidation An error with a message or a validation without message
*/
protected ErrorValidation validate() {
ErrorValidation errorValidation = new ErrorValidation();
// Require title
String title = entryTitleView.getText().toString();
if ( title.length() == 0 ) {
errorValidation.messageId = R.string.error_title_required;
return errorValidation;
}
// Validate password
String pass = entryPasswordView.getText().toString();
String conf = entryConfirmationPasswordView.getText().toString();
if ( ! pass.equals(conf) ) {
errorValidation.messageId = R.string.error_pass_match;
return errorValidation;
}
// Validate extra fields
if (mEntry.allowExtraFields()) {
for (int i = 0; i < entryExtraFieldsContainer.getChildCount(); i++) {
EntryEditCustomField entryEditCustomField = (EntryEditCustomField) entryExtraFieldsContainer.getChildAt(i);
String key = entryEditCustomField.getLabel();
if (key == null || key.length() == 0) {
errorValidation.messageId = R.string.error_string_key;
return errorValidation;
}
}
}
errorValidation.isValidate = true;
return errorValidation;
}
/**
* Launch a validation with {@link #validate()} and show the error if present
*
* @return true if the form was validate or false if not
*/
protected boolean validateBeforeSaving() {
ErrorValidation errorValidation = validate();
errorValidation.showValidationErrorIfNeeded();
return errorValidation.isValidate;
}
protected PwEntry populateNewEntry() {
PwDatabase db = App.getDB().getPwDatabase();
PwEntry newEntry = mEntry.clone();
newEntry.startToManageFieldReferences(db);
newEntry.createBackup(db);
newEntry.setLastAccessTime(new PwDate());
newEntry.setLastModificationTime(new PwDate());
newEntry.setTitle(entryTitleView.getText().toString());
newEntry.setIcon(retrieveIcon());
newEntry.setUrl(entryUrlView.getText().toString());
newEntry.setUsername(entryUserNameView.getText().toString());
newEntry.setNotes(entryCommentView.getText().toString());
newEntry.setPassword(entryPasswordView.getText().toString());
if (newEntry.allowExtraFields()) {
// Delete all extra strings
newEntry.removeAllCustomFields();
// Add extra fields from views
for (int i = 0; i < entryExtraFieldsContainer.getChildCount(); i++) {
EntryEditCustomField view = (EntryEditCustomField) entryExtraFieldsContainer.getChildAt(i);
String key = view.getLabel();
String value = view.getValue();
boolean protect = view.isProtected();
newEntry.addExtraField(key, new ProtectedString(protect, value));
}
}
newEntry.endToManageFieldReferences();
return newEntry;
}
/**
* Retrieve the icon by the selection, or the first icon in the list if the entry is new or the last one
* @return
*/
private PwIconStandard retrieveIcon() {
if(mSelectedIconID != UNDEFINED_ICON_ID)
return App.getDB().getPwDatabase().getIconFactory().getIcon(mSelectedIconID);
else {
if (mIsNew) {
return App.getDB().getPwDatabase().getIconFactory().getKeyIcon();
}
else {
// Keep previous icon, if no new one was selected
return mEntry.getIconStandard();
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
MenuUtil.contributionMenuInflater(inflater, menu);
return true;
}
public boolean onOptionsItemSelected(MenuItem item) {
switch ( item.getItemId() ) {
case R.id.menu_contribute:
return MenuUtil.onContributionItemSelected(this);
case android.R.id.home:
finish();
}
return super.onOptionsItemSelected(item);
}
protected void fillData() {
if (IconPackChooser.getSelectedIconPack(this).tintable()) {
App.getDB().getDrawFactory().assignDatabaseIconTo(this, entryIconView, mEntry.getIcon(), true, iconColor);
} else {
App.getDB().getDrawFactory().assignDatabaseIconTo(this, entryIconView, mEntry.getIcon());
}
// Don't start the field reference manager, we want to see the raw ref
mEntry.endToManageFieldReferences();
entryTitleView.setText(mEntry.getTitle());
entryUserNameView.setText(mEntry.getUsername());
entryUrlView.setText(mEntry.getUrl());
String password = mEntry.getPassword();
entryPasswordView.setText(password);
entryConfirmationPasswordView.setText(password);
entryCommentView.setText(mEntry.getNotes());
boolean visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this);
if (visibilityFontActivated) {
Util.applyFontVisibilityTo(this, entryUserNameView);
Util.applyFontVisibilityTo(this, entryPasswordView);
Util.applyFontVisibilityTo(this, entryConfirmationPasswordView);
Util.applyFontVisibilityTo(this, entryCommentView);
}
if (mEntry.allowExtraFields()) {
LinearLayout container = findViewById(R.id.advanced_container);
mEntry.getFields().doActionToAllCustomProtectedField((key, value) -> {
EntryEditCustomField entryEditCustomField = new EntryEditCustomField(EntryEditActivity.this);
entryEditCustomField.setData(key, value);
entryEditCustomField.setFontVisibility(visibilityFontActivated);
container.addView(entryEditCustomField);
});
}
}
@Override
public void iconPicked(Bundle bundle) {
mSelectedIconID = bundle.getInt(IconPickerDialogFragment.KEY_ICON_ID);
entryIconView.setImageResource(IconPackChooser.getSelectedIconPack(this).iconToResId(mSelectedIconID));
}
@Override
protected void onSaveInstanceState(Bundle outState) {
if (mSelectedIconID != UNDEFINED_ICON_ID) {
outState.putInt(IconPickerDialogFragment.KEY_ICON_ID, mSelectedIconID);
super.onSaveInstanceState(outState);
}
}
@Override
public void acceptPassword(Bundle bundle) {
String generatedPassword = bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID);
entryPasswordView.setText(generatedPassword);
entryConfirmationPasswordView.setText(generatedPassword);
checkAndPerformedEducation();
}
@Override
public void cancelPassword(Bundle bundle) {
// Do nothing here
}
@Override
public void finish() {
// Assign entry callback as a result in all case
try {
if (mCallbackNewEntry != null) {
Bundle bundle = new Bundle();
Intent intentEntry = new Intent();
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mCallbackNewEntry);
intentEntry.putExtras(bundle);
if (mIsNew) {
setResult(ADD_ENTRY_RESULT_CODE, intentEntry);
} else {
setResult(UPDATE_ENTRY_RESULT_CODE, intentEntry);
}
}
super.finish();
} catch (Exception e) {
// Exception when parcelable can't be done
Log.e(TAG, "Cant add entry as result", e);
}
}
private final class AfterSave extends AfterActionNodeOnFinish {
@Override
public void run(@Nullable PwNode oldNode, @Nullable PwNode newNode) {
runOnUiThread(() -> {
if ( mSuccess ) {
finish();
} else {
displayMessage(EntryEditActivity.this);
}
SaveDatabaseProgressTaskDialogFragment.stop(EntryEditActivity.this);
});
}
}
}

View File

@@ -0,0 +1,422 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.support.v7.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ScrollView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
import com.kunzisoft.keepass.database.action.node.ActionNodeValues
import com.kunzisoft.keepass.database.action.node.AddEntryRunnable
import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.EntryEditContentsView
class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPickerListener, GeneratePasswordDialogFragment.GeneratePasswordListener {
private var mDatabase: Database? = null
// Refs of an entry and group in database, are not modifiable
private var mEntry: EntryVersioned? = null
private var mParent: GroupVersioned? = null
// New or copy of mEntry in the database to be modifiable
private var mNewEntry: EntryVersioned? = null
private var mIsNew: Boolean = false
// Views
private var scrollView: ScrollView? = null
private var entryEditContentsView: EntryEditContentsView? = null
private var saveView: View? = null
// Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entry_edit)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
scrollView = findViewById(R.id.entry_edit_scroll)
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
entryEditContentsView = findViewById(R.id.entry_edit_contents)
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
// Likely the app has been killed exit the activity
mDatabase = Database.getInstance()
// Entry is retrieve, it's an entry to update
intent.getParcelableExtra<PwNodeId<*>>(KEY_ENTRY)?.let {
mIsNew = false
// Create an Entry copy to modify from the database entry
mEntry = mDatabase?.getEntryById(it)
// Retrieve the parent
mEntry?.let { entry ->
mParent = entry.parent
// If no parent, add root group as parent
if (mParent == null) {
mParent = mDatabase?.rootGroup
entry.parent = mParent
}
}
// Retrieve the icon after an orientation change
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY) as EntryVersioned
} else {
mEntry?.let { entry ->
// Create a copy to modify
mNewEntry = EntryVersioned(entry).also { newEntry ->
// WARNING Remove the parent to keep memory with parcelable
newEntry.parent = null
}
}
}
}
// Parent is retrieve, it's a new entry to create
intent.getParcelableExtra<PwNodeId<*>>(KEY_PARENT)?.let {
mIsNew = true
mNewEntry = mDatabase?.createEntry()
mParent = mDatabase?.getGroupById(it)
// Add the default icon
mDatabase?.drawFactory?.let { iconFactory ->
entryEditContentsView?.setDefaultIcon(iconFactory)
}
}
// Close the activity if entry or parent can't be retrieve
if (mNewEntry == null || mParent == null) {
finish()
return
}
populateViewsWithEntry(mNewEntry!!)
// Assign title
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
// Add listener to the icon
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
// Generate password button
entryEditContentsView?.setOnPasswordGeneratorClickListener { openPasswordGenerator() }
// Save button
saveView = findViewById(R.id.entry_edit_save)
saveView?.setOnClickListener { saveEntry() }
entryEditContentsView?.allowCustomField(mNewEntry?.allowExtraFields() == true) { addNewCustomField() }
// Verify the education views
entryEditActivityEducation = EntryEditActivityEducation(this)
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
}
}
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
val passwordView = entryEditContentsView?.generatePasswordView
val addNewFieldView = entryEditContentsView?.addNewFieldView
if (passwordView != null
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
passwordView,
{
openPasswordGenerator()
},
{
performedNextEducation(entryEditActivityEducation)
}
))
else if (mNewEntry != null && mNewEntry!!.allowExtraFields() && !mNewEntry!!.containsCustomFields()
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView,
{
addNewCustomField()
}))
;
}
private fun populateViewsWithEntry(newEntry: EntryVersioned) {
// Don't start the field reference manager, we want to see the raw ref
mDatabase?.stopManageEntry(newEntry)
// Set info in temp parameters
temporarilySaveAndShowSelectedIcon(newEntry.icon)
// Set info in view
entryEditContentsView?.apply {
title = newEntry.title
username = newEntry.username
url = newEntry.url
password = newEntry.password
notes = newEntry.notes
newEntry.fields.doActionToAllCustomProtectedField { key, value ->
addNewCustomField(key, value)
}
}
}
private fun populateEntryWithViews(newEntry: EntryVersioned) {
mDatabase?.startManageEntry(newEntry)
newEntry.apply {
// Build info from view
entryEditContentsView?.let { entryView ->
title = entryView.title
username = entryView.username
url = entryView.url
password = entryView.password
notes = entryView.notes
entryView.customFields.forEach { customField ->
addExtraField(customField.name, customField.protectedValue)
}
}
}
mDatabase?.stopManageEntry(newEntry)
}
private fun temporarilySaveAndShowSelectedIcon(icon: PwIcon) {
mNewEntry?.icon = icon
mDatabase?.drawFactory?.let { iconDrawFactory ->
entryEditContentsView?.setIcon(iconDrawFactory, icon)
}
}
/**
* Open the password generator fragment
*/
private fun openPasswordGenerator() {
GeneratePasswordDialogFragment().show(supportFragmentManager, "PasswordGeneratorFragment")
}
/**
* Add a new customized field view and scroll to bottom
*/
private fun addNewCustomField() {
entryEditContentsView?.addNewCustomField()
// Scroll bottom
scrollView?.post { scrollView?.fullScroll(ScrollView.FOCUS_DOWN) }
}
/**
* Saves the new entry or update an existing entry in the database
*/
private fun saveEntry() {
// Launch a validation and show the error if present
if (entryEditContentsView?.isValid() == true) {
// Clone the entry
mDatabase?.let { database ->
mNewEntry?.let { newEntry ->
// WARNING Add the parent previously deleted
newEntry.parent = mEntry?.parent
// Build info
newEntry.lastAccessTime = PwDate()
newEntry.lastModificationTime = PwDate()
populateEntryWithViews(newEntry)
// Open a progress dialog and save entry
var actionRunnable: ActionRunnable? = null
val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() {
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
if (actionNodeValues.result.isSuccess)
finish()
}
}
if (mIsNew) {
mParent?.let { parent ->
actionRunnable = AddEntryRunnable(this@EntryEditActivity,
database,
newEntry,
parent,
afterActionNodeFinishRunnable,
!readOnly)
}
} else {
mEntry?.let { oldEntry ->
actionRunnable = UpdateEntryRunnable(this@EntryEditActivity,
database,
oldEntry,
newEntry,
afterActionNodeFinishRunnable,
!readOnly)
}
}
actionRunnable?.let { runnable ->
ProgressDialogSaveDatabaseThread(this@EntryEditActivity) { runnable }.start()
}
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
val inflater = menuInflater
inflater.inflate(R.menu.database_lock, menu)
MenuUtil.contributionMenuInflater(inflater, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_lock -> {
lockAndExit()
return true
}
R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this)
android.R.id.home -> finish()
}
return super.onOptionsItemSelected(item)
}
override fun iconPicked(bundle: Bundle) {
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
temporarilySaveAndShowSelectedIcon(icon)
}
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putParcelable(KEY_NEW_ENTRY, mNewEntry)
super.onSaveInstanceState(outState)
}
override fun acceptPassword(bundle: Bundle) {
bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let {
entryEditContentsView?.password = it
}
entryEditActivityEducation?.let {
Handler().post { performedNextEducation(it) }
}
}
override fun cancelPassword(bundle: Bundle) {
// Do nothing here
}
override fun finish() {
// Assign entry callback as a result in all case
try {
mNewEntry?.let {
val bundle = Bundle()
val intentEntry = Intent()
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mNewEntry)
intentEntry.putExtras(bundle)
if (mIsNew) {
setResult(ADD_ENTRY_RESULT_CODE, intentEntry)
} else {
setResult(UPDATE_ENTRY_RESULT_CODE, intentEntry)
}
}
super.finish()
} catch (e: Exception) {
// Exception when parcelable can't be done
Log.e(TAG, "Cant add entry as result", e)
}
}
companion object {
private val TAG = EntryEditActivity::class.java.name
// Keys for current Activity
const val KEY_ENTRY = "entry"
const val KEY_PARENT = "parent"
// SaveInstanceState
const val KEY_NEW_ENTRY = "new_entry"
// Keys for callback
const val ADD_ENTRY_RESULT_CODE = 31
const val UPDATE_ENTRY_RESULT_CODE = 32
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
/**
* Launch EntryEditActivity to update an existing entry
*
* @param activity from activity
* @param pwEntry Entry to update
*/
fun launch(activity: Activity, pwEntry: EntryVersioned) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, pwEntry.nodeId)
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
}
}
/**
* Launch EntryEditActivity to add a new entry
*
* @param activity from activity
* @param pwGroup Group who will contains new entry
*/
fun launch(activity: Activity, pwGroup: GroupVersioned) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, pwGroup.nodeId)
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
}
}
}
}

View File

@@ -0,0 +1,569 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
import android.Manifest
import android.app.Activity
import android.app.assist.AssistStructure
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.preference.PreferenceManager
import android.support.annotation.RequiresApi
import android.support.v7.app.AlertDialog
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.dialogs.CreateFileDialogFragment
import com.kunzisoft.keepass.activities.dialogs.FileInformationDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.fileselect.DeleteFileHistoryAsyncTask
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
import com.kunzisoft.keepass.fileselect.OpenFileHistoryAsyncTask
import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import net.cachapa.expandablelayout.ExpandableLayout
import permissions.dispatcher.*
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.lang.ref.WeakReference
import java.net.URLDecoder
import java.util.*
@RuntimePermissions
class FileDatabaseSelectActivity : StylishActivity(),
CreateFileDialogFragment.DefinePathDialogListener,
AssignMasterKeyDialogFragment.AssignPasswordDialogListener,
FileDatabaseHistoryAdapter.FileItemOpenListener,
FileDatabaseHistoryAdapter.FileSelectClearListener,
FileDatabaseHistoryAdapter.FileInformationShowListener {
// Views
private var fileListContainer: View? = null
private var createButtonView: View? = null
private var browseButtonView: View? = null
private var openButtonView: View? = null
private var fileSelectExpandableButtonView: View? = null
private var fileSelectExpandableLayout: ExpandableLayout? = null
private var openFileNameView: EditText? = null
// Adapter to manage database history list
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
private var mFileDatabaseHistory: FileDatabaseHistory? = null
private var mDatabaseFileUri: Uri? = null
private var mKeyFileHelper: KeyFileHelper? = null
private var mDefaultPath: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mFileDatabaseHistory = FileDatabaseHistory.getInstance(WeakReference(applicationContext))
setContentView(R.layout.activity_file_selection)
fileListContainer = findViewById(R.id.container_file_list)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.title = ""
setSupportActionBar(toolbar)
openFileNameView = findViewById(R.id.file_filename)
// Set the initial value of the filename
mDefaultPath = (Environment.getExternalStorageDirectory().absolutePath
+ getString(R.string.database_file_path_default)
+ getString(R.string.database_file_name_default)
+ getString(R.string.database_file_extension_default))
openFileNameView?.setHint(R.string.open_link_database)
// Button to expand file selection
fileSelectExpandableButtonView = findViewById(R.id.file_select_expandable_button)
fileSelectExpandableLayout = findViewById(R.id.file_select_expandable)
fileSelectExpandableButtonView?.setOnClickListener { _ ->
if (fileSelectExpandableLayout?.isExpanded == true)
fileSelectExpandableLayout?.collapse()
else
fileSelectExpandableLayout?.expand()
}
// History list
val databaseFileListView = findViewById<RecyclerView>(R.id.file_list)
databaseFileListView.layoutManager = LinearLayoutManager(this)
// Open button
openButtonView = findViewById(R.id.open_database)
openButtonView?.setOnClickListener { _ ->
var fileName = openFileNameView?.text?.toString() ?: ""
mDefaultPath?.let {
if (fileName.isEmpty())
fileName = it
}
launchPasswordActivityWithPath(fileName)
}
// Create button
createButtonView = findViewById(R.id.create_database)
createButtonView?.setOnClickListener { openCreateFileDialogFragmentWithPermissionCheck() }
mKeyFileHelper = KeyFileHelper(this)
browseButtonView = findViewById(R.id.browse_button)
browseButtonView?.setOnClickListener(mKeyFileHelper!!.getOpenFileOnClickViewListener {
Uri.parse("file://" + openFileNameView!!.text.toString())
})
// Construct adapter with listeners
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this@FileDatabaseSelectActivity,
mFileDatabaseHistory?.databaseUriList ?: ArrayList())
mAdapterDatabaseHistory?.setOnItemClickListener(this)
mAdapterDatabaseHistory?.setFileSelectClearListener(this)
mAdapterDatabaseHistory?.setFileInformationShowListener(this)
databaseFileListView.adapter = mAdapterDatabaseHistory
// Load default database if not an orientation change
if (!(savedInstanceState != null
&& savedInstanceState.containsKey(EXTRA_STAY)
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val fileName = prefs.getString(PasswordActivity.KEY_DEFAULT_FILENAME, "")
if (fileName != null && fileName.isNotEmpty()) {
val dbUri = UriUtil.parseUriFile(fileName)
var scheme: String? = null
if (dbUri != null)
scheme = dbUri.scheme
if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) {
val path = dbUri!!.path
val db = File(path!!)
if (db.exists()) {
launchPasswordActivityWithPath(path)
}
} else {
if (dbUri != null)
launchPasswordActivityWithPath(dbUri.toString())
}
}
}
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
}
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
// If no recent files
if (createButtonView != null
&& mFileDatabaseHistory != null
&& !mFileDatabaseHistory!!.hasRecentFiles() && fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
createButtonView!!,
{
openCreateFileDialogFragmentWithPermissionCheck()
},
{
// But if the user cancel, it can also select a database
performedNextEducation(fileDatabaseSelectActivityEducation)
}))
else if (browseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
browseButtonView!!,
{tapTargetView ->
tapTargetView?.let {
mKeyFileHelper?.openFileOnClickViewListener?.onClick(it)
}
},
{
fileSelectExpandableButtonView?.let {
fileDatabaseSelectActivityEducation
.checkAndPerformedOpenLinkDatabaseEducation(it)
}
}
))
;
}
private fun fileNoFoundAction(e: FileNotFoundException) {
val error = getString(R.string.file_not_found_content)
Toast.makeText(this@FileDatabaseSelectActivity,
error, Toast.LENGTH_LONG).show()
Log.e(TAG, error, e)
}
private fun launchPasswordActivity(fileName: String, keyFile: String) {
EntrySelectionHelper.doEntrySelectionAction(intent,
{
try {
PasswordActivity.launch(this@FileDatabaseSelectActivity,
fileName, keyFile)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
},
{
try {
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
fileName, keyFile)
finish()
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
fileName, keyFile,
assistStructure)
} catch (e: FileNotFoundException) {
fileNoFoundAction(e)
}
}
})
}
private fun launchPasswordActivityWithPath(path: String) {
launchPasswordActivity(path, "")
// Delete flickering for kitkat <=
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
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() {
super.onResume()
updateExternalStorageWarning()
updateFileListVisibility()
mAdapterDatabaseHistory!!.notifyDataSetChanged()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// only to keep the current activity
outState.putBoolean(EXTRA_STAY, true)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// NOTE: delegate the permission handling to generated method
onRequestPermissionsResult(requestCode, grantResults)
}
@NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
fun openCreateFileDialogFragment() {
val createFileDialogFragment = CreateFileDialogFragment()
createFileDialogFragment.show(supportFragmentManager, "createFileDialogFragment")
}
private fun updateFileListVisibility() {
if (mAdapterDatabaseHistory?.itemCount == 0)
fileListContainer?.visibility = View.INVISIBLE
else
fileListContainer?.visibility = View.VISIBLE
}
/**
* Create file for database
* @return If not created, return false
*/
private fun createDatabaseFile(path: Uri): Boolean {
val pathString = URLDecoder.decode(path.path, "UTF-8")
// Make sure file name exists
if (pathString.isEmpty()) {
Log.e(TAG, getString(R.string.error_filename_required))
Toast.makeText(this@FileDatabaseSelectActivity,
R.string.error_filename_required,
Toast.LENGTH_LONG).show()
return false
}
// Try to create the file
val file = File(pathString)
try {
if (file.exists()) {
Log.e(TAG, getString(R.string.error_database_exists) + " " + file)
Toast.makeText(this@FileDatabaseSelectActivity,
R.string.error_database_exists,
Toast.LENGTH_LONG).show()
return false
}
val parent = file.parentFile
if (parent == null || parent.exists() && !parent.isDirectory) {
Log.e(TAG, getString(R.string.error_invalid_path) + " " + file)
Toast.makeText(this@FileDatabaseSelectActivity,
R.string.error_invalid_path,
Toast.LENGTH_LONG).show()
return false
}
if (!parent.exists()) {
// Create parent directory
if (!parent.mkdirs()) {
Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + parent)
Toast.makeText(this@FileDatabaseSelectActivity,
R.string.error_could_not_create_parent,
Toast.LENGTH_LONG).show()
return false
}
}
return file.createNewFile()
} catch (e: IOException) {
Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + e.localizedMessage)
e.printStackTrace()
Toast.makeText(
this@FileDatabaseSelectActivity,
getText(R.string.error_file_not_create).toString() + " "
+ e.localizedMessage,
Toast.LENGTH_LONG).show()
return false
}
}
override fun onDefinePathDialogPositiveClick(pathFile: Uri?): Boolean {
mDatabaseFileUri = pathFile
if (pathFile == null)
return false
return if (createDatabaseFile(pathFile)) {
AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog")
true
} else
false
}
override fun onDefinePathDialogNegativeClick(pathFile: Uri?): Boolean {
return true
}
override fun onAssignKeyDialogPositiveClick(
masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?) {
try {
UriUtil.parseUriFile(mDatabaseFileUri)?.let { databaseUri ->
// Create the new database
ProgressDialogThread(this@FileDatabaseSelectActivity,
{
CreateDatabaseRunnable(this@FileDatabaseSelectActivity,
databaseUri,
Database.getInstance(),
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile,
true, // TODO get readonly
LaunchGroupActivityFinish(databaseUri)
)
},
R.string.progress_create)
.start()
}
} catch (e: Exception) {
val error = "Unable to create database with this password and key file"
Toast.makeText(this, error, Toast.LENGTH_LONG).show()
Log.e(TAG, error + " " + e.message)
// TODO remove
e.printStackTrace()
}
}
private inner class LaunchGroupActivityFinish internal constructor(private val fileURI: Uri) : ActionRunnable() {
override fun run() {
finishRun(true, null)
}
override fun onFinishRun(result: Result) {
runOnUiThread {
if (result.isSuccess) {
// Add database to recent files
mFileDatabaseHistory?.addDatabaseUri(fileURI)
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
GroupActivity.launch(this@FileDatabaseSelectActivity)
} else {
Log.e(TAG, "Unable to open the database")
}
}
}
}
override fun onAssignKeyDialogNegativeClick(
masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?) {
}
override fun onFileItemOpenListener(itemPosition: Int) {
OpenFileHistoryAsyncTask({ fileName, keyFile ->
if (fileName != null && keyFile != null)
launchPasswordActivity(fileName, keyFile)
updateFileListVisibility()
}, mFileDatabaseHistory).execute(itemPosition)
}
override fun onClickFileInformation(fileDatabaseModel: FileDatabaseModel) {
FileInformationDialogFragment.newInstance(fileDatabaseModel).show(supportFragmentManager, "fileInformation")
}
override fun onFileSelectClearListener(fileDatabaseModel: FileDatabaseModel): Boolean {
DeleteFileHistoryAsyncTask({
fileDatabaseModel.fileUri?.let {
mFileDatabaseHistory?.deleteDatabaseUri(it)
}
mAdapterDatabaseHistory?.notifyDataSetChanged()
updateFileListVisibility()
}, mFileDatabaseHistory, mAdapterDatabaseHistory).execute(fileDatabaseModel)
return true
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) {
launchPasswordActivityWithPath(uri.toString())
} else {
fileSelectExpandableLayout?.expand(false)
openFileNameView?.setText(uri.toString())
}
}
}
}
@OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showRationaleForExternalStorage(request: PermissionRequest) {
AlertDialog.Builder(this)
.setMessage(R.string.permission_external_storage_rationale_write_database)
.setPositiveButton(R.string.allow) { _, _ -> request.proceed() }
.setNegativeButton(R.string.cancel) { _, _ -> request.cancel() }
.show()
}
@OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showDeniedForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show()
}
@OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showNeverAskForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
MenuUtil.defaultMenuInflater(menuInflater, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
}
companion object {
private const val TAG = "FileDbSelectActivity"
private const val EXTRA_STAY = "EXTRA_STAY"
/*
* -------------------------
* No Standard Launch, pass by PasswordActivity
* -------------------------
*/
/*
* -------------------------
* Keyboard Launch
* -------------------------
*/
fun launchForKeyboardSelection(activity: Activity) {
KeyboardHelper.startActivityForKeyboardSelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java))
}
/*
* -------------------------
* Autofill Launch
* -------------------------
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure) {
AutofillHelper.startActivityForAutofillResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
assistStructure)
}
}
}

View File

@@ -1,907 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities;
import android.app.Activity;
import android.app.Dialog;
import android.app.SearchManager;
import android.app.assist.AssistStructure;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ImageView;
import com.getkeepsafe.taptargetview.TapTarget;
import com.getkeepsafe.taptargetview.TapTargetView;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.adapters.NodeAdapter;
import com.kunzisoft.keepass.app.App;
import com.kunzisoft.keepass.autofill.AutofillHelper;
import com.kunzisoft.keepass.database.Database;
import com.kunzisoft.keepass.database.PwDatabase;
import com.kunzisoft.keepass.database.PwEntry;
import com.kunzisoft.keepass.database.PwGroup;
import com.kunzisoft.keepass.database.PwGroupId;
import com.kunzisoft.keepass.database.PwIcon;
import com.kunzisoft.keepass.database.PwIconStandard;
import com.kunzisoft.keepass.database.PwNode;
import com.kunzisoft.keepass.database.action.node.AddGroupRunnable;
import com.kunzisoft.keepass.database.action.node.AfterActionNodeOnFinish;
import com.kunzisoft.keepass.database.action.node.CopyEntryRunnable;
import com.kunzisoft.keepass.database.action.node.DeleteEntryRunnable;
import com.kunzisoft.keepass.database.action.node.DeleteGroupRunnable;
import com.kunzisoft.keepass.database.action.node.MoveEntryRunnable;
import com.kunzisoft.keepass.database.action.node.MoveGroupRunnable;
import com.kunzisoft.keepass.database.action.node.UpdateGroupRunnable;
import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment;
import com.kunzisoft.keepass.dialogs.GroupEditDialogFragment;
import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment;
import com.kunzisoft.keepass.dialogs.ReadOnlyDialog;
import com.kunzisoft.keepass.icons.IconPackChooser;
import com.kunzisoft.keepass.search.SearchResultsActivity;
import com.kunzisoft.keepass.selection.EntrySelectionHelper;
import com.kunzisoft.keepass.settings.PreferencesUtil;
import com.kunzisoft.keepass.tasks.SaveDatabaseProgressTaskDialogFragment;
import com.kunzisoft.keepass.tasks.UIToastTask;
import com.kunzisoft.keepass.tasks.UpdateProgressTaskStatus;
import com.kunzisoft.keepass.view.AddNodeButtonView;
import net.cachapa.expandablelayout.ExpandableLayout;
import static com.kunzisoft.keepass.activities.ReadOnlyHelper.READ_ONLY_DEFAULT;
public class GroupActivity extends ListNodesActivity
implements GroupEditDialogFragment.EditGroupListener,
IconPickerDialogFragment.IconPickerListener,
NodeAdapter.NodeMenuListener,
ListNodesFragment.OnScrollListener {
private static final String TAG = GroupActivity.class.getName();
protected static final String GROUP_ID_KEY = "GROUP_ID_KEY";
private Toolbar toolbar;
private ExpandableLayout toolbarPasteExpandableLayout;
private Toolbar toolbarPaste;
private ImageView iconView;
private AddNodeButtonView addNodeButtonView;
protected boolean addGroupEnabled = false;
protected boolean addEntryEnabled = false;
protected boolean isRoot = false;
private static final String OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY";
private static final String NODE_TO_COPY_KEY = "NODE_TO_COPY_KEY";
private static final String NODE_TO_MOVE_KEY = "NODE_TO_MOVE_KEY";
private PwGroup oldGroupToUpdate;
private PwNode nodeToCopy;
private PwNode nodeToMove;
// After a database creation
public static void launch(Activity act) {
launch(act, READ_ONLY_DEFAULT);
}
public static void launch(Activity act, boolean readOnly) {
startRecordTime(act);
launch(act, null, readOnly);
}
private static void buildAndLaunchIntent(Activity activity, PwGroup group, boolean readOnly,
IntentBuildLauncher intentBuildLauncher) {
if (checkTimeIsAllowedOrFinish(activity)) {
Intent intent = new Intent(activity, GroupActivity.class);
if (group != null) {
intent.putExtra(GROUP_ID_KEY, group.getId());
}
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly);
intentBuildLauncher.startActivityForResult(intent);
}
}
public static void launch(Activity activity, PwGroup group, boolean readOnly) {
buildAndLaunchIntent(activity, group, readOnly,
(intent) -> activity.startActivityForResult(intent, 0));
}
public static void launchForKeyboardResult(Activity act, boolean readOnly) {
startRecordTime(act);
launchForKeyboardResult(act, null, readOnly);
}
public static void launchForKeyboardResult(Activity activity, PwGroup group, boolean readOnly) {
// TODO implement pre search to directly open the direct group
buildAndLaunchIntent(activity, group, readOnly, (intent) -> {
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent);
activity.startActivityForResult(intent, EntrySelectionHelper.ENTRY_SELECTION_RESPONSE_REQUEST_CODE);
});
}
@RequiresApi(api = Build.VERSION_CODES.O)
public static void launchForAutofillResult(Activity act, AssistStructure assistStructure, boolean readOnly) {
if ( assistStructure != null ) {
startRecordTime(act);
launchForAutofillResult(act, null, assistStructure, readOnly);
} else {
launch(act, readOnly);
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
public static void launchForAutofillResult(Activity activity, PwGroup group, AssistStructure assistStructure, boolean readOnly) {
// TODO implement pre search to directly open the direct group
if ( assistStructure != null ) {
buildAndLaunchIntent(activity, group, readOnly, (intent) -> {
AutofillHelper.addAssistStructureExtraInIntent(intent, assistStructure);
activity.startActivityForResult(intent, AutofillHelper.AUTOFILL_RESPONSE_REQUEST_CODE);
});
} else {
launch(activity, group, readOnly);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i(TAG, "Started creating tree");
if ( mCurrentGroup == null ) {
Log.w(TAG, "Group was null");
return;
}
// Construct main view
setContentView(getLayoutInflater().inflate(R.layout.list_nodes_with_add_button, null));
attachFragmentToContentView();
iconView = findViewById(R.id.icon);
addNodeButtonView = findViewById(R.id.add_node_button);
addNodeButtonView.enableAddGroup(addGroupEnabled);
addNodeButtonView.enableAddEntry(addEntryEnabled);
toolbar = findViewById(R.id.toolbar);
toolbar.setTitle("");
setSupportActionBar(toolbar);
groupNameView = findViewById(R.id.group_name);
toolbarPasteExpandableLayout = findViewById(R.id.expandable_toolbar_paste_layout);
toolbarPaste = findViewById(R.id.toolbar_paste);
toolbarPaste.inflateMenu(R.menu.node_paste_menu);
toolbarPaste.setNavigationIcon(R.drawable.ic_arrow_left_white_24dp);
toolbarPaste.setNavigationOnClickListener(view -> {
toolbarPasteExpandableLayout.collapse();
nodeToCopy = null;
nodeToMove = null;
});
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY))
oldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY);
if (savedInstanceState.containsKey(NODE_TO_COPY_KEY)) {
nodeToCopy = savedInstanceState.getParcelable(NODE_TO_COPY_KEY);
toolbarPaste.setOnMenuItemClickListener(new OnCopyMenuItemClickListener());
}
else if (savedInstanceState.containsKey(NODE_TO_MOVE_KEY)) {
nodeToMove = savedInstanceState.getParcelable(NODE_TO_MOVE_KEY);
toolbarPaste.setOnMenuItemClickListener(new OnMoveMenuItemClickListener());
}
}
addNodeButtonView.setAddGroupClickListener(v -> {
GroupEditDialogFragment.build()
.show(getSupportFragmentManager(),
GroupEditDialogFragment.TAG_CREATE_GROUP);
});
addNodeButtonView.setAddEntryClickListener(v ->
EntryEditActivity.launch(GroupActivity.this, mCurrentGroup));
Log.i(TAG, "Finished creating tree");
if (isRoot) {
showWarnings();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putParcelable(GROUP_ID_KEY, mCurrentGroup.getId());
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, oldGroupToUpdate);
if (nodeToCopy != null)
outState.putParcelable(NODE_TO_COPY_KEY, nodeToCopy);
if (nodeToMove != null)
outState.putParcelable(NODE_TO_MOVE_KEY, nodeToMove);
super.onSaveInstanceState(outState);
}
protected PwGroup retrieveCurrentGroup(@Nullable Bundle savedInstanceState) {
PwGroupId pwGroupId = null;
if (savedInstanceState != null
&& savedInstanceState.containsKey(GROUP_ID_KEY)) {
pwGroupId = savedInstanceState.getParcelable(GROUP_ID_KEY);
} else {
if (getIntent() != null)
pwGroupId = getIntent().getParcelableExtra(GROUP_ID_KEY);
}
readOnly = database.isReadOnly() || readOnly; // Force read only if the database is like that
Log.w(TAG, "Creating tree view");
PwGroup currentGroup;
if ( pwGroupId == null ) {
currentGroup = rootGroup;
} else {
currentGroup = database.getPwDatabase().getGroupByGroupId(pwGroupId);
}
if (currentGroup != null) {
addGroupEnabled = !readOnly;
addEntryEnabled = !readOnly;
isRoot = (currentGroup == rootGroup);
if (!currentGroup.allowAddEntryIfIsRoot())
addEntryEnabled = !isRoot && addEntryEnabled;
}
return currentGroup;
}
@Override
public void assignToolbarElements() {
super.assignToolbarElements();
// Assign the group icon depending of IconPack or custom icon
if ( mCurrentGroup != null ) {
if (IconPackChooser.getSelectedIconPack(this).tintable()) {
// Retrieve the textColor to tint the icon
int[] attrs = {R.attr.textColorInverse};
TypedArray ta = getTheme().obtainStyledAttributes(attrs);
int iconColor = ta.getColor(0, Color.WHITE);
App.getDB().getDrawFactory().assignDatabaseIconTo(this, iconView, mCurrentGroup.getIcon(), true, iconColor);
} else {
App.getDB().getDrawFactory().assignDatabaseIconTo(this, iconView, mCurrentGroup.getIcon());
}
if (toolbar != null) {
if ( mCurrentGroup.containsParent() )
toolbar.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp);
else {
toolbar.setNavigationIcon(null);
}
}
}
}
@Override
public void onScrolled(int dy) {
if (addNodeButtonView != null)
addNodeButtonView.hideButtonOnScrollListener(dy);
}
@Override
public boolean onOpenMenuClick(PwNode node) {
onNodeClick(node);
return true;
}
@Override
public boolean onEditMenuClick(PwNode node) {
switch (node.getType()) {
case GROUP:
oldGroupToUpdate = (PwGroup) node;
GroupEditDialogFragment.build(node)
.show(getSupportFragmentManager(),
GroupEditDialogFragment.TAG_CREATE_GROUP);
break;
case ENTRY:
EntryEditActivity.launch(GroupActivity.this, (PwEntry) node);
break;
}
return true;
}
@Override
public boolean onCopyMenuClick(PwNode node) {
toolbarPasteExpandableLayout.expand();
nodeToCopy = node;
toolbarPaste.setOnMenuItemClickListener(new OnCopyMenuItemClickListener());
return false;
}
private class OnCopyMenuItemClickListener implements Toolbar.OnMenuItemClickListener{
@Override
public boolean onMenuItemClick(MenuItem item) {
toolbarPasteExpandableLayout.collapse();
switch (item.getItemId()) {
case R.id.menu_paste:
switch (nodeToCopy.getType()) {
case GROUP:
Log.e(TAG, "Copy not allowed for group");
break;
case ENTRY:
copyEntry((PwEntry) nodeToCopy, mCurrentGroup);
break;
}
nodeToCopy = null;
return true;
}
return true;
}
}
private void copyEntry(PwEntry entryToCopy, PwGroup newParent) {
CopyEntryRunnable task = new CopyEntryRunnable(this, App.getDB(), entryToCopy, newParent,
new AfterAddNode());
task.setUpdateProgressTaskStatus(
new UpdateProgressTaskStatus(this,
SaveDatabaseProgressTaskDialogFragment.start(
getSupportFragmentManager())
));
new Thread(task).start();
}
@Override
public boolean onMoveMenuClick(PwNode node) {
toolbarPasteExpandableLayout.expand();
nodeToMove = node;
toolbarPaste.setOnMenuItemClickListener(new OnMoveMenuItemClickListener());
return false;
}
private class OnMoveMenuItemClickListener implements Toolbar.OnMenuItemClickListener{
@Override
public boolean onMenuItemClick(MenuItem item) {
toolbarPasteExpandableLayout.collapse();
switch (item.getItemId()) {
case R.id.menu_paste:
switch (nodeToMove.getType()) {
case GROUP:
moveGroup((PwGroup) nodeToMove, mCurrentGroup);
break;
case ENTRY:
moveEntry((PwEntry) nodeToMove, mCurrentGroup);
break;
}
nodeToMove = null;
return true;
}
return true;
}
}
private void moveGroup(PwGroup groupToMove, PwGroup newParent) {
MoveGroupRunnable task = new MoveGroupRunnable(
this,
App.getDB(),
groupToMove,
newParent,
new AfterAddNode());
task.setUpdateProgressTaskStatus(
new UpdateProgressTaskStatus(this,
SaveDatabaseProgressTaskDialogFragment.start(
getSupportFragmentManager())
));
new Thread(task).start();
}
private void moveEntry(PwEntry entryToMove, PwGroup newParent) {
MoveEntryRunnable task = new MoveEntryRunnable(
this,
App.getDB(),
entryToMove,
newParent,
new AfterAddNode());
task.setUpdateProgressTaskStatus(
new UpdateProgressTaskStatus(this,
SaveDatabaseProgressTaskDialogFragment.start(
getSupportFragmentManager())
));
new Thread(task).start();
}
@Override
public boolean onDeleteMenuClick(PwNode node) {
switch (node.getType()) {
case GROUP:
deleteGroup((PwGroup) node);
break;
case ENTRY:
deleteEntry((PwEntry) node);
break;
}
return true;
}
private void deleteGroup(PwGroup group) {
//TODO Verify trash recycle bin
DeleteGroupRunnable task = new DeleteGroupRunnable(
this,
App.getDB(),
group,
new AfterDeleteNode());
task.setUpdateProgressTaskStatus(
new UpdateProgressTaskStatus(this,
SaveDatabaseProgressTaskDialogFragment.start(
getSupportFragmentManager())
));
new Thread(task).start();
}
private void deleteEntry(PwEntry entry) {
DeleteEntryRunnable task = new DeleteEntryRunnable(
this,
App.getDB(),
entry,
new AfterDeleteNode());
task.setUpdateProgressTaskStatus(
new UpdateProgressTaskStatus(this,
SaveDatabaseProgressTaskDialogFragment.start(
getSupportFragmentManager())
));
new Thread(task).start();
}
@Override
protected void onResume() {
super.onResume();
// Show button on resume
if (addNodeButtonView != null)
addNodeButtonView.showButton();
}
/**
* Check and display learning views
* Displays the explanation for a add, search, sort a new node and lock the database
*/
private void checkAndPerformedEducation(Menu menu) {
if (PreferencesUtil.isEducationScreensEnabled(this)) {
// If no node, show education to add new one
if (listNodesFragment != null
&& listNodesFragment.isEmpty()) {
if (!PreferencesUtil.isEducationNewNodePerformed(this)
&& addNodeButtonView.isVisible()) {
TapTargetView.showFor(this,
TapTarget.forView(findViewById(R.id.add_button),
getString(R.string.education_new_node_title),
getString(R.string.education_new_node_summary))
.textColorInt(Color.WHITE)
.tintTarget(false)
.cancelable(true),
new TapTargetView.Listener() {
@Override
public void onTargetClick(TapTargetView view) {
super.onTargetClick(view);
addNodeButtonView.openButtonIfClose();
}
@Override
public void onOuterCircleClick(TapTargetView view) {
super.onOuterCircleClick(view);
view.dismiss(false);
}
});
PreferencesUtil.saveEducationPreference(this,
R.string.education_new_node_key);
}
}
// Else show the search education
else if (!PreferencesUtil.isEducationSearchPerformed(this)) {
try {
TapTargetView.showFor(this,
TapTarget.forToolbarMenuItem(toolbar, R.id.menu_search,
getString(R.string.education_search_title),
getString(R.string.education_search_summary))
.textColorInt(Color.WHITE)
.tintTarget(true)
.cancelable(true),
new TapTargetView.Listener() {
@Override
public void onTargetClick(TapTargetView view) {
super.onTargetClick(view);
MenuItem searchItem = menu.findItem(R.id.menu_search);
searchItem.expandActionView();
}
@Override
public void onOuterCircleClick(TapTargetView view) {
super.onOuterCircleClick(view);
view.dismiss(false);
}
});
PreferencesUtil.saveEducationPreference(this,
R.string.education_search_key);
} catch (Exception e) {
// If icon not visible
Log.w(TAG, "Can't performed education for search");
}
}
// Else show the sort education
else if (!PreferencesUtil.isEducationSortPerformed(this)) {
try {
TapTargetView.showFor(this,
TapTarget.forToolbarMenuItem(toolbar, R.id.menu_sort,
getString(R.string.education_sort_title),
getString(R.string.education_sort_summary))
.textColorInt(Color.WHITE)
.tintTarget(true)
.cancelable(true),
new TapTargetView.Listener() {
@Override
public void onTargetClick(TapTargetView view) {
super.onTargetClick(view);
MenuItem sortItem = menu.findItem(R.id.menu_sort);
onOptionsItemSelected(sortItem);
}
@Override
public void onOuterCircleClick(TapTargetView view) {
super.onOuterCircleClick(view);
view.dismiss(false);
}
});
PreferencesUtil.saveEducationPreference(this,
R.string.education_sort_key);
} catch (Exception e) {
Log.w(TAG, "Can't performed education for sort");
}
}
// Else show the lock education
else if (!PreferencesUtil.isEducationLockPerformed(this)) {
try {
TapTargetView.showFor(this,
TapTarget.forToolbarMenuItem(toolbar, R.id.menu_lock,
getString(R.string.education_lock_title),
getString(R.string.education_lock_summary))
.textColorInt(Color.WHITE)
.tintTarget(true)
.cancelable(true),
new TapTargetView.Listener() {
@Override
public void onTargetClick(TapTargetView view) {
super.onTargetClick(view);
MenuItem lockItem = menu.findItem(R.id.menu_lock);
onOptionsItemSelected(lockItem);
}
@Override
public void onOuterCircleClick(TapTargetView view) {
super.onOuterCircleClick(view);
view.dismiss(false);
}
});
PreferencesUtil.saveEducationPreference(this,
R.string.education_lock_key);
} catch (Exception e) {
Log.w(TAG, "Can't performed education for lock");
}
}
}
}
@Override
protected void onStop() {
super.onStop();
// Hide button
if (addNodeButtonView != null)
addNodeButtonView.hideButton();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.search, menu);
if (!readOnly)
inflater.inflate(R.menu.database_master_key, menu);
inflater.inflate(R.menu.database_lock, menu);
// Get the SearchView and set the searchable configuration
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
assert searchManager != null;
MenuItem searchItem = menu.findItem(R.id.menu_search);
SearchView searchView = null;
if (searchItem != null) {
searchView = (SearchView) searchItem.getActionView();
}
if (searchView != null) {
// TODO Flickering when locking, will be better with content provider
searchView.setSearchableInfo(searchManager.getSearchableInfo(new ComponentName(this, SearchResultsActivity.class)));
searchView.setIconifiedByDefault(false); // Do not iconify the widget; expand it by default
}
super.onCreateOptionsMenu(menu);
// Launch education screen
new Handler().post(() -> checkAndPerformedEducation(menu));
return true;
}
@Override
public void startActivity(Intent intent) {
boolean customSearchQueryExecuted = false;
// Get the intent, verify the action and get the query
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String query = intent.getStringExtra(SearchManager.QUERY);
// manually launch the real search activity
final Intent searchIntent = new Intent(getApplicationContext(), SearchResultsActivity.class);
// add query to the Intent Extras
searchIntent.setAction(Intent.ACTION_SEARCH);
searchIntent.putExtra(SearchManager.QUERY, query);
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& autofillHelper.getAssistStructure() != null ) {
AutofillHelper.addAssistStructureExtraInIntent(searchIntent, autofillHelper.getAssistStructure());
startActivityForResult(searchIntent, AutofillHelper.AUTOFILL_RESPONSE_REQUEST_CODE);
customSearchQueryExecuted = true;
}
// To get the keyboard response, verify if the current intent contains the EntrySelection key
else if (EntrySelectionHelper.isIntentInEntrySelectionMode(getIntent())){
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(searchIntent);
startActivityForResult(searchIntent, EntrySelectionHelper.ENTRY_SELECTION_RESPONSE_REQUEST_CODE);
customSearchQueryExecuted = true;
}
}
if (!customSearchQueryExecuted) {
super.startActivity(intent);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
case R.id.menu_search:
onSearchRequested();
return true;
case R.id.menu_lock:
lockAndExit();
return true;
case R.id.menu_change_master_key:
setPassword();
return true;
}
return super.onOptionsItemSelected(item);
}
private void setPassword() {
AssignMasterKeyDialogFragment dialog = new AssignMasterKeyDialogFragment();
dialog.show(getSupportFragmentManager(), "passwordDialog");
}
@Override
public void approveEditGroup(GroupEditDialogFragment.EditGroupDialogAction action,
String name,
PwIcon icon) {
Database database = App.getDB();
PwIconStandard iconStandard = database.getPwDatabase().getIconFactory().getFolderIcon();
switch (action) {
case CREATION:
// If group creation
// Build the group
PwGroup newGroup = database.createGroup(mCurrentGroup);
newGroup.setName(name);
try {
iconStandard = (PwIconStandard) icon;
} catch (Exception ignored) {} // TODO custom icon
newGroup.setIcon(iconStandard);
// If group created save it in the database
AddGroupRunnable addGroupRunnable = new AddGroupRunnable(this,
App.getDB(),
newGroup,
new AfterAddNode());
addGroupRunnable.setUpdateProgressTaskStatus(
new UpdateProgressTaskStatus(this,
SaveDatabaseProgressTaskDialogFragment.start(
getSupportFragmentManager())
));
new Thread(addGroupRunnable).start();
break;
case UPDATE:
// If update add new elements
if (oldGroupToUpdate != null) {
PwGroup updateGroup = oldGroupToUpdate.clone();
updateGroup.setName(name);
try {
iconStandard = (PwIconStandard) icon;
} catch (Exception ignored) {} // TODO custom icon
updateGroup.setIcon(iconStandard);
if (listNodesFragment != null)
listNodesFragment.removeNode(oldGroupToUpdate);
// If group updated save it in the database
UpdateGroupRunnable updateGroupRunnable = new UpdateGroupRunnable(this,
App.getDB(),
oldGroupToUpdate,
updateGroup,
new AfterUpdateNode());
updateGroupRunnable.setUpdateProgressTaskStatus(
new UpdateProgressTaskStatus(this,
SaveDatabaseProgressTaskDialogFragment.start(
getSupportFragmentManager())
));
new Thread(updateGroupRunnable).start();
}
break;
}
}
class AfterAddNode extends AfterActionNodeOnFinish {
public void run(PwNode oldNode, PwNode newNode) {
super.run();
runOnUiThread(() -> {
if (mSuccess) {
if (listNodesFragment != null)
listNodesFragment.addNode(newNode);
} else {
displayMessage(GroupActivity.this);
}
SaveDatabaseProgressTaskDialogFragment.stop(GroupActivity.this);
});
}
}
class AfterUpdateNode extends AfterActionNodeOnFinish {
public void run(PwNode oldNode, PwNode newNode) {
super.run();
runOnUiThread(() -> {
if (mSuccess) {
if (listNodesFragment != null)
listNodesFragment.updateNode(oldNode, newNode);
} else {
displayMessage(GroupActivity.this);
}
SaveDatabaseProgressTaskDialogFragment.stop(GroupActivity.this);
});
}
}
class AfterDeleteNode extends AfterActionNodeOnFinish {
@Override
public void run(PwNode oldNode, PwNode newNode) {
super.run();
runOnUiThread(() -> {
if ( mSuccess) {
if (listNodesFragment != null)
listNodesFragment.removeNode(oldNode);
PwGroup parent = oldNode.getParent();
Database db = App.getDB();
PwDatabase database = db.getPwDatabase();
if (db.isRecycleBinAvailable() &&
db.isRecycleBinEnabled()) {
PwGroup recycleBin = database.getRecycleBin();
// Add trash if it doesn't exists
if (parent.equals(recycleBin)
&& mCurrentGroup != null
&& mCurrentGroup.getParent() == null
&& !mCurrentGroup.equals(recycleBin)) {
if (listNodesFragment != null)
listNodesFragment.addNode(parent);
}
}
} else {
mHandler.post(new UIToastTask(GroupActivity.this, "Unrecoverable error: " + mMessage));
App.setShutdown();
finish();
}
SaveDatabaseProgressTaskDialogFragment.stop(GroupActivity.this);
});
}
}
@Override
public void cancelEditGroup(GroupEditDialogFragment.EditGroupDialogAction action,
String name,
PwIcon iconId) {
// Do nothing here
}
@Override
// For icon in create tree dialog
public void iconPicked(Bundle bundle) {
GroupEditDialogFragment groupEditDialogFragment =
(GroupEditDialogFragment) getSupportFragmentManager()
.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP);
if (groupEditDialogFragment != null) {
groupEditDialogFragment.iconPicked(bundle);
}
}
protected void showWarnings() {
if (App.getDB().isReadOnly()) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
if (prefs.getBoolean(getString(R.string.show_read_only_warning), true)) {
Dialog dialog = new ReadOnlyDialog(this);
dialog.show();
}
}
}
@Override
protected void openGroup(PwGroup group) {
super.openGroup(group);
if (addNodeButtonView != null)
addNodeButtonView.showButton();
}
@Override
public void onBackPressed() {
super.onBackPressed();
if (addNodeButtonView != null)
addNodeButtonView.showButton();
}
}

View File

@@ -0,0 +1,988 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.activities
import android.annotation.SuppressLint
import android.app.Activity
import android.app.SearchManager
import android.app.assist.AssistStructure
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.support.annotation.RequiresApi
import android.support.v4.app.FragmentManager
import android.support.v7.widget.SearchView
import android.support.v7.widget.Toolbar
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.dialogs.ReadOnlyDialog
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.adapters.NodeAdapter
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.SortNodeEnum
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
import com.kunzisoft.keepass.database.action.node.*
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.AddNodeButtonView
import net.cachapa.expandablelayout.ExpandableLayout
class GroupActivity : LockingActivity(),
GroupEditDialogFragment.EditGroupListener,
IconPickerDialogFragment.IconPickerListener,
NodeAdapter.NodeMenuListener,
ListNodesFragment.OnScrollListener,
NodeAdapter.NodeClickCallback,
SortDialogFragment.SortSelectionListener {
// Views
private var toolbar: Toolbar? = null
private var searchTitleView: View? = null
private var toolbarPasteExpandableLayout: ExpandableLayout? = null
private var toolbarPaste: Toolbar? = null
private var iconView: ImageView? = null
private var modeTitleView: TextView? = null
private var addNodeButtonView: AddNodeButtonView? = null
private var groupNameView: TextView? = null
private var mDatabase: Database? = null
private var mListNodesFragment: ListNodesFragment? = null
private var mCurrentGroupIsASearch: Boolean = false
// Nodes
private var mRootGroup: GroupVersioned? = null
private var mCurrentGroup: GroupVersioned? = null
private var mOldGroupToUpdate: GroupVersioned? = null
private var mNodeToCopy: NodeVersioned? = null
private var mNodeToMove: NodeVersioned? = null
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
private var mIconColor: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isFinishing) {
return
}
mDatabase = Database.getInstance()
// Construct main view
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
// Initialize views
iconView = findViewById(R.id.icon)
addNodeButtonView = findViewById(R.id.add_node_button)
toolbar = findViewById(R.id.toolbar)
searchTitleView = findViewById(R.id.search_title)
groupNameView = findViewById(R.id.group_name)
toolbarPasteExpandableLayout = findViewById(R.id.expandable_toolbar_paste_layout)
toolbarPaste = findViewById(R.id.toolbar_paste)
modeTitleView = findViewById(R.id.mode_title_view)
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
// Retrieve elements after an orientation change
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY))
mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY)
if (savedInstanceState.containsKey(NODE_TO_COPY_KEY)) {
mNodeToCopy = savedInstanceState.getParcelable(NODE_TO_COPY_KEY)
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
} else if (savedInstanceState.containsKey(NODE_TO_MOVE_KEY)) {
mNodeToMove = savedInstanceState.getParcelable(NODE_TO_MOVE_KEY)
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
}
}
try {
mRootGroup = mDatabase?.rootGroup
} catch (e: NullPointerException) {
Log.e(TAG, "Unable to get rootGroup")
}
mCurrentGroup = retrieveCurrentGroup(intent, savedInstanceState)
mCurrentGroupIsASearch = Intent.ACTION_SEARCH == intent.action
Log.i(TAG, "Started creating tree")
if (mCurrentGroup == null) {
Log.w(TAG, "Group was null")
return
}
// Update last access time.
mCurrentGroup?.touch(modified = false, touchParents = false)
toolbar?.title = ""
setSupportActionBar(toolbar)
toolbarPaste?.inflateMenu(R.menu.node_paste_menu)
toolbarPaste?.setNavigationIcon(R.drawable.ic_arrow_left_white_24dp)
toolbarPaste?.setNavigationOnClickListener {
toolbarPasteExpandableLayout?.collapse()
mNodeToCopy = null
mNodeToMove = null
}
// Retrieve the textColor to tint the icon
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
mIconColor = taTextColor.getColor(0, Color.WHITE)
taTextColor.recycle()
var fragmentTag = LIST_NODES_FRAGMENT_TAG
if (mCurrentGroupIsASearch)
fragmentTag = SEARCH_FRAGMENT_TAG
// Initialize the fragment with the list
mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment?
if (mListNodesFragment == null)
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, readOnly, mCurrentGroupIsASearch)
// Attach fragment to content view
supportFragmentManager.beginTransaction().replace(
R.id.nodes_list_fragment_container,
mListNodesFragment,
fragmentTag)
.commit()
// Add listeners to the add buttons
addNodeButtonView?.setAddGroupClickListener(View.OnClickListener {
GroupEditDialogFragment.build()
.show(supportFragmentManager,
GroupEditDialogFragment.TAG_CREATE_GROUP)
})
addNodeButtonView?.setAddEntryClickListener(View.OnClickListener {
mCurrentGroup?.let { currentGroup ->
EntryEditActivity.launch(this@GroupActivity, currentGroup)
}
})
// Search suggestion
mDatabase?.let { database ->
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
}
Log.i(TAG, "Finished creating tree")
}
override fun onNewIntent(intent: Intent) {
Log.d(TAG, "setNewIntent: $intent")
setIntent(intent)
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intent.action) {
// only one instance of search in backstack
openSearchGroup(retrieveCurrentGroup(intent, null))
true
} else {
false
}
}
private fun openSearchGroup(group: GroupVersioned?) {
// Delete the previous search fragment
val searchFragment = supportFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG)
if (searchFragment != null) {
if (supportFragmentManager
.popBackStackImmediate(SEARCH_FRAGMENT_TAG,
FragmentManager.POP_BACK_STACK_INCLUSIVE))
supportFragmentManager.beginTransaction().remove(searchFragment).commit()
}
openGroup(group, true)
}
private fun openChildGroup(group: GroupVersioned) {
openGroup(group, false)
}
private fun openGroup(group: GroupVersioned?, isASearch: Boolean) {
// Check TimeoutHelper
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
// Open a group in a new fragment
val newListNodeFragment = ListNodesFragment.newInstance(group, readOnly, isASearch)
val fragmentTransaction = supportFragmentManager.beginTransaction()
// Different animation
val fragmentTag: String
fragmentTag = if (isASearch) {
fragmentTransaction.setCustomAnimations(R.anim.slide_in_top, R.anim.slide_out_bottom,
R.anim.slide_in_bottom, R.anim.slide_out_top)
SEARCH_FRAGMENT_TAG
} else {
fragmentTransaction.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left,
R.anim.slide_in_left, R.anim.slide_out_right)
LIST_NODES_FRAGMENT_TAG
}
fragmentTransaction.replace(R.id.nodes_list_fragment_container,
newListNodeFragment,
fragmentTag)
fragmentTransaction.addToBackStack(fragmentTag)
fragmentTransaction.commit()
mListNodesFragment = newListNodeFragment
mCurrentGroup = group
assignGroupViewElements()
}
}
override fun onSaveInstanceState(outState: Bundle) {
mCurrentGroup?.let {
outState.putParcelable(GROUP_ID_KEY, it.nodeId)
}
mOldGroupToUpdate?.let {
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it)
}
mNodeToCopy?.let {
outState.putParcelable(NODE_TO_COPY_KEY, it)
}
mNodeToMove?.let {
outState.putParcelable(NODE_TO_MOVE_KEY, it)
}
super.onSaveInstanceState(outState)
}
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): GroupVersioned? {
// If it's a search
if (Intent.ACTION_SEARCH == intent.action) {
return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' })
}
// else a real group
else {
var pwGroupId: PwNodeId<*>? = null
if (savedInstanceState != null && savedInstanceState.containsKey(GROUP_ID_KEY)) {
pwGroupId = savedInstanceState.getParcelable(GROUP_ID_KEY)
} else {
if (getIntent() != null)
pwGroupId = intent.getParcelableExtra(GROUP_ID_KEY)
}
readOnly = mDatabase?.isReadOnly == true || readOnly // Force read only if the database is like that
Log.w(TAG, "Creating tree view")
val currentGroup: GroupVersioned?
currentGroup = if (pwGroupId == null) {
mRootGroup
} else {
mDatabase?.getGroupById(pwGroupId)
}
return currentGroup
}
}
private fun assignGroupViewElements() {
// Assign title
if (mCurrentGroup != null) {
val title = mCurrentGroup?.title
if (title != null && title.isNotEmpty()) {
if (groupNameView != null) {
groupNameView?.text = title
groupNameView?.invalidate()
}
} else {
if (groupNameView != null) {
groupNameView?.text = getText(R.string.root)
groupNameView?.invalidate()
}
}
}
if (mCurrentGroupIsASearch) {
searchTitleView?.visibility = View.VISIBLE
} else {
searchTitleView?.visibility = View.GONE
}
// Assign icon
if (mCurrentGroupIsASearch) {
if (toolbar != null) {
toolbar?.navigationIcon = null
}
iconView?.visibility = View.GONE
} else {
// Assign the group icon depending of IconPack or custom icon
iconView?.visibility = View.VISIBLE
mCurrentGroup?.let {
if (mDatabase?.drawFactory != null)
iconView?.assignDatabaseIcon(mDatabase?.drawFactory!!, it.icon, mIconColor)
if (toolbar != null) {
if (mCurrentGroup?.containsParent() == true)
toolbar?.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp)
else {
toolbar?.navigationIcon = null
}
}
}
}
// Show selection mode message if needed
if (selectionMode) {
modeTitleView?.visibility = View.VISIBLE
} else {
modeTitleView?.visibility = View.GONE
}
// Show button if allowed
addNodeButtonView?.apply {
// To enable add button
val addGroupEnabled = !readOnly && !mCurrentGroupIsASearch
var addEntryEnabled = !readOnly && !mCurrentGroupIsASearch
mCurrentGroup?.let {
val isRoot = it == mRootGroup
if (!it.allowAddEntryIfIsRoot())
addEntryEnabled = !isRoot && addEntryEnabled
if (isRoot) {
showWarnings()
}
}
enableAddGroup(addGroupEnabled)
enableAddEntry(addEntryEnabled)
if (isEnable)
showButton()
}
}
override fun onScrolled(dy: Int) {
addNodeButtonView?.hideButtonOnScrollListener(dy)
}
override fun onNodeClick(node: NodeVersioned) {
when (node.type) {
Type.GROUP -> try {
openChildGroup(node as GroupVersioned)
} catch (e: ClassCastException) {
Log.e(TAG, "Node can't be cast in Group")
}
Type.ENTRY -> try {
val entryVersioned = node as EntryVersioned
EntrySelectionHelper.doEntrySelectionAction(intent,
{
EntryActivity.launch(this@GroupActivity, entryVersioned, readOnly)
},
{
// Populate Magikeyboard with entry
mDatabase?.let { database ->
MagikIME.addEntryAndLaunchNotificationIfAllowed(this@GroupActivity,
entryVersioned.getEntryInfo(database))
}
// Consume the selection mode
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
moveTaskToBack(true)
},
{
// Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
AutofillHelper.buildResponseWhenEntrySelected(this@GroupActivity,
entryVersioned.getEntryInfo(mDatabase!!))
}
finish()
})
} catch (e: ClassCastException) {
Log.e(TAG, "Node can't be cast in Entry")
}
}
}
override fun onOpenMenuClick(node: NodeVersioned): Boolean {
onNodeClick(node)
return true
}
override fun onEditMenuClick(node: NodeVersioned): Boolean {
when (node.type) {
Type.GROUP -> {
mOldGroupToUpdate = node as GroupVersioned
GroupEditDialogFragment.build(mOldGroupToUpdate!!)
.show(supportFragmentManager,
GroupEditDialogFragment.TAG_CREATE_GROUP)
}
Type.ENTRY -> EntryEditActivity.launch(this@GroupActivity, node as EntryVersioned)
}
return true
}
override fun onCopyMenuClick(node: NodeVersioned): Boolean {
toolbarPasteExpandableLayout?.expand()
mNodeToCopy = node
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
return false
}
private inner class OnCopyMenuItemClickListener : Toolbar.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean {
toolbarPasteExpandableLayout?.collapse()
when (item.itemId) {
R.id.menu_paste -> {
when (mNodeToCopy?.type) {
Type.GROUP -> Log.e(TAG, "Copy not allowed for group")
Type.ENTRY -> {
mCurrentGroup?.let { currentGroup ->
copyEntry(mNodeToCopy as EntryVersioned, currentGroup)
}
}
}
mNodeToCopy = null
return true
}
}
return true
}
}
private fun copyEntry(entryToCopy: EntryVersioned, newParent: GroupVersioned) {
ProgressDialogSaveDatabaseThread(this) {
CopyEntryRunnable(this,
Database.getInstance(),
entryToCopy,
newParent,
AfterAddNodeRunnable(),
!readOnly)
}.start()
}
override fun onMoveMenuClick(node: NodeVersioned): Boolean {
toolbarPasteExpandableLayout?.expand()
mNodeToMove = node
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
return false
}
private inner class OnMoveMenuItemClickListener : Toolbar.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean {
toolbarPasteExpandableLayout?.collapse()
when (item.itemId) {
R.id.menu_paste -> {
when (mNodeToMove?.type) {
Type.GROUP -> {
mCurrentGroup?.let { currentGroup ->
moveGroup(mNodeToMove as GroupVersioned, currentGroup)
}
}
Type.ENTRY -> {
mCurrentGroup?.let { currentGroup ->
moveEntry(mNodeToMove as EntryVersioned, currentGroup)
}
}
}
mNodeToMove = null
return true
}
}
return true
}
}
private fun moveGroup(groupToMove: GroupVersioned, newParent: GroupVersioned) {
ProgressDialogSaveDatabaseThread(this) {
MoveGroupRunnable(
this,
Database.getInstance(),
groupToMove,
newParent,
AfterAddNodeRunnable(),
!readOnly)
}.start()
}
private fun moveEntry(entryToMove: EntryVersioned, newParent: GroupVersioned) {
ProgressDialogSaveDatabaseThread(this) {
MoveEntryRunnable(
this,
Database.getInstance(),
entryToMove,
newParent,
AfterAddNodeRunnable(),
!readOnly)
}.start()
}
override fun onDeleteMenuClick(node: NodeVersioned): Boolean {
when (node.type) {
Type.GROUP -> deleteGroup(node as GroupVersioned)
Type.ENTRY -> deleteEntry(node as EntryVersioned)
}
return true
}
private fun deleteGroup(group: GroupVersioned) {
//TODO Verify trash recycle bin
ProgressDialogSaveDatabaseThread(this) {
DeleteGroupRunnable(
this,
Database.getInstance(),
group,
AfterDeleteNodeRunnable(),
!readOnly)
}.start()
}
private fun deleteEntry(entry: EntryVersioned) {
ProgressDialogSaveDatabaseThread(this) {
DeleteEntryRunnable(
this,
Database.getInstance(),
entry,
AfterDeleteNodeRunnable(),
!readOnly)
}.start()
}
override fun onResume() {
super.onResume()
// Refresh the elements
assignGroupViewElements()
// Refresh suggestions to change preferences
mSearchSuggestionAdapter?.reInit(this)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.search, menu)
inflater.inflate(R.menu.database_lock, menu)
if (!selectionMode) {
inflater.inflate(R.menu.default_menu, menu)
MenuUtil.contributionMenuInflater(inflater, menu)
}
// Get the SearchView and set the searchable configuration
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
menu.findItem(R.id.menu_search)?.let {
val searchView = it.actionView as SearchView?
searchView?.apply {
setSearchableInfo(searchManager.getSearchableInfo(
ComponentName(this@GroupActivity, GroupActivity::class.java)))
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
suggestionsAdapter = mSearchSuggestionAdapter
setOnSuggestionListener(object : SearchView.OnSuggestionListener {
override fun onSuggestionClick(position: Int): Boolean {
mSearchSuggestionAdapter?.let { searchAdapter ->
searchAdapter.getEntryFromPosition(position)?.let { entry ->
onNodeClick(entry)
}
}
return true
}
override fun onSuggestionSelect(position: Int): Boolean {
return true
}
})
}
}
super.onCreateOptionsMenu(menu)
// Launch education screen
Handler().post { performedNextEducation(GroupActivityEducation(this), menu) }
return true
}
private fun performedNextEducation(groupActivityEducation: GroupActivityEducation,
menu: Menu) {
// If no node, show education to add new one
if (mListNodesFragment != null
&& mListNodesFragment!!.isEmpty
&& addNodeButtonView?.addButtonView != null
&& addNodeButtonView!!.isEnable
&& groupActivityEducation.checkAndPerformedAddNodeButtonEducation(
addNodeButtonView?.addButtonView!!,
{
addNodeButtonView?.openButtonIfClose()
},
{
performedNextEducation(groupActivityEducation, menu)
}
))
else if (toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_search) != null
&& groupActivityEducation.checkAndPerformedSearchMenuEducation(
toolbar!!.findViewById(R.id.menu_search),
{
menu.findItem(R.id.menu_search).expandActionView()
},
{
performedNextEducation(groupActivityEducation, menu)
}))
else if (toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_sort) != null
&& groupActivityEducation.checkAndPerformedSortMenuEducation(
toolbar!!.findViewById(R.id.menu_sort),
{
onOptionsItemSelected(menu.findItem(R.id.menu_sort))
},
{
performedNextEducation(groupActivityEducation, menu)
}))
else if (toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_lock) != null
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
toolbar!!.findViewById(R.id.menu_lock),
{
onOptionsItemSelected(menu.findItem(R.id.menu_lock))
},
{
performedNextEducation(groupActivityEducation, menu)
}))
;
}
override fun startActivity(intent: Intent) {
// Get the intent, verify the action and get the query
if (Intent.ACTION_SEARCH == intent.action) {
val query = intent.getStringExtra(SearchManager.QUERY)
// manually launch the real search activity
val searchIntent = Intent(applicationContext, GroupActivity::class.java)
// add query to the Intent Extras
searchIntent.action = Intent.ACTION_SEARCH
searchIntent.putExtra(SearchManager.QUERY, query)
EntrySelectionHelper.doEntrySelectionAction(intent,
{
super@GroupActivity.startActivity(intent)
},
{
KeyboardHelper.startActivityForKeyboardSelection(
this@GroupActivity,
searchIntent)
finish()
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.startActivityForAutofillResult(
this@GroupActivity,
searchIntent,
assistStructure)
}
})
} else {
super.startActivity(intent)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
R.id.menu_search ->
//onSearchRequested();
return true
R.id.menu_lock -> {
lockAndExit()
return true
}
else -> {
// Check the time lock before launching settings
MenuUtil.onDefaultMenuOptionsItemSelected(this, item, readOnly, true)
return super.onOptionsItemSelected(item)
}
}
}
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
name: String?,
icon: PwIcon?) {
val database = Database.getInstance()
if (name != null && name.isNotEmpty() && icon != null) {
when (action) {
GroupEditDialogFragment.EditGroupDialogAction.CREATION -> {
// If group creation
mCurrentGroup?.let { currentGroup ->
// Build the group
database.createGroup()?.let { newGroup ->
newGroup.title = name
newGroup.icon = icon
// Not really needed here because added in runnable but safe
newGroup.parent = currentGroup
// If group created save it in the database
ProgressDialogSaveDatabaseThread(this) {
AddGroupRunnable(this,
Database.getInstance(),
newGroup,
currentGroup,
AfterAddNodeRunnable(),
!readOnly)
}.start()
}
}
}
GroupEditDialogFragment.EditGroupDialogAction.UPDATE ->
// If update add new elements
mOldGroupToUpdate?.let { oldGroupToUpdate ->
GroupVersioned(oldGroupToUpdate).let { updateGroup ->
updateGroup.title = name
// TODO custom icon
updateGroup.icon = icon
mListNodesFragment?.removeNode(oldGroupToUpdate)
// If group updated save it in the database
ProgressDialogSaveDatabaseThread(this) {
UpdateGroupRunnable(this,
Database.getInstance(),
oldGroupToUpdate,
updateGroup,
AfterUpdateNodeRunnable(),
!readOnly)
}.start()
}
}
else -> {
}
}
}
}
internal inner class AfterAddNodeRunnable : AfterActionNodeFinishRunnable() {
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
runOnUiThread {
if (actionNodeValues.result.isSuccess) {
if (actionNodeValues.newNode != null)
mListNodesFragment?.addNode(actionNodeValues.newNode)
}
}
}
}
internal inner class AfterUpdateNodeRunnable : AfterActionNodeFinishRunnable() {
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
runOnUiThread {
if (actionNodeValues.result.isSuccess) {
if (actionNodeValues.oldNode!= null && actionNodeValues.newNode != null)
mListNodesFragment?.updateNode(actionNodeValues.oldNode, actionNodeValues.newNode)
}
}
}
}
internal inner class AfterDeleteNodeRunnable : AfterActionNodeFinishRunnable() {
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
runOnUiThread {
if (actionNodeValues.result.isSuccess) {
actionNodeValues.oldNode?.let { oldNode ->
mListNodesFragment?.removeNode(oldNode)
// TODO Move trash view
// Add trash in views list if it doesn't exists
val database = Database.getInstance()
if (database.isRecycleBinEnabled) {
val recycleBin = database.recycleBin
if (mCurrentGroup != null && recycleBin != null
&& mCurrentGroup!!.parent == null
&& mCurrentGroup != recycleBin) {
mListNodesFragment?.addNode(recycleBin)
}
}
}
}
}
}
}
override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
name: String?,
icon: PwIcon?) {
// Do nothing here
}
override// For icon in create tree dialog
fun iconPicked(bundle: Bundle) {
(supportFragmentManager
.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment)
.iconPicked(bundle)
}
private fun showWarnings() {
// TODO Preferences
if (Database.getInstance().isReadOnly) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
if (prefs.getBoolean(getString(R.string.show_read_only_warning), true)) {
ReadOnlyDialog(this).show()
}
}
}
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
// Not directly get the entry from intent data but from database
mListNodesFragment?.rebuildList()
}
@SuppressLint("RestrictedApi")
override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
/*
* ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in
* another app such as Files or GoogleDrive and then Search for an entry. Here we remove the
* FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task.
*/
if (Intent.ACTION_SEARCH == intent.action) {
var flags = intent.flags
flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
intent.flags = flags
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
super.startActivityForResult(intent, requestCode, options)
}
}
private fun removeSearchInIntent(intent: Intent) {
if (Intent.ACTION_SEARCH == intent.action) {
mCurrentGroupIsASearch = false
intent.action = Intent.ACTION_DEFAULT
intent.removeExtra(SearchManager.QUERY)
}
}
override fun onBackPressed() {
// Normal way when we are not in root
if (mRootGroup != null && mRootGroup != mCurrentGroup)
super.onBackPressed()
// Else lock if needed
else {
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
lockAndExit()
super.onBackPressed()
} else {
moveTaskToBack(true)
}
}
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment
// to refresh fragment
mListNodesFragment?.rebuildList()
mCurrentGroup = mListNodesFragment?.mainGroup
removeSearchInIntent(intent)
assignGroupViewElements()
}
companion object {
private val TAG = GroupActivity::class.java.name
private const val GROUP_ID_KEY = "GROUP_ID_KEY"
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
private const val NODE_TO_COPY_KEY = "NODE_TO_COPY_KEY"
private const val NODE_TO_MOVE_KEY = "NODE_TO_MOVE_KEY"
private fun buildAndLaunchIntent(activity: Activity, group: GroupVersioned?, readOnly: Boolean,
intentBuildLauncher: (Intent) -> Unit) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, GroupActivity::class.java)
if (group != null) {
intent.putExtra(GROUP_ID_KEY, group.nodeId)
}
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
intentBuildLauncher.invoke(intent)
}
}
/*
* -------------------------
* Standard Launch
* -------------------------
*/
@JvmOverloads
fun launch(activity: Activity, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
TimeoutHelper.recordTime(activity)
buildAndLaunchIntent(activity, null, readOnly) { intent ->
activity.startActivity(intent)
}
}
/*
* -------------------------
* Keyboard Launch
* -------------------------
*/
// TODO implement pre search to directly open the direct group
fun launchForKeyboardSelection(activity: Activity, readOnly: Boolean) {
TimeoutHelper.recordTime(activity)
buildAndLaunchIntent(activity, null, readOnly) { intent ->
KeyboardHelper.startActivityForKeyboardSelection(activity, intent)
}
}
/*
* -------------------------
* Autofill Launch
* -------------------------
*/
// TODO implement pre search to directly open the direct group
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure, readOnly: Boolean) {
TimeoutHelper.recordTime(activity)
buildAndLaunchIntent(activity, null, readOnly) { intent ->
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure)
}
}
}
}

View File

@@ -1,7 +0,0 @@
package com.kunzisoft.keepass.activities;
import android.content.Intent;
public interface IntentBuildLauncher {
void startActivityForResult(Intent intent);
}

View File

@@ -1,294 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities;
import android.annotation.SuppressLint;
import android.app.assist.AssistStructure;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.TextView;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.adapters.NodeAdapter;
import com.kunzisoft.keepass.app.App;
import com.kunzisoft.keepass.autofill.AutofillHelper;
import com.kunzisoft.keepass.database.Database;
import com.kunzisoft.keepass.database.PwEntry;
import com.kunzisoft.keepass.database.PwGroup;
import com.kunzisoft.keepass.database.PwNode;
import com.kunzisoft.keepass.database.SortNodeEnum;
import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment;
import com.kunzisoft.keepass.dialogs.SortDialogFragment;
import com.kunzisoft.keepass.lock.LockingActivity;
import com.kunzisoft.keepass.password.AssignPasswordHelper;
import com.kunzisoft.keepass.selection.EntrySelectionHelper;
import com.kunzisoft.keepass.utils.MenuUtil;
public abstract class ListNodesActivity extends LockingActivity
implements AssignMasterKeyDialogFragment.AssignPasswordDialogListener,
NodeAdapter.NodeClickCallback,
SortDialogFragment.SortSelectionListener {
protected static final String LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG";
protected ListNodesFragment listNodesFragment;
protected Database database;
protected PwGroup rootGroup;
protected PwGroup mCurrentGroup;
protected TextView groupNameView;
protected boolean entrySelectionMode;
protected AutofillHelper autofillHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if ( isFinishing() ) {
return;
}
database = App.getDB();
// Likely the app has been killed exit the activity
if ( ! database.getLoaded() ) {
finish();
return;
}
invalidateOptionsMenu();
rootGroup = database.getPwDatabase().getRootGroup();
mCurrentGroup = retrieveCurrentGroup(savedInstanceState);
initializeListNodesFragment(mCurrentGroup);
entrySelectionMode = EntrySelectionHelper.isIntentInEntrySelectionMode(getIntent());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
autofillHelper = new AutofillHelper();
autofillHelper.retrieveAssistStructure(getIntent());
}
}
protected abstract PwGroup retrieveCurrentGroup(@Nullable Bundle savedInstanceState);
@Override
protected void onResume() {
super.onResume();
// Refresh the title
assignToolbarElements();
}
protected void initializeListNodesFragment(PwGroup currentGroup) {
listNodesFragment = (ListNodesFragment) getSupportFragmentManager()
.findFragmentByTag(LIST_NODES_FRAGMENT_TAG);
if (listNodesFragment == null)
listNodesFragment = ListNodesFragment.newInstance(currentGroup, readOnly);
}
/**
* Attach the fragment's list of node.
* <br />
* <strong>R.id.nodes_list_fragment_container</strong> must be the id of the container
*/
protected void attachFragmentToContentView() {
getSupportFragmentManager().beginTransaction().replace(
R.id.nodes_list_fragment_container,
listNodesFragment,
LIST_NODES_FRAGMENT_TAG)
.commit();
}
public void assignToolbarElements() {
if (mCurrentGroup != null) {
String title = mCurrentGroup.getName();
if (title != null && title.length() > 0) {
if (groupNameView != null) {
groupNameView.setText(title);
groupNameView.invalidate();
}
} else {
if (groupNameView != null) {
groupNameView.setText(getText(R.string.root));
groupNameView.invalidate();
}
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
MenuUtil.contributionMenuInflater(inflater, menu);
inflater.inflate(R.menu.default_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch ( item.getItemId() ) {
default:
// Check the time lock before launching settings
MenuUtil.onDefaultMenuOptionsItemSelected(this, item, readOnly, true);
return super.onOptionsItemSelected(item);
}
}
@Override
public void onNodeClick(PwNode node) {
// Add event when we have Autofill
AssistStructure assistStructure = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
assistStructure = autofillHelper.getAssistStructure();
if (assistStructure != null) {
switch (node.getType()) {
case GROUP:
openGroup((PwGroup) node);
break;
case ENTRY:
// Build response with the entry selected
autofillHelper.buildResponseWhenEntrySelected(this, (PwEntry) node);
finish();
break;
}
}
}
if ( assistStructure == null ){
if (entrySelectionMode) {
switch (node.getType()) {
case GROUP:
openGroup((PwGroup) node);
break;
case ENTRY:
EntrySelectionHelper.buildResponseWhenEntrySelected(this, (PwEntry) node);
finish();
break;
}
} else {
switch (node.getType()) {
case GROUP:
openGroup((PwGroup) node);
break;
case ENTRY:
EntryActivity.launch(this, (PwEntry) node, readOnly);
break;
}
}
}
}
protected void openGroup(PwGroup group) {
// Check Timeout
if (checkTimeIsAllowedOrFinish(this)) {
startRecordTime(this);
ListNodesFragment newListNodeFragment = ListNodesFragment.newInstance(group, readOnly);
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left,
R.anim.slide_in_left, R.anim.slide_out_right)
.replace(R.id.nodes_list_fragment_container,
newListNodeFragment,
LIST_NODES_FRAGMENT_TAG)
.addToBackStack(LIST_NODES_FRAGMENT_TAG)
.commit();
listNodesFragment = newListNodeFragment;
mCurrentGroup = group;
assignToolbarElements();
}
}
@Override
public void onAssignKeyDialogPositiveClick(
boolean masterPasswordChecked, String masterPassword,
boolean keyFileChecked, Uri keyFile) {
AssignPasswordHelper assignPasswordHelper =
new AssignPasswordHelper(this,
masterPasswordChecked, masterPassword, keyFileChecked, keyFile);
assignPasswordHelper.assignPasswordInDatabase(null);
}
@Override
public void onAssignKeyDialogNegativeClick(
boolean masterPasswordChecked, String masterPassword,
boolean keyFileChecked, Uri keyFile) {
}
@Override
public void onSortSelected(SortNodeEnum sortNodeEnum, boolean ascending, boolean groupsBefore, boolean recycleBinBottom) {
if (listNodesFragment != null)
listNodesFragment.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
EntrySelectionHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data);
}
}
@SuppressLint("RestrictedApi")
@Override
public void startActivityForResult(Intent intent, int requestCode, Bundle options) {
/*
* ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in
* another app such as Files or GoogleDrive and then Search for an entry. Here we remove the
* FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task.
*/
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
int flags = intent.getFlags();
flags &= ~Intent.FLAG_ACTIVITY_NEW_TASK;
intent.setFlags(flags);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
super.startActivityForResult(intent, requestCode, options);
}
}
@Override
public void onBackPressed() {
if (checkTimeIsAllowedOrFinish(this)) {
startRecordTime(this);
super.onBackPressed();
listNodesFragment = (ListNodesFragment) getSupportFragmentManager().findFragmentByTag(LIST_NODES_FRAGMENT_TAG);
// to refresh fragment
listNodesFragment.rebuildList();
mCurrentGroup = listNodesFragment.getMainGroup();
assignToolbarElements();
}
}
}

View File

@@ -1,275 +0,0 @@
package com.kunzisoft.keepass.activities;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.adapters.NodeAdapter;
import com.kunzisoft.keepass.app.App;
import com.kunzisoft.keepass.database.PwDatabase;
import com.kunzisoft.keepass.database.PwGroup;
import com.kunzisoft.keepass.database.PwNode;
import com.kunzisoft.keepass.database.SortNodeEnum;
import com.kunzisoft.keepass.dialogs.SortDialogFragment;
import com.kunzisoft.keepass.settings.PreferencesUtil;
import com.kunzisoft.keepass.stylish.StylishFragment;
public class ListNodesFragment extends StylishFragment implements
SortDialogFragment.SortSelectionListener {
private static final String TAG = ListNodesFragment.class.getName();
private static final String GROUP_KEY = "GROUP_KEY";
private NodeAdapter.NodeClickCallback nodeClickCallback;
private NodeAdapter.NodeMenuListener nodeMenuListener;
private OnScrollListener onScrollListener;
private RecyclerView listView;
protected PwGroup mCurrentGroup;
protected NodeAdapter mAdapter;
// Preferences for sorting
private SharedPreferences prefs;
private boolean readOnly;
public static ListNodesFragment newInstance(PwGroup group, boolean readOnly) {
Bundle bundle = new Bundle();
if (group != null) {
bundle.putParcelable(GROUP_KEY, group);
}
ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly);
ListNodesFragment listNodesFragment = new ListNodesFragment();
listNodesFragment.setArguments(bundle);
return listNodesFragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
nodeClickCallback = (NodeAdapter.NodeClickCallback) context;
} catch (ClassCastException e) {
// The activity doesn't implement the interface, throw exception
throw new ClassCastException(context.toString()
+ " must implement " + NodeAdapter.NodeClickCallback.class.getName());
}
try {
nodeMenuListener = (NodeAdapter.NodeMenuListener) context;
} catch (ClassCastException e) {
nodeMenuListener = null;
// Context menu can be omit
Log.w(TAG, context.toString()
+ " must implement " + NodeAdapter.NodeMenuListener.class.getName());
}
try {
onScrollListener = (OnScrollListener) context;
} catch (ClassCastException e) {
onScrollListener = null;
// Context menu can be omit
Log.w(TAG, context.toString()
+ " must implement " + RecyclerView.OnScrollListener.class.getName());
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if ( getActivity() != null ) {
setHasOptionsMenu(true);
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, getArguments());
if (getArguments() != null) {
// Contains all the group in element
if (getArguments().containsKey(GROUP_KEY)) {
mCurrentGroup = getArguments().getParcelable(GROUP_KEY);
}
}
mAdapter = new NodeAdapter(getContextThemed(), getActivity().getMenuInflater(), readOnly);
mAdapter.setOnNodeClickListener(nodeClickCallback);
if (nodeMenuListener != null) {
mAdapter.setActivateContextMenu(true);
mAdapter.setNodeMenuListener(nodeMenuListener);
}
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
ReadOnlyHelper.onSaveInstanceState(outState, readOnly);
super.onSaveInstanceState(outState);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
// To apply theme
View rootView = inflater.cloneInContext(getContextThemed())
.inflate(R.layout.list_nodes_fragment, container, false);
listView = rootView.findViewById(R.id.nodes_list);
if (onScrollListener != null) {
listView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
onScrollListener.onScrolled(dy);
}
});
}
return rootView;
}
@Override
public void onResume() {
super.onResume();
rebuildList();
}
public void rebuildList() {
// Add elements to the list
mAdapter.rebuildList(mCurrentGroup);
assignListToNodeAdapter(listView);
}
protected void assignListToNodeAdapter(RecyclerView recyclerView) {
recyclerView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(mAdapter);
}
@Override
public void onSortSelected(SortNodeEnum sortNodeEnum, boolean ascending, boolean groupsBefore, boolean recycleBinBottom) {
// Toggle setting
SharedPreferences.Editor editor = prefs.edit();
editor.putString(getString(R.string.sort_node_key), sortNodeEnum.name());
editor.putBoolean(getString(R.string.sort_ascending_key), ascending);
editor.putBoolean(getString(R.string.sort_group_before_key), groupsBefore);
editor.putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom);
editor.apply();
// Tell the adapter to refresh it's list
mAdapter.notifyChangeSort(sortNodeEnum, ascending, groupsBefore);
mAdapter.rebuildList(mCurrentGroup);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.tree, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch ( item.getItemId() ) {
case R.id.menu_sort:
SortDialogFragment sortDialogFragment;
PwDatabase database = App.getDB().getPwDatabase();
/*
// TODO Recycle bin bottom
if (database.isRecycleBinAvailable() && database.isRecycleBinEnabled()) {
sortDialogFragment =
SortDialogFragment.getInstance(
PrefsUtil.getListSort(this),
PrefsUtil.getAscendingSort(this),
PrefsUtil.getGroupsBeforeSort(this),
PrefsUtil.getRecycleBinBottomSort(this));
} else {
*/
sortDialogFragment =
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(getContext()),
PreferencesUtil.getAscendingSort(getContext()),
PreferencesUtil.getGroupsBeforeSort(getContext()));
//}
sortDialogFragment.show(getChildFragmentManager(), "sortDialog");
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE:
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE ||
resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
PwNode newNode = data.getParcelableExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY);
if (newNode != null) {
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
mAdapter.addNode(newNode);
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
//mAdapter.updateLastNodeRegister(newNode);
mAdapter.rebuildList(mCurrentGroup);
}
} else {
Log.e(this.getClass().getName(), "New node can be retrieve in Activity Result");
}
}
break;
}
}
public boolean isEmpty() {
return mAdapter == null || mAdapter.getItemCount() <= 0;
}
public void addNode(PwNode newNode) {
mAdapter.addNode(newNode);
}
public void updateNode(PwNode oldNode, PwNode newNode) {
mAdapter.updateNode(oldNode, newNode);
}
public void removeNode(PwNode pwNode) {
mAdapter.removeNode(pwNode);
}
public PwGroup getMainGroup() {
return mCurrentGroup;
}
public interface OnScrollListener {
/**
* Callback method to be invoked when the RecyclerView has been scrolled. This will be
* called after the scroll has completed.
*
* @param dy The amount of vertical scroll.
*/
void onScrolled(int dy);
}
}

View File

@@ -0,0 +1,300 @@
package com.kunzisoft.keepass.activities
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.NodeAdapter
import com.kunzisoft.keepass.database.SortNodeEnum
import com.kunzisoft.keepass.database.element.GroupVersioned
import com.kunzisoft.keepass.database.element.NodeVersioned
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
private var nodeClickCallback: NodeAdapter.NodeClickCallback? = null
private var nodeMenuListener: NodeAdapter.NodeMenuListener? = null
private var onScrollListener: OnScrollListener? = null
private var listView: RecyclerView? = null
var mainGroup: GroupVersioned? = null
private set
private var mAdapter: NodeAdapter? = null
private var notFoundView: View? = null
private var isASearchResult: Boolean = false
// Preferences for sorting
private var prefs: SharedPreferences? = null
private var readOnly: Boolean = false
get() {
return field || selectionMode
}
private var selectionMode: Boolean = false
val isEmpty: Boolean
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
override fun onAttach(context: Context?) {
super.onAttach(context)
try {
nodeClickCallback = context as NodeAdapter.NodeClickCallback?
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context?.toString()
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
}
try {
nodeMenuListener = context as NodeAdapter.NodeMenuListener?
} catch (e: ClassCastException) {
nodeMenuListener = null
// Context menu can be omit
Log.w(TAG, context?.toString()
+ " must implement " + NodeAdapter.NodeMenuListener::class.java.name)
}
try {
onScrollListener = context as OnScrollListener?
} catch (e: ClassCastException) {
onScrollListener = null
// Context menu can be omit
Log.w(TAG, context?.toString()
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.let { currentActivity ->
setHasOptionsMenu(true)
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
arguments?.let { args ->
// Contains all the group in element
if (args.containsKey(GROUP_KEY)) {
mainGroup = args.getParcelable(GROUP_KEY)
}
if (args.containsKey(IS_SEARCH)) {
isASearchResult = args.getBoolean(IS_SEARCH)
}
}
contextThemed?.let { context ->
mAdapter = NodeAdapter(context, currentActivity.menuInflater)
mAdapter?.apply {
setReadOnly(readOnly)
setIsASearchResult(isASearchResult)
setOnNodeClickListener(nodeClickCallback)
setActivateContextMenu(true)
setNodeMenuListener(nodeMenuListener)
}
}
prefs = PreferenceManager.getDefaultSharedPreferences(context)
}
}
override fun onSaveInstanceState(outState: Bundle) {
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
super.onSaveInstanceState(outState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
// To apply theme
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_list_nodes, container, false)
listView = rootView.findViewById(R.id.nodes_list)
notFoundView = rootView.findViewById(R.id.not_found_container)
onScrollListener?.let { onScrollListener ->
listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
onScrollListener.onScrolled(dy)
}
})
}
rebuildList()
return rootView
}
override fun onResume() {
super.onResume()
activity?.intent?.let {
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it)
}
// Force read only mode if selection mode
mAdapter?.apply {
setReadOnly(readOnly)
}
// Refresh data
mAdapter?.notifyDataSetChanged()
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
// To show the " no search entry found "
listView?.visibility = View.GONE
notFoundView?.visibility = View.VISIBLE
} else {
listView?.visibility = View.VISIBLE
notFoundView?.visibility = View.GONE
}
}
fun rebuildList() {
// Add elements to the list
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
}
listView?.apply {
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
layoutManager = LinearLayoutManager(context)
adapter = mAdapter
}
}
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
// Toggle setting
prefs?.edit()?.apply {
putString(getString(R.string.sort_node_key), sortNodeEnum.name)
putBoolean(getString(R.string.sort_ascending_key), ascending)
putBoolean(getString(R.string.sort_group_before_key), groupsBefore)
putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom)
apply()
}
// Tell the adapter to refresh it's list
mAdapter?.notifyChangeSort(sortNodeEnum, ascending, groupsBefore)
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
}
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.tree, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_sort -> {
context?.let { context ->
val sortDialogFragment: SortDialogFragment
/*
// TODO Recycle bin bottom
if (database.isRecycleBinAvailable() && database.isRecycleBinEnabled()) {
sortDialogFragment =
SortDialogFragment.getInstance(
PrefsUtil.getListSort(this),
PrefsUtil.getAscendingSort(this),
PrefsUtil.getGroupsBeforeSort(this),
PrefsUtil.getRecycleBinBottomSort(this));
} else {
*/
sortDialogFragment = SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context))
//}
sortDialogFragment.show(childFragmentManager, "sortDialog")
}
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
data?.getParcelableExtra<NodeVersioned>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode ->
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
mAdapter?.addNode(newNode)
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
//mAdapter.updateLastNodeRegister(newNode);
mainGroup?.let { mainGroup ->
mAdapter?.rebuildList(mainGroup)
}
}
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
}
}
}
}
fun addNode(newNode: NodeVersioned) {
mAdapter?.addNode(newNode)
}
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) {
mAdapter?.updateNode(oldNode, newNode)
}
fun removeNode(pwNode: NodeVersioned) {
mAdapter?.removeNode(pwNode)
}
interface OnScrollListener {
/**
* Callback method to be invoked when the RecyclerView has been scrolled. This will be
* called after the scroll has completed.
*
* @param dy The amount of vertical scroll.
*/
fun onScrolled(dy: Int)
}
companion object {
private val TAG = ListNodesFragment::class.java.name
private const val GROUP_KEY = "GROUP_KEY"
private const val IS_SEARCH = "IS_SEARCH"
fun newInstance(group: GroupVersioned?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
val bundle = Bundle()
if (group != null) {
bundle.putParcelable(GROUP_KEY, group)
}
bundle.putBoolean(IS_SEARCH, isASearch)
ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly)
val listNodesFragment = ListNodesFragment()
listNodesFragment.arguments = bundle
return listNodesFragment
}
}
}

View File

@@ -0,0 +1,677 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities
import android.Manifest
import android.app.Activity
import android.app.assist.AssistStructure
import android.app.backup.BackupManager
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.hardware.fingerprint.FingerprintManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.support.annotation.RequiresApi
import android.support.v7.app.AlertDialog
import android.support.v7.widget.Toolbar
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.widget.*
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.*
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.fingerprint.FingerPrintHelper
import com.kunzisoft.keepass.fingerprint.FingerPrintViewsManager
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.FingerPrintInfoView
import permissions.dispatcher.*
import java.io.File
import java.io.FileNotFoundException
import java.lang.ref.WeakReference
@RuntimePermissions
class PasswordActivity : StylishActivity(),
UriIntentInitTaskCallback {
// Views
private var toolbar: Toolbar? = null
private var filenameView: TextView? = null
private var passwordView: EditText? = null
private var keyFileView: EditText? = null
private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null
private var checkboxDefaultDatabaseView: CompoundButton? = null
private var fingerPrintInfoView: FingerPrintInfoView? = null
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private var mDatabaseFileUri: Uri? = null
private var prefs: SharedPreferences? = null
private var mRememberKeyFile: Boolean = false
private var mKeyFileHelper: KeyFileHelper? = null
private var readOnly: Boolean = false
private var fingerPrintViewsManager: FingerPrintViewsManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = PreferenceManager.getDefaultSharedPreferences(this)
mRememberKeyFile = prefs!!.getBoolean(getString(R.string.keyfile_key),
resources.getBoolean(R.bool.keyfile_default))
setContentView(R.layout.activity_password)
toolbar = findViewById(R.id.toolbar)
toolbar?.title = getString(R.string.app_name)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
confirmButtonView = findViewById(R.id.pass_ok)
filenameView = findViewById(R.id.filename)
passwordView = findViewById(R.id.password)
keyFileView = findViewById(R.id.pass_keyfile)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
fingerPrintInfoView = findViewById(R.id.fingerprint_info)
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
val browseView = findViewById<View>(R.id.browse_button)
mKeyFileHelper = KeyFileHelper(this@PasswordActivity)
browseView.setOnClickListener(mKeyFileHelper!!.openFileOnClickViewListener)
passwordView?.setOnEditorActionListener(onEditorActionListener)
passwordView?.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() && 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 { _, isChecked ->
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
confirmButtonView?.isEnabled = isChecked
}
}
}
private val onEditorActionListener = object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (actionId == IME_ACTION_DONE) {
verifyCheckboxesAndLoadDatabase()
return true
}
return false
}
}
override fun onResume() {
// If the database isn't accessible make sure to clear the password field, if it
// was saved in the instance state
if (Database.getInstance().loaded) {
setEmptyViews()
}
// For check shutdown
super.onResume()
// Enable or not the open button
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
checkboxPasswordView?.let {
confirmButtonView?.isEnabled = it.isChecked
}
} else {
confirmButtonView?.isEnabled = true
}
UriIntentInitTask(WeakReference(this), this, mRememberKeyFile)
.execute(intent)
}
override fun onSaveInstanceState(outState: Bundle) {
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
super.onSaveInstanceState(outState)
}
override fun onPostInitTask(databaseFileUri: Uri?, keyFileUri: Uri?, errorStringId: Int?) {
mDatabaseFileUri = databaseFileUri
if (errorStringId != null) {
Toast.makeText(this@PasswordActivity, errorStringId, Toast.LENGTH_LONG).show()
finish()
return
}
// Verify permission to read file
if (databaseFileUri != null && !databaseFileUri.scheme!!.contains("content"))
doNothingWithPermissionCheck()
// Define title
val dbUriString = databaseFileUri?.toString() ?: ""
if (dbUriString.isNotEmpty()) {
if (PreferencesUtil.isFullFilePathEnable(this))
filenameView?.text = dbUriString
else
filenameView?.text = File(databaseFileUri!!.path!!).name // TODO Encapsulate
}
// Define Key File text
val keyUriString = keyFileUri?.toString() ?: ""
if (keyUriString.isNotEmpty() && mRememberKeyFile) { // Bug KeepassDX #18
populateKeyFileTextView(keyUriString)
}
// Define listeners for default database checkbox and validate button
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
var newDefaultFileName = ""
if (isChecked) {
newDefaultFileName = databaseFileUri?.toString() ?: newDefaultFileName
}
prefs?.edit()?.apply() {
putString(KEY_DEFAULT_FILENAME, newDefaultFileName)
apply()
}
val backupManager = BackupManager(this@PasswordActivity)
backupManager.dataChanged()
}
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
// Retrieve settings for default database
val defaultFilename = prefs?.getString(KEY_DEFAULT_FILENAME, "")
if (databaseFileUri != null
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
&& databaseFileUri == UriUtil.parseUriFile(defaultFilename)) {
checkboxDefaultDatabaseView?.isChecked = true
}
// If Activity is launch with a password and want to open directly
val intent = intent
val password = intent.getStringExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) {
populatePasswordTextView(password)
}
if (launchImmediately) {
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
} else {
// Init FingerPrint elements
var fingerPrintInit = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isFingerprintEnable(this)) {
if (fingerPrintViewsManager == null) {
fingerPrintViewsManager = FingerPrintViewsManager(this,
databaseFileUri,
fingerPrintInfoView,
checkboxPasswordView,
enableButtonOnCheckedChangeListener,
passwordView) { passwordRetrieve ->
// Load the database if password is registered or retrieve
passwordRetrieve?.let {
// Retrieve from fingerprint
verifyKeyFileCheckboxAndLoadDatabase(it)
} ?: run {
// Register with fingerprint
verifyCheckboxesAndLoadDatabase()
}
}
}
fingerPrintViewsManager?.initFingerprint()
// checks if fingerprint is available, will also start listening for fingerprints when available
fingerPrintViewsManager?.checkFingerprintAvailability()
fingerPrintInit = true
} else {
fingerPrintViewsManager?.destroy()
}
}
if (!fingerPrintInit) {
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
}
}
}
private fun setEmptyViews() {
populatePasswordTextView(null)
// Bug KeepassDX #18
if (!mRememberKeyFile) {
populateKeyFileTextView(null)
}
}
private fun populatePasswordTextView(text: String?) {
if (text == null || text.isEmpty()) {
passwordView?.setText("")
if (checkboxPasswordView?.isChecked == true)
checkboxPasswordView?.isChecked = false
} else {
passwordView?.setText(text)
if (checkboxPasswordView?.isChecked != true)
checkboxPasswordView?.isChecked = true
}
}
private fun populateKeyFileTextView(text: String?) {
if (text == null || text.isEmpty()) {
keyFileView?.setText("")
if (checkboxKeyFileView?.isChecked == true)
checkboxKeyFileView?.isChecked = false
} else {
keyFileView?.setText(text)
if (checkboxKeyFileView?.isChecked != true)
checkboxKeyFileView?.isChecked = true
}
}
override fun onPause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerPrintViewsManager?.stopListening()
}
super.onPause()
}
override fun onDestroy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerPrintViewsManager?.destroy()
}
super.onDestroy()
}
private fun verifyCheckboxesAndLoadDatabase(password: String? = passwordView?.text?.toString(),
keyFile: Uri? = UriUtil.parseUriFile(keyFileView?.text?.toString())) {
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
loadDatabase(keyPassword, keyFileUri)
}
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String? = passwordView?.text?.toString(),
keyFile: Uri? = UriUtil.parseUriFile(keyFileView?.text?.toString())) {
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
loadDatabase(password, keyFileUri)
}
private fun removePassword() {
passwordView?.setText("")
checkboxPasswordView?.isChecked = false
}
private fun loadDatabase(password: String?, keyFile: Uri?) {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
removePassword()
}
// Clear before we load
val database = Database.getInstance()
database.closeAndClear(applicationContext.filesDir)
mDatabaseFileUri?.let { databaseUri ->
// Show the progress dialog and load the database
ProgressDialogThread(this,
{ progressTaskUpdater ->
LoadDatabaseRunnable(
WeakReference(this@PasswordActivity),
database,
databaseUri,
password,
keyFile,
progressTaskUpdater,
AfterLoadingDatabase(database, password))
},
R.string.loading_database).start()
}
}
/**
* Called after verify and try to opening the database
*/
private inner class AfterLoadingDatabase internal constructor(var database: Database,
val password: String?)
: ActionRunnable() {
override fun onFinishRun(result: Result) {
runOnUiThread {
// Recheck fingerprint if error
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isFingerprintEnable(this@PasswordActivity)) {
// Stay with the same mode
fingerPrintViewsManager?.reInitWithFingerprintMode()
}
}
if (result.isSuccess) {
// Remove the password in view in all cases
removePassword()
if (database.validatePasswordEncoding(password)) {
launchGroupActivity()
} else {
PasswordEncodingDialogFragment().apply {
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
launchGroupActivity()
}
show(supportFragmentManager, "passwordEncodingTag")
}
}
} else {
if (result.message != null && result.message!!.isNotEmpty()) {
Toast.makeText(this@PasswordActivity, result.message, Toast.LENGTH_LONG).show()
}
}
}
}
}
private fun launchGroupActivity() {
EntrySelectionHelper.doEntrySelectionAction(intent,
{
GroupActivity.launch(this@PasswordActivity, readOnly)
},
{
GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly)
// Do not keep history
finish()
},
{ assistStructure ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly)
}
})
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
// Read menu
inflater.inflate(R.menu.open_file, menu)
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
MenuUtil.defaultMenuInflater(inflater, menu)
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Fingerprint menu
fingerPrintViewsManager?.inflateOptionsMenu(inflater, menu)
}
super.onCreateOptionsMenu(menu)
// Show education views
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
return true
}
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
menu: Menu) {
if (toolbar != null
&& passwordActivityEducation.checkAndPerformedFingerprintUnlockEducation(
toolbar!!,
{
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu)
}))
else if (toolbar != null
&& toolbar!!.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
toolbar!!.findViewById(R.id.menu_open_file_read_mode_key),
{
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu)
}))
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& PreferencesUtil.isFingerprintEnable(applicationContext)
&& FingerPrintHelper.isFingerprintSupported(getSystemService(FingerprintManager::class.java))
&& fingerPrintInfoView != null && fingerPrintInfoView?.fingerPrintImageView != null
&& passwordActivityEducation.checkAndPerformedFingerprintEducation(fingerPrintInfoView?.fingerPrintImageView!!))
;
}
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
if (readOnly) {
togglePassword.setTitle(R.string.menu_file_selection_read_only)
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
} else {
togglePassword.setTitle(R.string.menu_open_file_read_and_write)
togglePassword.setIcon(R.drawable.ic_read_write_white_24dp)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> finish()
R.id.menu_open_file_read_mode_key -> {
readOnly = !readOnly
changeOpenFileReadIcon(item)
}
R.id.menu_fingerprint_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerPrintViewsManager?.deleteEntryKey()
}
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
}
return super.onOptionsItemSelected(item)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// NOTE: delegate the permission handling to generated method
onRequestPermissionsResult(requestCode, grantResults)
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// To get entry in result
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
var keyFileResult = false
mKeyFileHelper?.let {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
populateKeyFileTextView(uri.toString())
}
}
}
if (!keyFileResult) {
// this block if not a key file response
when (resultCode) {
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
setEmptyViews()
Database.getInstance().closeAndClear(applicationContext.filesDir)
}
}
}
}
@NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
fun doNothing() {
}
@OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showRationaleForExternalStorage(request: PermissionRequest) {
AlertDialog.Builder(this)
.setMessage(R.string.permission_external_storage_rationale_read_database)
.setPositiveButton(R.string.allow) { _, _ -> request.proceed() }
.setNegativeButton(R.string.cancel) { _, _ -> request.cancel() }
.show()
}
@OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showDeniedForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show()
finish()
}
@OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)
internal fun showNeverAskForExternalStorage() {
Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show()
finish()
}
companion object {
private val TAG = PasswordActivity::class.java.name
const val KEY_DEFAULT_FILENAME = "defaultFileName"
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private fun buildAndLaunchIntent(activity: Activity, fileName: String, keyFile: String,
intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, PasswordActivity::class.java)
intent.putExtra(UriIntentInitTask.KEY_FILENAME, fileName)
intent.putExtra(UriIntentInitTask.KEY_KEYFILE, keyFile)
intentBuildLauncher.invoke(intent)
}
@Throws(FileNotFoundException::class)
private fun verifyFileNameUriFromLaunch(fileName: String) {
if (fileName.isEmpty()) {
throw FileNotFoundException()
}
val uri = UriUtil.parseUriFile(fileName)
val scheme = uri?.scheme
if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) {
val dbFile = File(uri.path!!)
if (!dbFile.exists()) {
throw FileNotFoundException()
}
}
}
/*
* -------------------------
* Standard Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
fun launch(
activity: Activity,
fileName: String,
keyFile: String) {
verifyFileNameUriFromLaunch(fileName)
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
activity.startActivity(intent)
}
}
/*
* -------------------------
* Keyboard Launch
* -------------------------
*/
@Throws(FileNotFoundException::class)
fun launchForKeyboardResult(
activity: Activity,
fileName: String,
keyFile: String) {
verifyFileNameUriFromLaunch(fileName)
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
KeyboardHelper.startActivityForKeyboardSelection(activity, intent)
}
}
/*
* -------------------------
* Autofill Launch
* -------------------------
*/
@RequiresApi(api = Build.VERSION_CODES.O)
@Throws(FileNotFoundException::class)
fun launchForAutofillResult(
activity: Activity,
fileName: String,
keyFile: String,
assistStructure: AssistStructure?) {
verifyFileNameUriFromLaunch(fileName)
if (assistStructure != null) {
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
assistStructure)
}
} else {
launch(activity, fileName, keyFile)
}
}
}
}

View File

@@ -1,61 +0,0 @@
package com.kunzisoft.keepass.activities;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import com.kunzisoft.keepass.settings.PreferencesUtil;
public class ReadOnlyHelper {
public static final String READ_ONLY_KEY = "READ_ONLY_KEY";
public static final boolean READ_ONLY_DEFAULT = false;
public static boolean retrieveReadOnlyFromInstanceStateOrPreference(Context context, Bundle savedInstanceState) {
boolean readOnly;
if (savedInstanceState != null
&& savedInstanceState.containsKey(READ_ONLY_KEY)) {
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY);
} else {
readOnly = PreferencesUtil.enableReadOnlyDatabase(context);
}
return readOnly;
}
public static boolean retrieveReadOnlyFromInstanceStateOrArguments(Bundle savedInstanceState, Bundle arguments) {
boolean readOnly = READ_ONLY_DEFAULT;
if (savedInstanceState != null
&& savedInstanceState.containsKey(READ_ONLY_KEY)) {
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY);
} else if (arguments != null
&& arguments.containsKey(READ_ONLY_KEY)) {
readOnly = arguments.getBoolean(READ_ONLY_KEY);
}
return readOnly;
}
public static boolean retrieveReadOnlyFromInstanceStateOrIntent(Bundle savedInstanceState, Intent intent) {
boolean readOnly = READ_ONLY_DEFAULT;
if (savedInstanceState != null
&& savedInstanceState.containsKey(READ_ONLY_KEY)) {
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY);
} else {
if (intent != null)
readOnly = intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT);
}
return readOnly;
}
public static void putReadOnlyInIntent(Intent intent, boolean readOnly) {
intent.putExtra(READ_ONLY_KEY, readOnly);
}
public static void putReadOnlyInBundle(Bundle bundle, boolean readOnly) {
bundle.putBoolean(READ_ONLY_KEY, readOnly);
}
public static void onSaveInstanceState(Bundle outState, boolean readOnly) {
outState.putBoolean(READ_ONLY_KEY, readOnly);
}
}

View File

@@ -0,0 +1,236 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.CompoundButton
import android.widget.TextView
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
import com.kunzisoft.keepass.utils.UriUtil
class AssignMasterKeyDialogFragment : DialogFragment() {
private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null
private var rootView: View? = null
private var passwordCheckBox: CompoundButton? = null
private var passView: TextView? = null
private var passConfView: TextView? = null
private var keyFileCheckBox: CompoundButton? = null
private var keyFileView: TextView? = null
private var mListener: AssignPasswordDialogListener? = null
private var mKeyFileHelper: KeyFileHelper? = null
interface AssignPasswordDialogListener {
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?)
fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?)
}
override fun onAttach(activity: Context?) {
super.onAttach(activity)
try {
mListener = activity as AssignPasswordDialogListener?
} catch (e: ClassCastException) {
throw ClassCastException(activity?.toString()
+ " must implement " + AssignPasswordDialogListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
rootView = inflater.inflate(R.layout.fragment_set_password, null)
builder.setView(rootView)
.setTitle(R.string.assign_master_key)
// Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(R.string.cancel) { _, _ -> }
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
passView = rootView?.findViewById(R.id.pass_password)
passView?.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) {
passwordCheckBox?.isChecked = true
}
})
passConfView = rootView?.findViewById(R.id.pass_conf_password)
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
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) {
keyFileCheckBox?.isChecked = true
}
})
mKeyFileHelper = KeyFileHelper(this)
rootView?.findViewById<View>(R.id.browse_button)?.setOnClickListener { view ->
mKeyFileHelper?.openFileOnClickViewListener?.onClick(view) }
val dialog = builder.create()
if (passwordCheckBox != null && keyFileCheckBox!= null) {
dialog.setOnShowListener { dialog1 ->
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
positiveButton.setOnClickListener {
mMasterPassword = ""
mKeyFile = null
var error = verifyPassword() || verifyFile()
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
error = true
showNoKeyConfirmationDialog()
}
if (!error) {
mListener?.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
dismiss()
}
}
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
negativeButton.setOnClickListener {
mListener?.onAssignKeyDialogNegativeClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
dismiss()
}
}
}
return dialog
}
return super.onCreateDialog(savedInstanceState)
}
private fun verifyPassword(): Boolean {
var error = false
if (passwordCheckBox != null
&& passwordCheckBox!!.isChecked
&& passView != null
&& passConfView != null) {
mMasterPassword = passView!!.text.toString()
val confPassword = passConfView!!.text.toString()
// Verify that passwords match
if (mMasterPassword != confPassword) {
error = true
// Passwords do not match
Toast.makeText(context, R.string.error_pass_match, Toast.LENGTH_LONG).show()
}
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
error = true
showEmptyPasswordConfirmationDialog()
}
}
return error
}
private fun verifyFile(): Boolean {
var error = false
if (keyFileCheckBox != null
&& keyFileCheckBox!!.isChecked) {
val keyFile = UriUtil.parseUriFile(keyFileView?.text?.toString())
mKeyFile = keyFile
// Verify that a keyfile is set
if (keyFile == null || keyFile.toString().isEmpty()) {
error = true
Toast.makeText(context, R.string.error_nokeyfile, Toast.LENGTH_LONG).show()
}
}
return error
}
private fun showEmptyPasswordConfirmationDialog() {
activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyFile()) {
mListener?.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
this@AssignMasterKeyDialogFragment.dismiss()
}
}
.setNegativeButton(R.string.cancel) { _, _ -> }
builder.create().show()
}
}
private fun showNoKeyConfirmationDialog() {
activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_no_encryption_key)
.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
this@AssignMasterKeyDialogFragment.dismiss()
}
.setNegativeButton(R.string.cancel) { _, _ -> }
builder.create().show()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
UriUtil.parseUriFile(uri)?.let { pathUri ->
keyFileCheckBox?.isChecked = true
keyFileView?.text = pathUri.toString()
}
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.widget.Button
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.Util
class BrowserDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
// Get the layout inflater
val root = activity.layoutInflater.inflate(R.layout.fragment_browser_install, null)
builder.setView(root)
.setNegativeButton(R.string.cancel) { _, _ -> }
val market = root.findViewById<Button>(R.id.install_market)
market.setOnClickListener {
Util.gotoUrl(context!!, R.string.filemanager_play_store)
dismiss()
}
val web = root.findViewById<Button>(R.id.install_web)
web.setOnClickListener {
Util.gotoUrl(context!!, R.string.filemanager_f_droid)
dismiss()
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
}

View File

@@ -0,0 +1,213 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.FilePickerStylishActivity
import com.kunzisoft.keepass.utils.UriUtil
import com.nononsenseapps.filepicker.FilePickerActivity
import com.nononsenseapps.filepicker.Utils
class CreateFileDialogFragment : DialogFragment(), AdapterView.OnItemSelectedListener {
private val FILE_CODE = 3853
private var folderPathView: EditText? = null
private var fileNameView: EditText? = null
private var positiveButton: Button? = null
private var negativeButton: Button? = null
private var mDefinePathDialogListener: DefinePathDialogListener? = null
private var mDatabaseFileExtension: String? = null
private var mUriPath: Uri? = null
interface DefinePathDialogListener {
fun onDefinePathDialogPositiveClick(pathFile: Uri?): Boolean
fun onDefinePathDialogNegativeClick(pathFile: Uri?): Boolean
}
override fun onAttach(activity: Context?) {
super.onAttach(activity)
try {
mDefinePathDialogListener = activity as DefinePathDialogListener?
} catch (e: ClassCastException) {
throw ClassCastException(activity?.toString()
+ " must implement " + DefinePathDialogListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
val rootView = inflater.inflate(R.layout.fragment_file_creation, null)
builder.setView(rootView)
.setTitle(R.string.create_keepass_file)
// Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(R.string.cancel) { _, _ -> }
// To prevent crash issue #69 https://github.com/Kunzisoft/KeePassDX/issues/69
val actionCopyBarCallback = object : ActionMode.Callback {
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
positiveButton?.isEnabled = false
negativeButton?.isEnabled = false
return true
}
override fun onDestroyActionMode(mode: ActionMode) {
positiveButton?.isEnabled = true
negativeButton?.isEnabled = true
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
return true
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return true
}
}
// Folder selection
val browseView = rootView.findViewById<View>(R.id.browse_button)
folderPathView = rootView.findViewById(R.id.folder_path)
folderPathView?.customSelectionActionModeCallback = actionCopyBarCallback
fileNameView = rootView.findViewById(R.id.filename)
fileNameView?.customSelectionActionModeCallback = actionCopyBarCallback
val defaultPath = Environment.getExternalStorageDirectory().path + getString(R.string.database_file_path_default)
folderPathView?.setText(defaultPath)
browseView.setOnClickListener { _ ->
Intent(context, FilePickerStylishActivity::class.java).apply {
putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
putExtra(FilePickerActivity.EXTRA_START_PATH,
Environment.getExternalStorageDirectory().path)
startActivityForResult(this, FILE_CODE)
}
}
// Init path
mUriPath = null
// Extension
mDatabaseFileExtension = getString(R.string.database_file_extension_default)
val spinner = rootView.findViewById<Spinner>(R.id.file_types)
spinner.onItemSelectedListener = this
// Spinner Drop down elements
val fileTypes = resources.getStringArray(R.array.file_types)
val dataAdapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, fileTypes)
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = dataAdapter
// Or text if only one item https://github.com/Kunzisoft/KeePassDX/issues/105
if (fileTypes.size == 1) {
val params = spinner.layoutParams
spinner.visibility = View.GONE
val extensionTextView = TextView(context)
extensionTextView.text = mDatabaseFileExtension
extensionTextView.layoutParams = params
val parentView = spinner.parent as ViewGroup
parentView.addView(extensionTextView)
}
val dialog = builder.create()
dialog.setOnShowListener { _ ->
positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
positiveButton?.setOnClickListener { _ ->
mDefinePathDialogListener?.let {
if (it.onDefinePathDialogPositiveClick(buildPath()))
dismiss()
}
}
negativeButton?.setOnClickListener { _->
mDefinePathDialogListener?.let {
if (it.onDefinePathDialogNegativeClick(buildPath())) {
dismiss()
}
}
}
}
return dialog
}
return super.onCreateDialog(savedInstanceState)
}
private fun buildPath(): Uri? {
if (folderPathView != null && fileNameView != null && mDatabaseFileExtension != null) {
var path = Uri.Builder().path(folderPathView!!.text.toString())
.appendPath(fileNameView!!.text.toString() + mDatabaseFileExtension!!)
.build()
context?.let { context ->
path = UriUtil.translateUri(context, path)
}
return path
}
return null
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == FILE_CODE && resultCode == Activity.RESULT_OK) {
mUriPath = data?.data
mUriPath?.let {
val file = Utils.getFileForUri(it)
folderPathView?.setText(file.path)
}
}
}
override fun onItemSelected(adapterView: AdapterView<*>, view: View, position: Int, id: Long) {
mDatabaseFileExtension = adapterView.getItemAtPosition(position).toString()
}
override fun onNothingSelected(adapterView: AdapterView<*>) {
// Do nothing
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.view.View
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
import java.text.DateFormat
class FileInformationDialogFragment : DialogFragment() {
private var fileSizeContainerView: View? = null
private var fileModificationContainerView: View? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
val root = inflater.inflate(R.layout.fragment_file_selection_information, null)
val fileNameView = root.findViewById<TextView>(R.id.file_filename)
val filePathView = root.findViewById<TextView>(R.id.file_path)
fileSizeContainerView = root.findViewById(R.id.file_size_container)
val fileSizeView = root.findViewById<TextView>(R.id.file_size)
fileModificationContainerView = root.findViewById(R.id.file_modification_container)
val fileModificationView = root.findViewById<TextView>(R.id.file_modification)
arguments?.apply {
if (containsKey(FILE_SELECT_BEEN_ARG)) {
(getSerializable(FILE_SELECT_BEEN_ARG) as FileDatabaseModel?)?.let { fileDatabaseModel ->
fileDatabaseModel.fileUri?.let { fileUri ->
filePathView.text = Uri.decode(fileUri.toString())
}
fileNameView.text = fileDatabaseModel.fileName
if (fileDatabaseModel.notFound()) {
hideFileInfo()
} else {
showFileInfo()
fileSizeView.text = fileDatabaseModel.size.toString()
fileModificationView.text = DateFormat.getDateTimeInstance()
.format(fileDatabaseModel.lastModification)
}
} ?: hideFileInfo()
}
}
builder.setView(root)
builder.setPositiveButton(android.R.string.ok) { _, _ -> }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun showFileInfo() {
fileSizeContainerView?.visibility = View.VISIBLE
fileModificationContainerView?.visibility = View.VISIBLE
}
private fun hideFileInfo() {
fileSizeContainerView?.visibility = View.GONE
fileModificationContainerView?.visibility = View.GONE
}
companion object {
private const val FILE_SELECT_BEEN_ARG = "FILE_SELECT_BEEN_ARG"
fun newInstance(fileDatabaseModel: FileDatabaseModel): FileInformationDialogFragment {
val fileInformationDialogFragment = FileInformationDialogFragment()
val args = Bundle()
args.putSerializable(FILE_SELECT_BEEN_ARG, fileDatabaseModel)
fileInformationDialogFragment.arguments = args
return fileInformationDialogFragment
}
}
}

View File

@@ -0,0 +1,194 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.view.View
import android.widget.*
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.applyFontVisibility
class GeneratePasswordDialogFragment : DialogFragment() {
private var mListener: GeneratePasswordListener? = null
private var root: View? = null
private var lengthTextView: EditText? = null
private var passwordView: EditText? = null
private var uppercaseBox: CompoundButton? = null
private var lowercaseBox: CompoundButton? = null
private var digitsBox: CompoundButton? = null
private var minusBox: CompoundButton? = null
private var underlineBox: CompoundButton? = null
private var spaceBox: CompoundButton? = null
private var specialsBox: CompoundButton? = null
private var bracketsBox: CompoundButton? = null
private var extendedBox: CompoundButton? = null
override fun onAttach(context: Context?) {
super.onAttach(context)
try {
mListener = context as GeneratePasswordListener?
} catch (e: ClassCastException) {
throw ClassCastException(context?.toString()
+ " must implement " + GeneratePasswordListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
root = inflater.inflate(R.layout.fragment_generate_password, null)
passwordView = root?.findViewById(R.id.password)
passwordView?.applyFontVisibility()
lengthTextView = root?.findViewById(R.id.length)
uppercaseBox = root?.findViewById(R.id.cb_uppercase)
lowercaseBox = root?.findViewById(R.id.cb_lowercase)
digitsBox = root?.findViewById(R.id.cb_digits)
minusBox = root?.findViewById(R.id.cb_minus)
underlineBox = root?.findViewById(R.id.cb_underline)
spaceBox = root?.findViewById(R.id.cb_space)
specialsBox = root?.findViewById(R.id.cb_specials)
bracketsBox = root?.findViewById(R.id.cb_brackets)
extendedBox = root?.findViewById(R.id.cb_extended)
assignDefaultCharacters()
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
seekBar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
lengthTextView?.setText(progress.toString())
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
context?.let { context ->
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
}
root?.findViewById<Button>(R.id.generate_password_button)
?.setOnClickListener { fillPassword() }
builder.setView(root)
.setPositiveButton(R.string.accept) { _, _ ->
val bundle = Bundle()
bundle.putString(KEY_PASSWORD_ID, passwordView!!.text.toString())
mListener?.acceptPassword(bundle)
dismiss()
}
.setNegativeButton(R.string.cancel) { _, _ ->
val bundle = Bundle()
mListener?.cancelPassword(bundle)
dismiss()
}
// Pre-populate a password to possibly save the user a few clicks
fillPassword()
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun assignDefaultCharacters() {
uppercaseBox?.isChecked = false
lowercaseBox?.isChecked = false
digitsBox?.isChecked = false
minusBox?.isChecked = false
underlineBox?.isChecked = false
spaceBox?.isChecked = false
specialsBox?.isChecked = false
bracketsBox?.isChecked = false
extendedBox?.isChecked = false
context?.let { context ->
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
for (passwordChar in charSet) {
when (passwordChar) {
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
getString(R.string.value_password_digits) -> digitsBox?.isChecked = true
getString(R.string.value_password_minus) -> minusBox?.isChecked = true
getString(R.string.value_password_underline) -> underlineBox?.isChecked = true
getString(R.string.value_password_space) -> spaceBox?.isChecked = true
getString(R.string.value_password_special) -> specialsBox?.isChecked = true
getString(R.string.value_password_brackets) -> bracketsBox?.isChecked = true
getString(R.string.value_password_extended) -> extendedBox?.isChecked = true
}
}
}
}
}
private fun fillPassword() {
root?.findViewById<EditText>(R.id.password)?.setText(generatePassword())
}
fun generatePassword(): String {
var password = ""
try {
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
val generator = PasswordGenerator(resources)
password = generator.generatePassword(length,
uppercaseBox?.isChecked == true,
lowercaseBox?.isChecked == true,
digitsBox?.isChecked == true,
minusBox?.isChecked == true,
underlineBox?.isChecked == true,
spaceBox?.isChecked == true,
specialsBox?.isChecked == true,
bracketsBox?.isChecked == true,
extendedBox?.isChecked == true)
} catch (e: NumberFormatException) {
Toast.makeText(context, R.string.error_wrong_length, Toast.LENGTH_LONG).show()
} catch (e: IllegalArgumentException) {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}
return password
}
interface GeneratePasswordListener {
fun acceptPassword(bundle: Bundle)
fun cancelPassword(bundle: Bundle)
}
companion object {
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
}
}

View File

@@ -0,0 +1,219 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.support.design.widget.TextInputLayout
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.GroupVersioned
import com.kunzisoft.keepass.database.element.PwIcon
import com.kunzisoft.keepass.icons.assignDatabaseIcon
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
private var mDatabase: Database? = null
private var editGroupListener: EditGroupListener? = null
private var editGroupDialogAction: EditGroupDialogAction? = null
private var nameGroup: String? = null
private var iconGroup: PwIcon? = null
private var nameTextLayoutView: TextInputLayout? = null
private var nameTextView: TextView? = null
private var iconButtonView: ImageView? = null
private var iconColor: Int = 0
enum class EditGroupDialogAction {
CREATION, UPDATE, NONE;
companion object {
fun getActionFromOrdinal(ordinal: Int): EditGroupDialogAction {
return values()[ordinal]
}
}
}
override fun onAttach(context: Context?) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
// Instantiate the NoticeDialogListener so we can send events to the host
editGroupListener = context as EditGroupListener?
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context?.toString()
+ " must implement " + GroupEditDialogFragment::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null)
nameTextLayoutView = root?.findViewById(R.id.group_edit_name_container)
nameTextView = root?.findViewById(R.id.group_edit_name)
iconButtonView = root?.findViewById(R.id.group_edit_icon_button)
// Retrieve the textColor to tint the icon
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
iconColor = ta.getColor(0, Color.WHITE)
ta.recycle()
// Init elements
mDatabase = Database.getInstance()
editGroupDialogAction = EditGroupDialogAction.NONE
nameGroup = ""
iconGroup = mDatabase?.iconFactory?.folderIcon
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_ACTION_ID)
&& savedInstanceState.containsKey(KEY_NAME)
&& savedInstanceState.containsKey(KEY_ICON)) {
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
nameGroup = savedInstanceState.getString(KEY_NAME)
iconGroup = savedInstanceState.getParcelable(KEY_ICON)
} else {
arguments?.apply {
if (containsKey(KEY_ACTION_ID))
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
if (containsKey(KEY_NAME) && containsKey(KEY_ICON)) {
nameGroup = getString(KEY_NAME)
iconGroup = getParcelable(KEY_ICON)
}
}
}
// populate the name
nameTextView?.text = nameGroup
// populate the icon
assignIconView()
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(R.string.cancel) { _, _ ->
editGroupListener?.cancelEditGroup(
editGroupDialogAction,
nameTextView?.text?.toString(),
iconGroup)
}
iconButtonView?.setOnClickListener { _ ->
fragmentManager?.let {
IconPickerDialogFragment().show(it, "IconPickerDialogFragment")
}
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
override fun onResume() {
super.onResume()
// To prevent auto dismiss
val d = dialog as AlertDialog?
if (d != null) {
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
positiveButton.setOnClickListener {
if (isValid()) {
editGroupListener?.approveEditGroup(
editGroupDialogAction,
nameTextView?.text?.toString(),
iconGroup)
d.dismiss()
}
}
}
}
private fun assignIconView() {
if (mDatabase?.drawFactory != null && iconGroup != null) {
iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor)
}
}
override fun iconPicked(bundle: Bundle) {
iconGroup = IconPickerDialogFragment.getIconStandardFromBundle(bundle)
assignIconView()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(KEY_ACTION_ID, editGroupDialogAction!!.ordinal)
outState.putString(KEY_NAME, nameGroup)
outState.putParcelable(KEY_ICON, iconGroup)
super.onSaveInstanceState(outState)
}
private fun isValid(): Boolean {
if (nameTextView?.text?.toString()?.isNotEmpty() != true) {
nameTextLayoutView?.error = getString(R.string.error_no_name)
return false
}
return true
}
interface EditGroupListener {
fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: PwIcon?)
fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: PwIcon?)
}
companion object {
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
const val KEY_NAME = "KEY_NAME"
const val KEY_ICON = "KEY_ICON"
const val KEY_ACTION_ID = "KEY_ACTION_ID"
fun build(): GroupEditDialogFragment {
val bundle = Bundle()
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
val fragment = GroupEditDialogFragment()
fragment.arguments = bundle
return fragment
}
fun build(group: GroupVersioned): GroupEditDialogFragment {
val bundle = Bundle()
bundle.putString(KEY_NAME, group.title)
bundle.putParcelable(KEY_ICON, group.icon)
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
val fragment = GroupEditDialogFragment()
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v4.widget.ImageViewCompat
import android.support.v7.app.AlertDialog
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.GridView
import android.widget.ImageView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.element.PwIconStandard
import com.kunzisoft.keepass.icons.IconPack
import com.kunzisoft.keepass.icons.IconPackChooser
class IconPickerDialogFragment : DialogFragment() {
private var iconPickerListener: IconPickerListener? = null
private var iconPack: IconPack? = null
override fun onAttach(context: Context?) {
super.onAttach(context)
try {
iconPickerListener = context as IconPickerListener?
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context!!.toString()
+ " must implement " + IconPickerListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
iconPack = IconPackChooser.getSelectedIconPack(context!!)
// Inflate and set the layout for the dialog
// Pass null as the parent view because its going in the dialog layout
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_picker, null)
builder.setView(root)
val currIconGridView = root.findViewById<GridView>(R.id.IconGridView)
currIconGridView.adapter = ImageAdapter(activity)
currIconGridView.setOnItemClickListener { _, _, position, _ ->
val bundle = Bundle()
bundle.putParcelable(KEY_ICON_STANDARD, PwIconStandard(position))
iconPickerListener?.iconPicked(bundle)
dismiss()
}
builder.setNegativeButton(R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog.cancel() }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
inner class ImageAdapter internal constructor(private val context: Context) : BaseAdapter() {
override fun getCount(): Int {
return iconPack?.numberOfIcons() ?: 0
}
override fun getItem(position: Int): Any? {
return null
}
override fun getItemId(position: Int): Long {
return 0
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val currentView: View = convertView
?: (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
.inflate(R.layout.item_icon, parent, false)
iconPack?.let { iconPack ->
val iconImageView = currentView.findViewById<ImageView>(R.id.icon_image)
iconImageView.setImageResource(iconPack.iconToResId(position))
// Assign color if icons are tintable
if (iconPack.tintable()) {
// Retrieve the textColor to tint the icon
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
ta?.recycle()
}
}
return currentView
}
}
interface IconPickerListener {
fun iconPicked(bundle: Bundle)
}
companion object {
private const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD"
fun getIconStandardFromBundle(bundle: Bundle): PwIconStandard? {
return bundle.getParcelable(KEY_ICON_STANDARD)
}
fun launch(activity: StylishActivity) {
// Create an instance of the dialog fragment and show it
val dialog = IconPickerDialogFragment()
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.os.Bundle
import android.provider.Settings
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.view.View
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.Util
class KeyboardExplanationDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let {
val builder = AlertDialog.Builder(activity!!)
val inflater = activity!!.layoutInflater
val rootView = inflater.inflate(R.layout.fragment_keyboard_explanation, null)
rootView.findViewById<View>(R.id.keyboards_activate_setting_path1_text)
.setOnClickListener { launchActivateKeyboardSetting() }
rootView.findViewById<View>(R.id.keyboards_activate_setting_path2_text)
.setOnClickListener { launchActivateKeyboardSetting() }
val containerKeyboardSwitcher = rootView.findViewById<View>(R.id.container_keyboard_switcher)
if (BuildConfig.CLOSED_STORE) {
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
} else {
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context!!, R.string.keyboard_switcher_f_droid) }
}
builder.setView(rootView)
.setPositiveButton(android.R.string.ok) { _, _ -> }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun launchActivateKeyboardSetting() {
val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.AlertDialog
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.support.v4.app.DialogFragment
import com.kunzisoft.keepass.R
class PasswordEncodingDialogFragment : DialogFragment() {
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
builder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.cancel() }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.text.Html
import android.text.SpannableStringBuilder
import android.widget.Toast
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.Util
/**
* Custom Dialog that asks the user to download the pro version or make a donation.
*/
class ProFeatureDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
val stringBuilder = SpannableStringBuilder()
if (BuildConfig.CLOSED_STORE) {
// TODO HtmlCompat with androidX
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_ad_free))).append("\n\n")
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_buy_pro)))
builder.setPositiveButton(R.string.download) { _, _ ->
try {
Util.gotoUrl(context!!, R.string.app_pro_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
}
} else {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_feature_generosity))).append("\n\n")
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_donation)))
builder.setPositiveButton(R.string.contribute) { _, _ ->
try {
Util.gotoUrl(context!!, R.string.contribution_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
}
}
builder.setMessage(stringBuilder)
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.AlertDialog
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import com.kunzisoft.keepass.R
class ReadOnlyDialog(context: Context) : AlertDialog(context) {
override fun onCreate(savedInstanceState: Bundle) {
val ctx = context
var warning = ctx.getString(R.string.read_only_warning)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
warning = warning + "\n\n" + context.getString(R.string.read_only_kitkat_warning)
}
setMessage(warning)
setButton(BUTTON_POSITIVE, ctx.getText(android.R.string.ok)) { _, _ -> dismiss() }
setButton(BUTTON_NEGATIVE, ctx.getText(R.string.beta_dontask)) { _, _ ->
val prefs = PreferenceManager.getDefaultSharedPreferences(ctx)
val edit = prefs.edit()
edit.putBoolean(ctx.getString(R.string.show_read_only_warning), false)
edit.apply()
dismiss()
}
super.onCreate(savedInstanceState)
}
}

View File

@@ -0,0 +1,189 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.support.annotation.IdRes
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.view.View
import android.widget.CompoundButton
import android.widget.RadioGroup
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.SortNodeEnum
class SortDialogFragment : DialogFragment() {
private var mListener: SortSelectionListener? = null
private var mSortNodeEnum: SortNodeEnum? = null
@IdRes
private var mCheckedId: Int = 0
private var mGroupsBefore: Boolean = false
private var mAscending: Boolean = false
private var mRecycleBinBottom: Boolean = false
override fun onAttach(context: Context?) {
super.onAttach(context)
try {
mListener = context as SortSelectionListener?
} catch (e: ClassCastException) {
throw ClassCastException(context!!.toString()
+ " must implement " + SortSelectionListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
mSortNodeEnum = SortNodeEnum.TITLE
mAscending = true
mGroupsBefore = true
var recycleBinAllowed = false
mRecycleBinBottom = true
arguments?.apply {
if (containsKey(SORT_NODE_ENUM_BUNDLE_KEY))
getString(SORT_NODE_ENUM_BUNDLE_KEY)?.let {
mSortNodeEnum = SortNodeEnum.valueOf(it)
}
if (containsKey(SORT_ASCENDING_BUNDLE_KEY))
mAscending = getBoolean(SORT_ASCENDING_BUNDLE_KEY)
if (containsKey(SORT_GROUPS_BEFORE_BUNDLE_KEY))
mGroupsBefore = getBoolean(SORT_GROUPS_BEFORE_BUNDLE_KEY)
if (containsKey(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY)) {
recycleBinAllowed = true
mRecycleBinBottom = getBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY)
}
}
mCheckedId = retrieveViewFromEnum(mSortNodeEnum!!)
val rootView = activity.layoutInflater.inflate(R.layout.fragment_sort_selection, null)
builder.setTitle(R.string.sort_menu)
builder.setView(rootView)
// Add action buttons
.setPositiveButton(android.R.string.ok
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum!!, mAscending, mGroupsBefore, mRecycleBinBottom) }
.setNegativeButton(R.string.cancel) { _, _ -> }
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
// Check if is ascending or descending
ascendingView.isChecked = mAscending
ascendingView.setOnCheckedChangeListener { _, isChecked -> mAscending = isChecked }
val groupsBeforeView = rootView.findViewById<CompoundButton>(R.id.sort_selection_groups_before)
// Check if groups before
groupsBeforeView.isChecked = mGroupsBefore
groupsBeforeView.setOnCheckedChangeListener { _, isChecked -> mGroupsBefore = isChecked }
val recycleBinBottomView = rootView.findViewById<CompoundButton>(R.id.sort_selection_recycle_bin_bottom)
if (!recycleBinAllowed) {
recycleBinBottomView.visibility = View.GONE
} else {
// Check if recycle bin at the bottom
recycleBinBottomView.isChecked = mRecycleBinBottom
recycleBinBottomView.setOnCheckedChangeListener { _, isChecked -> mRecycleBinBottom = isChecked }
}
val sortSelectionRadioGroupView = rootView.findViewById<RadioGroup>(R.id.sort_selection_radio_group)
// Check value by default
sortSelectionRadioGroupView.check(mCheckedId)
sortSelectionRadioGroupView.setOnCheckedChangeListener { _, checkedId -> mSortNodeEnum = retrieveSortEnumFromViewId(checkedId) }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
@IdRes
private fun retrieveViewFromEnum(sortNodeEnum: SortNodeEnum): Int {
return when (sortNodeEnum) {
SortNodeEnum.DB -> R.id.sort_selection_db
SortNodeEnum.TITLE -> R.id.sort_selection_title
SortNodeEnum.USERNAME -> R.id.sort_selection_username
SortNodeEnum.CREATION_TIME -> R.id.sort_selection_creation_time
SortNodeEnum.LAST_MODIFY_TIME -> R.id.sort_selection_last_modify_time
SortNodeEnum.LAST_ACCESS_TIME -> R.id.sort_selection_last_access_time
}
}
private fun retrieveSortEnumFromViewId(@IdRes checkedId: Int): SortNodeEnum {
// Change enum
return when (checkedId) {
R.id.sort_selection_db -> SortNodeEnum.DB
R.id.sort_selection_title -> SortNodeEnum.TITLE
R.id.sort_selection_username -> SortNodeEnum.USERNAME
R.id.sort_selection_creation_time -> SortNodeEnum.CREATION_TIME
R.id.sort_selection_last_modify_time -> SortNodeEnum.LAST_MODIFY_TIME
R.id.sort_selection_last_access_time -> SortNodeEnum.LAST_ACCESS_TIME
else -> SortNodeEnum.TITLE
}
}
interface SortSelectionListener {
fun onSortSelected(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean,
recycleBinBottom: Boolean)
}
companion object {
private const val SORT_NODE_ENUM_BUNDLE_KEY = "SORT_NODE_ENUM_BUNDLE_KEY"
private const val SORT_ASCENDING_BUNDLE_KEY = "SORT_ASCENDING_BUNDLE_KEY"
private const val SORT_GROUPS_BEFORE_BUNDLE_KEY = "SORT_GROUPS_BEFORE_BUNDLE_KEY"
private const val SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY = "SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY"
private fun buildBundle(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean): Bundle {
val bundle = Bundle()
bundle.putString(SORT_NODE_ENUM_BUNDLE_KEY, sortNodeEnum.name)
bundle.putBoolean(SORT_ASCENDING_BUNDLE_KEY, ascending)
bundle.putBoolean(SORT_GROUPS_BEFORE_BUNDLE_KEY, groupsBefore)
return bundle
}
fun getInstance(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean): SortDialogFragment {
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
val fragment = SortDialogFragment()
fragment.arguments = bundle
return fragment
}
fun getInstance(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean,
recycleBinBottom: Boolean): SortDialogFragment {
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
val fragment = SortDialogFragment()
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Build
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.widget.TextView
import com.kunzisoft.keepass.R
class UnavailableFeatureDialogFragment : DialogFragment() {
private var minVersionRequired = Build.VERSION_CODES.M
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
arguments?.apply {
if (containsKey(MIN_REQUIRED_VERSION_ARG))
minVersionRequired = getInt(MIN_REQUIRED_VERSION_ARG)
}
val rootView = activity.layoutInflater.inflate(R.layout.fragment_unavailable_feature, null)
val messageView = rootView.findViewById<TextView>(R.id.unavailable_feature_message)
val builder = AlertDialog.Builder(activity)
val message = SpannableStringBuilder()
message.append(getString(R.string.unavailable_feature_text))
.append("\n\n")
if (Build.VERSION.SDK_INT < minVersionRequired) {
message.append(getString(R.string.unavailable_feature_version,
androidNameFromApiNumber(Build.VERSION.SDK_INT, Build.VERSION.RELEASE),
androidNameFromApiNumber(minVersionRequired)))
message.append("\n\n")
.append(Html.fromHtml("<a href=\"https://source.android.com/setup/build-numbers\">CodeNames</a>"))
} else
message.append(getString(R.string.unavailable_feature_hardware))
messageView.text = message
messageView.movementMethod = LinkMovementMethod.getInstance()
builder.setView(rootView)
.setPositiveButton(android.R.string.ok) { _, _ -> }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun androidNameFromApiNumber(apiNumber: Int, releaseVersion: String = ""): String {
var version = releaseVersion
val builder = StringBuilder()
val fields = Build.VERSION_CODES::class.java.fields
var apiName = ""
for (field in fields) {
val fieldName = field.name
var fieldValue = -1
try {
fieldValue = field.getInt(Any())
} catch (e: IllegalArgumentException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
} catch (e: NullPointerException) {
e.printStackTrace()
}
if (fieldValue == apiNumber) {
apiName = fieldName
}
}
if (apiName.isEmpty()) {
val mapper = arrayOf("ANDROID BASE", "ANDROID BASE 1.1", "CUPCAKE", "DONUT", "ECLAIR", "ECLAIR_0_1", "ECLAIR_MR1", "FROYO", "GINGERBREAD", "GINGERBREAD_MR1", "HONEYCOMB", "HONEYCOMB_MR1", "HONEYCOMB_MR2", "ICE_CREAM_SANDWICH", "ICE_CREAM_SANDWICH_MR1", "JELLY_BEAN", "JELLY_BEAN", "JELLY_BEAN", "KITKAT", "KITKAT", "LOLLIPOOP", "LOLLIPOOP_MR1", "MARSHMALLOW", "NOUGAT", "NOUGAT", "OREO", "OREO")
val index = apiNumber - 1
apiName = if (index < mapper.size) mapper[index] else "UNKNOWN_VERSION"
}
if (version.isEmpty()) {
val versions = arrayOf("1.0", "1.1", "1.5", "1.6", "2.0", "2.0.1", "2.1", "2.2.X", "2.3", "2.3.3", "3.0", "3.1", "3.2.0", "4.0.1", "4.0.3", "4.1.0", "4.2.0", "4.3.0", "4.4", "4.4", "5.0", "5.1", "6.0", "7.0", "7.1", "8.0.0", "8.1.0")
val index = apiNumber - 1
version = if (index < versions.size) versions[index] else "UNKNOWN_VERSION"
}
builder.append("\n\t")
if (apiName.isNotEmpty())
builder.append(apiName).append(" ")
if (version.isNotEmpty())
builder.append(version).append(" ")
builder.append("(API ").append(apiNumber).append(")")
builder.append("\n")
return builder.toString()
}
companion object {
private const val MIN_REQUIRED_VERSION_ARG = "MIN_REQUIRED_VERSION_ARG"
fun getInstance(minVersionRequired: Int): UnavailableFeatureDialogFragment {
val fragment = UnavailableFeatureDialogFragment()
val args = Bundle()
args.putInt(MIN_REQUIRED_VERSION_ARG, minVersionRequired)
fragment.arguments = args
return fragment
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.text.Html
import android.text.SpannableStringBuilder
import android.widget.Toast
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.Util
/**
* Custom Dialog that asks the user to download the pro version or make a donation.
*/
class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
val stringBuilder = SpannableStringBuilder()
if (BuildConfig.CLOSED_STORE) {
if (BuildConfig.FULL_VERSION) {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature_thanks))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_rose))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_work_hard))).append("\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_upgrade))).append(" ")
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
} else {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_buy_pro))).append("\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
builder.setPositiveButton(R.string.download) { _, _ ->
try {
Util.gotoUrl(context!!, R.string.app_pro_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
}
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
}
} else {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_contibute))).append(" ")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
builder.setPositiveButton(R.string.contribute) { _, _ ->
try {
Util.gotoUrl(context!!, R.string.contribution_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
}
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
}
builder.setMessage(stringBuilder)
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
}

View File

@@ -17,7 +17,7 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>. * along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.compat; package com.kunzisoft.keepass.activities.helpers;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;

View File

@@ -0,0 +1,45 @@
package com.kunzisoft.keepass.activities.helpers
import android.app.assist.AssistStructure
import android.content.Intent
import android.os.Build
import com.kunzisoft.keepass.autofill.AutofillHelper
object EntrySelectionHelper {
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
private const val DEFAULT_ENTRY_SELECTION_MODE = false
fun addEntrySelectionModeExtraInIntent(intent: Intent) {
intent.putExtra(EXTRA_ENTRY_SELECTION_MODE, true)
}
fun removeEntrySelectionModeFromIntent(intent: Intent) {
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
}
fun retrieveEntrySelectionModeFromIntent(intent: Intent): Boolean {
return intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)
}
fun doEntrySelectionAction(intent: Intent,
standardAction: () -> Unit,
keyboardAction: () -> Unit,
autofillAction: (assistStructure: AssistStructure) -> Unit) {
var assistStructureInit = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure ->
autofillAction.invoke(assistStructure)
assistStructureInit = true
}
}
if (!assistStructureInit) {
if (intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)) {
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
keyboardAction.invoke()
} else {
standardAction.invoke()
}
}
}
}

View File

@@ -0,0 +1,232 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.helpers
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentActivity
import android.util.Log
import android.view.View
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
import com.kunzisoft.keepass.fileselect.StorageAF
import com.kunzisoft.keepass.utils.UriUtil
class KeyFileHelper {
private var activity: Activity? = null
private var fragment: Fragment? = null
val openFileOnClickViewListener: OpenFileOnClickViewListener
get() = OpenFileOnClickViewListener(null)
constructor(context: Activity) {
this.activity = context
this.fragment = null
}
constructor(context: Fragment) {
this.activity = context.activity
this.fragment = context
}
inner class OpenFileOnClickViewListener(private val dataUri: (() -> Uri)?) : View.OnClickListener {
override fun onClick(v: View) {
try {
if (activity != null && StorageAF.useStorageFramework(activity!!)) {
openActivityWithActionOpenDocument()
} else {
openActivityWithActionGetContent()
}
} catch (e: Exception) {
Log.e(TAG, "Enable to start the file picker activity", e)
// Open File picker if can't open activity
if (lookForOpenIntentsFilePicker(dataUri?.invoke()))
showBrowserDialog()
}
}
}
private fun openActivityWithActionOpenDocument() {
val i = Intent(StorageAF.ACTION_OPEN_DOCUMENT)
i.addCategory(Intent.CATEGORY_OPENABLE)
i.type = "*/*"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
} else {
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
}
if (fragment != null)
fragment?.startActivityForResult(i, OPEN_DOC)
else
activity?.startActivityForResult(i, OPEN_DOC)
}
private fun openActivityWithActionGetContent() {
val i = Intent(Intent.ACTION_GET_CONTENT)
i.addCategory(Intent.CATEGORY_OPENABLE)
i.type = "*/*"
if (fragment != null)
fragment?.startActivityForResult(i, GET_CONTENT)
else
activity?.startActivityForResult(i, GET_CONTENT)
}
fun getOpenFileOnClickViewListener(dataUri: () -> Uri): OpenFileOnClickViewListener {
return OpenFileOnClickViewListener(dataUri)
}
private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean {
var showBrowser = false
try {
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
// Get file path parent if possible
if (dataUri != null
&& dataUri.toString().isNotEmpty()
&& dataUri.scheme == "file") {
intent.data = dataUri
} else {
Log.w(javaClass.name, "Unable to read the URI")
}
if (fragment != null)
fragment?.startActivityForResult(intent, FILE_BROWSE)
else
activity?.startActivityForResult(intent, FILE_BROWSE)
} else {
showBrowser = true
}
} catch (e: Exception) {
Log.w(TAG, "Enable to start OPEN_INTENTS_FILE_BROWSE", e)
showBrowser = true
}
return showBrowser
}
/**
* Indicates whether the specified action can be used as an intent. This
* method queries the package manager for installed packages that can
* respond to an intent with the specified action. If no suitable package is
* found, this method returns false.
*
* @param context The application's environment.
* @param action The Intent action to check for availability.
*
* @return True if an Intent with the specified action can be sent and
* responded to, false otherwise.
*/
private fun isIntentAvailable(context: Context, action: String): Boolean {
val packageManager = context.packageManager
val intent = Intent(action)
val list = packageManager.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY)
return list.size > 0
}
/**
* Show Browser dialog to select file picker app
*/
private fun showBrowserDialog() {
try {
val browserDialogFragment = BrowserDialogFragment()
if (fragment != null && fragment!!.fragmentManager != null)
browserDialogFragment.show(fragment!!.fragmentManager!!, "browserDialog")
else if (activity!!.fragmentManager != null)
browserDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
} catch (e: Exception) {
Log.e(TAG, "Can't open BrowserDialog", e)
}
}
/**
* To use in onActivityResultCallback in Fragment or Activity
* @param keyFileCallback Callback retrieve from data
* @return true if requestCode was captured, false elsechere
*/
fun onActivityResultCallback(
requestCode: Int,
resultCode: Int,
data: Intent?,
keyFileCallback: ((uri: Uri?) -> Unit)?): Boolean {
when (requestCode) {
FILE_BROWSE -> {
if (resultCode == RESULT_OK) {
val filename = data?.dataString
var keyUri: Uri? = null
if (filename != null) {
keyUri = UriUtil.parseUriFile(filename)
}
keyFileCallback?.invoke(keyUri)
}
return true
}
GET_CONTENT, OPEN_DOC -> {
if (resultCode == RESULT_OK) {
if (data != null) {
var uri = data.data
if (uri != null) {
if (StorageAF.useStorageFramework(activity!!)) {
try {
// try to persist read and write permissions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
activity?.contentResolver?.apply {
takePersistableUriPermission(uri!!, Intent.FLAG_GRANT_READ_URI_PERMISSION)
takePersistableUriPermission(uri!!, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
}
} catch (e: Exception) {
// nop
}
}
if (requestCode == GET_CONTENT) {
uri = UriUtil.translateUri(activity!!, uri)
}
keyFileCallback?.invoke(uri)
}
}
}
return true
}
}
return false
}
companion object {
private const val TAG = "KeyFileHelper"
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
private const val GET_CONTENT = 25745
private const val OPEN_DOC = 25845
private const val FILE_BROWSE = 25645
}
}

View File

@@ -0,0 +1,59 @@
package com.kunzisoft.keepass.activities.helpers
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.kunzisoft.keepass.settings.PreferencesUtil
object ReadOnlyHelper {
private const val READ_ONLY_KEY = "READ_ONLY_KEY"
const val READ_ONLY_DEFAULT = false
fun retrieveReadOnlyFromIntent(intent: Intent): Boolean {
return intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
}
fun retrieveReadOnlyFromInstanceStateOrPreference(context: Context, savedInstanceState: Bundle?): Boolean {
return if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
savedInstanceState.getBoolean(READ_ONLY_KEY)
} else {
PreferencesUtil.enableReadOnlyDatabase(context)
}
}
fun retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState: Bundle?, arguments: Bundle?): Boolean {
var readOnly = READ_ONLY_DEFAULT
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
} else if (arguments != null && arguments.containsKey(READ_ONLY_KEY)) {
readOnly = arguments.getBoolean(READ_ONLY_KEY)
}
return readOnly
}
fun retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState: Bundle?, intent: Intent?): Boolean {
var readOnly = READ_ONLY_DEFAULT
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
} else {
if (intent != null)
readOnly = intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
}
return readOnly
}
fun putReadOnlyInIntent(intent: Intent, readOnly: Boolean) {
intent.putExtra(READ_ONLY_KEY, readOnly)
}
fun putReadOnlyInBundle(bundle: Bundle, readOnly: Boolean) {
bundle.putBoolean(READ_ONLY_KEY, readOnly)
}
fun onSaveInstanceState(outState: Bundle, readOnly: Boolean) {
outState.putBoolean(READ_ONLY_KEY, readOnly)
}
}

View File

@@ -0,0 +1,88 @@
package com.kunzisoft.keepass.activities.helpers
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.AsyncTask
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory
import com.kunzisoft.keepass.utils.UriUtil
import java.io.File
import java.lang.ref.WeakReference
class UriIntentInitTask(private val weakContext: WeakReference<Context>,
private val uriIntentInitTaskCallback: UriIntentInitTaskCallback,
private val isKeyFileNeeded: Boolean)
: AsyncTask<Intent, Void, Int>() {
private var databaseUri: Uri? = null
private var keyFileUri: Uri? = null
override fun doInBackground(vararg args: Intent): Int? {
val intent = args[0]
val action = intent.action
// If is a view intent
if (action != null && action == VIEW_INTENT) {
val incoming = intent.data
databaseUri = incoming
keyFileUri = ClipDataCompat.getUriFromIntent(intent, KEY_KEYFILE)
if (incoming == null) {
return R.string.error_can_not_handle_uri
} else if (incoming.scheme == "file") {
val fileName = incoming.path
if (fileName?.isNotEmpty() == true) {
// No file name
return R.string.file_not_found
}
val dbFile = File(fileName)
if (!dbFile.exists()) {
// File does not exist
return R.string.file_not_found
}
if (keyFileUri == null) {
keyFileUri = getKeyFileUri(databaseUri)
}
} else if (incoming.scheme == "content") {
if (keyFileUri == null) {
keyFileUri = getKeyFileUri(databaseUri)
}
} else {
return R.string.error_can_not_handle_uri
}
} else {
databaseUri = UriUtil.parseUriFile(intent.getStringExtra(KEY_FILENAME))
keyFileUri = UriUtil.parseUriFile(intent.getStringExtra(KEY_KEYFILE))
if (keyFileUri == null || keyFileUri!!.toString().isEmpty()) {
keyFileUri = getKeyFileUri(databaseUri)
}
}
return null
}
public override fun onPostExecute(result: Int?) {
uriIntentInitTaskCallback.onPostInitTask(databaseUri, keyFileUri, result)
}
private fun getKeyFileUri(databaseUri: Uri?): Uri? {
return if (isKeyFileNeeded) {
FileDatabaseHistory.getInstance(weakContext).getKeyFileUriByDatabaseUri(databaseUri!!)
} else {
null
}
}
companion object {
const val KEY_FILENAME = "fileName"
const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW"
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2018 Jeremy Jamet / Kunzisoft. * Copyright 2019 Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
@@ -17,10 +17,10 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>. * along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.password; package com.kunzisoft.keepass.activities.helpers
import android.net.Uri; import android.net.Uri
interface UriIntentInitTaskCallback { interface UriIntentInitTaskCallback {
void onPostInitTask(Uri dbUri, Uri keyFileUri, Integer errorStringId); fun onPostInitTask(databaseFileUri: Uri?, keyFileUri: Uri?, errorStringId: Int?)
} }

View File

@@ -0,0 +1,212 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.lock
import android.app.Activity
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewGroup
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LOCK_ACTION
abstract class LockingActivity : StylishActivity() {
companion object {
private const val TAG = "LockingActivity"
const val RESULT_EXIT_LOCK = 1450
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
}
protected var timeoutEnable: Boolean = true
private var lockReceiver: LockReceiver? = null
private var exitLock: Boolean = false
// Force readOnly if Entry Selection mode
protected var readOnly: Boolean = false
get() {
return field || selectionMode
}
protected var selectionMode: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)) {
timeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
} else {
if (intent != null)
timeoutEnable = intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
}
if (timeoutEnable) {
lockReceiver = LockReceiver()
val intentFilter = IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(LOCK_ACTION)
}
registerReceiver(lockReceiver, intentFilter)
}
exitLock = false
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_EXIT_LOCK) {
exitLock = true
if (Database.getInstance().loaded) {
lockAndExit()
}
}
}
override fun onResume() {
super.onResume()
// To refresh when back to normal workflow from selection workflow
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
if (timeoutEnable) {
// End activity if database not loaded
if (!Database.getInstance().loaded) {
finish()
return
}
// After the first creation
// or If simply swipe with another application
// If the time is out -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
// If onCreate already record time
if (!exitLock)
TimeoutHelper.recordTime(this)
}
invalidateOptionsMenu()
}
override fun onSaveInstanceState(outState: Bundle) {
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
outState.putBoolean(TIMEOUT_ENABLE_KEY, timeoutEnable)
super.onSaveInstanceState(outState)
}
override fun onPause() {
super.onPause()
if (timeoutEnable) {
// If the time is out during our navigation in activity -> close the Activity
TimeoutHelper.checkTimeAndLockIfTimeout(this)
}
}
override fun onDestroy() {
super.onDestroy()
if (lockReceiver != null)
unregisterReceiver(lockReceiver)
}
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() {
lock()
}
/**
* To reset the app timeout when a view is focused or changed
*/
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
views.forEach {
it?.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
Log.d(TAG, "View focused, reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
}
}
if (it is ViewGroup) {
for (i in 0..it.childCount) {
resetAppTimeoutWhenViewFocusedOrChanged(it.getChildAt(i))
}
}
}
}
override fun onBackPressed() {
if (timeoutEnable) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
super.onBackPressed()
}
} else {
super.onBackPressed()
}
}
}
fun Activity.lock() {
// Stop the Magikeyboard service
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
MagikIME.removeEntry(this)
Log.i(Activity::class.java.name, "Shutdown " + localClassName +
" after inactivity or manual lock")
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
cancelAll()
}
// Clear data
Database.getInstance().closeAndClear(applicationContext.filesDir)
// Add onActivityForResult response
setResult(LockingActivity.RESULT_EXIT_LOCK)
finish()
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.lock
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.WindowManager
/**
* Locking Hide Activity that sets FLAG_SECURE to prevent screenshots, and from
* appearing in the recent app preview
*/
abstract class LockingHideActivity : LockingActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Several gingerbread devices have problems with FLAG_SECURE
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
}
/* (non-Javadoc) Workaround for HTC Linkify issues
* @see android.app.Activity#startActivity(android.content.Intent)
*/
override fun startActivity(intent: Intent) {
try {
if (intent.component != null && intent.component!!.shortClassName == ".HtcLinkifyDispatcherActivity") {
intent.component = null
}
super.startActivity(intent)
} catch (e: ActivityNotFoundException) {
/* Catch the bad HTC implementation case */
super.startActivity(Intent.createChooser(intent, null))
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.stylish
import android.os.Bundle
import android.support.annotation.StyleRes
import android.util.Log
import com.nononsenseapps.filepicker.FilePickerActivity
/**
* FilePickerActivity class with a style compatibility
*/
class FilePickerStylishActivity : FilePickerActivity() {
@StyleRes
private var themeId: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
this.themeId = Stylish.getFilePickerThemeId(this)
setTheme(themeId)
super.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
if (Stylish.getFilePickerThemeId(this) != this.themeId) {
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
this.recreate()
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.stylish
import android.content.Context
import android.support.annotation.StyleRes
import android.support.v7.preference.PreferenceManager
import android.util.Log
import com.kunzisoft.keepass.R
/**
* Class that provides functions to retrieve and assign a theme to a module
*/
object Stylish {
private var themeString: String? = null
/**
* Initialize the class with a theme preference
* @param context Context to retrieve the theme preference
*/
fun init(context: Context) {
val stylishPrefKey = context.getString(R.string.setting_style_key)
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
themeString = PreferenceManager.getDefaultSharedPreferences(context).getString(stylishPrefKey, context.getString(R.string.list_style_name_light))
}
/**
* Assign the style to the class attribute
* @param styleString Style id String
*/
fun assignStyle(styleString: String) {
themeString = styleString
}
/**
* Function that returns the current id of the style selected in the preference
* @param context Context to retrieve the id
* @return Id of the style
*/
@StyleRes
fun getThemeId(context: Context): Int {
return when (themeString) {
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
else -> R.style.KeepassDXStyle_Light
}
}
@StyleRes
fun getFilePickerThemeId(context: Context): Int {
return when {
themeString.equals(context.getString(R.string.list_style_name_dark)) -> R.style.KeepassDXStyle_FilePickerStyle_Dark
themeString.equals(context.getString(R.string.list_style_name_blue)) -> R.style.KeepassDXStyle_FilePickerStyle_Blue
themeString.equals(context.getString(R.string.list_style_name_red)) -> R.style.KeepassDXStyle_FilePickerStyle_Red
themeString.equals(context.getString(R.string.list_style_name_purple)) -> R.style.KeepassDXStyle_FilePickerStyle_Purple
else -> R.style.KeepassDXStyle_FilePickerStyle
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.stylish
import android.os.Bundle
import android.support.annotation.StyleRes
import android.support.v7.app.AppCompatActivity
import android.util.Log
abstract class StylishActivity : AppCompatActivity() {
@StyleRes
private var themeId: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.themeId = Stylish.getThemeId(this)
setTheme(themeId)
}
override fun onResume() {
super.onResume()
if (Stylish.getThemeId(this) != this.themeId) {
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
this.recreate()
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.stylish
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.support.annotation.StyleRes
import android.support.v4.app.Fragment
import android.support.v7.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
abstract class StylishFragment : Fragment() {
@StyleRes
protected var themeId: Int = 0
protected var contextThemed: Context? = null
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context != null) {
this.themeId = Stylish.getThemeId(context)
}
contextThemed = ContextThemeWrapper(context, themeId)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// To fix status bar color
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = activity!!.window
val attrColorPrimaryDark = intArrayOf(android.R.attr.colorPrimaryDark)
val taColorPrimaryDark = contextThemed?.theme?.obtainStyledAttributes(attrColorPrimaryDark)
val defaultColor = Color.BLACK
window.statusBarColor = taColorPrimaryDark?.getColor(0, defaultColor) ?: defaultColor
taColorPrimaryDark?.recycle()
}
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onDetach() {
contextThemed = null
super.onDetach()
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.net.Uri
import android.support.annotation.ColorInt
import android.support.v7.widget.RecyclerView
import android.util.TypedValue
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
import com.kunzisoft.keepass.settings.PreferencesUtil
class FileDatabaseHistoryAdapter(private val context: Context, private val listFiles: List<String>)
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var fileItemOpenListener: FileItemOpenListener? = null
private var fileSelectClearListener: FileSelectClearListener? = null
private var fileInformationShowListener: FileInformationShowListener? = null
@ColorInt
private val defaultColor: Int
@ColorInt
private val warningColor: Int
init {
val typedValue = TypedValue()
val theme = context.theme
theme.resolveAttribute(R.attr.colorAccent, typedValue, true)
warningColor = typedValue.data
theme.resolveAttribute(android.R.attr.textColorHintInverse, typedValue, true)
defaultColor = typedValue.data
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
val view = inflater.inflate(R.layout.item_file_row, parent, false)
return FileDatabaseHistoryViewHolder(view)
}
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
val fileDatabaseModel = FileDatabaseModel(context, listFiles[position])
// Context menu creation
holder.fileContainer.setOnCreateContextMenuListener(ContextMenuBuilder(fileDatabaseModel))
// Click item to open file
if (fileItemOpenListener != null)
holder.fileContainer.setOnClickListener(FileItemClickListener(position))
// Assign file name
if (PreferencesUtil.isFullFilePathEnable(context))
holder.fileName.text = Uri.decode(fileDatabaseModel.fileUri.toString())
else
holder.fileName.text = fileDatabaseModel.fileName
holder.fileName.textSize = PreferencesUtil.getListTextSize(context)
// Click on information
if (fileInformationShowListener != null)
holder.fileInformation.setOnClickListener(FileInformationClickListener(fileDatabaseModel))
}
override fun getItemCount(): Int {
return listFiles.size
}
fun setOnItemClickListener(fileItemOpenListener: FileItemOpenListener) {
this.fileItemOpenListener = fileItemOpenListener
}
fun setFileSelectClearListener(fileSelectClearListener: FileSelectClearListener) {
this.fileSelectClearListener = fileSelectClearListener
}
fun setFileInformationShowListener(fileInformationShowListener: FileInformationShowListener) {
this.fileInformationShowListener = fileInformationShowListener
}
interface FileItemOpenListener {
fun onFileItemOpenListener(itemPosition: Int)
}
interface FileSelectClearListener {
fun onFileSelectClearListener(fileDatabaseModel: FileDatabaseModel): Boolean
}
interface FileInformationShowListener {
fun onClickFileInformation(fileDatabaseModel: FileDatabaseModel)
}
private inner class FileItemClickListener(private val position: Int) : View.OnClickListener {
override fun onClick(v: View) {
fileItemOpenListener?.onFileItemOpenListener(position)
}
}
private inner class FileInformationClickListener(private val fileDatabaseModel: FileDatabaseModel) : View.OnClickListener {
override fun onClick(view: View) {
fileInformationShowListener?.onClickFileInformation(fileDatabaseModel)
}
}
private inner class ContextMenuBuilder(private val fileDatabaseModel: FileDatabaseModel) : View.OnCreateContextMenuListener {
private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item ->
if (fileSelectClearListener == null)
return@OnMenuItemClickListener false
when (item.itemId) {
MENU_CLEAR -> fileSelectClearListener!!.onFileSelectClearListener(fileDatabaseModel)
else -> false
}
}
override fun onCreateContextMenu(contextMenu: ContextMenu?, view: View?, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
contextMenu?.add(Menu.NONE, MENU_CLEAR, Menu.NONE, R.string.remove_from_filelist)
?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
}
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var fileContainer: View = itemView.findViewById(R.id.file_container)
var fileName: TextView = itemView.findViewById(R.id.file_filename)
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
}
companion object {
private const val MENU_CLEAR = 1
}
}

View File

@@ -1,354 +0,0 @@
/*
* Copyright 2018 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.adapters;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.support.annotation.NonNull;
import android.support.v7.util.SortedList;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.util.SortedListAdapterCallback;
import android.util.Log;
import android.util.TypedValue;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.app.App;
import com.kunzisoft.keepass.database.PwGroup;
import com.kunzisoft.keepass.database.PwNode;
import com.kunzisoft.keepass.database.SortNodeEnum;
import com.kunzisoft.keepass.icons.IconPackChooser;
import com.kunzisoft.keepass.settings.PreferencesUtil;
public class NodeAdapter extends RecyclerView.Adapter<BasicViewHolder> {
private static final String TAG = NodeAdapter.class.getName();
private SortedList<PwNode> nodeSortedList;
private Context context;
private LayoutInflater inflater;
private MenuInflater menuInflater;
private float textSize;
private float iconSize;
private SortNodeEnum listSort;
private boolean groupsBeforeSort;
private boolean ascendingSort;
private NodeClickCallback nodeClickCallback;
private NodeMenuListener nodeMenuListener;
private boolean activateContextMenu;
private boolean readOnly;
private int iconGroupColor;
private int iconEntryColor;
/**
* Create node list adapter with contextMenu or not
* @param context Context to use
*/
public NodeAdapter(final Context context, MenuInflater menuInflater, boolean readOnly) {
this.inflater = LayoutInflater.from(context);
this.menuInflater = menuInflater;
this.context = context;
assignPreferences();
this.activateContextMenu = false;
this.readOnly = readOnly;
this.nodeSortedList = new SortedList<>(PwNode.class, new SortedListAdapterCallback<PwNode>(this) {
@Override public int compare(PwNode item1, PwNode item2) {
return listSort.getNodeComparator(ascendingSort, groupsBeforeSort).compare(item1, item2);
}
@Override public boolean areContentsTheSame(PwNode oldItem, PwNode newItem) {
return oldItem.isContentVisuallyTheSame(newItem);
}
@Override public boolean areItemsTheSame(PwNode item1, PwNode item2) {
return item1.equals(item2);
}
});
// Retrieve the color to tint the icon
int[] attrTextColorPrimary = {android.R.attr.textColorPrimary};
TypedArray taTextColorPrimary = context.getTheme().obtainStyledAttributes(attrTextColorPrimary);
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK);
taTextColorPrimary.recycle();
int[] attrTextColor = {android.R.attr.textColor}; // In two times to fix bug compilation
TypedArray taTextColor = context.getTheme().obtainStyledAttributes(attrTextColor);
this.iconEntryColor = taTextColor.getColor(0, Color.BLACK);
taTextColor.recycle();
}
public void setActivateContextMenu(boolean activate) {
this.activateContextMenu = activate;
}
private void assignPreferences() {
this.textSize = PreferencesUtil.getListTextSize(context);
// Retrieve the icon size
int iconDefaultSize = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
context.getResources().getInteger(R.integer.list_icon_size_default),
context.getResources().getDisplayMetrics()
);
this.iconSize = iconDefaultSize * textSize / Float.parseFloat(context.getString(R.string.list_size_default));
this.listSort = PreferencesUtil.getListSort(context);
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context);
this.ascendingSort = PreferencesUtil.getAscendingSort(context);
}
/**
* Rebuild the list by clear and build children from the group
*/
public void rebuildList(PwGroup group) {
this.nodeSortedList.clear();
assignPreferences();
// TODO verify sort
try {
this.nodeSortedList.addAll(group.getDirectChildren());
} catch (Exception e) {
Log.e(TAG, "Can't add node elements to the list", e);
Toast.makeText(context, "Can't add node elements to the list : " + e.getMessage(), Toast.LENGTH_LONG).show();
}
}
/**
* Add a node to the list
* @param node Node to add
*/
public void addNode(PwNode node) {
nodeSortedList.add(node);
}
/**
* Remove a node in the list
* @param node Node to delete
*/
public void removeNode(PwNode node) {
nodeSortedList.remove(node);
}
/**
* Update a node in the list
* @param oldNode Node before the update
* @param newNode Node after the update
*/
public void updateNode(PwNode oldNode, PwNode newNode) {
nodeSortedList.beginBatchedUpdates();
nodeSortedList.remove(oldNode);
nodeSortedList.add(newNode);
nodeSortedList.endBatchedUpdates();
}
/**
* Notify a change sort of the list
*/
public void notifyChangeSort(SortNodeEnum sortNodeEnum, boolean ascending, boolean groupsBefore) {
this.listSort = sortNodeEnum;
this.ascendingSort = ascending;
this.groupsBeforeSort = groupsBefore;
}
@Override
public int getItemViewType(int position) {
return nodeSortedList.get(position).getType().ordinal();
}
@NonNull
@Override
public BasicViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
BasicViewHolder basicViewHolder;
View view;
if (viewType == PwNode.Type.GROUP.ordinal()) {
view = inflater.inflate(R.layout.list_nodes_group, parent, false);
basicViewHolder = new GroupViewHolder(view);
} else {
view = inflater.inflate(R.layout.list_nodes_entry, parent, false);
basicViewHolder = new EntryViewHolder(view);
}
return basicViewHolder;
}
@Override
public void onBindViewHolder(@NonNull BasicViewHolder holder, int position) {
PwNode subNode = nodeSortedList.get(position);
// Assign image
if (IconPackChooser.getSelectedIconPack(context).tintable()) {
int iconColor = Color.BLACK;
switch (subNode.getType()) {
case GROUP:
iconColor = iconGroupColor;
break;
case ENTRY:
iconColor = iconEntryColor;
break;
}
App.getDB().getDrawFactory().assignDatabaseIconTo(context, holder.icon, subNode.getIcon(), true, iconColor);
} else {
App.getDB().getDrawFactory().assignDatabaseIconTo(context, holder.icon, subNode.getIcon());
}
// Assign text
holder.text.setText(subNode.getDisplayTitle());
// Assign click
holder.container.setOnClickListener(
new OnNodeClickListener(subNode));
// Context menu
if (activateContextMenu) {
holder.container.setOnCreateContextMenuListener(
new ContextMenuBuilder(subNode, nodeMenuListener, readOnly));
}
// Assign image and text size
// Relative size of the icon
holder.icon.getLayoutParams().height = ((int) iconSize);
holder.icon.getLayoutParams().width = ((int) iconSize);
holder.text.setTextSize(textSize);
}
@Override
public int getItemCount() {
return nodeSortedList.size();
}
/**
* Assign a listener when a node is clicked
*/
public void setOnNodeClickListener(NodeClickCallback nodeClickCallback) {
this.nodeClickCallback = nodeClickCallback;
}
/**
* Assign a listener when an element of menu is clicked
*/
public void setNodeMenuListener(NodeMenuListener nodeMenuListener) {
this.nodeMenuListener = nodeMenuListener;
}
/**
* Callback listener to redefine to do an action when a node is click
*/
public interface NodeClickCallback {
void onNodeClick(PwNode node);
}
/**
* Menu listener to redefine to do an action in menu
*/
public interface NodeMenuListener {
boolean onOpenMenuClick(PwNode node);
boolean onEditMenuClick(PwNode node);
boolean onCopyMenuClick(PwNode node);
boolean onMoveMenuClick(PwNode node);
boolean onDeleteMenuClick(PwNode node);
}
/**
* Utility class for node listener
*/
private class OnNodeClickListener implements View.OnClickListener {
private PwNode node;
OnNodeClickListener(PwNode node) {
this.node = node;
}
@Override
public void onClick(View v) {
if (nodeClickCallback != null)
nodeClickCallback.onNodeClick(node);
}
}
/**
* Utility class for menu listener
*/
private class ContextMenuBuilder implements View.OnCreateContextMenuListener {
private PwNode node;
private NodeMenuListener menuListener;
private boolean readOnly;
ContextMenuBuilder(PwNode node, NodeMenuListener menuListener, boolean readOnly) {
this.menuListener = menuListener;
this.node = node;
this.readOnly = readOnly;
}
@Override
public void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo) {
menuInflater.inflate(R.menu.node_menu, contextMenu);
MenuItem menuItem = contextMenu.findItem(R.id.menu_open);
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
if (readOnly || node.equals(App.getDB().getPwDatabase().getRecycleBin())) {
contextMenu.removeItem(R.id.menu_edit);
contextMenu.removeItem(R.id.menu_copy);
contextMenu.removeItem(R.id.menu_move);
contextMenu.removeItem(R.id.menu_delete);
} else {
// Edition
menuItem = contextMenu.findItem(R.id.menu_edit);
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
// Copy (not for group)
if (node.getType().equals(PwNode.Type.ENTRY)) {
menuItem = contextMenu.findItem(R.id.menu_copy);
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
} else {
// TODO COPY For Group
contextMenu.removeItem(R.id.menu_copy);
}
// Move
menuItem = contextMenu.findItem(R.id.menu_move);
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
// Deletion
menuItem = contextMenu.findItem(R.id.menu_delete);
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
}
}
private MenuItem.OnMenuItemClickListener mOnMyActionClickListener = new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
if (menuListener == null)
return false;
switch ( item.getItemId() ) {
case R.id.menu_open:
return menuListener.onOpenMenuClick(node);
case R.id.menu_edit:
return menuListener.onEditMenuClick(node);
case R.id.menu_copy:
return menuListener.onCopyMenuClick(node);
case R.id.menu_move:
return menuListener.onMoveMenuClick(node);
case R.id.menu_delete:
return menuListener.onDeleteMenuClick(node);
default:
return false;
}
}
};
}
}

View File

@@ -0,0 +1,364 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.graphics.Color
import android.support.v7.util.SortedList
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.util.SortedListAdapterCallback
import android.util.Log
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.database.SortNodeEnum
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
class NodeAdapter
/**
* Create node list adapter with contextMenu or not
* @param context Context to use
*/
(private val context: Context, private val menuInflater: MenuInflater)
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
private val nodeSortedList: SortedList<NodeVersioned>
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var textSize: Float = 0.toFloat()
private var subtextSize: Float = 0.toFloat()
private var iconSize: Float = 0.toFloat()
private var listSort: SortNodeEnum? = null
private var groupsBeforeSort: Boolean = false
private var ascendingSort: Boolean = false
private var showUserNames: Boolean = false
private var nodeClickCallback: NodeClickCallback? = null
private var nodeMenuListener: NodeMenuListener? = null
private var activateContextMenu: Boolean = false
private var readOnly: Boolean = false
private var isASearchResult: Boolean = false
private val mDatabase: Database
private val iconGroupColor: Int
private val iconEntryColor: Int
/**
* Determine if the adapter contains or not any element
* @return true if the list is empty
*/
val isEmpty: Boolean
get() = nodeSortedList.size() <= 0
init {
assignPreferences()
this.activateContextMenu = false
this.readOnly = false
this.isASearchResult = false
this.nodeSortedList = SortedList(NodeVersioned::class.java, object : SortedListAdapterCallback<NodeVersioned>(this) {
override fun compare(item1: NodeVersioned, item2: NodeVersioned): Int {
return listSort?.getNodeComparator(ascendingSort, groupsBeforeSort)?.compare(item1, item2) ?: 0
}
override fun areContentsTheSame(oldItem: NodeVersioned, newItem: NodeVersioned): Boolean {
return oldItem.title == newItem.title && oldItem.icon == newItem.icon
}
override fun areItemsTheSame(item1: NodeVersioned, item2: NodeVersioned): Boolean {
return item1 == item2
}
})
// Database
this.mDatabase = Database.getInstance()
// Retrieve the color to tint the icon
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
taTextColorPrimary.recycle()
// In two times to fix bug compilation
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
this.iconEntryColor = taTextColor.getColor(0, Color.BLACK)
taTextColor.recycle()
}
fun setReadOnly(readOnly: Boolean) {
this.readOnly = readOnly
}
fun setIsASearchResult(isASearchResult: Boolean) {
this.isASearchResult = isASearchResult
}
fun setActivateContextMenu(activate: Boolean) {
this.activateContextMenu = activate
}
private fun assignPreferences() {
val textSizeDefault = java.lang.Float.parseFloat(context.getString(R.string.list_size_default))
this.textSize = PreferencesUtil.getListTextSize(context)
this.subtextSize = context.resources.getInteger(R.integer.list_small_size_default) * textSize / textSizeDefault
// Retrieve the icon size
val iconDefaultSize = context.resources.getDimension(R.dimen.list_icon_size_default)
this.iconSize = iconDefaultSize * textSize / textSizeDefault
this.listSort = PreferencesUtil.getListSort(context)
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
this.ascendingSort = PreferencesUtil.getAscendingSort(context)
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
}
/**
* Rebuild the list by clear and build children from the group
*/
fun rebuildList(group: GroupVersioned) {
this.nodeSortedList.clear()
assignPreferences()
// TODO verify sort
try {
this.nodeSortedList.addAll(group.getChildrenWithoutMetaStream())
} catch (e: Exception) {
Log.e(TAG, "Can't add node elements to the list", e)
Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show()
}
}
/**
* Add a node to the list
* @param node Node to add
*/
fun addNode(node: NodeVersioned) {
nodeSortedList.add(node)
}
/**
* Remove a node in the list
* @param node Node to delete
*/
fun removeNode(node: NodeVersioned) {
nodeSortedList.remove(node)
}
/**
* Update a node in the list
* @param oldNode Node before the update
* @param newNode Node after the update
*/
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) {
nodeSortedList.beginBatchedUpdates()
nodeSortedList.remove(oldNode)
nodeSortedList.add(newNode)
nodeSortedList.endBatchedUpdates()
}
/**
* Notify a change sort of the list
*/
fun notifyChangeSort(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean) {
this.listSort = sortNodeEnum
this.ascendingSort = ascending
this.groupsBeforeSort = groupsBefore
}
override fun getItemViewType(position: Int): Int {
return nodeSortedList.get(position).type.ordinal
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NodeViewHolder {
val view: View = if (viewType == Type.GROUP.ordinal) {
inflater.inflate(R.layout.item_list_nodes_group, parent, false)
} else {
inflater.inflate(R.layout.item_list_nodes_entry, parent, false)
}
return NodeViewHolder(view)
}
override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
val subNode = nodeSortedList.get(position)
// Assign image
val iconColor = when (subNode.type) {
Type.GROUP -> iconGroupColor
Type.ENTRY -> iconEntryColor
}
holder.icon.assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
// Assign text
holder.text.text = subNode.title
// Assign click
holder.container.setOnClickListener { nodeClickCallback?.onNodeClick(subNode) }
// Context menu
if (activateContextMenu) {
holder.container.setOnCreateContextMenuListener(
ContextMenuBuilder(menuInflater, subNode, readOnly, isASearchResult, nodeMenuListener))
}
// Add username
holder.subText.text = ""
holder.subText.visibility = View.GONE
if (subNode.type == Type.ENTRY) {
val entry = subNode as EntryVersioned
mDatabase.startManageEntry(entry)
holder.text.text = entry.getVisualTitle()
val username = entry.username
if (showUserNames && username.isNotEmpty()) {
holder.subText.visibility = View.VISIBLE
holder.subText.text = username
}
mDatabase.stopManageEntry(entry)
}
// Assign image and text size
// Relative size of the icon
holder.icon.layoutParams?.height = iconSize.toInt()
holder.icon.layoutParams?.width = iconSize.toInt()
holder.text.textSize = textSize
holder.subText.textSize = subtextSize
}
override fun getItemCount(): Int {
return nodeSortedList.size()
}
/**
* Assign a listener when a node is clicked
*/
fun setOnNodeClickListener(nodeClickCallback: NodeClickCallback?) {
this.nodeClickCallback = nodeClickCallback
}
/**
* Assign a listener when an element of menu is clicked
*/
fun setNodeMenuListener(nodeMenuListener: NodeMenuListener?) {
this.nodeMenuListener = nodeMenuListener
}
/**
* Callback listener to redefine to do an action when a node is click
*/
interface NodeClickCallback {
fun onNodeClick(node: NodeVersioned)
}
/**
* Menu listener to redefine to do an action in menu
*/
interface NodeMenuListener {
fun onOpenMenuClick(node: NodeVersioned): Boolean
fun onEditMenuClick(node: NodeVersioned): Boolean
fun onCopyMenuClick(node: NodeVersioned): Boolean
fun onMoveMenuClick(node: NodeVersioned): Boolean
fun onDeleteMenuClick(node: NodeVersioned): Boolean
}
/**
* Utility class for menu listener
*/
private class ContextMenuBuilder(val menuInflater: MenuInflater,
val node: NodeVersioned,
val readOnly: Boolean,
val isASearchResult: Boolean,
val menuListener: NodeMenuListener?)
: View.OnCreateContextMenuListener {
private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item ->
if (menuListener == null)
return@OnMenuItemClickListener false
when (item.itemId) {
R.id.menu_open -> menuListener.onOpenMenuClick(node)
R.id.menu_edit -> menuListener.onEditMenuClick(node)
R.id.menu_copy -> menuListener.onCopyMenuClick(node)
R.id.menu_move -> menuListener.onMoveMenuClick(node)
R.id.menu_delete -> menuListener.onDeleteMenuClick(node)
else -> false
}
}
override fun onCreateContextMenu(contextMenu: ContextMenu?,
view: View?,
contextMenuInfo: ContextMenu.ContextMenuInfo?) {
menuInflater.inflate(R.menu.node_menu, contextMenu)
// Opening
var menuItem = contextMenu?.findItem(R.id.menu_open)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
val database = Database.getInstance()
// Edition
if (readOnly || node == database.recycleBin) {
contextMenu?.removeItem(R.id.menu_edit)
} else {
menuItem = contextMenu?.findItem(R.id.menu_edit)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
// Copy (not for group)
if (readOnly
|| isASearchResult
|| node == database.recycleBin
|| node.type == Type.GROUP) {
// TODO COPY For Group
contextMenu?.removeItem(R.id.menu_copy)
} else {
menuItem = contextMenu?.findItem(R.id.menu_copy)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
// Move
if (readOnly
|| isASearchResult
|| node == database.recycleBin) {
contextMenu?.removeItem(R.id.menu_move)
} else {
menuItem = contextMenu?.findItem(R.id.menu_move)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
// Deletion
if (readOnly || node == database.recycleBin) {
contextMenu?.removeItem(R.id.menu_delete)
} else {
menuItem = contextMenu?.findItem(R.id.menu_delete)
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
}
}
}
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var container: View = itemView.findViewById(R.id.node_container)
var icon: ImageView = itemView.findViewById(R.id.node_icon)
var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView = itemView.findViewById(R.id.node_subtext)
}
companion object {
private val TAG = NodeAdapter::class.java.name
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.database.Cursor
import android.graphics.Color
import android.support.v4.widget.CursorAdapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.cursor.EntryCursor
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.EntryVersioned
import com.kunzisoft.keepass.database.element.PwIcon
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
class SearchEntryCursorAdapter(context: Context, private val database: Database)
: CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
private val cursorInflater: LayoutInflater = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private var displayUsername: Boolean = false
private val iconColor: Int
init {
// Get the icon color
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
this.iconColor = taTextColor.getColor(0, Color.WHITE)
taTextColor.recycle()
reInit(context)
}
fun reInit(context: Context) {
this.displayUsername = PreferencesUtil.showUsernamesListEntries(context)
}
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
val view = cursorInflater.inflate(R.layout.item_search_entry, parent, false)
val viewHolder = ViewHolder()
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext)
view.tag = viewHolder
return view
}
override fun bindView(view: View, context: Context, cursor: Cursor) {
// Retrieve elements from cursor
val uuid = UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS)))
val iconFactory = database.iconFactory
var icon: PwIcon = iconFactory.getIcon(
UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
if (icon.isUnknown) {
icon = iconFactory.getIcon(cursor.getInt(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_STANDARD)))
if (icon.isUnknown)
icon = iconFactory.keyIcon
}
val title = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_TITLE))
val username = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_USERNAME))
val url = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_URL))
val viewHolder = view.tag as ViewHolder
// Assign image
viewHolder.imageViewIcon?.assignDatabaseIcon(database.drawFactory, icon, iconColor)
// Assign title
val showTitle = EntryVersioned.getVisualTitle(false, title, username, url, uuid.toString())
viewHolder.textViewTitle?.text = showTitle
if (displayUsername && username.isNotEmpty()) {
viewHolder.textViewSubTitle?.text = String.format("(%s)", username)
} else {
viewHolder.textViewSubTitle?.text = ""
}
}
private class ViewHolder {
internal var imageViewIcon: ImageView? = null
internal var textViewTitle: TextView? = null
internal var textViewSubTitle: TextView? = null
}
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
return database.searchEntries(constraint.toString())
}
fun getEntryFromPosition(position: Int): EntryVersioned? {
var pwEntry: EntryVersioned? = null
val cursor = this.cursor
if (cursor.moveToFirst() && cursor.move(position)) {
pwEntry = database.getEntryFrom(cursor)
}
return pwEntry
}
}

View File

@@ -1,99 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.app;
import android.support.multidex.MultiDexApplication;
import com.kunzisoft.keepass.compat.PRNGFixes;
import com.kunzisoft.keepass.database.Database;
import com.kunzisoft.keepass.fileselect.RecentFileHistory;
import com.kunzisoft.keepass.stylish.Stylish;
import java.util.Calendar;
public class App extends MultiDexApplication {
private static Database db = null;
private static boolean shutdown = false;
private static CharSequence mMessage = "";
private static Calendar calendar = null;
private static RecentFileHistory fileHistory = null;
public static Database getDB() {
if ( db == null ) {
db = new Database();
}
return db;
}
public static RecentFileHistory getFileHistory() {
return fileHistory;
}
public static void setDB(Database d) {
db = d;
}
public static boolean isShutdown() {
return shutdown;
}
public static void setShutdown() {
shutdown = true;
mMessage = "";
}
public static void setShutdown(CharSequence message) {
shutdown = true;
mMessage = message;
}
public static CharSequence getMessage() {
return mMessage;
}
public static void clearShutdown() {
shutdown = false;
mMessage = "";
}
public static Calendar getCalendar() {
if ( calendar == null ) {
calendar = Calendar.getInstance();
}
return calendar;
}
@Override
public void onCreate() {
super.onCreate();
Stylish.init(this);
fileHistory = new RecentFileHistory(this);
PRNGFixes.apply();
}
@Override
public void onTerminate() {
if ( db != null ) {
db.clear();
}
super.onTerminate();
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2018 Jeremy Jamet / Kunzisoft. * Copyright 2019 Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
@@ -17,18 +17,23 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>. * along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.adapters; package com.kunzisoft.keepass.app
import android.view.View; import android.support.multidex.MultiDexApplication
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.R; class App : MultiDexApplication() {
class GroupViewHolder extends BasicViewHolder { override fun onCreate() {
super.onCreate()
GroupViewHolder(View itemView) { Stylish.init(this)
super(itemView); PRNGFixes.apply()
container = itemView.findViewById(R.id.group_container); }
icon = itemView.findViewById(R.id.group_icon);
text = itemView.findViewById(R.id.group_text); override fun onTerminate() {
Database.getInstance().closeAndClear(applicationContext.filesDir)
super.onTerminate()
} }
} }

View File

@@ -1,4 +1,4 @@
package com.kunzisoft.keepass.compat; package com.kunzisoft.keepass.app;
/* /*
* This software is provided 'as-is', without any express or implied * This software is provided 'as-is', without any express or implied
@@ -13,7 +13,7 @@ package com.kunzisoft.keepass.compat;
import android.os.Build; import android.os.Build;
import android.os.Process; import android.os.Process;
import com.kunzisoft.keepass.utils.StrUtil; import com.kunzisoft.keepass.utils.StringUtil;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@@ -66,7 +66,7 @@ public final class PRNGFixes {
private static boolean supportedOnThisDevice() { private static boolean supportedOnThisDevice() {
// Blacklist on samsung devices // Blacklist on samsung devices
if (StrUtil.indexOfIgnoreCase(Build.MANUFACTURER, "samsung", Locale.ENGLISH) >= 0) { if (StringUtil.INSTANCE.indexOfIgnoreCase(Build.MANUFACTURER, "samsung", Locale.ENGLISH) >= 0) {
return false; return false;
} }

View File

@@ -1,68 +0,0 @@
/*
* Copyright 2017 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.autofill;
import android.app.PendingIntent;
import android.app.assist.AssistStructure;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v7.app.AppCompatActivity;
import com.kunzisoft.keepass.fileselect.FileSelectActivity;
@RequiresApi(api = Build.VERSION_CODES.O)
public class AutoFillAuthActivity extends AppCompatActivity {
private AutofillHelper autofillHelper;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
autofillHelper = new AutofillHelper();
startFileSelectActivity();
super.onCreate(savedInstanceState);
}
public static IntentSender getAuthIntentSenderForResponse(Context context) {
final Intent intent = new Intent(context, AutoFillAuthActivity.class);
return PendingIntent.getActivity(context, 0,
intent, PendingIntent.FLAG_CANCEL_CURRENT).getIntentSender();
}
protected void startFileSelectActivity() {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
AssistStructure assistStructure = autofillHelper.retrieveAssistStructure(getIntent());
if (assistStructure != null) {
FileSelectActivity.launchForAutofillResult(this, assistStructure);
} else {
setResult(RESULT_CANCELED);
finish();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data);
}
}

View File

@@ -1,167 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.autofill;
import android.app.Activity;
import android.app.assist.AssistStructure;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.service.autofill.Dataset;
import android.service.autofill.FillResponse;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.Log;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
import android.widget.RemoteViews;
import com.kunzisoft.keepass.R;
import com.kunzisoft.keepass.database.PwEntry;
import com.kunzisoft.keepass.selection.EntrySelectionHelper;
import java.util.ArrayList;
import java.util.List;
@RequiresApi(api = Build.VERSION_CODES.O)
public class AutofillHelper {
public static final int AUTOFILL_RESPONSE_REQUEST_CODE = 8165;
private AssistStructure assistStructure = null;
public AssistStructure retrieveAssistStructure(Intent intent) {
if (intent != null && intent.getExtras() != null) {
assistStructure = intent.getParcelableExtra(android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE);
}
return assistStructure;
}
/**
* Call retrieveAssistStructure before
*/
public AssistStructure getAssistStructure() {
return assistStructure;
}
public static void addAssistStructureExtraInIntent(Intent intent, AssistStructure assistStructure) {
if (assistStructure != null) {
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent);
intent.putExtra(android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE, assistStructure);
}
}
/**
* Define if android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE is an extra bundle key present in the Intent
*/
public static boolean isIntentContainsExtraAssistStructureKey(Intent intent) {
return (intent != null
&& intent.getExtras() != null
&& intent.getExtras().containsKey(android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE));
}
private @Nullable Dataset buildDataset(Context context, PwEntry entry,
StructureParser.Result struct) {
String title = makeEntryTitle(entry);
RemoteViews views = newRemoteViews(context.getPackageName(), title);
Dataset.Builder builder = new Dataset.Builder(views);
builder.setId(entry.getUUID().toString());
if (entry.getPassword() != null) {
AutofillValue value = AutofillValue.forText(entry.getPassword());
struct.password.forEach(id -> builder.setValue(id, value));
}
if (entry.getUsername() != null) {
AutofillValue value = AutofillValue.forText(entry.getUsername());
List<AutofillId> ids = new ArrayList<>(struct.username);
if (entry.getUsername().contains("@") || struct.username.isEmpty())
ids.addAll(struct.email);
ids.forEach(id -> builder.setValue(id, value));
}
try {
return builder.build();
} catch (IllegalArgumentException e) {
// if not value be set
return null;
}
}
static private String makeEntryTitle(PwEntry entry) {
if (!entry.getTitle().isEmpty() && !entry.getUsername().isEmpty())
return String.format("%s (%s)", entry.getTitle(), entry.getUsername());
if (!entry.getTitle().isEmpty())
return entry.getTitle();
if (!entry.getUsername().isEmpty())
return entry.getUsername();
if (!entry.getNotes().isEmpty())
return entry.getNotes().trim();
return ""; // TODO No title
}
/**
* Method to hit when right key is selected
*/
public void buildResponseWhenEntrySelected(Activity activity, PwEntry entry) {
Intent mReplyIntent;
Intent intent = activity.getIntent();
if (isIntentContainsExtraAssistStructureKey(intent)) {
AssistStructure structure = intent.getParcelableExtra(android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE);
StructureParser.Result result = new StructureParser(structure).parse();
// New Response
FillResponse.Builder responseBuilder = new FillResponse.Builder();
Dataset dataset = buildDataset(activity, entry, result);
responseBuilder.addDataset(dataset);
mReplyIntent = new Intent();
Log.d(activity.getClass().getName(), "Successed Autofill auth.");
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
responseBuilder.build());
activity.setResult(Activity.RESULT_OK, mReplyIntent);
} else {
Log.w(activity.getClass().getName(), "Failed Autofill auth.");
activity.setResult(Activity.RESULT_CANCELED);
}
}
/**
* Utility method to loop and close each activity with return data
*/
public static void onActivityResultSetResultAndFinish(Activity activity, int requestCode, int resultCode, Intent data) {
if (requestCode == AUTOFILL_RESPONSE_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
activity.setResult(resultCode, data);
}
if (resultCode == Activity.RESULT_CANCELED) {
activity.setResult(Activity.RESULT_CANCELED);
}
activity.finish();
}
}
private static RemoteViews newRemoteViews(String packageName, String remoteViewsText) {
RemoteViews presentation =
new RemoteViews(packageName, R.layout.autofill_service_list_item);
presentation.setTextViewText(R.id.text, remoteViewsText);
return presentation;
}
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.autofill
import android.app.Activity
import android.app.assist.AssistStructure
import android.content.Context
import android.content.Intent
import android.os.Build
import android.service.autofill.Dataset
import android.service.autofill.FillResponse
import android.support.annotation.RequiresApi
import android.util.Log
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.model.EntryInfo
import java.util.*
@RequiresApi(api = Build.VERSION_CODES.O)
object AutofillHelper {
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
fun retrieveAssistStructure(intent: Intent?): AssistStructure? {
intent?.let {
return it.getParcelableExtra(ASSIST_STRUCTURE)
}
return null
}
private fun makeEntryTitle(entryInfo: EntryInfo): String {
if (entryInfo.title.isNotEmpty() && entryInfo.username.isNotEmpty())
return String.format("%s (%s)", entryInfo.title, entryInfo.username)
if (entryInfo.title.isNotEmpty())
return entryInfo.title
if (entryInfo.username.isNotEmpty())
return entryInfo.username
if (entryInfo.url.isNotEmpty())
return entryInfo.url
return ""
}
private fun buildDataset(context: Context,
entryInfo: EntryInfo,
struct: StructureParser.Result): Dataset? {
val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context.packageName, title)
val builder = Dataset.Builder(views)
builder.setId(entryInfo.id)
struct.password.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.password)) }
val ids = ArrayList(struct.username)
if (entryInfo.username.contains("@") || struct.username.isEmpty())
ids.addAll(struct.email)
ids.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.username)) }
return try {
builder.build()
} catch (e: IllegalArgumentException) {
// if not value be set
null
}
}
/**
* Method to hit when right key is selected
*/
fun buildResponseWhenEntrySelected(activity: Activity, entryInfo: EntryInfo) {
var setResultOk = false
activity.intent?.extras?.let { extras ->
if (extras.containsKey(ASSIST_STRUCTURE)) {
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
StructureParser(structure).parse()?.let { result ->
// New Response
val responseBuilder = FillResponse.Builder()
val dataset = buildDataset(activity, entryInfo, result)
responseBuilder.addDataset(dataset)
val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.")
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
responseBuilder.build())
setResultOk = true
activity.setResult(Activity.RESULT_OK, mReplyIntent)
}
}
}
if (!setResultOk) {
Log.w(activity.javaClass.name, "Failed Autofill auth.")
activity.setResult(Activity.RESULT_CANCELED)
}
}
}
/**
* Utility method to start an activity with an Autofill for result
*/
fun startActivityForAutofillResult(activity: Activity, intent: Intent, assistStructure: AssistStructure) {
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
}
/**
* Utility method to loop and close each activity with return data
*/
fun onActivityResultSetResultAndFinish(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == AUTOFILL_RESPONSE_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
activity.setResult(resultCode, data)
}
if (resultCode == Activity.RESULT_CANCELED) {
activity.setResult(Activity.RESULT_CANCELED)
}
activity.finish()
}
}
private fun newRemoteViews(packageName: String, remoteViewsText: String): RemoteViews {
val presentation = RemoteViews(packageName, R.layout.item_autofill_service)
presentation.setTextViewText(R.id.text, remoteViewsText)
return presentation
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.autofill
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.os.Bundle
import android.support.annotation.RequiresApi
import android.support.v7.app.AppCompatActivity
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
if (assistStructure != null) {
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
GroupActivity.launchForAutofillResult(this, assistStructure, PreferencesUtil.enableReadOnlyDatabase(this))
else {
FileDatabaseSelectActivity.launchForAutofillResult(this, assistStructure)
}
} else {
setResult(Activity.RESULT_CANCELED)
finish()
}
super.onCreate(savedInstanceState)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
companion object {
fun getAuthIntentSenderForResponse(context: Context): IntentSender {
val intent = Intent(context, AutofillLauncherActivity::class.java)
return PendingIntent.getActivity(context, 0,
intent, PendingIntent.FLAG_CANCEL_CURRENT).intentSender
}
}
}

View File

@@ -1,87 +0,0 @@
/*
* Copyright 2017 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.autofill;
import android.app.assist.AssistStructure;
import android.content.IntentSender;
import android.os.Build;
import android.os.CancellationSignal;
import android.service.autofill.AutofillService;
import android.service.autofill.FillCallback;
import android.service.autofill.FillContext;
import android.service.autofill.FillRequest;
import android.service.autofill.FillResponse;
import android.service.autofill.SaveCallback;
import android.service.autofill.SaveRequest;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.util.Log;
import android.view.autofill.AutofillId;
import android.widget.RemoteViews;
import com.kunzisoft.keepass.R;
import java.util.Arrays;
import java.util.List;
@RequiresApi(api = Build.VERSION_CODES.O)
public class KeeAutofillService extends AutofillService {
private static final String TAG = "KeeAutofillService";
@Override
public void onFillRequest(@NonNull FillRequest request, @NonNull CancellationSignal cancellationSignal,
@NonNull FillCallback callback) {
List<FillContext> fillContexts = request.getFillContexts();
AssistStructure latestStructure = fillContexts.get(fillContexts.size() - 1).getStructure();
cancellationSignal.setOnCancelListener(() ->
Log.e(TAG, "Cancel autofill not implemented in this sample.")
);
FillResponse.Builder responseBuilder = new FillResponse.Builder();
// Check user's settings for authenticating Responses and Datasets.
StructureParser.Result parseResult = new StructureParser(latestStructure).parse();
AutofillId[] autofillIds = parseResult.allAutofillIds();
if (!Arrays.asList(autofillIds).isEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
IntentSender sender = AutoFillAuthActivity.getAuthIntentSenderForResponse(this);
RemoteViews presentation = new RemoteViews(getPackageName(), R.layout.autofill_service_unlock);
responseBuilder.setAuthentication(autofillIds, sender, presentation);
callback.onSuccess(responseBuilder.build());
}
}
@Override
public void onSaveRequest(@NonNull SaveRequest request, @NonNull SaveCallback callback) {
// TODO Save autofill
//callback.onFailure(getString(R.string.autofill_not_support_save));
}
@Override
public void onConnected() {
Log.d(TAG, "onConnected");
}
@Override
public void onDisconnected() {
Log.d(TAG, "onDisconnected");
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.autofill
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.*
import android.support.annotation.RequiresApi
import android.util.Log
import android.widget.RemoteViews
import com.kunzisoft.keepass.R
@RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() {
override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal,
callback: FillCallback) {
val fillContexts = request.fillContexts
val latestStructure = fillContexts[fillContexts.size - 1].structure
cancellationSignal.setOnCancelListener { Log.e(TAG, "Cancel autofill not implemented in this sample.") }
val responseBuilder = FillResponse.Builder()
// Check user's settings for authenticating Responses and Datasets.
val parseResult = StructureParser(latestStructure).parse()
parseResult?.allAutofillIds()?.let { autofillIds ->
if (listOf(*autofillIds).isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this)
val presentation = RemoteViews(packageName, R.layout.item_autofill_service_unlock)
responseBuilder.setAuthentication(autofillIds, sender, presentation)
callback.onSuccess(responseBuilder.build())
}
}
}
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
// TODO Save autofill
//callback.onFailure(getString(R.string.autofill_not_support_save));
}
override fun onConnected() {
Log.d(TAG, "onConnected")
}
override fun onDisconnected() {
Log.d(TAG, "onDisconnected")
}
companion object {
private val TAG = KeeAutofillService::class.java.name
}
}

View File

@@ -1,115 +0,0 @@
/*
* Copyright 2018 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.autofill;
import android.app.assist.AssistStructure;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.text.InputType;
import android.util.Log;
import android.view.View;
import android.view.autofill.AutofillId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Parse AssistStructure and guess username and password fields.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
class StructureParser {
static private final String TAG = StructureParser.class.getName();
final private AssistStructure structure;
private Result result;
private AutofillId usernameCandidate;
StructureParser(AssistStructure structure) {
this.structure = structure;
}
Result parse() {
result = new Result();
usernameCandidate = null;
for (int i=0; i<structure.getWindowNodeCount(); ++i) {
AssistStructure.WindowNode windowNode = structure.getWindowNodeAt(i);
result.title.add(windowNode.getTitle());
result.webDomain.add(windowNode.getRootViewNode().getWebDomain());
parseViewNode(windowNode.getRootViewNode());
}
// If not explicit username field found, add the field just before password field.
if (result.username.isEmpty() && result.email.isEmpty()
&& !result.password.isEmpty() && usernameCandidate != null)
result.username.add(usernameCandidate);
return result;
}
private void parseViewNode(AssistStructure.ViewNode node) {
String[] hints = node.getAutofillHints();
if (hints != null && hints.length > 0) {
if (Arrays.stream(hints).anyMatch(View.AUTOFILL_HINT_USERNAME::equals))
result.username.add(node.getAutofillId());
else if (Arrays.stream(hints).anyMatch(View.AUTOFILL_HINT_EMAIL_ADDRESS::equals))
result.email.add(node.getAutofillId());
else if (Arrays.stream(hints).anyMatch(View.AUTOFILL_HINT_PASSWORD::equals))
result.password.add(node.getAutofillId());
else
Log.d(TAG, "unsupported hints");
} else if (node.getAutofillType() == View.AUTOFILL_TYPE_TEXT) {
int inputType = node.getInputType();
if ((inputType & InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) > 0)
result.email.add(node.getAutofillId());
else if ((inputType & InputType.TYPE_TEXT_VARIATION_PASSWORD) > 0)
result.password.add(node.getAutofillId());
else if (result.password.isEmpty())
usernameCandidate = node.getAutofillId();
}
for (int i=0; i<node.getChildCount(); ++i)
parseViewNode(node.getChildAt(i));
}
@RequiresApi(api = Build.VERSION_CODES.O)
static class Result {
final List<CharSequence> title;
final List<String> webDomain;
final List<AutofillId> username;
final List<AutofillId> email;
final List<AutofillId> password;
private Result() {
title = new ArrayList<>();
webDomain = new ArrayList<>();
username = new ArrayList<>();
email = new ArrayList<>();
password = new ArrayList<>();
}
AutofillId[] allAutofillIds() {
ArrayList<AutofillId> all = new ArrayList<>();
all.addAll(username);
all.addAll(email);
all.addAll(password);
return all.toArray(new AutofillId[0]);
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure
import android.os.Build
import android.support.annotation.RequiresApi
import android.text.InputType
import android.util.Log
import android.view.View
import android.view.autofill.AutofillId
import java.util.*
/**
* Parse AssistStructure and guess username and password fields.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
internal class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null
private var usernameCandidate: AutofillId? = null
fun parse(): Result? {
result = Result()
result?.apply {
usernameCandidate = null
for (i in 0 until structure.windowNodeCount) {
val windowNode = structure.getWindowNodeAt(i)
title.add(windowNode.title)
windowNode.rootViewNode.webDomain?.let {
webDomain.add(it)
}
parseViewNode(windowNode.rootViewNode)
}
// If 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
}
private fun parseViewNode(node: AssistStructure.ViewNode) {
val hints = node.autofillHints
val autofillId = node.autofillId
if (autofillId != null) {
if (hints != null && hints.isNotEmpty()) {
when {
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_USERNAME == it } -> result?.username?.add(autofillId)
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_EMAIL_ADDRESS == it } -> result?.email?.add(autofillId)
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_PASSWORD == it } -> result?.password?.add(autofillId)
else -> Log.d(TAG, "unsupported hints")
}
} else if (node.autofillType == View.AUTOFILL_TYPE_TEXT) {
val inputType = node.inputType
when {
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 -> result?.email?.add(autofillId)
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD > 0 -> result?.password?.add(autofillId)
result?.password?.isEmpty() == true -> usernameCandidate = autofillId
}
}
}
for (i in 0 until node.childCount)
parseViewNode(node.getChildAt(i))
}
@RequiresApi(api = Build.VERSION_CODES.O)
internal class Result {
val title: MutableList<CharSequence>
val webDomain: MutableList<String>
val username: MutableList<AutofillId>
val email: MutableList<AutofillId>
val password: MutableList<AutofillId>
init {
title = ArrayList()
webDomain = ArrayList()
username = ArrayList()
email = ArrayList()
password = ArrayList()
}
fun allAutofillIds(): Array<AutofillId> {
val all = ArrayList<AutofillId>()
all.addAll(username)
all.addAll(email)
all.addAll(password)
return all.toTypedArray()
}
}
companion object {
private val TAG = StructureParser::class.java.name
}
}

View File

@@ -1,39 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.backup;
import android.annotation.SuppressLint;
import android.app.backup.BackupAgentHelper;
import android.app.backup.SharedPreferencesBackupHelper;
@SuppressLint("NewApi")
public class SettingsBackupAgent extends BackupAgentHelper {
private static final String PREFS_BACKUP_KEY = "prefs";
@Override
public void onCreate() {
String defaultPrefs = this.getPackageName() + "_preferences";
SharedPreferencesBackupHelper prefHelper = new SharedPreferencesBackupHelper(this, defaultPrefs);
addHelper(PREFS_BACKUP_KEY, prefHelper);
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.backup
import android.annotation.SuppressLint
import android.app.backup.BackupAgentHelper
import android.app.backup.SharedPreferencesBackupHelper
@SuppressLint("NewApi")
class SettingsBackupAgent : BackupAgentHelper() {
override fun onCreate() {
val defaultPrefs = this.packageName + "_preferences"
val prefHelper = SharedPreferencesBackupHelper(this, defaultPrefs)
addHelper(PREFS_BACKUP_KEY, prefHelper)
}
companion object {
private const val PREFS_BACKUP_KEY = "prefs"
}
}

View File

@@ -1,57 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.compat;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import com.kunzisoft.keepass.R;
import java.lang.reflect.Field;
/**
* Created by bpellin on 3/10/16.
*/
public class StorageAF {
public static String ACTION_OPEN_DOCUMENT;
static {
try {
Field openDocument = Intent.class.getField("ACTION_OPEN_DOCUMENT");
ACTION_OPEN_DOCUMENT = (String) openDocument.get(null);
} catch (Exception e) {
ACTION_OPEN_DOCUMENT = "android.intent.action.OPEN_DOCUMENT";
}
}
public static boolean supportsStorageFramework() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; }
public static boolean useStorageFramework(Context ctx) {
if (!supportsStorageFramework()) { return false; }
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
return prefs.getBoolean(ctx.getString(R.string.saf_key), ctx.getResources().getBoolean(R.bool.settings_saf_default));
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.crypto;
import java.security.Provider;
public final class AESProvider extends Provider {
/**
*
*/
private static final long serialVersionUID = -3846349284296062658L;
public AESProvider() {
super("AESProvider", 1.0, "");
put("Cipher.AES",NativeAESCipherSpi.class.getName());
}
}

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. * Copyright 2019 Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -17,14 +17,18 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>. * along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.database; package com.kunzisoft.keepass.crypto
import java.security.Provider
class AESProvider : Provider("AESProvider", 1.0, "") {
init {
put("Cipher.AES", NativeAESCipherSpi::class.java.name)
}
companion object {
private const val serialVersionUID = -3846349284296062658L
}
/** "Delegate" class for operating on each entry when traversing all of
* them
* @author bpellin
*
*/
public abstract class EntryHandler<T extends PwEntry> {
public abstract boolean operate(T entry);
} }

View File

@@ -1,95 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.crypto;
import android.os.Build;
import com.kunzisoft.keepass.crypto.engine.AesEngine;
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine;
import com.kunzisoft.keepass.crypto.engine.CipherEngine;
import com.kunzisoft.keepass.crypto.engine.TwofishEngine;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
public class CipherFactory {
private static boolean blacklistInit = false;
private static boolean blacklisted;
static {
Security.addProvider(new BouncyCastleProvider());
}
public static Cipher getInstance(String transformation) throws NoSuchAlgorithmException, NoSuchPaddingException {
return getInstance(transformation, false);
}
public static Cipher getInstance(String transformation, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException {
// Return the native AES if it is possible
if ( (!deviceBlacklisted()) && (!androidOverride) && hasNativeImplementation(transformation) && NativeLib.loaded() ) {
return Cipher.getInstance(transformation, new AESProvider());
} else {
return Cipher.getInstance(transformation);
}
}
public static boolean deviceBlacklisted() {
if (!blacklistInit) {
blacklistInit = true;
// The Acer Iconia A500 is special and seems to always crash in the native crypto libraries
blacklisted = Build.MODEL.equals("A500");
}
return blacklisted;
}
private static boolean hasNativeImplementation(String transformation) {
return transformation.equals("AES/CBC/PKCS5Padding");
}
/** Generate appropriate cipher based on KeePass 2.x UUID's
* @param uuid
* @return
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws InvalidAlgorithmParameterException
* @throws InvalidKeyException
*/
public static CipherEngine getInstance(UUID uuid) throws NoSuchAlgorithmException {
if ( uuid.equals(AesEngine.CIPHER_UUID) ) {
return new AesEngine();
} else if ( uuid.equals(TwofishEngine.CIPHER_UUID) ) {
return new TwofishEngine();
} else if ( uuid.equals(ChaCha20Engine.CIPHER_UUID)) {
return new ChaCha20Engine();
}
throw new NoSuchAlgorithmException("UUID unrecognized.");
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.crypto
import android.os.Build
import com.kunzisoft.keepass.crypto.engine.AesEngine
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine
import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.crypto.engine.TwofishEngine
import org.spongycastle.jce.provider.BouncyCastleProvider
import java.security.NoSuchAlgorithmException
import java.security.Security
import java.util.*
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
object CipherFactory {
private var blacklistInit = false
private var blacklisted: Boolean = false
init {
Security.addProvider(BouncyCastleProvider())
}
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
@JvmOverloads
fun getInstance(transformation: String, androidOverride: Boolean = false): Cipher {
// Return the native AES if it is possible
return if (!deviceBlacklisted() && !androidOverride && hasNativeImplementation(transformation) && NativeLib.loaded()) {
Cipher.getInstance(transformation, AESProvider())
} else {
Cipher.getInstance(transformation)
}
}
fun deviceBlacklisted(): Boolean {
if (!blacklistInit) {
blacklistInit = true
// The Acer Iconia A500 is special and seems to always crash in the native crypto libraries
blacklisted = Build.MODEL == "A500"
}
return blacklisted
}
private fun hasNativeImplementation(transformation: String): Boolean {
return transformation == "AES/CBC/PKCS5Padding"
}
/**
* Generate appropriate cipher based on KeePass 2.x UUID's
*/
@Throws(NoSuchAlgorithmException::class)
fun getInstance(uuid: UUID): CipherEngine {
return when (uuid) {
AesEngine.CIPHER_UUID -> AesEngine()
TwofishEngine.CIPHER_UUID -> TwofishEngine()
ChaCha20Engine.CIPHER_UUID -> ChaCha20Engine()
else -> throw NoSuchAlgorithmException("UUID unrecognized.")
}
}
}

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright 2018 Jeremy Jamet / Kunzisoft. * Copyright 2019 Jeremy Jamet / Kunzisoft.
* *
* This file is part of KeePass DX. * This file is part of KeePass DX.
* *
* KeePass DX is free software: you can redistribute it and/or modify * KeePass DX is free software: you can redistribute it and/or modify
@@ -17,20 +17,25 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>. * along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.database; package com.kunzisoft.keepass.crypto
public enum PwVersion { enum class CrsAlgorithm constructor(val id: Int) {
V3, V4;
@Override Null(0),
public String toString() { ArcFourVariant(1),
switch (this) { Salsa20(2),
case V3: ChaCha20(3);
return "KeePass 1";
case V4: companion object {
return "KeePass 2";
default: fun fromId(num: Int): CrsAlgorithm? {
return "unknown"; for (e in values()) {
if (e.id == num) {
return e
}
}
return null
} }
} }
} }

View File

@@ -1,113 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.crypto;
import com.kunzisoft.keepass.stream.LEDataOutputStream;
import com.kunzisoft.keepass.stream.NullOutputStream;
import java.io.IOException;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.Mac;
public class CryptoUtil {
public static byte[] resizeKey(byte[] in, int inOffset, int cbIn, int cbOut) {
if (cbOut == 0) return new byte[0];
byte[] hash;
if (cbOut <= 32) { hash = hashSha256(in, inOffset, cbIn); }
else { hash = hashSha512(in, inOffset, cbIn); }
if (cbOut == hash.length) { return hash; }
byte[] ret = new byte[cbOut];
if (cbOut < hash.length) {
System.arraycopy(hash, 0, ret, 0, cbOut);
}
else {
int pos = 0;
long r = 0;
while (pos < cbOut) {
Mac hmac;
try {
hmac = Mac.getInstance("HmacSHA256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] pbR = LEDataOutputStream.writeLongBuf(r);
byte[] part = hmac.doFinal(pbR);
int copy = Math.min(cbOut - pos, part.length);
assert(copy > 0);
System.arraycopy(part, 0, ret, pos, copy);
pos += copy;
r++;
Arrays.fill(part, (byte)0);
}
assert(pos == cbOut);
}
Arrays.fill(hash, (byte)0);
return ret;
}
public static byte[] hashSha256(byte[] data) {
return hashSha256(data, 0, data.length);
}
public static byte[] hashSha256(byte[] data, int offset, int count) {
return hashGen("SHA-256", data, offset, count);
}
public static byte[] hashSha512(byte[] data) {
return hashSha512(data, 0, data.length);
}
public static byte[] hashSha512(byte[] data, int offset, int count) {
return hashGen("SHA-512", data, offset, count);
}
public static byte[] hashGen(String transform, byte[] data, int offset, int count) {
MessageDigest hash;
try {
hash = MessageDigest.getInstance(transform);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
NullOutputStream nos = new NullOutputStream();
DigestOutputStream dos = new DigestOutputStream(nos, hash);
try {
dos.write(data, offset, count);
dos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
return hash.digest();
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
* KeePass DX 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.
*
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.crypto
import com.kunzisoft.keepass.stream.LEDataOutputStream
import com.kunzisoft.keepass.stream.NullOutputStream
import java.io.IOException
import java.security.DigestOutputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.Arrays
import javax.crypto.Mac
import kotlin.math.min
object CryptoUtil {
fun resizeKey(inBytes: ByteArray, inOffset: Int, cbIn: Int, cbOut: Int): ByteArray {
if (cbOut == 0) return ByteArray(0)
val hash: ByteArray = if (cbOut <= 32) {
hashSha256(inBytes, inOffset, cbIn)
} else {
hashSha512(inBytes, inOffset, cbIn)
}
if (cbOut == hash.size) {
return hash
}
val ret = ByteArray(cbOut)
if (cbOut < hash.size) {
System.arraycopy(hash, 0, ret, 0, cbOut)
} else {
var pos = 0
var r: Long = 0
while (pos < cbOut) {
val hmac: Mac
try {
hmac = Mac.getInstance("HmacSHA256")
} catch (e: NoSuchAlgorithmException) {
throw RuntimeException(e)
}
val pbR = LEDataOutputStream.writeLongBuf(r)
val part = hmac.doFinal(pbR)
val copy = min(cbOut - pos, part.size)
System.arraycopy(part, 0, ret, pos, copy)
pos += copy
r++
Arrays.fill(part, 0.toByte())
}
}
Arrays.fill(hash, 0.toByte())
return ret
}
@JvmOverloads
fun hashSha256(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
return hashGen("SHA-256", data, offset, count)
}
@JvmOverloads
fun hashSha512(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
return hashGen("SHA-512", data, offset, count)
}
private fun hashGen(transform: String, data: ByteArray, offset: Int, count: Int): ByteArray {
val hash: MessageDigest
try {
hash = MessageDigest.getInstance(transform)
} catch (e: NoSuchAlgorithmException) {
throw RuntimeException(e)
}
val nos = NullOutputStream()
val dos = DigestOutputStream(nos, hash)
try {
dos.write(data, offset, count)
dos.close()
} catch (e: IOException) {
throw RuntimeException(e)
}
return hash.digest()
}
}

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