mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
303 Commits
2.5.0.0bet
...
feature/Ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df33c5feb5 | ||
|
|
f8134307f6 | ||
|
|
fe461f2e7c | ||
|
|
023c841747 | ||
|
|
af95c0903a | ||
|
|
0d756db8aa | ||
|
|
2c5dcc9b11 | ||
|
|
87760ab4f6 | ||
|
|
88ebe58a88 | ||
|
|
2de6bbc6c0 | ||
|
|
4ef436629d | ||
|
|
9d160db281 | ||
|
|
2e58c2f1b3 | ||
|
|
d1d2b99e09 | ||
|
|
def9744f75 | ||
|
|
214e2cf109 | ||
|
|
b25180c617 | ||
|
|
6a5263df77 | ||
|
|
2982f67717 | ||
|
|
b559670dff | ||
|
|
891d3142d2 | ||
|
|
2637788429 | ||
|
|
a21de3b892 | ||
|
|
e087e19120 | ||
|
|
721d61dda7 | ||
|
|
e0e7e431cf | ||
|
|
93948e7c61 | ||
|
|
b150c718a0 | ||
|
|
a71e4c3902 | ||
|
|
1e3c58e359 | ||
|
|
5f75599e9f | ||
|
|
b602f9b77d | ||
|
|
aa948c1ece | ||
|
|
e599a51152 | ||
|
|
ee6052f4d1 | ||
|
|
eba527f477 | ||
|
|
09e0d6d3cc | ||
|
|
9aefc984be | ||
|
|
2ce3b21f1b | ||
|
|
4d2f3cb4b1 | ||
|
|
e62b46c4c0 | ||
|
|
6472601170 | ||
|
|
89dd7bfefb | ||
|
|
fb2ea4c0ed | ||
|
|
8d84358d48 | ||
|
|
1d8661c633 | ||
|
|
48130eee45 | ||
|
|
2cf83962fe | ||
|
|
aecf7c0c39 | ||
|
|
39606e2676 | ||
|
|
f79aa339e9 | ||
|
|
f412fce912 | ||
|
|
cc20b7503c | ||
|
|
ab1fc8c5d5 | ||
|
|
8477f4ba08 | ||
|
|
e6518ffdc8 | ||
|
|
99917c7f28 | ||
|
|
fcc29f67a3 | ||
|
|
7dd49f050c | ||
|
|
5f96de84b0 | ||
|
|
54c2f5a61f | ||
|
|
921c6f88aa | ||
|
|
a0cb579df4 | ||
|
|
d6a7c34ff3 | ||
|
|
bf2e61f149 | ||
|
|
eaf5dc5988 | ||
|
|
879ee013db | ||
|
|
e13d53eae4 | ||
|
|
d72c8184c9 | ||
|
|
02a3d85f80 | ||
|
|
19b0722f1f | ||
|
|
f14222b192 | ||
|
|
4f4f6d30d9 | ||
|
|
fdd329e982 | ||
|
|
55a4d388b3 | ||
|
|
5c6be448ec | ||
|
|
3e79ddcc21 | ||
|
|
5362758424 | ||
|
|
c10e3df2a7 | ||
|
|
166784021a | ||
|
|
5615c31e08 | ||
|
|
fb60dd5921 | ||
|
|
ff4c1b779b | ||
|
|
53a7b99567 | ||
|
|
a57103bafb | ||
|
|
2540f32dbf | ||
|
|
499ccd6b7c | ||
|
|
a4359560b9 | ||
|
|
149483cc2d | ||
|
|
a1d2022492 | ||
|
|
891036c35c | ||
|
|
94a9942db5 | ||
|
|
5f347fe106 | ||
|
|
a34a84ae16 | ||
|
|
40b0982298 | ||
|
|
4100258476 | ||
|
|
5f3f6661b7 | ||
|
|
75af97e0ae | ||
|
|
58f158c457 | ||
|
|
ce27eae1f0 | ||
|
|
1cc5a08236 | ||
|
|
4c587eeb03 | ||
|
|
ab70c2d014 | ||
|
|
8413160ac5 | ||
|
|
5abc403171 | ||
|
|
9b891013b8 | ||
|
|
9413987355 | ||
|
|
f95b514b41 | ||
|
|
9c9980bba6 | ||
|
|
2226c15d29 | ||
|
|
82a859bd9c | ||
|
|
83873fab81 | ||
|
|
31f2be7b91 | ||
|
|
16458e6646 | ||
|
|
2b9678707d | ||
|
|
cdbb23d7f1 | ||
|
|
23fd1b83f4 | ||
|
|
40b0ebe49b | ||
|
|
7cd8682544 | ||
|
|
d0dd478ac8 | ||
|
|
ffb547c452 | ||
|
|
bd829f129f | ||
|
|
5ad3f62de5 | ||
|
|
116643a45a | ||
|
|
2f0eb283ed | ||
|
|
6d46fccdcd | ||
|
|
f5dc94bfec | ||
|
|
a8ccb67a87 | ||
|
|
66051382f1 | ||
|
|
0fb3028c91 | ||
|
|
a1b692abe5 | ||
|
|
4e06842d0f | ||
|
|
f04c2ee1da | ||
|
|
9700dbcc3f | ||
|
|
4e344458b2 | ||
|
|
6f95cc7296 | ||
|
|
cd66f8f57e | ||
|
|
83f0eb9a33 | ||
|
|
ca89bba768 | ||
|
|
2d2bd5013e | ||
|
|
b93d7bbf41 | ||
|
|
000277705a | ||
|
|
088712e784 | ||
|
|
117592387e | ||
|
|
76bb1a369c | ||
|
|
9331c281fe | ||
|
|
f2666316e1 | ||
|
|
7c2ff5067d | ||
|
|
5fed641c7c | ||
|
|
18bd62ee5a | ||
|
|
bc57e6e257 | ||
|
|
69b1aba218 | ||
|
|
df04d998c2 | ||
|
|
e986fe5f60 | ||
|
|
e4ac0ee258 | ||
|
|
426aa0e7da | ||
|
|
6c0a48af48 | ||
|
|
d865da1613 | ||
|
|
6fa4c1e06e | ||
|
|
52a6b3e046 | ||
|
|
fa26d2f938 | ||
|
|
4777cdc7ae | ||
|
|
cd8d3cbf6a | ||
|
|
e6a6feb5c0 | ||
|
|
98d6fb9214 | ||
|
|
94244cd15b | ||
|
|
c5e2ca9907 | ||
|
|
e4fef44caf | ||
|
|
5bd9da9bb1 | ||
|
|
5f0e899679 | ||
|
|
4b1806900b | ||
|
|
30e2912885 | ||
|
|
fc3608ff69 | ||
|
|
6de47ec9b2 | ||
|
|
f7253764a2 | ||
|
|
c54b134c31 | ||
|
|
a6bdca52be | ||
|
|
29eec05f8f | ||
|
|
250aef9738 | ||
|
|
50097914a2 | ||
|
|
ecff4fb2c5 | ||
|
|
ab3d17f352 | ||
|
|
a75d237e53 | ||
|
|
a0c7786e1b | ||
|
|
0d32c38c79 | ||
|
|
b846eda410 | ||
|
|
7c33c9ec02 | ||
|
|
74c08340a6 | ||
|
|
82161536be | ||
|
|
752dbca356 | ||
|
|
9ef56f6fd8 | ||
|
|
0239f115ae | ||
|
|
9775e09221 | ||
|
|
c975a1bfc0 | ||
|
|
c263536078 | ||
|
|
b559eeaad0 | ||
|
|
63373083ab | ||
|
|
bf525807b0 | ||
|
|
921021078b | ||
|
|
e5b60a8413 | ||
|
|
e01402f2fa | ||
|
|
27f92e1bb5 | ||
|
|
63db6de30b | ||
|
|
7a038126cf | ||
|
|
edcfa8cf7b | ||
|
|
c9594948a2 | ||
|
|
66988ecb66 | ||
|
|
186ca30be8 | ||
|
|
027d581dcc | ||
|
|
adcc1c745a | ||
|
|
8682856c01 | ||
|
|
9ec976d246 | ||
|
|
9d17b49586 | ||
|
|
698496d37c | ||
|
|
2af8c4f3c8 | ||
|
|
b164099b6d | ||
|
|
c19357605f | ||
|
|
5a08fa0088 | ||
|
|
bff87d16b1 | ||
|
|
5b0afa447c | ||
|
|
5a882a954f | ||
|
|
47f340d576 | ||
|
|
41df139c17 | ||
|
|
469c267161 | ||
|
|
f336d4fe58 | ||
|
|
e1d997cc91 | ||
|
|
53cf4bba1b | ||
|
|
030417dbe1 | ||
|
|
59abcb115c | ||
|
|
598dbd3794 | ||
|
|
2c4a7e5576 | ||
|
|
02429d5790 | ||
|
|
d990cb24ea | ||
|
|
e549b16dce | ||
|
|
e11864a64f | ||
|
|
a3e74f8ee5 | ||
|
|
36cb683404 | ||
|
|
f7f0028033 | ||
|
|
f06088fa12 | ||
|
|
b62ef8a2ed | ||
|
|
622ba65841 | ||
|
|
3a48d20d12 | ||
|
|
8d6db78f55 | ||
|
|
f3ba6e800a | ||
|
|
1c4aaf9807 | ||
|
|
c65ed41efd | ||
|
|
1965336077 | ||
|
|
9b7095ad4c | ||
|
|
33767c2bf9 | ||
|
|
82f7e861e7 | ||
|
|
846b5fa449 | ||
|
|
baf4e676eb | ||
|
|
a9bf3e83c4 | ||
|
|
710a1b0996 | ||
|
|
c4671b84a0 | ||
|
|
c0c98d0299 | ||
|
|
ffdec77d11 | ||
|
|
868bbe2e70 | ||
|
|
4aac655a5d | ||
|
|
510244aa70 | ||
|
|
50840b04b4 | ||
|
|
4d5962f5ca | ||
|
|
c9456c771c | ||
|
|
41a7a583d4 | ||
|
|
2e5220fa8a | ||
|
|
b75475d785 | ||
|
|
362ef06bb5 | ||
|
|
4a80c9f9f9 | ||
|
|
f1fdb9fc84 | ||
|
|
5bb9168c29 | ||
|
|
0245dcd8e8 | ||
|
|
b31bfa1d4f | ||
|
|
0778f22b68 | ||
|
|
4808696398 | ||
|
|
0ea7b5b25f | ||
|
|
995785de9f | ||
|
|
5deef427c0 | ||
|
|
024c6631d8 | ||
|
|
78354e4736 | ||
|
|
b9a792e6bd | ||
|
|
c533d21250 | ||
|
|
74572c8102 | ||
|
|
b8de64fab0 | ||
|
|
877b909205 | ||
|
|
1b1dcc0f45 | ||
|
|
94e5988794 | ||
|
|
b22333fda5 | ||
|
|
400c6bef78 | ||
|
|
edf6c2ff07 | ||
|
|
5eec1a276c | ||
|
|
f707fd7649 | ||
|
|
75b028daf3 | ||
|
|
c6f259d18f | ||
|
|
954d522341 | ||
|
|
1f8d17d27e | ||
|
|
b737501d4d | ||
|
|
c303ffafb5 | ||
|
|
6f513b4920 | ||
|
|
a83c60583f | ||
|
|
cde8950257 | ||
|
|
0b4dd1e909 | ||
|
|
28e2600271 | ||
|
|
53cc4f74c8 |
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
** Keepass Database **
|
||||||
|
- Created with: [e.g Windows KeePass 2.42]
|
||||||
|
- Version: [e.g. 2]
|
||||||
|
- Location: [e.g. Remote file retrieved with GDrive app]
|
||||||
|
- Size: [e.g. 150Mo]
|
||||||
|
- Contains attachment: [e.g. Yes]
|
||||||
|
|
||||||
|
**KeePass DX (please complete the following information):**
|
||||||
|
- Version: [e.g. 2.5.0.0beta23]
|
||||||
|
- Build: [e.g. Free]
|
||||||
|
- Language: [e.g. French]
|
||||||
|
|
||||||
|
**Android (please complete the following information):**
|
||||||
|
- Device: [e.g. GalaxyS8]
|
||||||
|
- Version: [e.g. 8.1]
|
||||||
|
- Browser: [e.g. Chrome]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: feature
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
26
CHANGELOG
26
CHANGELOG
@@ -1,3 +1,29 @@
|
|||||||
|
KeepassDX (2.5.0.0beta24)
|
||||||
|
* Add settings (Color, Security, Master Key)
|
||||||
|
* Show history of each entry
|
||||||
|
* Auto repair database for nodes with same UUID
|
||||||
|
* Management of expired nodes
|
||||||
|
* Multi-selection for actions (Cut - Copy - Delete)
|
||||||
|
* Fix settings
|
||||||
|
* Fix edit group
|
||||||
|
* Fix small bugs
|
||||||
|
|
||||||
|
KeepassDX (2.5.0.0beta23)
|
||||||
|
* New, more secure database creation workflow
|
||||||
|
* Recognize more database files
|
||||||
|
* Add alias for history files (WARNING: history is erased)
|
||||||
|
* New Biometric unlock (Fingerprint with new API)
|
||||||
|
* Fix entry references
|
||||||
|
* Fix OOM with KeyFile
|
||||||
|
* Fix small issues
|
||||||
|
|
||||||
|
KeepassDX (2.5.0.0beta22)
|
||||||
|
* Rebuild code for actions
|
||||||
|
* Add UUID as entry view
|
||||||
|
* Fix bug with natural order
|
||||||
|
* Fix number of entries in databaseV1
|
||||||
|
* New entry views
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta21)
|
KeepassDX (2.5.0.0beta21)
|
||||||
* Fix nested groups no longer visible in V1 databases
|
* Fix nested groups no longer visible in V1 databases
|
||||||
* Improved data import algorithm for V1 databases
|
* Improved data import algorithm for V1 databases
|
||||||
|
|||||||
45
CONTRIBUTORS
45
CONTRIBUTORS
@@ -1,45 +0,0 @@
|
|||||||
Original author:
|
|
||||||
Brian Pellin
|
|
||||||
|
|
||||||
Achim Weimert
|
|
||||||
Johan Berts - search patches
|
|
||||||
Mike Mohr - Better native code for aes and sha
|
|
||||||
Tobias Selig - icon support
|
|
||||||
Tolga Onbay, Dirk Bergstrom - password generator
|
|
||||||
Space Cowboy - holo theme
|
|
||||||
josefwells
|
|
||||||
Nicholas FitzRoy-Dale - auto launch intents
|
|
||||||
yulin2 - responsiveness improvements
|
|
||||||
Tadashi Saito
|
|
||||||
vhschlenker
|
|
||||||
bumper314 - Samsung multiwindow support
|
|
||||||
Hans Cappelle - fingerprint sensor integration
|
|
||||||
Jeremy Jamet - Keepass DX Material Design - Patches
|
|
||||||
|
|
||||||
Translations:
|
|
||||||
Diego Pierotto - Italian
|
|
||||||
Laurent, Norman Obry, Nam, Bruno Parmentier, Credomo - French
|
|
||||||
Maciej Bieniek, cod3r - Polish
|
|
||||||
Максим Сёмочкин, i.nedoboy, filimonic, bboa - Russian
|
|
||||||
MaWi, rvs2008, meviox, MaDill, EdlerProgrammierer, Jan Thomas - German
|
|
||||||
yslandro - Norwegian Nynorsk
|
|
||||||
王科峰 - Chinese
|
|
||||||
Typhoon - Slovak
|
|
||||||
Masahiro Inamura - Japanese
|
|
||||||
Matsuu Takuto - Japanese
|
|
||||||
Carlos Schlyter - Portugese (Brazil)
|
|
||||||
YSmhXQDd6Z - Portugese (Portugal)
|
|
||||||
andriykopanytsia - Ukranian
|
|
||||||
intel, Zoltán Antal - Hungarian
|
|
||||||
H Vanek - Czech
|
|
||||||
jipanos - Spanish
|
|
||||||
Erik Fdevriendt, Erik Jan Meijer - Dutch
|
|
||||||
Frederik Svarre - Danish
|
|
||||||
Oriol Garrote - Catalan
|
|
||||||
Mika Takala - Finnish
|
|
||||||
Niclas Burgren - Swedish
|
|
||||||
Raimonds - Latvian
|
|
||||||
dgarciabad - Basque
|
|
||||||
Arthur Zamarin - Hebrew
|
|
||||||
RaptorTFX - Greek
|
|
||||||
zygimantus - Lithuanian
|
|
||||||
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
|
.cxx
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ apply plugin: 'kotlin-android-extensions'
|
|||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 27
|
compileSdkVersion 28
|
||||||
buildToolsVersion '28.0.3'
|
buildToolsVersion '28.0.3'
|
||||||
|
ndkVersion "20.0.5594570"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 14
|
||||||
targetSdkVersion 27
|
targetSdkVersion 28
|
||||||
versionCode = 21
|
versionCode = 24
|
||||||
versionName = "2.5.0.0beta21"
|
versionName = "2.5.0.0beta24"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -79,42 +80,35 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def supportVersion = "27.1.1"
|
|
||||||
def spongycastleVersion = "1.58.0.0"
|
def spongycastleVersion = "1.58.0.0"
|
||||||
def permissionDispatcherVersion = "3.3.1"
|
def room_version = "2.2.0"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation "com.android.support:appcompat-v7:$supportVersion"
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
implementation "com.android.support:design:$supportVersion"
|
implementation 'androidx.preference:preference:1.1.0'
|
||||||
implementation "com.android.support:preference-v7:$supportVersion"
|
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||||
implementation "com.android.support:preference-v14:$supportVersion"
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation "com.android.support:cardview-v7:$supportVersion"
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
implementation 'androidx.biometric:biometric:1.0.0-rc01'
|
||||||
|
implementation 'com.google.android.material:material:1.0.0'
|
||||||
|
|
||||||
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
|
|
||||||
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
|
|
||||||
implementation 'net.cachapa.expandablelayout:expandablelayout:2.9.2'
|
|
||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.9.9'
|
implementation 'joda-time:joda-time:2.9.9'
|
||||||
implementation 'org.sufficientlysecure:html-textview:3.5'
|
// Color
|
||||||
implementation 'com.nononsenseapps:filepicker:4.1.0'
|
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
|
||||||
|
// Education
|
||||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0'
|
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0'
|
||||||
// Permissions
|
|
||||||
implementation("com.github.hotchemi:permissionsdispatcher:$permissionDispatcherVersion") {
|
|
||||||
// if you don't use android.app.Fragment you can exclude support for them
|
|
||||||
exclude module: "support-v13"
|
|
||||||
}
|
|
||||||
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'
|
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.guava:guava:23.0-android'
|
|
||||||
// Icon pack
|
// 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')
|
||||||
|
|||||||
@@ -1,44 +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 android.test.AndroidTestCase;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.tests.database.TestData;
|
|
||||||
|
|
||||||
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 {
|
|
||||||
TestData.GetDb(getContext(), ASSET, PASSWORD, KEYFILE, FILENAME);
|
|
||||||
} catch (Exception e) {
|
|
||||||
assertTrue("Failed to open database", false);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,34 +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.Test;
|
|
||||||
import junit.framework.TestSuite;
|
|
||||||
|
|
||||||
import android.test.suitebuilder.TestSuiteBuilder;
|
|
||||||
|
|
||||||
public class AllTests extends TestSuite {
|
|
||||||
|
|
||||||
public static Test suite() {
|
|
||||||
return new TestSuiteBuilder(AllTests.class)
|
|
||||||
.includeAllPackagesUnderHere()
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +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.Test;
|
|
||||||
import junit.framework.TestSuite;
|
|
||||||
|
|
||||||
import android.test.suitebuilder.TestSuiteBuilder;
|
|
||||||
|
|
||||||
public class OutputTests extends TestSuite {
|
|
||||||
|
|
||||||
public static Test suite() {
|
|
||||||
|
|
||||||
return new TestSuiteBuilder(AllTests.class)
|
|
||||||
.includePackages("com.kunzisoft.keepass.tests.output")
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +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 static org.junit.Assert.assertArrayEquals;
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.util.Calendar;
|
|
||||||
|
|
||||||
|
|
||||||
import android.test.AndroidTestCase;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.PwEntryV3;
|
|
||||||
import com.kunzisoft.keepass.tests.database.TestData;
|
|
||||||
|
|
||||||
public class PwEntryTestV3 extends AndroidTestCase {
|
|
||||||
PwEntryV3 mPE;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setUp() throws Exception {
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
// mPE = (PwEntryV3) TestData.GetTest1(getContext()).getEntryAt(0);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testName() {
|
|
||||||
assertTrue("Name was " + mPE.getTitle(), mPE.getTitle().equals("Amazon"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testPassword() throws UnsupportedEncodingException {
|
|
||||||
String sPass = "12345";
|
|
||||||
byte[] password = sPass.getBytes("UTF-8");
|
|
||||||
|
|
||||||
assertArrayEquals(password, mPE.getPasswordBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testCreation() {
|
|
||||||
Calendar cal = Calendar.getInstance();
|
|
||||||
cal.setTime(mPE.getCreationTime().getDate());
|
|
||||||
|
|
||||||
assertEquals("Incorrect year.", cal.get(Calendar.YEAR), 2009);
|
|
||||||
assertEquals("Incorrect month.", cal.get(Calendar.MONTH), 3);
|
|
||||||
assertEquals("Incorrect day.", cal.get(Calendar.DAY_OF_MONTH), 23);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +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;
|
|
||||||
|
|
||||||
public class PwEntryTestV4 extends TestCase {
|
|
||||||
public void testAssign() {
|
|
||||||
/*
|
|
||||||
TODO Test
|
|
||||||
PwEntryV4 entry = new PwEntryV4();
|
|
||||||
|
|
||||||
entry.setAdditional("test223");
|
|
||||||
|
|
||||||
entry.setAutoType(new AutoType());
|
|
||||||
entry.getAutoType().defaultSequence = "1324";
|
|
||||||
entry.getAutoType().enabled = true;
|
|
||||||
entry.getAutoType().obfuscationOptions = 123412432109L;
|
|
||||||
entry.getAutoType().put("key", "value");
|
|
||||||
|
|
||||||
entry.setBackgroundColor("blue");
|
|
||||||
entry.putProtectedBinary("key1", new ProtectedBinary(false, new byte[] {0,1}));
|
|
||||||
entry.setIconCustom(new PwIconCustom(UUID.randomUUID(), new byte[0]));
|
|
||||||
entry.setForegroundColor("red");
|
|
||||||
entry.addToHistory(new PwEntryV4());
|
|
||||||
entry.setIconStandard(new PwIconStandard(5));
|
|
||||||
entry.setOverrideURL("override");
|
|
||||||
entry.setParent(new PwGroupV4());
|
|
||||||
entry.addExtraField("key2", new ProtectedString(false, "value2"));
|
|
||||||
entry.setUrl("http://localhost");
|
|
||||||
entry.setNodeId(UUID.randomUUID());
|
|
||||||
|
|
||||||
PwEntryV4 target = new PwEntryV4();
|
|
||||||
target.updateWith(entry);
|
|
||||||
|
|
||||||
/* This test is not so useful now that I am not implementing value equality for Entries
|
|
||||||
assertTrue("Entries do not match.", entry.equals(target));
|
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,44 +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 android.test.AndroidTestCase;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.PwGroupV3;
|
|
||||||
import com.kunzisoft.keepass.tests.database.TestData;
|
|
||||||
|
|
||||||
public class PwGroupTest extends AndroidTestCase {
|
|
||||||
|
|
||||||
PwGroupV3 mPG;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setUp() throws Exception {
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
//mPG = (PwGroupV3) TestData.GetTest1(getContext()).getGroups().get(0);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testGroupName() {
|
|
||||||
//assertTrue("Name was " + mPG.getTitle(), mPG.getTitle().equals("Internet"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,56 +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 android.content.Context;
|
|
||||||
import android.content.res.AssetManager;
|
|
||||||
import android.os.Environment;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class TestUtil {
|
|
||||||
private static final File sdcard = Environment.getExternalStorageDirectory();
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getSdPath(String filename) {
|
|
||||||
File file = new File(sdcard, filename);
|
|
||||||
return file.getAbsolutePath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,211 +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 static org.junit.Assert.assertArrayEquals;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Random;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.PwDate;
|
|
||||||
import com.kunzisoft.keepass.stream.LEDataInputStream;
|
|
||||||
import com.kunzisoft.keepass.stream.LEDataOutputStream;
|
|
||||||
import com.kunzisoft.keepass.utils.Types;
|
|
||||||
|
|
||||||
public class TypesTest extends TestCase {
|
|
||||||
|
|
||||||
public void testReadWriteLongZero() {
|
|
||||||
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 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.junit.Assert.assertArrayEquals
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Random
|
||||||
|
|
||||||
|
import junit.framework.TestCase
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.database.element.PwDate
|
||||||
|
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||||
|
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
||||||
|
import com.kunzisoft.keepass.utils.Types
|
||||||
|
|
||||||
|
class TypesTest : TestCase() {
|
||||||
|
|
||||||
|
fun testReadWriteLongZero() {
|
||||||
|
testReadWriteLong(0.toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReadWriteLongMax() {
|
||||||
|
testReadWriteLong(java.lang.Byte.MAX_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReadWriteLongMin() {
|
||||||
|
testReadWriteLong(java.lang.Byte.MIN_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReadWriteLongRnd() {
|
||||||
|
val rnd = Random()
|
||||||
|
val buf = ByteArray(1)
|
||||||
|
rnd.nextBytes(buf)
|
||||||
|
|
||||||
|
testReadWriteLong(buf[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testReadWriteLong(value: Byte) {
|
||||||
|
val orig = ByteArray(8)
|
||||||
|
val dest = ByteArray(8)
|
||||||
|
|
||||||
|
setArray(orig, value, 0, 8)
|
||||||
|
|
||||||
|
val one = LEDataInputStream.readLong(orig, 0)
|
||||||
|
LEDataOutputStream.writeLong(one, dest, 0)
|
||||||
|
|
||||||
|
assertArrayEquals(orig, dest)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReadWriteIntZero() {
|
||||||
|
testReadWriteInt(0.toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReadWriteIntMin() {
|
||||||
|
testReadWriteInt(java.lang.Byte.MIN_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReadWriteIntMax() {
|
||||||
|
testReadWriteInt(java.lang.Byte.MAX_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testReadWriteInt(value: Byte) {
|
||||||
|
val orig = ByteArray(4)
|
||||||
|
val dest = ByteArray(4)
|
||||||
|
|
||||||
|
for (i in 0..3) {
|
||||||
|
orig[i] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
setArray(orig, value, 0, 4)
|
||||||
|
|
||||||
|
val one = LEDataInputStream.readInt(orig, 0)
|
||||||
|
|
||||||
|
LEDataOutputStream.writeInt(one, dest, 0)
|
||||||
|
|
||||||
|
assertArrayEquals(orig, dest)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setArray(buf: ByteArray, value: Byte, offset: Int, size: Int) {
|
||||||
|
for (i in offset until offset + size) {
|
||||||
|
buf[i] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReadWriteShortOne() {
|
||||||
|
val orig = ByteArray(2)
|
||||||
|
|
||||||
|
orig[0] = 0
|
||||||
|
orig[1] = 1
|
||||||
|
|
||||||
|
val one = LEDataInputStream.readUShort(orig, 0)
|
||||||
|
val dest = LEDataOutputStream.writeUShortBuf(one)
|
||||||
|
|
||||||
|
assertArrayEquals(orig, dest)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReadWriteShortMin() {
|
||||||
|
testReadWriteShort(java.lang.Byte.MIN_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReadWriteShortMax() {
|
||||||
|
testReadWriteShort(java.lang.Byte.MAX_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testReadWriteShort(value: Byte) {
|
||||||
|
val orig = ByteArray(2)
|
||||||
|
val dest = ByteArray(2)
|
||||||
|
|
||||||
|
setArray(orig, value, 0, 2)
|
||||||
|
|
||||||
|
val one = LEDataInputStream.readUShort(orig, 0)
|
||||||
|
LEDataOutputStream.writeUShort(one, dest, 0)
|
||||||
|
|
||||||
|
assertArrayEquals(orig, dest)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReadWriteByteZero() {
|
||||||
|
testReadWriteByte(0.toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReadWriteByteMin() {
|
||||||
|
testReadWriteByte(java.lang.Byte.MIN_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReadWriteByteMax() {
|
||||||
|
testReadWriteShort(java.lang.Byte.MAX_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testReadWriteByte(value: Byte) {
|
||||||
|
val orig = ByteArray(1)
|
||||||
|
val dest = ByteArray(1)
|
||||||
|
|
||||||
|
setArray(orig, value, 0, 1)
|
||||||
|
|
||||||
|
val one = Types.readUByte(orig, 0)
|
||||||
|
Types.writeUByte(one, dest, 0)
|
||||||
|
|
||||||
|
assertArrayEquals(orig, dest)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDate() {
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
|
||||||
|
val expected = Calendar.getInstance()
|
||||||
|
expected.set(2008, 1, 2, 3, 4, 5)
|
||||||
|
|
||||||
|
val buf = PwDate.writeTime(expected.time, cal)
|
||||||
|
val actual = Calendar.getInstance()
|
||||||
|
actual.time = 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testUUID() {
|
||||||
|
val rnd = Random()
|
||||||
|
val bUUID = ByteArray(16)
|
||||||
|
rnd.nextBytes(bUUID)
|
||||||
|
|
||||||
|
val uuid = Types.bytestoUUID(bUUID)
|
||||||
|
val eUUID = Types.UUIDtoBytes(uuid)
|
||||||
|
|
||||||
|
assertArrayEquals("UUID match failed", bUUID, eUUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testULongMax() {
|
||||||
|
val ulongBytes = ByteArray(8)
|
||||||
|
for (i in ulongBytes.indices) {
|
||||||
|
ulongBytes[i] = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
val bos = ByteArrayOutputStream()
|
||||||
|
val leos = LEDataOutputStream(bos)
|
||||||
|
leos.writeLong(Types.ULONG_MAX_VALUE)
|
||||||
|
leos.close()
|
||||||
|
|
||||||
|
val uLongMax = bos.toByteArray()
|
||||||
|
|
||||||
|
assertArrayEquals(ulongBytes, uLongMax)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,83 +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.crypto;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory;
|
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
|
||||||
|
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
|
||||||
import javax.crypto.NoSuchPaddingException;
|
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
|
||||||
|
|
||||||
public class AESTest extends TestCase {
|
|
||||||
|
|
||||||
private Random mRand = new Random();
|
|
||||||
|
|
||||||
public void testEncrypt() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
|
||||||
// Test above below and at the blocksize
|
|
||||||
testFinal(15);
|
|
||||||
testFinal(16);
|
|
||||||
testFinal(17);
|
|
||||||
|
|
||||||
// Test random larger sizes
|
|
||||||
int size = mRand.nextInt(494) + 18;
|
|
||||||
testFinal(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void testFinal(int dataSize) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
|
|
||||||
|
|
||||||
// Generate some input
|
|
||||||
byte[] input = new byte[dataSize];
|
|
||||||
mRand.nextBytes(input);
|
|
||||||
|
|
||||||
// Generate key
|
|
||||||
byte[] keyArray = new byte[32];
|
|
||||||
mRand.nextBytes(keyArray);
|
|
||||||
SecretKeySpec key = new SecretKeySpec(keyArray, "AES");
|
|
||||||
|
|
||||||
// Generate IV
|
|
||||||
byte[] ivArray = new byte[16];
|
|
||||||
mRand.nextBytes(ivArray);
|
|
||||||
IvParameterSpec iv = new IvParameterSpec(ivArray);
|
|
||||||
|
|
||||||
Cipher android = CipherFactory.INSTANCE.getInstance("AES/CBC/PKCS5Padding", true);
|
|
||||||
android.init(Cipher.ENCRYPT_MODE, key, iv);
|
|
||||||
byte[] outAndroid = android.doFinal(input, 0, dataSize);
|
|
||||||
|
|
||||||
Cipher nat = CipherFactory.INSTANCE.getInstance("AES/CBC/PKCS5Padding");
|
|
||||||
nat.init(Cipher.ENCRYPT_MODE, key, iv);
|
|
||||||
byte[] outNative = nat.doFinal(input, 0, dataSize);
|
|
||||||
|
|
||||||
assertArrayEquals("Arrays differ on size: " + dataSize, outAndroid, outNative);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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.crypto
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||||
|
|
||||||
|
import junit.framework.TestCase
|
||||||
|
|
||||||
|
import java.security.InvalidAlgorithmParameterException
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.util.Random
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.IllegalBlockSizeException
|
||||||
|
import javax.crypto.NoSuchPaddingException
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
|
||||||
|
class AESTest : TestCase() {
|
||||||
|
|
||||||
|
private val mRand = Random()
|
||||||
|
|
||||||
|
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidAlgorithmParameterException::class)
|
||||||
|
fun testEncrypt() {
|
||||||
|
// Test above below and at the blocksize
|
||||||
|
testFinal(15)
|
||||||
|
testFinal(16)
|
||||||
|
testFinal(17)
|
||||||
|
|
||||||
|
// Test random larger sizes
|
||||||
|
val size = mRand.nextInt(494) + 18
|
||||||
|
testFinal(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||||
|
private fun testFinal(dataSize: Int) {
|
||||||
|
|
||||||
|
// Generate some input
|
||||||
|
val input = ByteArray(dataSize)
|
||||||
|
mRand.nextBytes(input)
|
||||||
|
|
||||||
|
// Generate key
|
||||||
|
val keyArray = ByteArray(32)
|
||||||
|
mRand.nextBytes(keyArray)
|
||||||
|
val key = SecretKeySpec(keyArray, "AES")
|
||||||
|
|
||||||
|
// Generate IV
|
||||||
|
val ivArray = ByteArray(16)
|
||||||
|
mRand.nextBytes(ivArray)
|
||||||
|
val iv = IvParameterSpec(ivArray)
|
||||||
|
|
||||||
|
val android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true)
|
||||||
|
android.init(Cipher.ENCRYPT_MODE, key, iv)
|
||||||
|
val outAndroid = android.doFinal(input, 0, dataSize)
|
||||||
|
|
||||||
|
val nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
nat.init(Cipher.ENCRYPT_MODE, key, iv)
|
||||||
|
val outNative = nat.doFinal(input, 0, dataSize)
|
||||||
|
|
||||||
|
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,100 +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.crypto;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.CipherOutputStream;
|
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
|
||||||
import javax.crypto.NoSuchPaddingException;
|
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory;
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.AesEngine;
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine;
|
|
||||||
import com.kunzisoft.keepass.stream.BetterCipherInputStream;
|
|
||||||
import com.kunzisoft.keepass.stream.LEDataInputStream;
|
|
||||||
|
|
||||||
public class CipherTest extends TestCase {
|
|
||||||
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.INSTANCE.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);
|
|
||||||
byte[] decrypttext = decrypt.doFinal(secrettext);
|
|
||||||
|
|
||||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testCipherStreams() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException {
|
|
||||||
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.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* 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.crypto
|
||||||
|
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.security.InvalidAlgorithmParameterException
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.util.Random
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.CipherOutputStream
|
||||||
|
import javax.crypto.IllegalBlockSizeException
|
||||||
|
import javax.crypto.NoSuchPaddingException
|
||||||
|
|
||||||
|
import junit.framework.TestCase
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||||
|
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
||||||
|
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||||
|
import com.kunzisoft.keepass.stream.BetterCipherInputStream
|
||||||
|
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||||
|
|
||||||
|
class CipherTest : TestCase() {
|
||||||
|
private val rand = Random()
|
||||||
|
|
||||||
|
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class)
|
||||||
|
fun testCipherFactory() {
|
||||||
|
val key = ByteArray(32)
|
||||||
|
val iv = ByteArray(16)
|
||||||
|
|
||||||
|
val plaintext = ByteArray(1024)
|
||||||
|
|
||||||
|
rand.nextBytes(key)
|
||||||
|
rand.nextBytes(iv)
|
||||||
|
rand.nextBytes(plaintext)
|
||||||
|
|
||||||
|
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID)
|
||||||
|
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
||||||
|
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
||||||
|
|
||||||
|
val secrettext = encrypt.doFinal(plaintext)
|
||||||
|
val decrypttext = decrypt.doFinal(secrettext)
|
||||||
|
|
||||||
|
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class, IOException::class)
|
||||||
|
fun testCipherStreams() {
|
||||||
|
val MESSAGE_LENGTH = 1024
|
||||||
|
|
||||||
|
val key = ByteArray(32)
|
||||||
|
val iv = ByteArray(16)
|
||||||
|
|
||||||
|
val plaintext = ByteArray(MESSAGE_LENGTH)
|
||||||
|
|
||||||
|
rand.nextBytes(key)
|
||||||
|
rand.nextBytes(iv)
|
||||||
|
rand.nextBytes(plaintext)
|
||||||
|
|
||||||
|
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID)
|
||||||
|
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
||||||
|
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
||||||
|
|
||||||
|
val bos = ByteArrayOutputStream()
|
||||||
|
val cos = CipherOutputStream(bos, encrypt)
|
||||||
|
cos.write(plaintext)
|
||||||
|
cos.close()
|
||||||
|
|
||||||
|
val secrettext = bos.toByteArray()
|
||||||
|
|
||||||
|
val bis = ByteArrayInputStream(secrettext)
|
||||||
|
val cis = BetterCipherInputStream(bis, decrypt)
|
||||||
|
val lis = LEDataInputStream(cis)
|
||||||
|
|
||||||
|
val decrypttext = lis.readBytes(MESSAGE_LENGTH)
|
||||||
|
|
||||||
|
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,66 +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.crypto;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey;
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey;
|
|
||||||
|
|
||||||
public class FinalKeyTest extends TestCase {
|
|
||||||
private Random mRand;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setUp() throws Exception {
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
mRand = new Random();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testNativeAndroid() throws IOException {
|
|
||||||
// Test both an old and an even number to test my flip variable
|
|
||||||
testNativeFinalKey(5);
|
|
||||||
testNativeFinalKey(6);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void testNativeFinalKey(int rounds) throws IOException {
|
|
||||||
byte[] seed = new byte[32];
|
|
||||||
byte[] key = new byte[32];
|
|
||||||
byte[] nativeKey;
|
|
||||||
byte[] androidKey;
|
|
||||||
|
|
||||||
mRand.nextBytes(seed);
|
|
||||||
mRand.nextBytes(key);
|
|
||||||
|
|
||||||
AndroidFinalKey aKey = new AndroidFinalKey();
|
|
||||||
androidKey = aKey.transformMasterKey(seed, key, rounds);
|
|
||||||
|
|
||||||
NativeFinalKey nKey = new NativeFinalKey();
|
|
||||||
nativeKey = nKey.transformMasterKey(seed, key, rounds);
|
|
||||||
|
|
||||||
assertArrayEquals("Does not match", androidKey, nativeKey);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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.crypto
|
||||||
|
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.Random
|
||||||
|
|
||||||
|
import junit.framework.TestCase
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey
|
||||||
|
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey
|
||||||
|
|
||||||
|
class FinalKeyTest : TestCase() {
|
||||||
|
private var mRand: Random? = null
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
|
||||||
|
mRand = Random()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun testNativeAndroid() {
|
||||||
|
// Test both an old and an even number to test my flip variable
|
||||||
|
testNativeFinalKey(5)
|
||||||
|
testNativeFinalKey(6)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun testNativeFinalKey(rounds: Int) {
|
||||||
|
val seed = ByteArray(32)
|
||||||
|
val key = ByteArray(32)
|
||||||
|
val nativeKey: ByteArray
|
||||||
|
val androidKey: ByteArray
|
||||||
|
|
||||||
|
mRand!!.nextBytes(seed)
|
||||||
|
mRand!!.nextBytes(key)
|
||||||
|
|
||||||
|
val aKey = AndroidFinalKey()
|
||||||
|
androidKey = aKey.transformMasterKey(seed, key, rounds.toLong())
|
||||||
|
|
||||||
|
val nKey = NativeFinalKey()
|
||||||
|
nativeKey = nKey.transformMasterKey(seed, key, rounds.toLong())
|
||||||
|
|
||||||
|
assertArrayEquals("Does not match", androidKey, nativeKey)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,111 +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.database;
|
|
||||||
|
|
||||||
import android.test.AndroidTestCase;
|
|
||||||
import com.kunzisoft.keepass.database.element.GroupVersioned;
|
|
||||||
import com.kunzisoft.keepass.database.element.PwDatabase;
|
|
||||||
import com.kunzisoft.keepass.database.element.PwDatabaseV3;
|
|
||||||
import com.kunzisoft.keepass.database.element.PwEntryV3;
|
|
||||||
|
|
||||||
public class DeleteEntry extends AndroidTestCase {
|
|
||||||
private static final String GROUP1_NAME = "Group1";
|
|
||||||
private static final String ENTRY1_NAME = "Test1";
|
|
||||||
private static final String ENTRY2_NAME = "Test2";
|
|
||||||
private static final String KEYFILE = "";
|
|
||||||
private static final String PASSWORD = "12345";
|
|
||||||
private static final String ASSET = "delete.kdb";
|
|
||||||
private static final String FILENAME = "/sdcard/delete.kdb";
|
|
||||||
|
|
||||||
public void testDelete() {
|
|
||||||
|
|
||||||
/*
|
|
||||||
Database db;
|
|
||||||
|
|
||||||
Context ctx = getContext();
|
|
||||||
|
|
||||||
try {
|
|
||||||
db = TestData.GetDb(ctx, ASSET, PASSWORD, KEYFILE, FILENAME);
|
|
||||||
} catch (Exception e) {
|
|
||||||
assertTrue("Failed to open database: " + e.getMessage(), false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PwDatabaseV3 pm = (PwDatabaseV3) db.getPwDatabase();
|
|
||||||
GroupVersioned group1 = getGroup(pm, GROUP1_NAME);
|
|
||||||
assertNotNull("Could not find group1", group1);
|
|
||||||
|
|
||||||
// Delete the group
|
|
||||||
DeleteGroupRunnable task = new DeleteGroupRunnable(null, db, group1, null, true);
|
|
||||||
task.run();
|
|
||||||
|
|
||||||
// Verify the entries were deleted
|
|
||||||
PwEntryInterface entry1 = getEntry(pm, ENTRY1_NAME);
|
|
||||||
assertNull("Entry 1 was not removed", entry1);
|
|
||||||
|
|
||||||
PwEntryInterface entry2 = getEntry(pm, ENTRY2_NAME);
|
|
||||||
assertNull("Entry 2 was not removed", entry2);
|
|
||||||
|
|
||||||
// Verify the entries were removed from the search index
|
|
||||||
SearchDbHelper dbHelp = new SearchDbHelper(ctx);
|
|
||||||
GroupVersioned results1 = dbHelp.search(db.getPwDatabase(), ENTRY1_NAME, 100);
|
|
||||||
GroupVersioned results2 = dbHelp.search(db.getPwDatabase(), ENTRY2_NAME, 100);
|
|
||||||
|
|
||||||
assertEquals("Entry1 was not removed from the search results", 0, results1.numbersOfChildEntries());
|
|
||||||
assertEquals("Entry2 was not removed from the search results", 0, results2.numbersOfChildEntries());
|
|
||||||
|
|
||||||
// Verify the group was deleted
|
|
||||||
group1 = getGroup(pm, GROUP1_NAME);
|
|
||||||
assertNull("Group 1 was not removed.", group1);
|
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private PwEntryV3 getEntry(PwDatabaseV3 pm, String name) {
|
|
||||||
/*
|
|
||||||
TODO test
|
|
||||||
List<PwEntryV3> entries = pm.getEntries();
|
|
||||||
for ( int i = 0; i < entries.size(); i++ ) {
|
|
||||||
PwEntryV3 entry = entries.get(i);
|
|
||||||
if ( entry.getTitle().equals(name) ) {
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private GroupVersioned getGroup(PwDatabase pm, String name) {
|
|
||||||
/*
|
|
||||||
List<GroupVersioned> groups = pm.getGroups();
|
|
||||||
for ( int i = 0; i < groups.size(); i++ ) {
|
|
||||||
GroupVersioned group = groups.get(i);
|
|
||||||
if ( group.getTitle().equals(name) ) {
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,56 +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.database;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.PwDatabaseV4;
|
|
||||||
import com.kunzisoft.keepass.database.element.PwEntryV4;
|
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
|
||||||
|
|
||||||
public class EntryV4 extends TestCase {
|
|
||||||
|
|
||||||
public void testBackup() {
|
|
||||||
/*
|
|
||||||
PwDatabaseV4 db = new PwDatabaseV4();
|
|
||||||
|
|
||||||
db.setHistoryMaxItems(2);
|
|
||||||
|
|
||||||
PwEntryV4 entry = new PwEntryV4();
|
|
||||||
entry.startToManageFieldReferences(db);
|
|
||||||
entry.setTitle("Title1");
|
|
||||||
entry.setUsername("User1");
|
|
||||||
entry.createBackup(db);
|
|
||||||
|
|
||||||
entry.setTitle("Title2");
|
|
||||||
entry.setUsername("User2");
|
|
||||||
entry.createBackup(db);
|
|
||||||
|
|
||||||
entry.setTitle("Title3");
|
|
||||||
entry.setUsername("User3");
|
|
||||||
entry.createBackup(db);
|
|
||||||
|
|
||||||
PwEntryV4 backup = entry.getHistory().get(0);
|
|
||||||
entry.stopToManageFieldReferences();
|
|
||||||
assertEquals("Title2", backup.getTitle());
|
|
||||||
assertEquals("User2", backup.getUsername());
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,53 +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.database;
|
|
||||||
|
|
||||||
import android.test.AndroidTestCase;
|
|
||||||
|
|
||||||
public class Kdb3 extends AndroidTestCase {
|
|
||||||
|
|
||||||
private void testKeyfile(String dbAsset, String keyAsset, String password) throws Exception {
|
|
||||||
/*
|
|
||||||
Context ctx = getContext();
|
|
||||||
|
|
||||||
File sdcard = Environment.getExternalStorageDirectory();
|
|
||||||
String keyPath = sdcard.getAbsolutePath() + "/key";
|
|
||||||
|
|
||||||
TestUtil.extractKey(ctx, keyAsset, keyPath);
|
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
|
||||||
InputStream is = am.open(dbAsset, AssetManager.ACCESS_STREAMING);
|
|
||||||
|
|
||||||
ImporterV3 importer = new ImporterV3();
|
|
||||||
importer.openDatabase(is, password, TestUtil.getKeyFileInputStream(ctx, keyPath));
|
|
||||||
|
|
||||||
is.close();
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,41 +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.database;
|
|
||||||
|
|
||||||
import android.test.AndroidTestCase;
|
|
||||||
|
|
||||||
public class Kdb3Twofish extends AndroidTestCase {
|
|
||||||
public void testReadTwofish() throws Exception {
|
|
||||||
/*
|
|
||||||
Context ctx = getContext();
|
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
|
||||||
InputStream is = am.open("twofish.kdb", AssetManager.ACCESS_STREAMING);
|
|
||||||
|
|
||||||
ImporterV3 importer = new ImporterV3();
|
|
||||||
|
|
||||||
PwDatabaseV3 db = importer.openDatabase(is, "12345", null);
|
|
||||||
|
|
||||||
assertTrue(db.getEncryptionAlgorithm() == PwEncryptionAlgorithm.Twofish);
|
|
||||||
|
|
||||||
is.close();
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,171 +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.database;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.AssetManager;
|
|
||||||
import android.test.AndroidTestCase;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.exception.InvalidDBException;
|
|
||||||
import com.kunzisoft.keepass.database.exception.PwDbOutputException;
|
|
||||||
import com.kunzisoft.keepass.tests.TestUtil;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class Kdb4 extends AndroidTestCase {
|
|
||||||
|
|
||||||
public void testDetection() throws IOException, InvalidDBException {
|
|
||||||
Context ctx = getContext();
|
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
|
||||||
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO Test
|
|
||||||
Importer importer = ImporterFactory.createImporter(is);
|
|
||||||
|
|
||||||
assertTrue(importer instanceof ImporterV4);
|
|
||||||
is.close();
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testParsing() throws IOException, InvalidDBException {
|
|
||||||
Context ctx = getContext();
|
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
|
||||||
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO Test
|
|
||||||
ImporterV4 importer = new ImporterV4();
|
|
||||||
importer.openDatabase(is, "12345", null);
|
|
||||||
|
|
||||||
is.close();
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testSavingKDBXV3() throws IOException, InvalidDBException, PwDbOutputException {
|
|
||||||
testSaving("test.kdbx", "12345", "test-out.kdbx");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testSavingKDBXV4() throws IOException, InvalidDBException, PwDbOutputException {
|
|
||||||
testSaving("test-kdbxv4.kdbx", "1", "test-kdbxv4-out.kdbx");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void testSaving(String inputFile, String password, String outputFile) throws IOException, InvalidDBException, PwDbOutputException {
|
|
||||||
Context ctx = getContext();
|
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
|
||||||
InputStream is = am.open(inputFile, AssetManager.ACCESS_STREAMING);
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO Test
|
|
||||||
ImporterV4 importer = new ImporterV4();
|
|
||||||
PwDatabaseV4 db = importer.openDatabase(is, password, null);
|
|
||||||
is.close();
|
|
||||||
|
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
|
||||||
|
|
||||||
PwDbV4Output output = (PwDbV4Output) PwDbOutput.getInstance(db, bos);
|
|
||||||
output.output();
|
|
||||||
|
|
||||||
byte[] data = bos.toByteArray();
|
|
||||||
|
|
||||||
FileOutputStream fos = new FileOutputStream(TestUtil.getSdPath(outputFile), false);
|
|
||||||
|
|
||||||
InputStream bis = new ByteArrayInputStream(data);
|
|
||||||
bis = new CopyInputStream(bis, fos);
|
|
||||||
importer = new ImporterV4();
|
|
||||||
db = importer.openDatabase(bis, password, null);
|
|
||||||
bis.close();
|
|
||||||
|
|
||||||
fos.close();
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setUp() throws Exception {
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
TestUtil.extractKey(getContext(), "keyfile.key", TestUtil.getSdPath("key"));
|
|
||||||
TestUtil.extractKey(getContext(), "binary.key", TestUtil.getSdPath("key-binary"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testComposite() throws IOException, InvalidDBException {
|
|
||||||
Context ctx = getContext();
|
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
|
||||||
InputStream is = am.open("keyfile.kdbx", AssetManager.ACCESS_STREAMING);
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO Test
|
|
||||||
ImporterV4 importer = new ImporterV4();
|
|
||||||
importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key")));
|
|
||||||
|
|
||||||
is.close();
|
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testCompositeBinary() throws IOException, InvalidDBException {
|
|
||||||
Context ctx = getContext();
|
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
|
||||||
InputStream is = am.open("keyfile-binary.kdbx", AssetManager.ACCESS_STREAMING);
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO Test
|
|
||||||
ImporterV4 importer = new ImporterV4();
|
|
||||||
importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx,TestUtil.getSdPath("key-binary")));
|
|
||||||
|
|
||||||
is.close();
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testKeyfile() throws IOException, InvalidDBException {
|
|
||||||
Context ctx = getContext();
|
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
|
||||||
InputStream is = am.open("key-only.kdbx", AssetManager.ACCESS_STREAMING);
|
|
||||||
/*
|
|
||||||
TODO Test
|
|
||||||
ImporterV4 importer = new ImporterV4();
|
|
||||||
importer.openDatabase(is, "", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key")));
|
|
||||||
|
|
||||||
is.close();
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testNoGzip() throws IOException, InvalidDBException {
|
|
||||||
Context ctx = getContext();
|
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
|
||||||
InputStream is = am.open("no-encrypt.kdbx", AssetManager.ACCESS_STREAMING);
|
|
||||||
/*
|
|
||||||
TODO Test
|
|
||||||
ImporterV4 importer = new ImporterV4();
|
|
||||||
importer.openDatabase(is, "12345", null);
|
|
||||||
|
|
||||||
is.close();
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,49 +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.database;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.AssetManager;
|
|
||||||
import android.test.AndroidTestCase;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class Kdb4Header extends AndroidTestCase {
|
|
||||||
public void testReadHeader() throws Exception {
|
|
||||||
Context ctx = getContext();
|
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
|
||||||
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO Test
|
|
||||||
ImporterV4 importer = new ImporterV4();
|
|
||||||
|
|
||||||
PwDatabaseV4 db = importer.openDatabase(is, "12345", null);
|
|
||||||
|
|
||||||
assertEquals(6000, db.getNumberKeyEncryptionRounds());
|
|
||||||
|
|
||||||
assertTrue(db.getDataCipher().equals(AesEngine.CIPHER_UUID));
|
|
||||||
|
|
||||||
is.close();
|
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +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.database;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.AssetManager;
|
|
||||||
import android.test.AndroidTestCase;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.PwDatabaseV4;
|
|
||||||
import com.kunzisoft.keepass.database.element.SprEngineV4;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class SprEngineTest extends AndroidTestCase {
|
|
||||||
private PwDatabaseV4 db;
|
|
||||||
private SprEngineV4 spr;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setUp() throws Exception {
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
Context ctx = getContext();
|
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
|
||||||
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO Test
|
|
||||||
ImporterV4 importer = new ImporterV4();
|
|
||||||
db = importer.openDatabase(is, "12345", null);
|
|
||||||
|
|
||||||
is.close();
|
|
||||||
|
|
||||||
spr = new SprEngineV4();
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
private final String REF = "{REF:P@I:2B1D56590D961F48A8CE8C392CE6CD35}";
|
|
||||||
private final String ENCODE_UUID = "IN7RkON49Ui1UZ2ddqmLcw==";
|
|
||||||
private final String RESULT = "Password";
|
|
||||||
public void testRefReplace() {
|
|
||||||
/*
|
|
||||||
TODO TEST
|
|
||||||
UUID entryUUID = decodeUUID(ENCODE_UUID);
|
|
||||||
|
|
||||||
PwEntryV4 entry = (PwEntryV4) db.getEntryById(entryUUID);
|
|
||||||
|
|
||||||
|
|
||||||
assertEquals(RESULT, spr.compile(REF, entry, db));
|
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +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.database;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.Database;
|
|
||||||
|
|
||||||
public class TestData {
|
|
||||||
private static final String TEST1_KEYFILE = "";
|
|
||||||
private static final String TEST1_KDB = "test1.kdb";
|
|
||||||
private static final String TEST1_PASSWORD = "12345";
|
|
||||||
|
|
||||||
private static Database mDb1;
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
public static Database GetDb1(Context ctx) throws Exception {
|
|
||||||
return GetDb1(ctx, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Database GetDb1(Context ctx, boolean forceReload) throws Exception {
|
|
||||||
if ( mDb1 == null || forceReload ) {
|
|
||||||
mDb1 = GetDb(ctx, TEST1_KDB, TEST1_PASSWORD, TEST1_KEYFILE, "/sdcard/test1.kdb");
|
|
||||||
}
|
|
||||||
|
|
||||||
return mDb1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Database GetDb(Context ctx, String asset, String password, String keyfile, String filename) throws Exception {
|
|
||||||
AssetManager am = ctx.getAssets();
|
|
||||||
InputStream is = am.open(asset, AssetManager.ACCESS_STREAMING);
|
|
||||||
|
|
||||||
Database Db = new Database();
|
|
||||||
|
|
||||||
InputStream keyIs = TestUtil.getKeyFileInputStream(ctx, keyfile);
|
|
||||||
|
|
||||||
Db.loadData(ctx, is, password, keyIs, Importer.DEBUG);
|
|
||||||
Uri.Builder b = new Uri.Builder();
|
|
||||||
|
|
||||||
Db.setUri(b.scheme("file").path(filename).build());
|
|
||||||
|
|
||||||
return Db;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PwDatabaseV3Debug GetTest1(Context ctx) throws Exception {
|
|
||||||
if ( mDb1 == null ) {
|
|
||||||
GetDb1(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
//return (PwDatabaseV3Debug) mDb1.getPwDatabase();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
@@ -1,126 +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.output;
|
|
||||||
|
|
||||||
import android.test.AndroidTestCase;
|
|
||||||
|
|
||||||
public class PwManagerOutputTest extends AndroidTestCase {
|
|
||||||
// PwDatabaseV3Debug mPM;
|
|
||||||
|
|
||||||
/*
|
|
||||||
@Override
|
|
||||||
protected void setUp() throws Exception {
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
mPM = TestData.GetTest1(getContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testPlainContent() throws IOException, PwDbOutputException {
|
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
|
||||||
|
|
||||||
PwDbV3Output pos = new PwDbV3OutputDebug(mPM, bos, true);
|
|
||||||
pos.outputPlanGroupAndEntries(bos);
|
|
||||||
|
|
||||||
assertTrue("No output", bos.toByteArray().length > 0);
|
|
||||||
assertArrayEquals("Group and entry output doesn't match.", mPM.getPostHeader(), bos.toByteArray());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testChecksum() throws NoSuchAlgorithmException, IOException, PwDbOutputException {
|
|
||||||
//FileOutputStream fos = new FileOutputStream("/dev/null");
|
|
||||||
NullOutputStream nos = new NullOutputStream();
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
||||||
|
|
||||||
DigestOutputStream dos = new DigestOutputStream(nos, md);
|
|
||||||
|
|
||||||
PwDbV3Output pos = new PwDbV3OutputDebug(mPM, dos, true);
|
|
||||||
pos.outputPlanGroupAndEntries(dos);
|
|
||||||
dos.close();
|
|
||||||
|
|
||||||
byte[] digest = md.digest();
|
|
||||||
assertTrue("No output", digest.length > 0);
|
|
||||||
assertArrayEquals("Hash of groups and entries failed.", mPM.getDbHeader().contentsHash, digest);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertHeadersEquals(PwDbHeaderV3 expected, PwDbHeaderV3 actual) {
|
|
||||||
assertEquals("Flags unequal", expected.flags, actual.flags);
|
|
||||||
assertEquals("Entries unequal", expected.numEntries, actual.numEntries);
|
|
||||||
assertEquals("Groups unequal", expected.numGroups, actual.numGroups);
|
|
||||||
assertEquals("Key Rounds unequal", expected.numKeyEncRounds, actual.numKeyEncRounds);
|
|
||||||
assertEquals("Signature1 unequal", expected.signature1, actual.signature1);
|
|
||||||
assertEquals("Signature2 unequal", expected.signature2, actual.signature2);
|
|
||||||
assertTrue("Version incompatible", PwDbHeaderV3.compatibleHeaders(expected.version, actual.version));
|
|
||||||
assertArrayEquals("Hash unequal", expected.contentsHash, actual.contentsHash);
|
|
||||||
assertArrayEquals("IV unequal", expected.encryptionIV, actual.encryptionIV);
|
|
||||||
assertArrayEquals("Seed unequal", expected.masterSeed, actual.masterSeed);
|
|
||||||
assertArrayEquals("Seed2 unequal", expected.transformSeed, actual.transformSeed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testHeader() throws PwDbOutputException, IOException {
|
|
||||||
ByteArrayOutputStream bActual = new ByteArrayOutputStream();
|
|
||||||
PwDbV3Output pActual = new PwDbV3OutputDebug(mPM, bActual, true);
|
|
||||||
PwDbHeaderV3 header = pActual.outputHeader(bActual);
|
|
||||||
|
|
||||||
ByteArrayOutputStream bExpected = new ByteArrayOutputStream();
|
|
||||||
PwDbHeaderOutputV3 outExpected = new PwDbHeaderOutputV3(mPM.getDbHeader(), bExpected);
|
|
||||||
outExpected.output();
|
|
||||||
|
|
||||||
assertHeadersEquals(mPM.getDbHeader(), header);
|
|
||||||
assertTrue("No output", bActual.toByteArray().length > 0);
|
|
||||||
assertArrayEquals("Header does not match.", bExpected.toByteArray(), bActual.toByteArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testFinalKey() throws PwDbOutputException {
|
|
||||||
ByteArrayOutputStream bActual = new ByteArrayOutputStream();
|
|
||||||
PwDbV3Output pActual = new PwDbV3OutputDebug(mPM, bActual, true);
|
|
||||||
PwDbHeader hActual = pActual.outputHeader(bActual);
|
|
||||||
byte[] finalKey = pActual.getFinalKey(hActual);
|
|
||||||
|
|
||||||
assertArrayEquals("Keys mismatched", mPM.getFinalKey(), finalKey);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testFullWrite() throws IOException, PwDbOutputException {
|
|
||||||
AssetManager am = getContext().getAssets();
|
|
||||||
InputStream is = am.open("test1.kdb");
|
|
||||||
|
|
||||||
// Pull file into byte array (for streaming fun)
|
|
||||||
ByteArrayOutputStream bExpected = new ByteArrayOutputStream();
|
|
||||||
while (true) {
|
|
||||||
int data = is.read();
|
|
||||||
if ( data == -1 ) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
bExpected.write(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteArrayOutputStream bActual = new ByteArrayOutputStream();
|
|
||||||
PwDbV3Output pActual = new PwDbV3OutputDebug(mPM, bActual, true);
|
|
||||||
pActual.output();
|
|
||||||
//pActual.close();
|
|
||||||
|
|
||||||
FileOutputStream fos = new FileOutputStream(TestUtil.getSdPath("test1_out.kdb"));
|
|
||||||
fos.write(bActual.toByteArray());
|
|
||||||
fos.close();
|
|
||||||
assertArrayEquals("Databases do not match.", bExpected.toByteArray(), bActual.toByteArray());
|
|
||||||
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
@@ -1,70 +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.search;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.test.AndroidTestCase;
|
|
||||||
import com.kunzisoft.keepass.database.element.Database;
|
|
||||||
import com.kunzisoft.keepass.database.element.GroupVersioned;
|
|
||||||
|
|
||||||
public class SearchTest extends AndroidTestCase {
|
|
||||||
|
|
||||||
private Database mDb;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setUp() throws Exception {
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
//mDb = TestData.GetDb1(getContext(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testSearch() {
|
|
||||||
GroupVersioned results = mDb.search("Amazon");
|
|
||||||
//assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testBackupIncluded() {
|
|
||||||
updateOmitSetting(false);
|
|
||||||
GroupVersioned results = mDb.search("BackupOnly");
|
|
||||||
|
|
||||||
//assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testBackupExcluded() {
|
|
||||||
updateOmitSetting(true);
|
|
||||||
GroupVersioned results = mDb.search("BackupOnly");
|
|
||||||
|
|
||||||
//assertFalse("Search result found, but should not have been.", results.numbersOfChildEntries() > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateOmitSetting(boolean setting) {
|
|
||||||
Context ctx = getContext();
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
|
|
||||||
SharedPreferences.Editor editor = prefs.edit();
|
|
||||||
|
|
||||||
editor.putBoolean("settings_omitbackup_key", setting);
|
|
||||||
editor.commit();
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +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.stream;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Random;
|
|
||||||
import java.util.zip.GZIPInputStream;
|
|
||||||
import java.util.zip.GZIPOutputStream;
|
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.stream.HashedBlockInputStream;
|
|
||||||
import com.kunzisoft.keepass.stream.HashedBlockOutputStream;
|
|
||||||
|
|
||||||
public class HashedBlock extends TestCase {
|
|
||||||
|
|
||||||
private static Random rand = new Random();
|
|
||||||
|
|
||||||
public void testBlockAligned() throws IOException {
|
|
||||||
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();
|
|
||||||
while ( true ) {
|
|
||||||
byte[] buf = new byte[1024];
|
|
||||||
int read = input.read(buf);
|
|
||||||
if ( read == -1 ) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
decoded.write(buf, 0, read);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] out = decoded.toByteArray();
|
|
||||||
|
|
||||||
assertArrayEquals(orig, out);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testGZIPStream() throws IOException {
|
|
||||||
final int testLength = 32000;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* 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.stream
|
||||||
|
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.Random
|
||||||
|
import java.util.zip.GZIPInputStream
|
||||||
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
|
import junit.framework.TestCase
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.stream.HashedBlockInputStream
|
||||||
|
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
|
||||||
|
|
||||||
|
class HashedBlock : TestCase() {
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun testBlockAligned() {
|
||||||
|
testSize(1024, 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun testOffset() {
|
||||||
|
testSize(1500, 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun testSize(blockSize: Int, bufferSize: Int) {
|
||||||
|
val orig = ByteArray(blockSize)
|
||||||
|
|
||||||
|
rand.nextBytes(orig)
|
||||||
|
|
||||||
|
val bos = ByteArrayOutputStream()
|
||||||
|
val output = HashedBlockOutputStream(bos, bufferSize)
|
||||||
|
output.write(orig)
|
||||||
|
output.close()
|
||||||
|
|
||||||
|
val encoded = bos.toByteArray()
|
||||||
|
|
||||||
|
val bis = ByteArrayInputStream(encoded)
|
||||||
|
val input = HashedBlockInputStream(bis)
|
||||||
|
|
||||||
|
val decoded = ByteArrayOutputStream()
|
||||||
|
while (true) {
|
||||||
|
val buf = ByteArray(1024)
|
||||||
|
val read = input.read(buf)
|
||||||
|
if (read == -1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded.write(buf, 0, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
val out = decoded.toByteArray()
|
||||||
|
|
||||||
|
assertArrayEquals(orig, out)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun testGZIPStream() {
|
||||||
|
val testLength = 32000
|
||||||
|
|
||||||
|
val orig = ByteArray(testLength)
|
||||||
|
rand.nextBytes(orig)
|
||||||
|
|
||||||
|
val bos = ByteArrayOutputStream()
|
||||||
|
val hos = HashedBlockOutputStream(bos)
|
||||||
|
val zos = GZIPOutputStream(hos)
|
||||||
|
|
||||||
|
zos.write(orig)
|
||||||
|
zos.close()
|
||||||
|
|
||||||
|
val compressed = bos.toByteArray()
|
||||||
|
val bis = ByteArrayInputStream(compressed)
|
||||||
|
val his = HashedBlockInputStream(bis)
|
||||||
|
val zis = GZIPInputStream(his)
|
||||||
|
|
||||||
|
val uncompressed = ByteArray(testLength)
|
||||||
|
|
||||||
|
var read = 0
|
||||||
|
while (read != -1 && testLength - read > 0) {
|
||||||
|
read += zis.read(uncompressed, read, testLength - read)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
assertArrayEquals("Output not equal to input", orig, uncompressed)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val rand = Random()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
class StringUtilTest : TestCase() {
|
||||||
|
private val text = "AbCdEfGhIj"
|
||||||
|
private val search = "BcDe"
|
||||||
|
private val badSearch = "Ed"
|
||||||
|
|
||||||
|
private val repText = "AbCtestingaBc"
|
||||||
|
private val repSearch = "ABc"
|
||||||
|
private val repSearchBad = "CCCCCC"
|
||||||
|
private val repNew = "12345"
|
||||||
|
private val repResult = "12345testing12345"
|
||||||
|
|
||||||
|
fun testIndexOfIgnoreCase1() {
|
||||||
|
assertEquals(1, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testIndexOfIgnoreCase2() {
|
||||||
|
assertEquals(-1f, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH).toFloat(), 2f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testIndexOfIgnoreCase3() {
|
||||||
|
assertEquals(-1, StringUtil.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReplaceAllIgnoresCase1() {
|
||||||
|
assertEquals(repResult, StringUtil.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testReplaceAllIgnoresCase2() {
|
||||||
|
assertEquals(repText, StringUtil.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="com.kunzisoft.keepass"
|
package="com.kunzisoft.keepass"
|
||||||
android:installLocation="auto">
|
android:installLocation="auto">
|
||||||
<supports-screens
|
<supports-screens
|
||||||
@@ -8,7 +7,8 @@
|
|||||||
android:normalScreens="true"
|
android:normalScreens="true"
|
||||||
android:largeScreens="true"
|
android:largeScreens="true"
|
||||||
android:anyDensity="true" />
|
android:anyDensity="true" />
|
||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="file" />
|
<data android:scheme="file" />
|
||||||
<data android:scheme="content" />
|
<data android:scheme="content" />
|
||||||
<data android:mimeType="application/octet-stream" />
|
<data android:mimeType="*/*" />
|
||||||
<data android:host="*" />
|
<data android:host="*" />
|
||||||
<data android:pathPattern=".*\\.kdb" />
|
<data android:pathPattern=".*\\.kdb" />
|
||||||
<data android:pathPattern=".*\\..*\\.kdb" />
|
<data android:pathPattern=".*\\..*\\.kdb" />
|
||||||
@@ -71,29 +71,28 @@
|
|||||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter tools:ignore="AppLinkUrlError">
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
<data android:mimeType="application/octet-stream"/>
|
<data android:scheme="file" />
|
||||||
</intent-filter>
|
<data android:scheme="content" />
|
||||||
</activity>
|
<data android:mimeType="application/octet-stream" />
|
||||||
<!-- Folder picker -->
|
<data android:mimeType="application/x-kdb" />
|
||||||
<provider
|
<data android:mimeType="application/x-kdbx" />
|
||||||
android:name="android.support.v4.content.FileProvider"
|
<data android:mimeType="application/x-keepass" />
|
||||||
android:authorities="${applicationId}.provider"
|
<data android:host="*" />
|
||||||
android:exported="false"
|
<data android:pathPattern=".*" />
|
||||||
android:grantUriPermissions="true">
|
<data android:pathPattern=".*\\.*" />
|
||||||
<meta-data
|
<data android:pathPattern=".*\\..*\\.*" />
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
<data android:pathPattern=".*\\..*\\..*\\.*" />
|
||||||
android:resource="@xml/nnf_provider_paths" />
|
<data android:pathPattern=".*\\..*\\..*\\..*\\.*" />
|
||||||
</provider>
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.*" />
|
||||||
<activity
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||||
android:name=".activities.stylish.FilePickerStylishActivity"
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||||
android:label="@string/app_name">
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||||
<intent-filter>
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||||
<action android:name="android.intent.action.GET_CONTENT" />
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Main Activity -->
|
<!-- Main Activity -->
|
||||||
@@ -129,9 +128,11 @@
|
|||||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||||
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" />
|
<activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" />
|
||||||
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
|
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
|
||||||
android:label="@string/keyboard_name">
|
android:label="@string/keyboard_name"
|
||||||
|
android:exported="true">
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.MagikIMESettings"
|
<activity android:name="com.kunzisoft.keepass.settings.MagikIMESettings"
|
||||||
android:label="@string/keyboard_setting_label">
|
android:label="@string/keyboard_setting_label">
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
|||||||
v7<EFBFBD><07>gx<67><78><EFBFBD>"<04>Dm<44>]tIWRP<52>g<18>y<15>/˰1<CBB0><31><13>X<0B><>fW[<5B>F%<25><1E>\<5C>up4
|
|
||||||
<EFBFBD><EFBFBD>-t;<3B>z<EFBFBD>
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<KeyFile>
|
|
||||||
<Meta>
|
|
||||||
<Version>1.00</Version>
|
|
||||||
</Meta>
|
|
||||||
<Key>
|
|
||||||
<Data>zaTWphVNtRbspnwkqjy8FGTy5IqCUx9+FNb5H+VdB24=</Data>
|
|
||||||
</Key>
|
|
||||||
</KeyFile>
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.activities
|
|||||||
|
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v7.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
|||||||
@@ -19,15 +19,14 @@
|
|||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.support.design.widget.CollapsingToolbarLayout
|
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
import android.support.v7.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.support.v7.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@@ -45,22 +44,27 @@ import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
|||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields
|
|
||||||
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
|
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.Util
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.EntryContentsView
|
import com.kunzisoft.keepass.view.EntryContentsView
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class EntryActivity : LockingHideActivity() {
|
class EntryActivity : LockingHideActivity() {
|
||||||
|
|
||||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||||
private var titleIconView: ImageView? = null
|
private var titleIconView: ImageView? = null
|
||||||
|
private var historyView: View? = null
|
||||||
private var entryContentsView: EntryContentsView? = null
|
private var entryContentsView: EntryContentsView? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
|
|
||||||
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
private var mEntry: EntryVersioned? = null
|
private var mEntry: EntryVersioned? = null
|
||||||
|
private var mIsHistory: Boolean = false
|
||||||
|
|
||||||
private var mShowPassword: Boolean = false
|
private var mShowPassword: Boolean = false
|
||||||
|
|
||||||
private var clipboardHelper: ClipboardHelper? = null
|
private var clipboardHelper: ClipboardHelper? = null
|
||||||
@@ -78,31 +82,14 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
val currentDatabase = Database.getInstance()
|
mDatabase = Database.getInstance()
|
||||||
readOnly = currentDatabase.isReadOnly || readOnly
|
mReadOnly = mDatabase!!.isReadOnly || mReadOnly
|
||||||
|
|
||||||
mShowPassword = !PreferencesUtil.isPasswordMask(this)
|
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
|
// Retrieve the textColor to tint the icon
|
||||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||||
iconColor = taIconColor.getColor(0, Color.WHITE)
|
iconColor = taIconColor.getColor(0, Color.BLACK)
|
||||||
taIconColor.recycle()
|
taIconColor.recycle()
|
||||||
|
|
||||||
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
|
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
|
||||||
@@ -111,6 +98,7 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
// Get views
|
// Get views
|
||||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||||
titleIconView = findViewById(R.id.entry_icon)
|
titleIconView = findViewById(R.id.entry_icon)
|
||||||
|
historyView = findViewById(R.id.history_container)
|
||||||
entryContentsView = findViewById(R.id.entry_contents)
|
entryContentsView = findViewById(R.id.entry_contents)
|
||||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||||
|
|
||||||
@@ -122,6 +110,29 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
// Get Entry from UUID
|
||||||
|
try {
|
||||||
|
val keyEntry: PwNodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
|
||||||
|
mEntry = mDatabase?.getEntryById(keyEntry)
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
Log.e(TAG, "Unable to retrieve the entry key")
|
||||||
|
}
|
||||||
|
|
||||||
|
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||||
|
if (historyPosition >= 0) {
|
||||||
|
mIsHistory = true
|
||||||
|
mEntry = mEntry?.getHistory()?.get(historyPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mEntry == null) {
|
||||||
|
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last access time.
|
||||||
|
mEntry?.touch(modified = false, touchParents = false)
|
||||||
|
|
||||||
mEntry?.let { entry ->
|
mEntry?.let { entry ->
|
||||||
// Fill data in resume to update from EntryEditActivity
|
// Fill data in resume to update from EntryEditActivity
|
||||||
fillEntryDataInContentsView(entry)
|
fillEntryDataInContentsView(entry)
|
||||||
@@ -152,90 +163,126 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor)
|
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor)
|
||||||
|
|
||||||
// Assign title text
|
// Assign title text
|
||||||
val entryTitle = entry.getVisualTitle()
|
val entryTitle = entry.title
|
||||||
collapsingToolbarLayout?.title = entryTitle
|
collapsingToolbarLayout?.title = entryTitle
|
||||||
toolbar?.title = entryTitle
|
toolbar?.title = entryTitle
|
||||||
|
|
||||||
// Assign basic fields
|
// Assign basic fields
|
||||||
entryContentsView?.assignUserName(entry.username)
|
entryContentsView?.assignUserName(entry.username)
|
||||||
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
|
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
|
||||||
|
database.startManageEntry(entry)
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
||||||
getString(R.string.copy_field,
|
getString(R.string.copy_field,
|
||||||
getString(R.string.entry_user_name)))
|
getString(R.string.entry_user_name)))
|
||||||
|
database.stopManageEntry(entry)
|
||||||
})
|
})
|
||||||
|
|
||||||
val allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
|
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||||
entryContentsView?.assignPassword(entry.password, allowCopyPassword)
|
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||||
if (allowCopyPassword) {
|
val allowCopyPasswordAndProtectedFields =
|
||||||
|
PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
|
||||||
|
|
||||||
|
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
|
||||||
|
AlertDialog.Builder(this@EntryActivity)
|
||||||
|
.setMessage(getString(R.string.allow_copy_password_warning) +
|
||||||
|
"\n\n" +
|
||||||
|
getString(R.string.clipboard_warning))
|
||||||
|
.create().apply {
|
||||||
|
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) {dialog, _ ->
|
||||||
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
|
||||||
|
dialog.dismiss()
|
||||||
|
fillEntryDataInContentsView(entry)
|
||||||
|
}
|
||||||
|
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
||||||
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
|
||||||
|
dialog.dismiss()
|
||||||
|
fillEntryDataInContentsView(entry)
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
|
||||||
|
if (allowCopyPasswordAndProtectedFields) {
|
||||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
||||||
|
database.startManageEntry(entry)
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
||||||
getString(R.string.copy_field,
|
getString(R.string.copy_field,
|
||||||
getString(R.string.entry_password)))
|
getString(R.string.entry_password)))
|
||||||
|
database.stopManageEntry(entry)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// If dialog not already shown
|
// If dialog not already shown
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)) {
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
|
||||||
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 {
|
} else {
|
||||||
entryContentsView?.assignPasswordCopyListener(null)
|
entryContentsView?.assignPasswordCopyListener(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entryContentsView?.assignURL(entry.url)
|
entryContentsView?.assignURL(entry.url)
|
||||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
|
||||||
entryContentsView?.assignComment(entry.notes)
|
entryContentsView?.assignComment(entry.notes)
|
||||||
|
|
||||||
// Assign custom fields
|
// Assign custom fields
|
||||||
if (entry.allowExtraFields()) {
|
if (entry.allowCustomFields()) {
|
||||||
entryContentsView?.clearExtraFields()
|
entryContentsView?.clearExtraFields()
|
||||||
|
|
||||||
entry.fields.doActionToAllCustomProtectedField { label, value ->
|
for (element in entry.customFields.entries) {
|
||||||
val showAction = !value.isProtected || PreferencesUtil.allowCopyPasswordAndProtectedFields(this@EntryActivity)
|
val label = element.key
|
||||||
entryContentsView?.addExtraField(label, value, showAction, View.OnClickListener {
|
val value = element.value
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
|
||||||
value.toString(),
|
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||||
getString(R.string.copy_field, label)
|
if (allowCopyProtectedField) {
|
||||||
)
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
|
||||||
})
|
clipboardHelper?.timeoutCopyToClipboard(
|
||||||
|
value.toString(),
|
||||||
|
getString(R.string.copy_field, label)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// If dialog not already shown
|
||||||
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||||
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
||||||
|
} else {
|
||||||
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
||||||
|
|
||||||
// Assign dates
|
// Assign dates
|
||||||
entry.creationTime.date?.let {
|
entryContentsView?.assignCreationDate(entry.creationTime)
|
||||||
entryContentsView?.assignCreationDate(it)
|
entryContentsView?.assignModificationDate(entry.lastModificationTime)
|
||||||
}
|
entryContentsView?.assignLastAccessDate(entry.lastAccessTime)
|
||||||
entry.lastModificationTime.date?.let {
|
entryContentsView?.setExpires(entry.isCurrentlyExpires)
|
||||||
entryContentsView?.assignModificationDate(it)
|
if (entry.expires) {
|
||||||
}
|
entryContentsView?.assignExpiresDate(entry.expiryTime)
|
||||||
entry.lastAccessTime.date?.let {
|
|
||||||
entryContentsView?.assignLastAccessDate(it)
|
|
||||||
}
|
|
||||||
val expires = entry.expiryTime.date
|
|
||||||
if (entry.isExpires && expires != null) {
|
|
||||||
entryContentsView?.assignExpiresDate(expires)
|
|
||||||
} else {
|
} else {
|
||||||
entryContentsView?.assignExpiresDate(getString(R.string.never))
|
entryContentsView?.assignExpiresDate(getString(R.string.never))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assign special data
|
||||||
|
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||||
|
|
||||||
|
// Manage history
|
||||||
|
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
||||||
|
if (mIsHistory) {
|
||||||
|
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||||
|
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||||
|
taColorAccent.recycle()
|
||||||
|
}
|
||||||
|
val entryHistory = entry.getHistory()
|
||||||
|
// isMainEntry = not an history
|
||||||
|
val showHistoryView = entryHistory.isNotEmpty()
|
||||||
|
entryContentsView?.showHistory(showHistoryView)
|
||||||
|
if (showHistoryView) {
|
||||||
|
entryContentsView?.assignHistory(entryHistory)
|
||||||
|
entryContentsView?.onHistoryClick { historyItem, position ->
|
||||||
|
launch(this, historyItem, true, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +315,7 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
inflater.inflate(R.menu.entry, menu)
|
inflater.inflate(R.menu.entry, menu)
|
||||||
inflater.inflate(R.menu.database_lock, menu)
|
inflater.inflate(R.menu.database_lock, menu)
|
||||||
|
|
||||||
if (readOnly) {
|
if (mReadOnly) {
|
||||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +350,7 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
|
|
||||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||||
menu: Menu) {
|
menu: Menu) {
|
||||||
if (entryContentsView?.isUserNamePresent == true
|
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
|
||||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||||
findViewById(R.id.entry_user_name_action_image),
|
findViewById(R.id.entry_user_name_action_image),
|
||||||
{
|
{
|
||||||
@@ -314,23 +361,29 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
{
|
{
|
||||||
// Launch autofill settings
|
// Launch autofill settings
|
||||||
startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java))
|
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),
|
if (!entryCopyEducationPerformed) {
|
||||||
{
|
// entryEditEducationPerformed
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||||
},
|
toolbar!!.findViewById(R.id.menu_edit),
|
||||||
{
|
{
|
||||||
// Open Keepass doc to create field references
|
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||||
startActivity(Intent(Intent.ACTION_VIEW,
|
},
|
||||||
Uri.parse(getString(R.string.field_references_url))))
|
{
|
||||||
}))
|
// Open Keepass doc to create field references
|
||||||
;
|
startActivity(Intent(Intent.ACTION_VIEW,
|
||||||
|
UriUtil.parse(getString(R.string.field_references_url))))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this)
|
R.id.menu_contribute -> {
|
||||||
|
MenuUtil.onContributionItemSelected(this)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.menu_toggle_pass -> {
|
R.id.menu_toggle_pass -> {
|
||||||
mShowPassword = !mShowPassword
|
mShowPassword = !mShowPassword
|
||||||
@@ -354,11 +407,7 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
url = "http://$url"
|
url = "http://$url"
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
UriUtil.gotoUrl(this, url)
|
||||||
Util.gotoUrl(this, url)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -389,13 +438,16 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
companion object {
|
companion object {
|
||||||
private val TAG = EntryActivity::class.java.name
|
private val TAG = EntryActivity::class.java.name
|
||||||
|
|
||||||
const val KEY_ENTRY = "entry"
|
const val KEY_ENTRY = "KEY_ENTRY"
|
||||||
|
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
||||||
|
|
||||||
fun launch(activity: Activity, pw: EntryVersioned, readOnly: Boolean) {
|
fun launch(activity: Activity, entry: EntryVersioned, readOnly: Boolean, historyPosition: Int? = null) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, pw.nodeId)
|
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
||||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||||
|
if (historyPosition != null)
|
||||||
|
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||||
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,30 +22,30 @@ import android.app.Activity
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.support.v7.widget.Toolbar
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ScrollView
|
import android.widget.ScrollView
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
|
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||||
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.database.element.*
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.view.EntryEditContentsView
|
import com.kunzisoft.keepass.view.EntryEditContentsView
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPickerListener, GeneratePasswordDialogFragment.GeneratePasswordListener {
|
class EntryEditActivity : LockingHideActivity(),
|
||||||
|
IconPickerDialogFragment.IconPickerListener,
|
||||||
|
GeneratePasswordDialogFragment.GeneratePasswordListener {
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
@@ -58,11 +58,12 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var scrollView: ScrollView? = null
|
private var scrollView: ScrollView? = null
|
||||||
|
|
||||||
private var entryEditContentsView: EntryEditContentsView? = null
|
private var entryEditContentsView: EntryEditContentsView? = null
|
||||||
|
|
||||||
private var saveView: View? = null
|
private var saveView: View? = null
|
||||||
|
|
||||||
|
// Dialog thread
|
||||||
|
private var progressDialogThread: ProgressDialogThread? = null
|
||||||
|
|
||||||
// Education
|
// Education
|
||||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
mDatabase = Database.getInstance()
|
mDatabase = Database.getInstance()
|
||||||
|
|
||||||
// Entry is retrieve, it's an entry to update
|
// Entry is retrieve, it's an entry to update
|
||||||
intent.getParcelableExtra<PwNodeId<*>>(KEY_ENTRY)?.let {
|
intent.getParcelableExtra<PwNodeId<UUID>>(KEY_ENTRY)?.let {
|
||||||
mIsNew = false
|
mIsNew = false
|
||||||
// Create an Entry copy to modify from the database entry
|
// Create an Entry copy to modify from the database entry
|
||||||
mEntry = mDatabase?.getEntryById(it)
|
mEntry = mDatabase?.getEntryById(it)
|
||||||
@@ -103,16 +104,14 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the icon after an orientation change
|
// Create the new entry from the current one
|
||||||
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
if (savedInstanceState == null
|
||||||
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY) as EntryVersioned
|
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||||
} else {
|
|
||||||
mEntry?.let { entry ->
|
mEntry?.let { entry ->
|
||||||
// Create a copy to modify
|
// Create a copy to modify
|
||||||
mNewEntry = EntryVersioned(entry).also { newEntry ->
|
mNewEntry = EntryVersioned(entry).also { newEntry ->
|
||||||
|
|
||||||
// WARNING Remove the parent to keep memory with parcelable
|
// WARNING Remove the parent to keep memory with parcelable
|
||||||
newEntry.parent = null
|
newEntry.removeParent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +120,11 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
// Parent is retrieve, it's a new entry to create
|
// Parent is retrieve, it's a new entry to create
|
||||||
intent.getParcelableExtra<PwNodeId<*>>(KEY_PARENT)?.let {
|
intent.getParcelableExtra<PwNodeId<*>>(KEY_PARENT)?.let {
|
||||||
mIsNew = true
|
mIsNew = true
|
||||||
mNewEntry = mDatabase?.createEntry()
|
// Create an empty new entry
|
||||||
|
if (savedInstanceState == null
|
||||||
|
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||||
|
mNewEntry = mDatabase?.createEntry()
|
||||||
|
}
|
||||||
mParent = mDatabase?.getGroupById(it)
|
mParent = mDatabase?.getGroupById(it)
|
||||||
// Add the default icon
|
// Add the default icon
|
||||||
mDatabase?.drawFactory?.let { iconFactory ->
|
mDatabase?.drawFactory?.let { iconFactory ->
|
||||||
@@ -129,6 +132,12 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retrieve the new entry after an orientation change
|
||||||
|
if (savedInstanceState != null
|
||||||
|
&& savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||||
|
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
|
||||||
|
}
|
||||||
|
|
||||||
// Close the activity if entry or parent can't be retrieve
|
// Close the activity if entry or parent can't be retrieve
|
||||||
if (mNewEntry == null || mParent == null) {
|
if (mNewEntry == null || mParent == null) {
|
||||||
finish()
|
finish()
|
||||||
@@ -150,39 +159,23 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
saveView = findViewById(R.id.entry_edit_save)
|
saveView = findViewById(R.id.entry_edit_save)
|
||||||
saveView?.setOnClickListener { saveEntry() }
|
saveView?.setOnClickListener { saveEntry() }
|
||||||
|
|
||||||
entryEditContentsView?.allowCustomField(mNewEntry?.allowExtraFields() == true) { addNewCustomField() }
|
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) { addNewCustomField() }
|
||||||
|
|
||||||
// Verify the education views
|
// Verify the education views
|
||||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||||
entryEditActivityEducation?.let {
|
|
||||||
Handler().post { performedNextEducation(it) }
|
// Create progress dialog
|
||||||
|
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
||||||
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
|
if (result.isSuccess)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
private fun populateViewsWithEntry(newEntry: EntryVersioned) {
|
||||||
// Don't start the field reference manager, we want to see the raw ref
|
// Don't start the field reference manager, we want to see the raw ref
|
||||||
mDatabase?.stopManageEntry(newEntry)
|
mDatabase?.stopManageEntry(newEntry)
|
||||||
@@ -193,12 +186,14 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
// Set info in view
|
// Set info in view
|
||||||
entryEditContentsView?.apply {
|
entryEditContentsView?.apply {
|
||||||
title = newEntry.title
|
title = newEntry.title
|
||||||
username = newEntry.username
|
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
|
||||||
url = newEntry.url
|
url = newEntry.url
|
||||||
password = newEntry.password
|
password = newEntry.password
|
||||||
notes = newEntry.notes
|
notes = newEntry.notes
|
||||||
newEntry.fields.doActionToAllCustomProtectedField { key, value ->
|
for (entry in newEntry.customFields.entries) {
|
||||||
addNewCustomField(key, value)
|
post {
|
||||||
|
addNewCustomField(entry.key, entry.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,6 +205,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
newEntry.apply {
|
newEntry.apply {
|
||||||
// Build info from view
|
// Build info from view
|
||||||
entryEditContentsView?.let { entryView ->
|
entryEditContentsView?.let { entryView ->
|
||||||
|
removeAllFields()
|
||||||
title = entryView.title
|
title = entryView.title
|
||||||
username = entryView.username
|
username = entryView.username
|
||||||
url = entryView.url
|
url = entryView.url
|
||||||
@@ -243,8 +239,6 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
*/
|
*/
|
||||||
private fun addNewCustomField() {
|
private fun addNewCustomField() {
|
||||||
entryEditContentsView?.addNewCustomField()
|
entryEditContentsView?.addNewCustomField()
|
||||||
// Scroll bottom
|
|
||||||
scrollView?.post { scrollView?.fullScroll(ScrollView.FOCUS_DOWN) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -255,53 +249,50 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
// Launch a validation and show the error if present
|
// Launch a validation and show the error if present
|
||||||
if (entryEditContentsView?.isValid() == true) {
|
if (entryEditContentsView?.isValid() == true) {
|
||||||
// Clone the entry
|
// Clone the entry
|
||||||
mDatabase?.let { database ->
|
mNewEntry?.let { newEntry ->
|
||||||
mNewEntry?.let { newEntry ->
|
|
||||||
|
|
||||||
// WARNING Add the parent previously deleted
|
// WARNING Add the parent previously deleted
|
||||||
newEntry.parent = mEntry?.parent
|
newEntry.parent = mEntry?.parent
|
||||||
// Build info
|
// Build info
|
||||||
newEntry.lastAccessTime = PwDate()
|
newEntry.lastAccessTime = PwDate()
|
||||||
newEntry.lastModificationTime = PwDate()
|
newEntry.lastModificationTime = PwDate()
|
||||||
|
|
||||||
populateEntryWithViews(newEntry)
|
populateEntryWithViews(newEntry)
|
||||||
|
|
||||||
// Open a progress dialog and save entry
|
// Open a progress dialog and save entry
|
||||||
var actionRunnable: ActionRunnable? = null
|
if (mIsNew) {
|
||||||
val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() {
|
mParent?.let { parent ->
|
||||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
progressDialogThread?.startDatabaseCreateEntry(
|
||||||
if (actionNodeValues.result.isSuccess)
|
newEntry,
|
||||||
finish()
|
parent,
|
||||||
}
|
!mReadOnly
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (mIsNew) {
|
} else {
|
||||||
mParent?.let { parent ->
|
mEntry?.let { oldEntry ->
|
||||||
actionRunnable = AddEntryRunnable(this@EntryEditActivity,
|
progressDialogThread?.startDatabaseUpdateEntry(
|
||||||
database,
|
oldEntry,
|
||||||
newEntry,
|
newEntry,
|
||||||
parent,
|
!mReadOnly
|
||||||
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 onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
progressDialogThread?.registerProgressTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
progressDialogThread?.unregisterProgressTask()
|
||||||
|
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
@@ -309,9 +300,39 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
inflater.inflate(R.menu.database_lock, menu)
|
inflater.inflate(R.menu.database_lock, menu)
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||||
|
|
||||||
|
entryEditActivityEducation?.let {
|
||||||
|
Handler().post { performedNextEducation(it) }
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||||
|
val passwordView = entryEditContentsView?.generatePasswordView
|
||||||
|
val addNewFieldView = entryEditContentsView?.addNewFieldView
|
||||||
|
|
||||||
|
val generatePasswordEducationPerformed = passwordView != null
|
||||||
|
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||||
|
passwordView,
|
||||||
|
{
|
||||||
|
openPasswordGenerator()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(entryEditActivityEducation)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!generatePasswordEducationPerformed) {
|
||||||
|
// entryNewFieldEducationPerformed
|
||||||
|
mNewEntry != null && mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
|
||||||
|
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
|
||||||
|
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||||
|
addNewFieldView,
|
||||||
|
{
|
||||||
|
addNewCustomField()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_lock -> {
|
R.id.menu_lock -> {
|
||||||
@@ -319,7 +340,10 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this)
|
R.id.menu_contribute -> {
|
||||||
|
MenuUtil.onContributionItemSelected(this)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
android.R.id.home -> finish()
|
android.R.id.home -> finish()
|
||||||
}
|
}
|
||||||
@@ -334,7 +358,10 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putParcelable(KEY_NEW_ENTRY, mNewEntry)
|
mNewEntry?.let {
|
||||||
|
populateEntryWithViews(it)
|
||||||
|
outState.putParcelable(KEY_NEW_ENTRY, it)
|
||||||
|
}
|
||||||
|
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.Manifest
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -29,81 +29,59 @@ import android.os.Bundle
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.preference.PreferenceManager
|
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.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.CreateFileDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileInformationDialogFragment
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
|
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
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.action.ProgressDialogThread
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
import com.kunzisoft.keepass.fileselect.DeleteFileHistoryAsyncTask
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||||
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.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import net.cachapa.expandablelayout.ExpandableLayout
|
import com.kunzisoft.keepass.view.asError
|
||||||
import permissions.dispatcher.*
|
import kotlinx.android.synthetic.main.activity_file_selection.*
|
||||||
import java.io.File
|
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
import java.net.URLDecoder
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@RuntimePermissions
|
|
||||||
class FileDatabaseSelectActivity : StylishActivity(),
|
class FileDatabaseSelectActivity : StylishActivity(),
|
||||||
CreateFileDialogFragment.DefinePathDialogListener,
|
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener,
|
|
||||||
FileDatabaseHistoryAdapter.FileItemOpenListener,
|
|
||||||
FileDatabaseHistoryAdapter.FileSelectClearListener,
|
|
||||||
FileDatabaseHistoryAdapter.FileInformationShowListener {
|
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var fileListContainer: View? = null
|
private var fileListContainer: View? = null
|
||||||
private var createButtonView: View? = null
|
private var createButtonView: View? = null
|
||||||
private var browseButtonView: View? = null
|
private var openDatabaseButtonView: 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
|
// Adapter to manage database history list
|
||||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||||
|
|
||||||
private var mFileDatabaseHistory: FileDatabaseHistory? = null
|
private var mFileDatabaseHistoryAction: FileDatabaseHistoryAction? = null
|
||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
private var mKeyFileHelper: KeyFileHelper? = null
|
private var mOpenFileHelper: OpenFileHelper? = null
|
||||||
|
|
||||||
private var mDefaultPath: String? = null
|
private var progressDialogThread: ProgressDialogThread? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
mFileDatabaseHistory = FileDatabaseHistory.getInstance(WeakReference(applicationContext))
|
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||||
|
|
||||||
setContentView(R.layout.activity_file_selection)
|
setContentView(R.layout.activity_file_selection)
|
||||||
fileListContainer = findViewById(R.id.container_file_list)
|
fileListContainer = findViewById(R.id.container_file_list)
|
||||||
@@ -112,132 +90,122 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
toolbar.title = ""
|
toolbar.title = ""
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
openFileNameView = findViewById(R.id.file_filename)
|
// Create button
|
||||||
|
createButtonView = findViewById(R.id.create_database_button)
|
||||||
// Set the initial value of the filename
|
if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
mDefaultPath = (Environment.getExternalStorageDirectory().absolutePath
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
+ getString(R.string.database_file_path_default)
|
type = "application/x-keepass"
|
||||||
+ getString(R.string.database_file_name_default)
|
}.resolveActivity(packageManager) == null) {
|
||||||
+ getString(R.string.database_file_extension_default))
|
// No Activity found that can handle this intent.
|
||||||
openFileNameView?.setHint(R.string.open_link_database)
|
createButtonView?.visibility = View.GONE
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
|
else{
|
||||||
|
// There is an activity which can handle this intent.
|
||||||
|
createButtonView?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
createButtonView?.setOnClickListener { createNewFile() }
|
||||||
|
|
||||||
|
mOpenFileHelper = OpenFileHelper(this)
|
||||||
|
openDatabaseButtonView = findViewById(R.id.open_database_button)
|
||||||
|
openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
||||||
|
|
||||||
// History list
|
// History list
|
||||||
val databaseFileListView = findViewById<RecyclerView>(R.id.file_list)
|
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||||
databaseFileListView.layoutManager = LinearLayoutManager(this)
|
fileDatabaseHistoryRecyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
|
||||||
|
// Removes blinks
|
||||||
// Open button
|
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
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
|
// Construct adapter with listeners
|
||||||
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this@FileDatabaseSelectActivity,
|
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
|
||||||
mFileDatabaseHistory?.databaseUriList ?: ArrayList())
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||||
mAdapterDatabaseHistory?.setOnItemClickListener(this)
|
UriUtil.parse(fileDatabaseHistoryEntityToOpen.databaseUri)?.let { databaseFileUri ->
|
||||||
mAdapterDatabaseHistory?.setFileSelectClearListener(this)
|
launchPasswordActivity(
|
||||||
mAdapterDatabaseHistory?.setFileInformationShowListener(this)
|
databaseFileUri,
|
||||||
databaseFileListView.adapter = mAdapterDatabaseHistory
|
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
|
||||||
|
}
|
||||||
|
updateFileListVisibility()
|
||||||
|
}
|
||||||
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||||
|
// Remove from app database
|
||||||
|
mFileDatabaseHistoryAction?.deleteFileDatabaseHistory(fileDatabaseHistoryToDelete) { fileHistoryDeleted ->
|
||||||
|
// Remove from adapter
|
||||||
|
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
|
||||||
|
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
|
||||||
|
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
|
updateFileListVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
|
||||||
|
mFileDatabaseHistoryAction?.addOrUpdateFileDatabaseHistory(fileDatabaseHistoryWithNewAlias)
|
||||||
|
}
|
||||||
|
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
||||||
|
|
||||||
// Load default database if not an orientation change
|
// Load default database if not an orientation change
|
||||||
if (!(savedInstanceState != null
|
if (!(savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(EXTRA_STAY)
|
&& savedInstanceState.containsKey(EXTRA_STAY)
|
||||||
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
val fileName = prefs.getString(PasswordActivity.KEY_DEFAULT_FILENAME, "")
|
val databasePath = prefs.getString(PasswordActivity.KEY_DEFAULT_DATABASE_PATH, "")
|
||||||
|
|
||||||
if (fileName != null && fileName.isNotEmpty()) {
|
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
||||||
val dbUri = UriUtil.parseUriFile(fileName)
|
launchPasswordActivityWithPath(databaseFileUri)
|
||||||
var scheme: String? = null
|
} ?: run {
|
||||||
if (dbUri != null)
|
Log.i(TAG, "Unable to launch Password Activity")
|
||||||
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)) }
|
// Retrieve the database URI provided by file manager after an orientation change
|
||||||
|
if (savedInstanceState != null
|
||||||
|
&& savedInstanceState.containsKey(EXTRA_DATABASE_URI)) {
|
||||||
|
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach the dialog thread to this activity
|
||||||
|
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_CREATE_TASK -> {
|
||||||
|
// TODO Check
|
||||||
|
// mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
|
// updateFileListVisibility()
|
||||||
|
GroupActivity.launch(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
/**
|
||||||
// If no recent files
|
* Create a new file by calling the content provider
|
||||||
if (createButtonView != null
|
*/
|
||||||
&& mFileDatabaseHistory != null
|
@SuppressLint("InlinedApi")
|
||||||
&& !mFileDatabaseHistory!!.hasRecentFiles() && fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
private fun createNewFile() {
|
||||||
createButtonView!!,
|
try {
|
||||||
{
|
startActivityForResult(Intent(
|
||||||
openCreateFileDialogFragmentWithPermissionCheck()
|
Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
},
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
{
|
type = "application/x-keepass"
|
||||||
// But if the user cancel, it can also select a database
|
putExtra(Intent.EXTRA_TITLE, getString(R.string.database_file_name_default) +
|
||||||
performedNextEducation(fileDatabaseSelectActivityEducation)
|
getString(R.string.database_file_extension_default))
|
||||||
}))
|
},
|
||||||
else if (browseButtonView != null
|
CREATE_FILE_REQUEST_CODE)
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
} catch (e: Exception) {
|
||||||
browseButtonView!!,
|
BrowserDialogFragment().show(supportFragmentManager, "browserDialog")
|
||||||
{tapTargetView ->
|
}
|
||||||
tapTargetView?.let {
|
|
||||||
mKeyFileHelper?.openFileOnClickViewListener?.onClick(it)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fileSelectExpandableButtonView?.let {
|
|
||||||
fileDatabaseSelectActivityEducation
|
|
||||||
.checkAndPerformedOpenLinkDatabaseEducation(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
))
|
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||||
val error = getString(R.string.file_not_found_content)
|
val error = getString(R.string.file_not_found_content)
|
||||||
Toast.makeText(this@FileDatabaseSelectActivity,
|
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
error, Toast.LENGTH_LONG).show()
|
|
||||||
Log.e(TAG, error, e)
|
Log.e(TAG, error, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(fileName: String, keyFile: String) {
|
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
||||||
fileName, keyFile)
|
databaseUri, keyFile)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
fileNoFoundAction(e)
|
fileNoFoundAction(e)
|
||||||
}
|
}
|
||||||
@@ -245,7 +213,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
||||||
fileName, keyFile)
|
databaseUri, keyFile)
|
||||||
finish()
|
finish()
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
fileNoFoundAction(e)
|
fileNoFoundAction(e)
|
||||||
@@ -255,7 +223,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
try {
|
try {
|
||||||
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||||
fileName, keyFile,
|
databaseUri, keyFile,
|
||||||
assistStructure)
|
assistStructure)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
fileNoFoundAction(e)
|
fileNoFoundAction(e)
|
||||||
@@ -265,8 +233,25 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivityWithPath(path: String) {
|
private fun launchGroupActivity(readOnly: Boolean) {
|
||||||
launchPasswordActivity(path, "")
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
|
{
|
||||||
|
GroupActivity.launch(this@FileDatabaseSelectActivity, readOnly)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity, readOnly)
|
||||||
|
// Do not keep history
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
{ assistStructure ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity, assistStructure, readOnly)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||||
|
launchPasswordActivity(databaseUri, null)
|
||||||
// Delete flickering for kitkat <=
|
// Delete flickering for kitkat <=
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||||
overridePendingTransition(0, 0)
|
overridePendingTransition(0, 0)
|
||||||
@@ -292,29 +277,41 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
val database = Database.getInstance()
|
||||||
|
if (database.loaded) {
|
||||||
|
launchGroupActivity(database.isReadOnly)
|
||||||
|
}
|
||||||
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
updateExternalStorageWarning()
|
updateExternalStorageWarning()
|
||||||
updateFileListVisibility()
|
|
||||||
mAdapterDatabaseHistory!!.notifyDataSetChanged()
|
// Construct adapter with listeners
|
||||||
|
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
||||||
|
databaseFileHistoryList?.let {
|
||||||
|
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(it)
|
||||||
|
updateFileListVisibility()
|
||||||
|
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register progress task
|
||||||
|
progressDialogThread?.registerProgressTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
// Unregister progress task
|
||||||
|
progressDialogThread?.unregisterProgressTask()
|
||||||
|
|
||||||
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
// only to keep the current activity
|
// only to keep the current activity
|
||||||
outState.putBoolean(EXTRA_STAY, true)
|
outState.putBoolean(EXTRA_STAY, true)
|
||||||
}
|
// to retrieve the URI of a created database after an orientation change
|
||||||
|
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||||
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() {
|
private fun updateFileListVisibility() {
|
||||||
@@ -324,133 +321,26 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
fileListContainer?.visibility = View.VISIBLE
|
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(
|
override fun onAssignKeyDialogPositiveClick(
|
||||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
masterPasswordChecked: Boolean, masterPassword: String?,
|
||||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
keyFileChecked: Boolean, keyFile: Uri?) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
UriUtil.parseUriFile(mDatabaseFileUri)?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
|
|
||||||
// Create the new database
|
// Create the new database
|
||||||
ProgressDialogThread(this@FileDatabaseSelectActivity,
|
progressDialogThread?.startDatabaseCreate(
|
||||||
{
|
databaseUri,
|
||||||
CreateDatabaseRunnable(this@FileDatabaseSelectActivity,
|
masterPasswordChecked,
|
||||||
databaseUri,
|
masterPassword,
|
||||||
Database.getInstance(),
|
keyFileChecked,
|
||||||
masterPasswordChecked,
|
keyFile
|
||||||
masterPassword,
|
)
|
||||||
keyFileChecked,
|
|
||||||
keyFile,
|
|
||||||
true, // TODO get readonly
|
|
||||||
LaunchGroupActivityFinish(databaseUri)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
R.string.progress_create)
|
|
||||||
.start()
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val error = "Unable to create database with this password and key file"
|
val error = getString(R.string.error_create_database_file)
|
||||||
Toast.makeText(this, error, Toast.LENGTH_LONG).show()
|
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
Log.e(TAG, error + " " + e.message)
|
Log.e(TAG, error, e)
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,29 +350,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
@@ -490,44 +357,64 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||||
) { uri ->
|
) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) {
|
launchPasswordActivityWithPath(uri)
|
||||||
launchPasswordActivityWithPath(uri.toString())
|
|
||||||
} else {
|
|
||||||
fileSelectExpandableLayout?.expand(false)
|
|
||||||
openFileNameView?.setText(uri.toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
// Retrieve the created URI from the file manager
|
||||||
internal fun showRationaleForExternalStorage(request: PermissionRequest) {
|
if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
||||||
AlertDialog.Builder(this)
|
mDatabaseFileUri = data?.data
|
||||||
.setMessage(R.string.permission_external_storage_rationale_write_database)
|
if (mDatabaseFileUri != null) {
|
||||||
.setPositiveButton(R.string.allow) { _, _ -> request.proceed() }
|
AssignMasterKeyDialogFragment.getInstance(true)
|
||||||
.setNegativeButton(R.string.cancel) { _, _ -> request.cancel() }
|
.show(supportFragmentManager, "passwordDialog")
|
||||||
.show()
|
}
|
||||||
}
|
// else {
|
||||||
|
// TODO Show error
|
||||||
@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 {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||||
|
|
||||||
|
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
||||||
|
// If no recent files
|
||||||
|
val createDatabaseEducationPerformed = createButtonView != null && createButtonView!!.visibility == View.VISIBLE
|
||||||
|
&& mAdapterDatabaseHistory != null
|
||||||
|
&& mAdapterDatabaseHistory!!.itemCount > 0
|
||||||
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||||
|
createButtonView!!,
|
||||||
|
{
|
||||||
|
createNewFile()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// But if the user cancel, it can also select a database
|
||||||
|
performedNextEducation(fileDatabaseSelectActivityEducation)
|
||||||
|
})
|
||||||
|
if (!createDatabaseEducationPerformed) {
|
||||||
|
// selectDatabaseEducationPerformed
|
||||||
|
openDatabaseButtonView != null
|
||||||
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||||
|
openDatabaseButtonView!!,
|
||||||
|
{tapTargetView ->
|
||||||
|
tapTargetView?.let {
|
||||||
|
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
@@ -536,6 +423,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
|
|
||||||
private const val TAG = "FileDbSelectActivity"
|
private const val TAG = "FileDbSelectActivity"
|
||||||
private const val EXTRA_STAY = "EXTRA_STAY"
|
private const val EXTRA_STAY = "EXTRA_STAY"
|
||||||
|
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
|
||||||
|
|
||||||
|
private const val CREATE_FILE_REQUEST_CODE = 3853
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
@@ -550,7 +440,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
fun launchForKeyboardSelection(activity: Activity) {
|
fun launchForKeyboardSelection(activity: Activity) {
|
||||||
KeyboardHelper.startActivityForKeyboardSelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java))
|
EntrySelectionHelper.startActivityForEntrySelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.SearchManager
|
import android.app.SearchManager
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
@@ -29,17 +28,19 @@ import android.graphics.Color
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import android.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.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||||
@@ -48,38 +49,44 @@ import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
|||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
|
||||||
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
|
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||||
import com.kunzisoft.keepass.database.action.node.*
|
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
|
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.view.AddNodeButtonView
|
import com.kunzisoft.keepass.view.AddNodeButtonView
|
||||||
import net.cachapa.expandablelayout.ExpandableLayout
|
import com.kunzisoft.keepass.view.ToolbarAction
|
||||||
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
|
||||||
class GroupActivity : LockingActivity(),
|
class GroupActivity : LockingActivity(),
|
||||||
GroupEditDialogFragment.EditGroupListener,
|
GroupEditDialogFragment.EditGroupListener,
|
||||||
IconPickerDialogFragment.IconPickerListener,
|
IconPickerDialogFragment.IconPickerListener,
|
||||||
NodeAdapter.NodeMenuListener,
|
ListNodesFragment.NodeClickListener,
|
||||||
|
ListNodesFragment.NodesActionMenuListener,
|
||||||
ListNodesFragment.OnScrollListener,
|
ListNodesFragment.OnScrollListener,
|
||||||
NodeAdapter.NodeClickCallback,
|
|
||||||
SortDialogFragment.SortSelectionListener {
|
SortDialogFragment.SortSelectionListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var searchTitleView: View? = null
|
private var searchTitleView: View? = null
|
||||||
private var toolbarPasteExpandableLayout: ExpandableLayout? = null
|
private var toolbarAction: ToolbarAction? = null
|
||||||
private var toolbarPaste: Toolbar? = null
|
|
||||||
private var iconView: ImageView? = null
|
private var iconView: ImageView? = null
|
||||||
|
private var numberChildrenView: TextView? = null
|
||||||
private var modeTitleView: TextView? = null
|
private var modeTitleView: TextView? = null
|
||||||
private var addNodeButtonView: AddNodeButtonView? = null
|
private var addNodeButtonView: AddNodeButtonView? = null
|
||||||
private var groupNameView: TextView? = null
|
private var groupNameView: TextView? = null
|
||||||
@@ -89,12 +96,14 @@ class GroupActivity : LockingActivity(),
|
|||||||
private var mListNodesFragment: ListNodesFragment? = null
|
private var mListNodesFragment: ListNodesFragment? = null
|
||||||
private var mCurrentGroupIsASearch: Boolean = false
|
private var mCurrentGroupIsASearch: Boolean = false
|
||||||
|
|
||||||
|
private var progressDialogThread: ProgressDialogThread? = null
|
||||||
|
|
||||||
// Nodes
|
// Nodes
|
||||||
private var mRootGroup: GroupVersioned? = null
|
private var mRootGroup: GroupVersioned? = null
|
||||||
private var mCurrentGroup: GroupVersioned? = null
|
private var mCurrentGroup: GroupVersioned? = null
|
||||||
private var mOldGroupToUpdate: GroupVersioned? = null
|
private var mOldGroupToUpdate: GroupVersioned? = null
|
||||||
private var mNodeToCopy: NodeVersioned? = null
|
// TODO private var mNodeToCopy: NodeVersioned? = null
|
||||||
private var mNodeToMove: NodeVersioned? = null
|
// TODO private var mNodeToMove: NodeVersioned? = null
|
||||||
|
|
||||||
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
|
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
|
||||||
|
|
||||||
@@ -112,15 +121,27 @@ class GroupActivity : LockingActivity(),
|
|||||||
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
|
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
|
||||||
|
|
||||||
// Initialize views
|
// Initialize views
|
||||||
iconView = findViewById(R.id.icon)
|
coordinatorLayout = findViewById(R.id.group_coordinator)
|
||||||
|
iconView = findViewById(R.id.group_icon)
|
||||||
|
numberChildrenView = findViewById(R.id.group_numbers)
|
||||||
addNodeButtonView = findViewById(R.id.add_node_button)
|
addNodeButtonView = findViewById(R.id.add_node_button)
|
||||||
toolbar = findViewById(R.id.toolbar)
|
toolbar = findViewById(R.id.toolbar)
|
||||||
searchTitleView = findViewById(R.id.search_title)
|
searchTitleView = findViewById(R.id.search_title)
|
||||||
groupNameView = findViewById(R.id.group_name)
|
groupNameView = findViewById(R.id.group_name)
|
||||||
toolbarPasteExpandableLayout = findViewById(R.id.expandable_toolbar_paste_layout)
|
toolbarAction = findViewById(R.id.toolbar_action)
|
||||||
toolbarPaste = findViewById(R.id.toolbar_paste)
|
|
||||||
modeTitleView = findViewById(R.id.mode_title_view)
|
modeTitleView = findViewById(R.id.mode_title_view)
|
||||||
|
|
||||||
|
toolbar?.title = ""
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
|
/*
|
||||||
|
toolbarAction?.setNavigationOnClickListener {
|
||||||
|
toolbarAction?.collapse()
|
||||||
|
mNodeToCopy = null
|
||||||
|
mNodeToMove = null
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Focus view to reinitialize timeout
|
// Focus view to reinitialize timeout
|
||||||
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
|
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
|
||||||
|
|
||||||
@@ -128,13 +149,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY))
|
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY))
|
||||||
mOldGroupToUpdate = savedInstanceState.getParcelable(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 {
|
try {
|
||||||
@@ -155,17 +169,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
// Update last access time.
|
// Update last access time.
|
||||||
mCurrentGroup?.touch(modified = false, touchParents = false)
|
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
|
// Retrieve the textColor to tint the icon
|
||||||
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||||
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
||||||
@@ -178,12 +181,12 @@ class GroupActivity : LockingActivity(),
|
|||||||
// Initialize the fragment with the list
|
// Initialize the fragment with the list
|
||||||
mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment?
|
mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment?
|
||||||
if (mListNodesFragment == null)
|
if (mListNodesFragment == null)
|
||||||
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, readOnly, mCurrentGroupIsASearch)
|
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, mCurrentGroupIsASearch)
|
||||||
|
|
||||||
// Attach fragment to content view
|
// Attach fragment to content view
|
||||||
supportFragmentManager.beginTransaction().replace(
|
supportFragmentManager.beginTransaction().replace(
|
||||||
R.id.nodes_list_fragment_container,
|
R.id.nodes_list_fragment_container,
|
||||||
mListNodesFragment,
|
mListNodesFragment!!,
|
||||||
fragmentTag)
|
fragmentTag)
|
||||||
.commit()
|
.commit()
|
||||||
|
|
||||||
@@ -199,23 +202,90 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Search suggestion
|
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
|
// Search suggestion
|
||||||
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
|
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
|
||||||
|
|
||||||
|
// Init dialog thread
|
||||||
|
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
|
||||||
|
|
||||||
|
var oldNodes: List<NodeVersioned> = ArrayList()
|
||||||
|
result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle ->
|
||||||
|
oldNodes = getListNodesFromBundle(database, oldNodesBundle)
|
||||||
|
}
|
||||||
|
var newNodes: List<NodeVersioned> = ArrayList()
|
||||||
|
result.data?.getBundle(NEW_NODES_KEY)?.let { newNodesBundle ->
|
||||||
|
newNodes = getListNodesFromBundle(database, newNodesBundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
|
||||||
|
if (result.isSuccess) {
|
||||||
|
mListNodesFragment?.updateNodes(oldNodes, newNodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ACTION_DATABASE_CREATE_GROUP_TASK,
|
||||||
|
ACTION_DATABASE_COPY_NODES_TASK,
|
||||||
|
ACTION_DATABASE_MOVE_NODES_TASK -> {
|
||||||
|
if (result.isSuccess) {
|
||||||
|
mListNodesFragment?.addNodes(newNodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ACTION_DATABASE_DELETE_NODES_TASK -> {
|
||||||
|
if (result.isSuccess) {
|
||||||
|
|
||||||
|
// Rebuild all the list the avoid bug when delete node from db sort
|
||||||
|
if (PreferencesUtil.getListSort(this@GroupActivity) == SortNodeEnum.DB) {
|
||||||
|
mListNodesFragment?.rebuildList()
|
||||||
|
} else {
|
||||||
|
// Use the old Nodes / entries unchanged with the old parent
|
||||||
|
mListNodesFragment?.removeNodes(oldNodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add trash in views list if it doesn't exists
|
||||||
|
if (database.isRecycleBinEnabled) {
|
||||||
|
val recycleBin = database.recycleBin
|
||||||
|
if (mCurrentGroup != null && recycleBin != null
|
||||||
|
&& mCurrentGroup!!.parent == null
|
||||||
|
&& mCurrentGroup != recycleBin) {
|
||||||
|
if (mListNodesFragment?.contains(recycleBin) == true)
|
||||||
|
mListNodesFragment?.updateNode(recycleBin)
|
||||||
|
else
|
||||||
|
mListNodesFragment?.addNode(recycleBin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.isSuccess) {
|
||||||
|
result.exception?.errorId?.let { errorId ->
|
||||||
|
coordinatorLayout?.let { coordinatorLayout ->
|
||||||
|
Snackbar.make(coordinatorLayout, errorId, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finishNodeAction()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Finished creating tree")
|
Log.i(TAG, "Finished creating tree")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
Log.d(TAG, "setNewIntent: $intent")
|
super.onNewIntent(intent)
|
||||||
setIntent(intent)
|
|
||||||
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intent.action) {
|
intent?.let { intentNotNull ->
|
||||||
// only one instance of search in backstack
|
Log.d(TAG, "setNewIntent: $intentNotNull")
|
||||||
openSearchGroup(retrieveCurrentGroup(intent, null))
|
setIntent(intentNotNull)
|
||||||
true
|
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intentNotNull.action) {
|
||||||
} else {
|
// only one instance of search in backstack
|
||||||
false
|
openSearchGroup(retrieveCurrentGroup(intentNotNull, null))
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +309,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
// Check TimeoutHelper
|
// Check TimeoutHelper
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
||||||
// Open a group in a new fragment
|
// Open a group in a new fragment
|
||||||
val newListNodeFragment = ListNodesFragment.newInstance(group, readOnly, isASearch)
|
val newListNodeFragment = ListNodesFragment.newInstance(group, mReadOnly, isASearch)
|
||||||
val fragmentTransaction = supportFragmentManager.beginTransaction()
|
val fragmentTransaction = supportFragmentManager.beginTransaction()
|
||||||
// Different animation
|
// Different animation
|
||||||
val fragmentTag: String
|
val fragmentTag: String
|
||||||
@@ -272,17 +342,14 @@ class GroupActivity : LockingActivity(),
|
|||||||
mOldGroupToUpdate?.let {
|
mOldGroupToUpdate?.let {
|
||||||
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it)
|
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)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): GroupVersioned? {
|
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): GroupVersioned? {
|
||||||
|
|
||||||
|
// Force read only if the database is like that
|
||||||
|
mReadOnly = mDatabase?.isReadOnly == true || mReadOnly
|
||||||
|
|
||||||
// If it's a search
|
// If it's a search
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' })
|
return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' })
|
||||||
@@ -297,8 +364,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
pwGroupId = intent.getParcelableExtra(GROUP_ID_KEY)
|
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")
|
Log.w(TAG, "Creating tree view")
|
||||||
val currentGroup: GroupVersioned?
|
val currentGroup: GroupVersioned?
|
||||||
currentGroup = if (pwGroupId == null) {
|
currentGroup = if (pwGroupId == null) {
|
||||||
@@ -356,8 +421,18 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assign number of children
|
||||||
|
numberChildrenView?.apply {
|
||||||
|
if (PreferencesUtil.showNumberEntries(context)) {
|
||||||
|
text = mCurrentGroup?.getChildEntries(true)?.size?.toString() ?: ""
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show selection mode message if needed
|
// Show selection mode message if needed
|
||||||
if (selectionMode) {
|
if (mSelectionMode) {
|
||||||
modeTitleView?.visibility = View.VISIBLE
|
modeTitleView?.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
modeTitleView?.visibility = View.GONE
|
modeTitleView?.visibility = View.GONE
|
||||||
@@ -367,8 +442,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
addNodeButtonView?.apply {
|
addNodeButtonView?.apply {
|
||||||
|
|
||||||
// To enable add button
|
// To enable add button
|
||||||
val addGroupEnabled = !readOnly && !mCurrentGroupIsASearch
|
val addGroupEnabled = !mReadOnly && !mCurrentGroupIsASearch
|
||||||
var addEntryEnabled = !readOnly && !mCurrentGroupIsASearch
|
var addEntryEnabled = !mReadOnly && !mCurrentGroupIsASearch
|
||||||
mCurrentGroup?.let {
|
mCurrentGroup?.let {
|
||||||
val isRoot = it == mRootGroup
|
val isRoot = it == mRootGroup
|
||||||
if (!it.allowAddEntryIfIsRoot())
|
if (!it.allowAddEntryIfIsRoot())
|
||||||
@@ -401,7 +476,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
val entryVersioned = node as EntryVersioned
|
val entryVersioned = node as EntryVersioned
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
{
|
{
|
||||||
EntryActivity.launch(this@GroupActivity, entryVersioned, readOnly)
|
EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Populate Magikeyboard with entry
|
// Populate Magikeyboard with entry
|
||||||
@@ -427,12 +502,36 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var actionNodeMode: ActionMode? = null
|
||||||
|
|
||||||
|
private fun finishNodeAction() {
|
||||||
|
actionNodeMode?.finish()
|
||||||
|
actionNodeMode = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNodeSelected(nodes: List<NodeVersioned>): Boolean {
|
||||||
|
if (nodes.isNotEmpty()) {
|
||||||
|
if (actionNodeMode == null || toolbarAction?.getSupportActionModeCallback() == null) {
|
||||||
|
mListNodesFragment?.actionNodesCallback(nodes, this)?.let {
|
||||||
|
actionNodeMode = toolbarAction?.startSupportActionMode(it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actionNodeMode?.invalidate()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finishNodeAction()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOpenMenuClick(node: NodeVersioned): Boolean {
|
override fun onOpenMenuClick(node: NodeVersioned): Boolean {
|
||||||
|
finishNodeAction()
|
||||||
onNodeClick(node)
|
onNodeClick(node)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEditMenuClick(node: NodeVersioned): Boolean {
|
override fun onEditMenuClick(node: NodeVersioned): Boolean {
|
||||||
|
finishNodeAction()
|
||||||
when (node.type) {
|
when (node.type) {
|
||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
mOldGroupToUpdate = node as GroupVersioned
|
mOldGroupToUpdate = node as GroupVersioned
|
||||||
@@ -445,132 +544,56 @@ class GroupActivity : LockingActivity(),
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCopyMenuClick(node: NodeVersioned): Boolean {
|
override fun onCopyMenuClick(nodes: List<NodeVersioned>): Boolean {
|
||||||
toolbarPasteExpandableLayout?.expand()
|
actionNodeMode?.invalidate()
|
||||||
mNodeToCopy = node
|
|
||||||
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class OnCopyMenuItemClickListener : Toolbar.OnMenuItemClickListener {
|
// Nothing here fragment calls onPasteMenuClick internally
|
||||||
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteGroup(group: GroupVersioned) {
|
override fun onMoveMenuClick(nodes: List<NodeVersioned>): Boolean {
|
||||||
//TODO Verify trash recycle bin
|
actionNodeMode?.invalidate()
|
||||||
ProgressDialogSaveDatabaseThread(this) {
|
|
||||||
DeleteGroupRunnable(
|
// Nothing here fragment calls onPasteMenuClick internally
|
||||||
this,
|
return true
|
||||||
Database.getInstance(),
|
|
||||||
group,
|
|
||||||
AfterDeleteNodeRunnable(),
|
|
||||||
!readOnly)
|
|
||||||
}.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteEntry(entry: EntryVersioned) {
|
override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?,
|
||||||
ProgressDialogSaveDatabaseThread(this) {
|
nodes: List<NodeVersioned>): Boolean {
|
||||||
DeleteEntryRunnable(
|
when (pasteMode) {
|
||||||
this,
|
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
|
||||||
Database.getInstance(),
|
// Copy
|
||||||
entry,
|
mCurrentGroup?.let { newParent ->
|
||||||
AfterDeleteNodeRunnable(),
|
progressDialogThread?.startDatabaseCopyNodes(
|
||||||
!readOnly)
|
nodes,
|
||||||
}.start()
|
newParent,
|
||||||
|
!mReadOnly
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
|
||||||
|
// Move
|
||||||
|
mCurrentGroup?.let { newParent ->
|
||||||
|
progressDialogThread?.startDatabaseMoveNodes(
|
||||||
|
nodes,
|
||||||
|
newParent,
|
||||||
|
!mReadOnly
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
finishNodeAction()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteMenuClick(nodes: List<NodeVersioned>): Boolean {
|
||||||
|
progressDialogThread?.startDatabaseDeleteNodes(
|
||||||
|
nodes,
|
||||||
|
!mReadOnly
|
||||||
|
)
|
||||||
|
finishNodeAction()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -579,6 +602,16 @@ class GroupActivity : LockingActivity(),
|
|||||||
assignGroupViewElements()
|
assignGroupViewElements()
|
||||||
// Refresh suggestions to change preferences
|
// Refresh suggestions to change preferences
|
||||||
mSearchSuggestionAdapter?.reInit(this)
|
mSearchSuggestionAdapter?.reInit(this)
|
||||||
|
|
||||||
|
progressDialogThread?.registerProgressTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
progressDialogThread?.unregisterProgressTask()
|
||||||
|
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
finishNodeAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
@@ -586,7 +619,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
val inflater = menuInflater
|
val inflater = menuInflater
|
||||||
inflater.inflate(R.menu.search, menu)
|
inflater.inflate(R.menu.search, menu)
|
||||||
inflater.inflate(R.menu.database_lock, menu)
|
inflater.inflate(R.menu.database_lock, menu)
|
||||||
if (!selectionMode) {
|
if (!mSelectionMode) {
|
||||||
inflater.inflate(R.menu.default_menu, menu)
|
inflater.inflate(R.menu.default_menu, menu)
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||||
}
|
}
|
||||||
@@ -628,8 +661,9 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
private fun performedNextEducation(groupActivityEducation: GroupActivityEducation,
|
private fun performedNextEducation(groupActivityEducation: GroupActivityEducation,
|
||||||
menu: Menu) {
|
menu: Menu) {
|
||||||
|
|
||||||
// If no node, show education to add new one
|
// If no node, show education to add new one
|
||||||
if (mListNodesFragment != null
|
val addNodeButtonEducationPerformed = mListNodesFragment != null
|
||||||
&& mListNodesFragment!!.isEmpty
|
&& mListNodesFragment!!.isEmpty
|
||||||
&& addNodeButtonView?.addButtonView != null
|
&& addNodeButtonView?.addButtonView != null
|
||||||
&& addNodeButtonView!!.isEnable
|
&& addNodeButtonView!!.isEnable
|
||||||
@@ -641,71 +675,47 @@ class GroupActivity : LockingActivity(),
|
|||||||
{
|
{
|
||||||
performedNextEducation(groupActivityEducation, menu)
|
performedNextEducation(groupActivityEducation, menu)
|
||||||
}
|
}
|
||||||
))
|
)
|
||||||
else if (toolbar != null
|
if (!addNodeButtonEducationPerformed) {
|
||||||
&& toolbar!!.findViewById<View>(R.id.menu_search) != null
|
|
||||||
&& groupActivityEducation.checkAndPerformedSearchMenuEducation(
|
val searchMenuEducationPerformed = toolbar != null
|
||||||
toolbar!!.findViewById(R.id.menu_search),
|
&& toolbar!!.findViewById<View>(R.id.menu_search) != null
|
||||||
{
|
&& groupActivityEducation.checkAndPerformedSearchMenuEducation(
|
||||||
menu.findItem(R.id.menu_search).expandActionView()
|
toolbar!!.findViewById(R.id.menu_search),
|
||||||
},
|
{
|
||||||
{
|
menu.findItem(R.id.menu_search).expandActionView()
|
||||||
performedNextEducation(groupActivityEducation, menu)
|
},
|
||||||
}))
|
{
|
||||||
else if (toolbar != null
|
performedNextEducation(groupActivityEducation, menu)
|
||||||
&& toolbar!!.findViewById<View>(R.id.menu_sort) != null
|
})
|
||||||
&& groupActivityEducation.checkAndPerformedSortMenuEducation(
|
|
||||||
|
if (!searchMenuEducationPerformed) {
|
||||||
|
|
||||||
|
val sortMenuEducationPerformed = toolbar != null
|
||||||
|
&& toolbar!!.findViewById<View>(R.id.menu_sort) != null
|
||||||
|
&& groupActivityEducation.checkAndPerformedSortMenuEducation(
|
||||||
toolbar!!.findViewById(R.id.menu_sort),
|
toolbar!!.findViewById(R.id.menu_sort),
|
||||||
{
|
{
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_sort))
|
onOptionsItemSelected(menu.findItem(R.id.menu_sort))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(groupActivityEducation, menu)
|
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) {
|
if (!sortMenuEducationPerformed) {
|
||||||
|
// lockMenuEducationPerformed
|
||||||
// Get the intent, verify the action and get the query
|
toolbar != null
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
&& toolbar!!.findViewById<View>(R.id.menu_lock) != null
|
||||||
val query = intent.getStringExtra(SearchManager.QUERY)
|
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
|
||||||
// manually launch the real search activity
|
toolbar!!.findViewById(R.id.menu_lock),
|
||||||
val searchIntent = Intent(applicationContext, GroupActivity::class.java)
|
{
|
||||||
// add query to the Intent Extras
|
onOptionsItemSelected(menu.findItem(R.id.menu_lock))
|
||||||
searchIntent.action = Intent.ACTION_SEARCH
|
},
|
||||||
searchIntent.putExtra(SearchManager.QUERY, query)
|
{
|
||||||
|
performedNextEducation(groupActivityEducation, menu)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,7 +734,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Check the time lock before launching settings
|
// Check the time lock before launching settings
|
||||||
MenuUtil.onDefaultMenuOptionsItemSelected(this, item, readOnly, true)
|
MenuUtil.onDefaultMenuOptionsItemSelected(this, item, mReadOnly, true)
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -733,7 +743,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
|
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
|
||||||
name: String?,
|
name: String?,
|
||||||
icon: PwIcon?) {
|
icon: PwIcon?) {
|
||||||
val database = Database.getInstance()
|
|
||||||
|
|
||||||
if (name != null && name.isNotEmpty() && icon != null) {
|
if (name != null && name.isNotEmpty() && icon != null) {
|
||||||
when (action) {
|
when (action) {
|
||||||
@@ -741,94 +750,37 @@ class GroupActivity : LockingActivity(),
|
|||||||
// If group creation
|
// If group creation
|
||||||
mCurrentGroup?.let { currentGroup ->
|
mCurrentGroup?.let { currentGroup ->
|
||||||
// Build the group
|
// Build the group
|
||||||
database.createGroup()?.let { newGroup ->
|
mDatabase?.createGroup()?.let { newGroup ->
|
||||||
newGroup.title = name
|
newGroup.title = name
|
||||||
newGroup.icon = icon
|
newGroup.icon = icon
|
||||||
// Not really needed here because added in runnable but safe
|
// Not really needed here because added in runnable but safe
|
||||||
newGroup.parent = currentGroup
|
newGroup.parent = currentGroup
|
||||||
|
|
||||||
// If group created save it in the database
|
progressDialogThread?.startDatabaseCreateGroup(
|
||||||
ProgressDialogSaveDatabaseThread(this) {
|
newGroup, currentGroup, !mReadOnly)
|
||||||
AddGroupRunnable(this,
|
|
||||||
Database.getInstance(),
|
|
||||||
newGroup,
|
|
||||||
currentGroup,
|
|
||||||
AfterAddNodeRunnable(),
|
|
||||||
!readOnly)
|
|
||||||
}.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GroupEditDialogFragment.EditGroupDialogAction.UPDATE ->
|
GroupEditDialogFragment.EditGroupDialogAction.UPDATE -> {
|
||||||
// If update add new elements
|
// If update add new elements
|
||||||
mOldGroupToUpdate?.let { oldGroupToUpdate ->
|
mOldGroupToUpdate?.let { oldGroupToUpdate ->
|
||||||
GroupVersioned(oldGroupToUpdate).let { updateGroup ->
|
GroupVersioned(oldGroupToUpdate).let { updateGroup ->
|
||||||
updateGroup.title = name
|
updateGroup.apply {
|
||||||
// TODO custom icon
|
// WARNING remove parent and children to keep memory
|
||||||
updateGroup.icon = icon
|
removeParent()
|
||||||
|
removeChildren() // TODO concurrent exception
|
||||||
|
|
||||||
mListNodesFragment?.removeNode(oldGroupToUpdate)
|
title = name
|
||||||
|
this.icon = icon // TODO custom icon
|
||||||
|
}
|
||||||
|
|
||||||
// If group updated save it in the database
|
// If group updated save it in the database
|
||||||
ProgressDialogSaveDatabaseThread(this) {
|
progressDialogThread?.startDatabaseUpdateGroup(
|
||||||
UpdateGroupRunnable(this,
|
oldGroupToUpdate, updateGroup, !mReadOnly)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -847,11 +799,9 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showWarnings() {
|
private fun showWarnings() {
|
||||||
// TODO Preferences
|
|
||||||
if (Database.getInstance().isReadOnly) {
|
if (Database.getInstance().isReadOnly) {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
if (PreferencesUtil.showReadOnlyWarning(this)) {
|
||||||
if (prefs.getBoolean(getString(R.string.show_read_only_warning), true)) {
|
ReadOnlyDialog().show(supportFragmentManager, "readOnlyDialog")
|
||||||
ReadOnlyDialog(this).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -860,6 +810,40 @@ class GroupActivity : LockingActivity(),
|
|||||||
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
|
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun startActivity(intent: Intent) {
|
||||||
|
|
||||||
|
// Get the intent, verify the action and get the query
|
||||||
|
if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
|
// manually launch the real search activity
|
||||||
|
val searchIntent = Intent(applicationContext, GroupActivity::class.java).apply {
|
||||||
|
// Add bundle of current intent
|
||||||
|
putExtras(this@GroupActivity.intent)
|
||||||
|
// add query to the Intent Extras
|
||||||
|
action = Intent.ACTION_SEARCH
|
||||||
|
putExtra(SearchManager.QUERY, intent.getStringExtra(SearchManager.QUERY))
|
||||||
|
}
|
||||||
|
|
||||||
|
super.startActivity(searchIntent)
|
||||||
|
} else {
|
||||||
|
super.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
super.startActivityForResult(intent, requestCode, options)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
@@ -871,24 +855,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
mListNodesFragment?.rebuildList()
|
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) {
|
private fun removeSearchInIntent(intent: Intent) {
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
mCurrentGroupIsASearch = false
|
mCurrentGroupIsASearch = false
|
||||||
@@ -898,25 +864,29 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
// Normal way when we are not in root
|
if (mListNodesFragment?.nodeActionSelectionMode == true) {
|
||||||
if (mRootGroup != null && mRootGroup != mCurrentGroup)
|
finishNodeAction()
|
||||||
super.onBackPressed()
|
} else {
|
||||||
// Else lock if needed
|
// Normal way when we are not in root
|
||||||
else {
|
if (mRootGroup != null && mRootGroup != mCurrentGroup)
|
||||||
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
|
||||||
lockAndExit()
|
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
} else {
|
// Else lock if needed
|
||||||
moveTaskToBack(true)
|
else {
|
||||||
|
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
||||||
|
lockAndExit()
|
||||||
|
super.onBackPressed()
|
||||||
|
} else {
|
||||||
|
moveTaskToBack(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment
|
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment
|
||||||
// to refresh fragment
|
// to refresh fragment
|
||||||
mListNodesFragment?.rebuildList()
|
mListNodesFragment?.rebuildList()
|
||||||
mCurrentGroup = mListNodesFragment?.mainGroup
|
mCurrentGroup = mListNodesFragment?.mainGroup
|
||||||
removeSearchInIntent(intent)
|
removeSearchInIntent(intent)
|
||||||
assignGroupViewElements()
|
assignGroupViewElements()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -927,13 +897,15 @@ class GroupActivity : LockingActivity(),
|
|||||||
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
|
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
|
||||||
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
|
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
|
||||||
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
|
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
|
||||||
private 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,
|
private fun buildAndLaunchIntent(context: Context, group: GroupVersioned?, readOnly: Boolean,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
val checkTime = if (context is Activity)
|
||||||
val intent = Intent(activity, GroupActivity::class.java)
|
TimeoutHelper.checkTimeAndLockIfTimeout(context)
|
||||||
|
else
|
||||||
|
TimeoutHelper.checkTime(context)
|
||||||
|
if (checkTime) {
|
||||||
|
val intent = Intent(context, GroupActivity::class.java)
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
intent.putExtra(GROUP_ID_KEY, group.nodeId)
|
intent.putExtra(GROUP_ID_KEY, group.nodeId)
|
||||||
}
|
}
|
||||||
@@ -949,10 +921,10 @@ class GroupActivity : LockingActivity(),
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun launch(activity: Activity, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
|
fun launch(context: Context, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||||
TimeoutHelper.recordTime(activity)
|
TimeoutHelper.recordTime(context)
|
||||||
buildAndLaunchIntent(activity, null, readOnly) { intent ->
|
buildAndLaunchIntent(context, null, readOnly) { intent ->
|
||||||
activity.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -963,10 +935,10 @@ class GroupActivity : LockingActivity(),
|
|||||||
*/
|
*/
|
||||||
// TODO implement pre search to directly open the direct group
|
// TODO implement pre search to directly open the direct group
|
||||||
|
|
||||||
fun launchForKeyboardSelection(activity: Activity, readOnly: Boolean) {
|
fun launchForKeyboardSelection(context: Context, readOnly: Boolean) {
|
||||||
TimeoutHelper.recordTime(activity)
|
TimeoutHelper.recordTime(context)
|
||||||
buildAndLaunchIntent(activity, null, readOnly) { intent ->
|
buildAndLaunchIntent(context, null, readOnly) { intent ->
|
||||||
KeyboardHelper.startActivityForKeyboardSelection(activity, intent)
|
EntrySelectionHelper.startActivityForEntrySelection(context, intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import android.content.Intent
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.preference.PreferenceManager
|
import android.preference.PreferenceManager
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import android.support.v7.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@@ -14,6 +14,7 @@ import android.view.MenuInflater
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||||
@@ -26,11 +27,12 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.Type
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
||||||
|
|
||||||
private var nodeClickCallback: NodeAdapter.NodeClickCallback? = null
|
private var nodeClickListener: NodeClickListener? = null
|
||||||
private var nodeMenuListener: NodeAdapter.NodeMenuListener? = null
|
|
||||||
private var onScrollListener: OnScrollListener? = null
|
private var onScrollListener: OnScrollListener? = null
|
||||||
|
|
||||||
private var listView: RecyclerView? = null
|
private var listView: RecyclerView? = null
|
||||||
@@ -38,6 +40,13 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
private set
|
private set
|
||||||
private var mAdapter: NodeAdapter? = null
|
private var mAdapter: NodeAdapter? = null
|
||||||
|
|
||||||
|
var nodeActionSelectionMode = false
|
||||||
|
private set
|
||||||
|
var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED
|
||||||
|
private set
|
||||||
|
private val listActionNodes = LinkedList<NodeVersioned>()
|
||||||
|
private val listPasteNodes = LinkedList<NodeVersioned>()
|
||||||
|
|
||||||
private var notFoundView: View? = null
|
private var notFoundView: View? = null
|
||||||
private var isASearchResult: Boolean = false
|
private var isASearchResult: Boolean = false
|
||||||
|
|
||||||
@@ -53,31 +62,22 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
val isEmpty: Boolean
|
val isEmpty: Boolean
|
||||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
||||||
|
|
||||||
override fun onAttach(context: Context?) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
try {
|
try {
|
||||||
nodeClickCallback = context as NodeAdapter.NodeClickCallback?
|
nodeClickListener = context as NodeClickListener
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
// The activity doesn't implement the interface, throw exception
|
// The activity doesn't implement the interface, throw exception
|
||||||
throw ClassCastException(context?.toString()
|
throw ClassCastException(context.toString()
|
||||||
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
|
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
nodeMenuListener = context as NodeAdapter.NodeMenuListener?
|
onScrollListener = context as OnScrollListener
|
||||||
} 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) {
|
} catch (e: ClassCastException) {
|
||||||
onScrollListener = null
|
onScrollListener = null
|
||||||
// Context menu can be omit
|
// Context menu can be omit
|
||||||
Log.w(TAG, context?.toString()
|
Log.w(TAG, context.toString()
|
||||||
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
|
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,33 +85,58 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
activity?.let { currentActivity ->
|
setHasOptionsMenu(true)
|
||||||
setHasOptionsMenu(true)
|
|
||||||
|
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
|
||||||
|
|
||||||
arguments?.let { args ->
|
arguments?.let { args ->
|
||||||
// Contains all the group in element
|
// Contains all the group in element
|
||||||
if (args.containsKey(GROUP_KEY)) {
|
if (args.containsKey(GROUP_KEY)) {
|
||||||
mainGroup = args.getParcelable(GROUP_KEY)
|
mainGroup = args.getParcelable(GROUP_KEY)
|
||||||
}
|
|
||||||
if (args.containsKey(IS_SEARCH)) {
|
|
||||||
isASearchResult = args.getBoolean(IS_SEARCH)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (args.containsKey(IS_SEARCH)) {
|
||||||
contextThemed?.let { context ->
|
isASearchResult = args.getBoolean(IS_SEARCH)
|
||||||
mAdapter = NodeAdapter(context, currentActivity.menuInflater)
|
|
||||||
mAdapter?.apply {
|
|
||||||
setReadOnly(readOnly)
|
|
||||||
setIsASearchResult(isASearchResult)
|
|
||||||
setOnNodeClickListener(nodeClickCallback)
|
|
||||||
setActivateContextMenu(true)
|
|
||||||
setNodeMenuListener(nodeMenuListener)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contextThemed?.let { context ->
|
||||||
|
mAdapter = NodeAdapter(context)
|
||||||
|
mAdapter?.apply {
|
||||||
|
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
||||||
|
override fun onNodeClick(node: NodeVersioned) {
|
||||||
|
if (nodeActionSelectionMode) {
|
||||||
|
if (listActionNodes.contains(node)) {
|
||||||
|
// Remove selected item if already selected
|
||||||
|
listActionNodes.remove(node)
|
||||||
|
} else {
|
||||||
|
// Add selected item if not already selected
|
||||||
|
listActionNodes.add(node)
|
||||||
|
}
|
||||||
|
nodeClickListener?.onNodeSelected(listActionNodes)
|
||||||
|
setActionNodes(listActionNodes)
|
||||||
|
notifyNodeChanged(node)
|
||||||
|
} else {
|
||||||
|
nodeClickListener?.onNodeClick(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNodeLongClick(node: NodeVersioned): Boolean {
|
||||||
|
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||||
|
// Select the first item after a long click
|
||||||
|
if (!listActionNodes.contains(node))
|
||||||
|
listActionNodes.add(node)
|
||||||
|
|
||||||
|
nodeClickListener?.onNodeSelected(listActionNodes)
|
||||||
|
|
||||||
|
setActionNodes(listActionNodes)
|
||||||
|
notifyNodeChanged(node)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
@@ -130,7 +155,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
|
|
||||||
onScrollListener?.let { onScrollListener ->
|
onScrollListener?.let { onScrollListener ->
|
||||||
listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
onScrollListener.onScrolled(dy)
|
onScrollListener.onScrolled(dy)
|
||||||
}
|
}
|
||||||
@@ -148,10 +173,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
activity?.intent?.let {
|
activity?.intent?.let {
|
||||||
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it)
|
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it)
|
||||||
}
|
}
|
||||||
// Force read only mode if selection mode
|
|
||||||
mAdapter?.apply {
|
|
||||||
setReadOnly(readOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh data
|
// Refresh data
|
||||||
mAdapter?.notifyDataSetChanged()
|
mAdapter?.notifyDataSetChanged()
|
||||||
@@ -195,19 +216,19 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater?.inflate(R.menu.tree, menu)
|
inflater.inflate(R.menu.tree, menu)
|
||||||
|
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item?.itemId) {
|
when (item.itemId) {
|
||||||
|
|
||||||
R.id.menu_sort -> {
|
R.id.menu_sort -> {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
val sortDialogFragment: SortDialogFragment =
|
val sortDialogFragment: SortDialogFragment =
|
||||||
if (Database.getInstance().isRecycleBinAvailable
|
if (Database.getInstance().allowRecycleBin
|
||||||
&& Database.getInstance().isRecycleBinEnabled) {
|
&& Database.getInstance().isRecycleBinEnabled) {
|
||||||
SortDialogFragment.getInstance(
|
SortDialogFragment.getInstance(
|
||||||
PreferencesUtil.getListSort(context),
|
PreferencesUtil.getListSort(context),
|
||||||
@@ -230,6 +251,102 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun actionNodesCallback(nodes: List<NodeVersioned>,
|
||||||
|
menuListener: NodesActionMenuListener?) : ActionMode.Callback {
|
||||||
|
|
||||||
|
return object : ActionMode.Callback {
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
|
nodeActionSelectionMode = false
|
||||||
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
|
menu?.clear()
|
||||||
|
|
||||||
|
if (nodeActionPasteMode != PasteMode.UNDEFINED) {
|
||||||
|
mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu)
|
||||||
|
} else {
|
||||||
|
nodeActionSelectionMode = true
|
||||||
|
mode?.menuInflater?.inflate(R.menu.node_menu, menu)
|
||||||
|
|
||||||
|
val database = Database.getInstance()
|
||||||
|
|
||||||
|
// Open and Edit for a single item
|
||||||
|
if (nodes.size == 1) {
|
||||||
|
// Edition
|
||||||
|
if (readOnly || nodes[0] == database.recycleBin) {
|
||||||
|
menu?.removeItem(R.id.menu_edit)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
menu?.removeItem(R.id.menu_open)
|
||||||
|
menu?.removeItem(R.id.menu_edit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy and Move (not for groups)
|
||||||
|
if (readOnly
|
||||||
|
|| isASearchResult
|
||||||
|
|| nodes.any { it == database.recycleBin }
|
||||||
|
|| nodes.any { it.type == Type.GROUP }) {
|
||||||
|
// TODO COPY For Group
|
||||||
|
menu?.removeItem(R.id.menu_copy)
|
||||||
|
menu?.removeItem(R.id.menu_move)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletion
|
||||||
|
if (readOnly || nodes.any { it == database.recycleBin }) {
|
||||||
|
menu?.removeItem(R.id.menu_delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the number of items selected in title
|
||||||
|
mode?.title = nodes.size.toString()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||||
|
if (menuListener == null)
|
||||||
|
return false
|
||||||
|
return when (item?.itemId) {
|
||||||
|
R.id.menu_open -> menuListener.onOpenMenuClick(nodes[0])
|
||||||
|
R.id.menu_edit -> menuListener.onEditMenuClick(nodes[0])
|
||||||
|
R.id.menu_copy -> {
|
||||||
|
nodeActionPasteMode = PasteMode.PASTE_FROM_COPY
|
||||||
|
mAdapter?.unselectActionNodes()
|
||||||
|
val returnValue = menuListener.onCopyMenuClick(nodes)
|
||||||
|
nodeActionSelectionMode = false
|
||||||
|
returnValue
|
||||||
|
}
|
||||||
|
R.id.menu_move -> {
|
||||||
|
nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE
|
||||||
|
mAdapter?.unselectActionNodes()
|
||||||
|
val returnValue = menuListener.onMoveMenuClick(nodes)
|
||||||
|
nodeActionSelectionMode = false
|
||||||
|
returnValue
|
||||||
|
}
|
||||||
|
R.id.menu_delete -> menuListener.onDeleteMenuClick(nodes)
|
||||||
|
R.id.menu_paste -> {
|
||||||
|
val returnValue = menuListener.onPasteMenuClick(nodeActionPasteMode, nodes)
|
||||||
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
|
nodeActionSelectionMode = false
|
||||||
|
returnValue
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||||
|
listActionNodes.clear()
|
||||||
|
listPasteNodes.clear()
|
||||||
|
mAdapter?.unselectActionNodes()
|
||||||
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
|
nodeActionSelectionMode = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
@@ -252,18 +369,66 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun contains(node: NodeVersioned): Boolean {
|
||||||
|
return mAdapter?.contains(node) ?: false
|
||||||
|
}
|
||||||
|
|
||||||
fun addNode(newNode: NodeVersioned) {
|
fun addNode(newNode: NodeVersioned) {
|
||||||
mAdapter?.addNode(newNode)
|
mAdapter?.addNode(newNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) {
|
fun addNodes(newNodes: List<NodeVersioned>) {
|
||||||
mAdapter?.updateNode(oldNode, newNode)
|
mAdapter?.addNodes(newNodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned? = null) {
|
||||||
|
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateNodes(oldNodes: List<NodeVersioned>, newNodes: List<NodeVersioned>) {
|
||||||
|
mAdapter?.updateNodes(oldNodes, newNodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeNode(pwNode: NodeVersioned) {
|
fun removeNode(pwNode: NodeVersioned) {
|
||||||
mAdapter?.removeNode(pwNode)
|
mAdapter?.removeNode(pwNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeNodes(nodes: List<NodeVersioned>) {
|
||||||
|
mAdapter?.removeNodes(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeNodeAt(position: Int) {
|
||||||
|
mAdapter?.removeNodeAt(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeNodesAt(positions: IntArray) {
|
||||||
|
mAdapter?.removeNodesAt(positions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback listener to redefine to do an action when a node is click
|
||||||
|
*/
|
||||||
|
interface NodeClickListener {
|
||||||
|
fun onNodeClick(node: NodeVersioned)
|
||||||
|
fun onNodeSelected(nodes: List<NodeVersioned>): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menu listener to redefine to do an action in menu
|
||||||
|
*/
|
||||||
|
interface NodesActionMenuListener {
|
||||||
|
fun onOpenMenuClick(node: NodeVersioned): Boolean
|
||||||
|
fun onEditMenuClick(node: NodeVersioned): Boolean
|
||||||
|
fun onCopyMenuClick(nodes: List<NodeVersioned>): Boolean
|
||||||
|
fun onMoveMenuClick(nodes: List<NodeVersioned>): Boolean
|
||||||
|
fun onDeleteMenuClick(nodes: List<NodeVersioned>): Boolean
|
||||||
|
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<NodeVersioned>): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PasteMode {
|
||||||
|
UNDEFINED, PASTE_FROM_COPY, PASTE_FROM_MOVE
|
||||||
|
}
|
||||||
|
|
||||||
interface OnScrollListener {
|
interface OnScrollListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,60 +19,69 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.app.backup.BackupManager
|
import android.app.backup.BackupManager
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.hardware.fingerprint.FingerprintManager
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.preference.PreferenceManager
|
import android.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.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||||
import android.widget.*
|
import android.widget.Button
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
import com.kunzisoft.keepass.activities.helpers.*
|
import com.kunzisoft.keepass.activities.dialogs.FingerPrintExplanationDialog
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
import com.kunzisoft.keepass.fingerprint.FingerPrintHelper
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.fingerprint.FingerPrintViewsManager
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||||
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_KEY
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.FingerPrintInfoView
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
import permissions.dispatcher.*
|
import com.kunzisoft.keepass.view.asError
|
||||||
import java.io.File
|
import kotlinx.android.synthetic.main.activity_password.*
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
|
|
||||||
@RuntimePermissions
|
class PasswordActivity : StylishActivity() {
|
||||||
class PasswordActivity : StylishActivity(),
|
|
||||||
UriIntentInitTaskCallback {
|
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
|
|
||||||
|
private var containerView: View? = null
|
||||||
private var filenameView: TextView? = null
|
private var filenameView: TextView? = null
|
||||||
private var passwordView: EditText? = null
|
private var passwordView: EditText? = null
|
||||||
private var keyFileView: EditText? = null
|
private var keyFileView: EditText? = null
|
||||||
@@ -80,26 +89,29 @@ class PasswordActivity : StylishActivity(),
|
|||||||
private var checkboxPasswordView: CompoundButton? = null
|
private var checkboxPasswordView: CompoundButton? = null
|
||||||
private var checkboxKeyFileView: CompoundButton? = null
|
private var checkboxKeyFileView: CompoundButton? = null
|
||||||
private var checkboxDefaultDatabaseView: CompoundButton? = null
|
private var checkboxDefaultDatabaseView: CompoundButton? = null
|
||||||
private var fingerPrintInfoView: FingerPrintInfoView? = null
|
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
||||||
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
private var mDatabaseKeyFileUri: Uri? = null
|
||||||
|
|
||||||
private var prefs: SharedPreferences? = null
|
private var prefs: SharedPreferences? = null
|
||||||
|
|
||||||
private var mRememberKeyFile: Boolean = false
|
private var mRememberKeyFile: Boolean = false
|
||||||
private var mKeyFileHelper: KeyFileHelper? = null
|
private var mOpenFileHelper: OpenFileHelper? = null
|
||||||
|
|
||||||
private var readOnly: Boolean = false
|
private var readOnly: Boolean = false
|
||||||
|
|
||||||
private var fingerPrintViewsManager: FingerPrintViewsManager? = null
|
private var progressDialogThread: ProgressDialogThread? = null
|
||||||
|
|
||||||
|
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
mRememberKeyFile = prefs!!.getBoolean(getString(R.string.keyfile_key),
|
mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this)
|
||||||
resources.getBoolean(R.bool.keyfile_default))
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_password)
|
setContentView(R.layout.activity_password)
|
||||||
|
|
||||||
@@ -109,6 +121,7 @@ class PasswordActivity : StylishActivity(),
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
containerView = findViewById(R.id.container)
|
||||||
confirmButtonView = findViewById(R.id.pass_ok)
|
confirmButtonView = findViewById(R.id.pass_ok)
|
||||||
filenameView = findViewById(R.id.filename)
|
filenameView = findViewById(R.id.filename)
|
||||||
passwordView = findViewById(R.id.password)
|
passwordView = findViewById(R.id.password)
|
||||||
@@ -116,13 +129,13 @@ class PasswordActivity : StylishActivity(),
|
|||||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||||
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
||||||
fingerPrintInfoView = findViewById(R.id.fingerprint_info)
|
advancedUnlockInfoView = findViewById(R.id.fingerprint_info)
|
||||||
|
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||||
|
|
||||||
val browseView = findViewById<View>(R.id.browse_button)
|
val browseView = findViewById<View>(R.id.open_database_button)
|
||||||
mKeyFileHelper = KeyFileHelper(this@PasswordActivity)
|
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
||||||
browseView.setOnClickListener(mKeyFileHelper!!.openFileOnClickViewListener)
|
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
|
||||||
|
|
||||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
passwordView?.addTextChangedListener(object : TextWatcher {
|
||||||
@@ -150,6 +163,88 @@ class PasswordActivity : StylishActivity(),
|
|||||||
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
|
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||||
enableOrNotTheConfirmationButton()
|
enableOrNotTheConfirmationButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
|
// Recheck fingerprint if error
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) {
|
||||||
|
// Stay with the same mode and init it
|
||||||
|
advancedUnlockedManager?.initBiometricMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var databaseUri: Uri? = null
|
||||||
|
var masterPassword: String? = null
|
||||||
|
var keyFileUri: Uri? = null
|
||||||
|
var readOnly = true
|
||||||
|
var cipherEntity: CipherDatabaseEntity? = null
|
||||||
|
|
||||||
|
result.data?.let { resultData ->
|
||||||
|
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||||
|
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
|
||||||
|
keyFileUri = resultData.getParcelable(KEY_FILE_KEY)
|
||||||
|
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||||
|
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseUri?.let { databaseFileUri ->
|
||||||
|
// Remove the password in view in all cases
|
||||||
|
removePassword()
|
||||||
|
|
||||||
|
if (result.isSuccess) {
|
||||||
|
launchGroupActivity()
|
||||||
|
} else {
|
||||||
|
var resultError = ""
|
||||||
|
val resultException = result.exception
|
||||||
|
val resultMessage = result.message
|
||||||
|
|
||||||
|
if (resultException != null) {
|
||||||
|
resultError = resultException.getLocalizedMessage(resources)
|
||||||
|
if (resultException is LoadDatabaseDuplicateUuidException)
|
||||||
|
showLoadDatabaseDuplicateUuidMessage {
|
||||||
|
showProgressDialogAndLoadDatabase(
|
||||||
|
databaseFileUri,
|
||||||
|
masterPassword,
|
||||||
|
keyFileUri,
|
||||||
|
readOnly,
|
||||||
|
cipherEntity,
|
||||||
|
true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||||
|
resultError = "$resultError $resultMessage"
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.e(TAG, resultError, resultException)
|
||||||
|
|
||||||
|
Snackbar.make(activity_password_coordinator_layout,
|
||||||
|
resultError,
|
||||||
|
Snackbar.LENGTH_LONG).asError().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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
||||||
@@ -163,6 +258,9 @@ class PasswordActivity : StylishActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
if (Database.getInstance().loaded)
|
||||||
|
launchGroupActivity()
|
||||||
|
|
||||||
// If the database isn't accessible make sure to clear the password field, if it
|
// If the database isn't accessible make sure to clear the password field, if it
|
||||||
// was saved in the instance state
|
// was saved in the instance state
|
||||||
if (Database.getInstance().loaded) {
|
if (Database.getInstance().loaded) {
|
||||||
@@ -172,8 +270,9 @@ class PasswordActivity : StylishActivity(),
|
|||||||
// For check shutdown
|
// For check shutdown
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
UriIntentInitTask(WeakReference(this), this, mRememberKeyFile)
|
progressDialogThread?.registerProgressTask()
|
||||||
.execute(intent)
|
|
||||||
|
initUriFromIntent()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
@@ -181,26 +280,45 @@ class PasswordActivity : StylishActivity(),
|
|||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPostInitTask(databaseFileUri: Uri?, keyFileUri: Uri?, errorStringId: Int?) {
|
private fun initUriFromIntent() {
|
||||||
mDatabaseFileUri = databaseFileUri
|
|
||||||
|
|
||||||
if (errorStringId != null) {
|
val databaseUri: Uri?
|
||||||
Toast.makeText(this@PasswordActivity, errorStringId, Toast.LENGTH_LONG).show()
|
val keyFileUri: Uri?
|
||||||
finish()
|
|
||||||
return
|
// If is a view intent
|
||||||
|
val action = intent.action
|
||||||
|
if (action != null
|
||||||
|
&& action == VIEW_INTENT) {
|
||||||
|
databaseUri = intent.data
|
||||||
|
keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
||||||
|
} else {
|
||||||
|
databaseUri = intent.getParcelableExtra(KEY_FILENAME)
|
||||||
|
keyFileUri = intent.getParcelableExtra(KEY_KEYFILE)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify permission to read file
|
// Post init uri with KeyFile if needed
|
||||||
if (databaseFileUri != null && !databaseFileUri.scheme!!.contains("content"))
|
if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) {
|
||||||
doNothingWithPermissionCheck()
|
// Retrieve KeyFile in a thread
|
||||||
|
databaseUri?.let { databaseUriNotNull ->
|
||||||
|
FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||||
|
.getKeyFileUriByDatabaseUri(databaseUriNotNull) {
|
||||||
|
onPostInitUri(databaseUri, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onPostInitUri(databaseUri, keyFileUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||||
|
mDatabaseFileUri = databaseFileUri
|
||||||
|
mDatabaseKeyFileUri = keyFileUri
|
||||||
|
|
||||||
// Define title
|
// Define title
|
||||||
val dbUriString = databaseFileUri?.toString() ?: ""
|
databaseFileUri?.let {
|
||||||
if (dbUriString.isNotEmpty()) {
|
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
|
||||||
if (PreferencesUtil.isFullFilePathEnable(this))
|
filenameView?.text = title
|
||||||
filenameView?.text = dbUriString
|
}
|
||||||
else
|
|
||||||
filenameView?.text = File(databaseFileUri!!.path!!).name // TODO Encapsulate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define Key File text
|
// Define Key File text
|
||||||
@@ -211,13 +329,17 @@ class PasswordActivity : StylishActivity(),
|
|||||||
|
|
||||||
// Define listeners for default database checkbox and validate button
|
// Define listeners for default database checkbox and validate button
|
||||||
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
|
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
|
||||||
var newDefaultFileName = ""
|
var newDefaultFileName: Uri? = null
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
newDefaultFileName = databaseFileUri?.toString() ?: newDefaultFileName
|
newDefaultFileName = databaseFileUri ?: newDefaultFileName
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs?.edit()?.apply() {
|
prefs?.edit()?.apply {
|
||||||
putString(KEY_DEFAULT_FILENAME, newDefaultFileName)
|
newDefaultFileName?.let {
|
||||||
|
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
|
||||||
|
} ?: kotlin.run {
|
||||||
|
remove(KEY_DEFAULT_DATABASE_PATH)
|
||||||
|
}
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,10 +349,10 @@ class PasswordActivity : StylishActivity(),
|
|||||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
||||||
|
|
||||||
// Retrieve settings for default database
|
// Retrieve settings for default database
|
||||||
val defaultFilename = prefs?.getString(KEY_DEFAULT_FILENAME, "")
|
val defaultFilename = prefs?.getString(KEY_DEFAULT_DATABASE_PATH, "")
|
||||||
if (databaseFileUri != null
|
if (databaseFileUri != null
|
||||||
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
|
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
|
||||||
&& databaseFileUri == UriUtil.parseUriFile(defaultFilename)) {
|
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
|
||||||
checkboxDefaultDatabaseView?.isChecked = true
|
checkboxDefaultDatabaseView?.isChecked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,30 +369,42 @@ class PasswordActivity : StylishActivity(),
|
|||||||
// Init FingerPrint elements
|
// Init FingerPrint elements
|
||||||
var fingerPrintInit = false
|
var fingerPrintInit = false
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (PreferencesUtil.isFingerprintEnable(this)) {
|
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
||||||
if (fingerPrintViewsManager == null) {
|
|
||||||
fingerPrintViewsManager = FingerPrintViewsManager(this,
|
advancedUnlockInfoView?.setOnClickListener {
|
||||||
|
FingerPrintExplanationDialog().show(supportFragmentManager, "fingerPrintExplanationDialog")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
||||||
|
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
fingerPrintInfoView,
|
advancedUnlockInfoView,
|
||||||
checkboxPasswordView,
|
checkboxPasswordView,
|
||||||
enableButtonOnCheckedChangeListener,
|
enableButtonOnCheckedChangeListener,
|
||||||
passwordView) { passwordRetrieve ->
|
passwordView,
|
||||||
// Load the database if password is registered or retrieve
|
{ passwordEncrypted, ivSpec ->
|
||||||
passwordRetrieve?.let {
|
// Load the database if password is registered with biometric
|
||||||
|
if (passwordEncrypted != null && ivSpec != null) {
|
||||||
|
verifyCheckboxesAndLoadDatabase(
|
||||||
|
CipherDatabaseEntity(
|
||||||
|
databaseFileUri.toString(),
|
||||||
|
passwordEncrypted,
|
||||||
|
ivSpec)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passwordDecrypted ->
|
||||||
|
// Load the database if password is retrieve from biometric
|
||||||
|
passwordDecrypted?.let {
|
||||||
// Retrieve from fingerprint
|
// Retrieve from fingerprint
|
||||||
verifyKeyFileCheckboxAndLoadDatabase(it)
|
verifyKeyFileCheckboxAndLoadDatabase(it)
|
||||||
} ?: run {
|
|
||||||
// Register with fingerprint
|
|
||||||
verifyCheckboxesAndLoadDatabase()
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
fingerPrintViewsManager?.initFingerprint()
|
advancedUnlockedManager?.initBiometric()
|
||||||
// checks if fingerprint is available, will also start listening for fingerprints when available
|
|
||||||
fingerPrintViewsManager?.checkFingerprintAvailability()
|
|
||||||
fingerPrintInit = true
|
fingerPrintInit = true
|
||||||
} else {
|
} else {
|
||||||
fingerPrintViewsManager?.destroy()
|
advancedUnlockedManager?.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!fingerPrintInit) {
|
if (!fingerPrintInit) {
|
||||||
@@ -328,29 +462,43 @@ class PasswordActivity : StylishActivity(),
|
|||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
fingerPrintViewsManager?.stopListening()
|
advancedUnlockedManager?.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progressDialogThread?.unregisterProgressTask()
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
fingerPrintViewsManager?.destroy()
|
advancedUnlockedManager?.destroy()
|
||||||
}
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyCheckboxesAndLoadDatabase(password: String? = passwordView?.text?.toString(),
|
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||||
keyFile: Uri? = UriUtil.parseUriFile(keyFileView?.text?.toString())) {
|
val password: String? = passwordView?.text?.toString()
|
||||||
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
|
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
|
||||||
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
||||||
loadDatabase(keyPassword, keyFileUri)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String? = passwordView?.text?.toString(),
|
private fun verifyCheckboxesAndLoadDatabase(password: String?,
|
||||||
keyFile: Uri? = UriUtil.parseUriFile(keyFileView?.text?.toString())) {
|
keyFile: Uri?,
|
||||||
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||||
loadDatabase(password, keyFileUri)
|
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
|
||||||
|
verifyKeyFileCheckbox(keyFile)
|
||||||
|
loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
|
||||||
|
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
|
||||||
|
verifyKeyFileCheckbox(keyFile)
|
||||||
|
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
|
||||||
|
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removePassword() {
|
private fun removePassword() {
|
||||||
@@ -358,90 +506,52 @@ class PasswordActivity : StylishActivity(),
|
|||||||
checkboxPasswordView?.isChecked = false
|
checkboxPasswordView?.isChecked = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadDatabase(password: String?, keyFile: Uri?) {
|
private fun loadDatabase(databaseFileUri: Uri?,
|
||||||
|
password: String?,
|
||||||
|
keyFileUri: Uri?,
|
||||||
|
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||||
|
|
||||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||||
removePassword()
|
removePassword()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear before we load
|
databaseFileUri?.let { databaseUri ->
|
||||||
val database = Database.getInstance()
|
|
||||||
database.closeAndClear(applicationContext.filesDir)
|
|
||||||
|
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
|
||||||
// Show the progress dialog and load the database
|
// Show the progress dialog and load the database
|
||||||
ProgressDialogThread(this,
|
showProgressDialogAndLoadDatabase(
|
||||||
{ progressTaskUpdater ->
|
databaseUri,
|
||||||
LoadDatabaseRunnable(
|
password,
|
||||||
WeakReference(this@PasswordActivity),
|
keyFileUri,
|
||||||
database,
|
readOnly,
|
||||||
databaseUri,
|
cipherDatabaseEntity,
|
||||||
password,
|
false)
|
||||||
keyFile,
|
|
||||||
progressTaskUpdater,
|
|
||||||
AfterLoadingDatabase(database, password))
|
|
||||||
},
|
|
||||||
R.string.loading_database).start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
||||||
* Called after verify and try to opening the database
|
password: String?,
|
||||||
*/
|
keyFile: Uri?,
|
||||||
private inner class AfterLoadingDatabase internal constructor(var database: Database,
|
readOnly: Boolean,
|
||||||
val password: String?)
|
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||||
: ActionRunnable() {
|
fixDuplicateUUID: Boolean) {
|
||||||
|
progressDialogThread?.startDatabaseLoad(
|
||||||
override fun onFinishRun(result: Result) {
|
databaseUri,
|
||||||
runOnUiThread {
|
password,
|
||||||
// Recheck fingerprint if error
|
keyFile,
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
readOnly,
|
||||||
if (PreferencesUtil.isFingerprintEnable(this@PasswordActivity)) {
|
cipherDatabaseEntity,
|
||||||
// Stay with the same mode
|
fixDuplicateUUID
|
||||||
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() {
|
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
DuplicateUuidDialog().apply {
|
||||||
{
|
positiveAction = loadDatabaseWithFix
|
||||||
GroupActivity.launch(this@PasswordActivity, readOnly)
|
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
||||||
},
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To fix multiple view education
|
||||||
|
private var performedEductionInProgress = false
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
val inflater = menuInflater
|
val inflater = menuInflater
|
||||||
// Read menu
|
// Read menu
|
||||||
@@ -452,45 +562,58 @@ class PasswordActivity : StylishActivity(),
|
|||||||
|
|
||||||
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// Fingerprint menu
|
// Fingerprint menu
|
||||||
fingerPrintViewsManager?.inflateOptionsMenu(inflater, menu)
|
advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
// Show education views
|
if (!performedEductionInProgress) {
|
||||||
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
performedEductionInProgress = true
|
||||||
|
// Show education views
|
||||||
|
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||||
menu: Menu) {
|
menu: Menu) {
|
||||||
if (toolbar != null
|
val educationContainerView = containerView
|
||||||
&& passwordActivityEducation.checkAndPerformedFingerprintUnlockEducation(
|
val unlockEducationPerformed = educationContainerView != null
|
||||||
toolbar!!,
|
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
||||||
|
educationContainerView,
|
||||||
{
|
{
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
}))
|
})
|
||||||
else if (toolbar != null
|
if (!unlockEducationPerformed) {
|
||||||
&& toolbar!!.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
val educationToolbar = toolbar
|
||||||
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
val readOnlyEducationPerformed =
|
||||||
toolbar!!.findViewById(R.id.menu_open_file_read_mode_key),
|
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||||
{
|
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
|
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
{
|
||||||
},
|
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
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
&& PreferencesUtil.isFingerprintEnable(applicationContext)
|
})
|
||||||
&& FingerPrintHelper.isFingerprintSupported(getSystemService(FingerprintManager::class.java))
|
|
||||||
&& fingerPrintInfoView != null && fingerPrintInfoView?.fingerPrintImageView != null
|
if (!readOnlyEducationPerformed) {
|
||||||
&& passwordActivityEducation.checkAndPerformedFingerprintEducation(fingerPrintInfoView?.fingerPrintImageView!!))
|
|
||||||
;
|
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
|
||||||
|
// fingerprintEducationPerformed
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
|
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
|
||||||
|
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||||
|
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
|
||||||
|
&& passwordActivityEducation.checkAndPerformedFingerprintEducation(advancedUnlockInfoView?.unlockIconImageView!!)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
||||||
@@ -512,7 +635,7 @@ class PasswordActivity : StylishActivity(),
|
|||||||
changeOpenFileReadIcon(item)
|
changeOpenFileReadIcon(item)
|
||||||
}
|
}
|
||||||
R.id.menu_fingerprint_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
R.id.menu_fingerprint_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
fingerPrintViewsManager?.deleteEntryKey()
|
advancedUnlockedManager?.deleteEntryKey()
|
||||||
}
|
}
|
||||||
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
}
|
}
|
||||||
@@ -520,12 +643,6 @@ class PasswordActivity : StylishActivity(),
|
|||||||
return super.onOptionsItemSelected(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(
|
override fun onActivityResult(
|
||||||
requestCode: Int,
|
requestCode: Int,
|
||||||
resultCode: Int,
|
resultCode: Int,
|
||||||
@@ -538,7 +655,7 @@ class PasswordActivity : StylishActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
var keyFileResult = false
|
var keyFileResult = false
|
||||||
mKeyFileHelper?.let {
|
mOpenFileHelper?.let {
|
||||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||||
) { uri ->
|
) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
@@ -557,64 +674,28 @@ class PasswordActivity : StylishActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
companion object {
|
||||||
|
|
||||||
private val TAG = PasswordActivity::class.java.name
|
private val TAG = PasswordActivity::class.java.name
|
||||||
|
|
||||||
const val KEY_DEFAULT_FILENAME = "defaultFileName"
|
const val KEY_DEFAULT_DATABASE_PATH = "KEY_DEFAULT_DATABASE_PATH"
|
||||||
|
|
||||||
|
private const val KEY_FILENAME = "fileName"
|
||||||
|
private const val KEY_KEYFILE = "keyFile"
|
||||||
|
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||||
|
|
||||||
private const val KEY_PASSWORD = "password"
|
private const val KEY_PASSWORD = "password"
|
||||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||||
|
|
||||||
private fun buildAndLaunchIntent(activity: Activity, fileName: String, keyFile: String,
|
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
val intent = Intent(activity, PasswordActivity::class.java)
|
val intent = Intent(activity, PasswordActivity::class.java)
|
||||||
intent.putExtra(UriIntentInitTask.KEY_FILENAME, fileName)
|
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||||
intent.putExtra(UriIntentInitTask.KEY_KEYFILE, keyFile)
|
if (keyFile != null)
|
||||||
|
intent.putExtra(KEY_KEYFILE, keyFile)
|
||||||
intentBuildLauncher.invoke(intent)
|
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
|
* Standard Launch
|
||||||
@@ -624,10 +705,9 @@ class PasswordActivity : StylishActivity(),
|
|||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launch(
|
fun launch(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
fileName: String,
|
databaseFile: Uri,
|
||||||
keyFile: String) {
|
keyFile: Uri?) {
|
||||||
verifyFileNameUriFromLaunch(fileName)
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
|
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -641,12 +721,10 @@ class PasswordActivity : StylishActivity(),
|
|||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForKeyboardResult(
|
fun launchForKeyboardResult(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
fileName: String,
|
databaseFile: Uri,
|
||||||
keyFile: String) {
|
keyFile: Uri?) {
|
||||||
verifyFileNameUriFromLaunch(fileName)
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForEntrySelection(activity, intent)
|
||||||
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
|
|
||||||
KeyboardHelper.startActivityForKeyboardSelection(activity, intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,20 +738,18 @@ class PasswordActivity : StylishActivity(),
|
|||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForAutofillResult(
|
fun launchForAutofillResult(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
fileName: String,
|
databaseFile: Uri,
|
||||||
keyFile: String,
|
keyFile: Uri?,
|
||||||
assistStructure: AssistStructure?) {
|
assistStructure: AssistStructure?) {
|
||||||
verifyFileNameUriFromLaunch(fileName)
|
|
||||||
|
|
||||||
if (assistStructure != null) {
|
if (assistStructure != null) {
|
||||||
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
assistStructure)
|
assistStructure)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
launch(activity, fileName, keyFile)
|
launch(activity, databaseFile, keyFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,16 +25,16 @@ import android.content.DialogInterface
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.app.DialogFragment
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import android.support.v7.app.AlertDialog
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
|
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||||
@@ -43,15 +43,41 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
private var mKeyFile: Uri? = null
|
private var mKeyFile: Uri? = null
|
||||||
|
|
||||||
private var rootView: View? = null
|
private var rootView: View? = null
|
||||||
|
|
||||||
private var passwordCheckBox: CompoundButton? = null
|
private var passwordCheckBox: CompoundButton? = null
|
||||||
private var passView: TextView? = null
|
|
||||||
private var passConfView: TextView? = null
|
private var passwordTextInputLayout: TextInputLayout? = null
|
||||||
|
private var passwordView: TextView? = null
|
||||||
|
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||||
|
private var passwordRepeatView: TextView? = null
|
||||||
|
|
||||||
|
private var keyFileTextInputLayout: TextInputLayout? = null
|
||||||
private var keyFileCheckBox: CompoundButton? = null
|
private var keyFileCheckBox: CompoundButton? = null
|
||||||
private var keyFileView: TextView? = null
|
private var keyFileView: TextView? = null
|
||||||
|
|
||||||
private var mListener: AssignPasswordDialogListener? = null
|
private var mListener: AssignPasswordDialogListener? = null
|
||||||
|
|
||||||
private var mKeyFileHelper: KeyFileHelper? = null
|
private var mOpenFileHelper: OpenFileHelper? = null
|
||||||
|
|
||||||
|
private val passwordTextWatcher = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val keyFileTextWatcher = object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
|
||||||
|
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
|
||||||
|
override fun afterTextChanged(editable: Editable) {
|
||||||
|
keyFileCheckBox?.isChecked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface AssignPasswordDialogListener {
|
interface AssignPasswordDialogListener {
|
||||||
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
||||||
@@ -60,18 +86,25 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
keyFileChecked: Boolean, keyFile: Uri?)
|
keyFileChecked: Boolean, keyFile: Uri?)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttach(activity: Context?) {
|
override fun onAttach(activity: Context) {
|
||||||
super.onAttach(activity)
|
super.onAttach(activity)
|
||||||
try {
|
try {
|
||||||
mListener = activity as AssignPasswordDialogListener?
|
mListener = activity as AssignPasswordDialogListener
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
throw ClassCastException(activity?.toString()
|
throw ClassCastException(activity.toString()
|
||||||
+ " must implement " + AssignPasswordDialogListener::class.java.name)
|
+ " must implement " + AssignPasswordDialogListener::class.java.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
|
|
||||||
|
var allowNoMasterKey = false
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
|
||||||
|
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
||||||
|
}
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
val inflater = activity.layoutInflater
|
val inflater = activity.layoutInflater
|
||||||
|
|
||||||
@@ -80,36 +113,21 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
.setTitle(R.string.assign_master_key)
|
.setTitle(R.string.assign_master_key)
|
||||||
// Add action buttons
|
// Add action buttons
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||||
passView = rootView?.findViewById(R.id.pass_password)
|
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
||||||
passView?.addTextChangedListener(object : TextWatcher {
|
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||||
|
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
||||||
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)
|
|
||||||
|
|
||||||
|
keyFileTextInputLayout = rootView?.findViewById(R.id.keyfile_input_layout)
|
||||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||||
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
|
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) {}
|
mOpenFileHelper = OpenFileHelper(this)
|
||||||
|
rootView?.findViewById<View>(R.id.open_database_button)?.setOnClickListener { view ->
|
||||||
override fun afterTextChanged(editable: Editable) {
|
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
|
||||||
keyFileCheckBox?.isChecked = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
mKeyFileHelper = KeyFileHelper(this)
|
|
||||||
rootView?.findViewById<View>(R.id.browse_button)?.setOnClickListener { view ->
|
|
||||||
mKeyFileHelper?.openFileOnClickViewListener?.onClick(view) }
|
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
|
|
||||||
@@ -124,7 +142,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
var error = verifyPassword() || verifyFile()
|
var error = verifyPassword() || verifyFile()
|
||||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||||
error = true
|
error = true
|
||||||
showNoKeyConfirmationDialog()
|
if (allowNoMasterKey)
|
||||||
|
showNoKeyConfirmationDialog()
|
||||||
|
else {
|
||||||
|
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!error) {
|
if (!error) {
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(
|
||||||
@@ -149,20 +171,35 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// To check checkboxes if a text is present
|
||||||
|
passwordView?.addTextChangedListener(passwordTextWatcher)
|
||||||
|
keyFileView?.addTextChangedListener(keyFileTextWatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
passwordView?.removeTextChangedListener(passwordTextWatcher)
|
||||||
|
keyFileView?.removeTextChangedListener(keyFileTextWatcher)
|
||||||
|
}
|
||||||
|
|
||||||
private fun verifyPassword(): Boolean {
|
private fun verifyPassword(): Boolean {
|
||||||
var error = false
|
var error = false
|
||||||
if (passwordCheckBox != null
|
if (passwordCheckBox != null
|
||||||
&& passwordCheckBox!!.isChecked
|
&& passwordCheckBox!!.isChecked
|
||||||
&& passView != null
|
&& passwordView != null
|
||||||
&& passConfView != null) {
|
&& passwordRepeatView != null) {
|
||||||
mMasterPassword = passView!!.text.toString()
|
mMasterPassword = passwordView!!.text.toString()
|
||||||
val confPassword = passConfView!!.text.toString()
|
val confPassword = passwordRepeatView!!.text.toString()
|
||||||
|
|
||||||
// Verify that passwords match
|
// Verify that passwords match
|
||||||
if (mMasterPassword != confPassword) {
|
if (mMasterPassword != confPassword) {
|
||||||
error = true
|
error = true
|
||||||
// Passwords do not match
|
// Passwords do not match
|
||||||
Toast.makeText(context, R.string.error_pass_match, Toast.LENGTH_LONG).show()
|
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
|
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
|
||||||
@@ -170,6 +207,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
showEmptyPasswordConfirmationDialog()
|
showEmptyPasswordConfirmationDialog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,13 +215,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
var error = false
|
var error = false
|
||||||
if (keyFileCheckBox != null
|
if (keyFileCheckBox != null
|
||||||
&& keyFileCheckBox!!.isChecked) {
|
&& keyFileCheckBox!!.isChecked) {
|
||||||
val keyFile = UriUtil.parseUriFile(keyFileView?.text?.toString())
|
|
||||||
mKeyFile = keyFile
|
|
||||||
|
|
||||||
// Verify that a keyfile is set
|
UriUtil.parse(keyFileView?.text?.toString())?.let { uri ->
|
||||||
if (keyFile == null || keyFile.toString().isEmpty()) {
|
mKeyFile = uri
|
||||||
|
} ?: run {
|
||||||
error = true
|
error = true
|
||||||
Toast.makeText(context, R.string.error_nokeyfile, Toast.LENGTH_LONG).show()
|
keyFileTextInputLayout?.error = getString(R.string.error_nokeyfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return error
|
return error
|
||||||
@@ -201,7 +238,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
this@AssignMasterKeyDialogFragment.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
builder.create().show()
|
builder.create().show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,7 +253,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
this@AssignMasterKeyDialogFragment.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
builder.create().show()
|
builder.create().show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,13 +261,26 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||||
) { uri ->
|
) { uri ->
|
||||||
UriUtil.parseUriFile(uri)?.let { pathUri ->
|
uri?.let { pathUri ->
|
||||||
keyFileCheckBox?.isChecked = true
|
keyFileCheckBox?.isChecked = true
|
||||||
keyFileView?.text = pathUri.toString()
|
keyFileView?.text = pathUri.toString()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
||||||
|
|
||||||
|
fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment {
|
||||||
|
val fragment = AssignMasterKeyDialogFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import android.support.v7.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
|
import android.widget.TextView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.Util
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class BrowserDialogFragment : DialogFragment() {
|
class BrowserDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -35,17 +36,20 @@ class BrowserDialogFragment : DialogFragment() {
|
|||||||
// Get the layout inflater
|
// Get the layout inflater
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_browser_install, null)
|
val root = activity.layoutInflater.inflate(R.layout.fragment_browser_install, null)
|
||||||
builder.setView(root)
|
builder.setView(root)
|
||||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
val market = root.findViewById<Button>(R.id.install_market)
|
val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description)
|
||||||
|
textDescription.text = getString(R.string.file_manager_install_description)
|
||||||
|
|
||||||
|
val market = root.findViewById<Button>(R.id.file_manager_install_play_store)
|
||||||
market.setOnClickListener {
|
market.setOnClickListener {
|
||||||
Util.gotoUrl(context!!, R.string.filemanager_play_store)
|
UriUtil.gotoUrl(context!!, R.string.file_manager_play_store)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
val web = root.findViewById<Button>(R.id.install_web)
|
val web = root.findViewById<Button>(R.id.file_manager_install_f_droid)
|
||||||
web.setOnClickListener {
|
web.setOnClickListener {
|
||||||
Util.gotoUrl(context!!, R.string.filemanager_f_droid)
|
UriUtil.gotoUrl(context!!, R.string.file_manager_f_droid)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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 androidx.fragment.app.DialogFragment
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
class DuplicateUuidDialog : DialogFragment() {
|
||||||
|
|
||||||
|
var positiveAction: (() -> Unit)? = null
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
// Use the Builder class for convenient dialog construction
|
||||||
|
val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply {
|
||||||
|
val message = getString(R.string.contains_duplicate_uuid) +
|
||||||
|
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
|
||||||
|
setMessage(message)
|
||||||
|
setPositiveButton(getString(android.R.string.ok)) { _, _ ->
|
||||||
|
positiveAction?.invoke()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
setNegativeButton(getString(android.R.string.cancel)) { _, _ -> dismiss() }
|
||||||
|
}
|
||||||
|
// Create the AlertDialog object and return it
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
this.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,17 +17,19 @@
|
|||||||
* 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.fingerprint
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import android.support.v4.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import android.support.v7.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.biometric.FingerPrintAnimatedVector
|
||||||
|
import com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
class FingerPrintExplanationDialog : DialogFragment() {
|
class FingerPrintExplanationDialog : DialogFragment() {
|
||||||
@@ -41,11 +43,16 @@ class FingerPrintExplanationDialog : DialogFragment() {
|
|||||||
|
|
||||||
val rootView = inflater.inflate(R.layout.fragment_fingerprint_explanation, null)
|
val rootView = inflater.inflate(R.layout.fragment_fingerprint_explanation, null)
|
||||||
|
|
||||||
val fingerprintSettingWayTextView = rootView.findViewById<View>(R.id.fingerprint_setting_way_text)
|
rootView.findViewById<View>(R.id.fingerprint_setting_link_text).setOnClickListener {
|
||||||
fingerprintSettingWayTextView.setOnClickListener { startActivity(Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS)) }
|
startActivity(Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS))
|
||||||
|
}
|
||||||
|
|
||||||
|
rootView.findViewById<View>(R.id.auto_open_biometric_prompt_button).setOnClickListener {
|
||||||
|
startActivity(Intent(activity, SettingsAdvancedUnlockActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
fingerPrintAnimatedVector = FingerPrintAnimatedVector(activity,
|
fingerPrintAnimatedVector = FingerPrintAnimatedVector(activity,
|
||||||
rootView.findViewById(R.id.fingerprint_image))
|
rootView.findViewById(R.id.biometric_image))
|
||||||
|
|
||||||
builder.setView(rootView)
|
builder.setView(rootView)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
@@ -22,14 +22,18 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.app.DialogFragment
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import android.support.v7.app.AlertDialog
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.Button
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.SeekBar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.applyFontVisibility
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
|
|
||||||
class GeneratePasswordDialogFragment : DialogFragment() {
|
class GeneratePasswordDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -37,6 +41,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private var root: View? = null
|
private var root: View? = null
|
||||||
private var lengthTextView: EditText? = null
|
private var lengthTextView: EditText? = null
|
||||||
|
private var passwordInputLayoutView: TextInputLayout? = null
|
||||||
private var passwordView: EditText? = null
|
private var passwordView: EditText? = null
|
||||||
|
|
||||||
private var uppercaseBox: CompoundButton? = null
|
private var uppercaseBox: CompoundButton? = null
|
||||||
@@ -49,12 +54,12 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
private var bracketsBox: CompoundButton? = null
|
private var bracketsBox: CompoundButton? = null
|
||||||
private var extendedBox: CompoundButton? = null
|
private var extendedBox: CompoundButton? = null
|
||||||
|
|
||||||
override fun onAttach(context: Context?) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
try {
|
try {
|
||||||
mListener = context as GeneratePasswordListener?
|
mListener = context as GeneratePasswordListener
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
throw ClassCastException(context?.toString()
|
throw ClassCastException(context.toString()
|
||||||
+ " must implement " + GeneratePasswordListener::class.java.name)
|
+ " must implement " + GeneratePasswordListener::class.java.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,6 +70,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
val inflater = activity.layoutInflater
|
val inflater = activity.layoutInflater
|
||||||
root = inflater.inflate(R.layout.fragment_generate_password, null)
|
root = inflater.inflate(R.layout.fragment_generate_password, null)
|
||||||
|
|
||||||
|
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
|
||||||
passwordView = root?.findViewById(R.id.password)
|
passwordView = root?.findViewById(R.id.password)
|
||||||
passwordView?.applyFontVisibility()
|
passwordView?.applyFontVisibility()
|
||||||
|
|
||||||
@@ -108,7 +114,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
mListener?.cancelPassword(bundle)
|
mListener?.cancelPassword(bundle)
|
||||||
|
|
||||||
@@ -162,8 +168,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
try {
|
try {
|
||||||
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
||||||
|
|
||||||
val generator = PasswordGenerator(resources)
|
password = PasswordGenerator(resources).generatePassword(length,
|
||||||
password = generator.generatePassword(length,
|
|
||||||
uppercaseBox?.isChecked == true,
|
uppercaseBox?.isChecked == true,
|
||||||
lowercaseBox?.isChecked == true,
|
lowercaseBox?.isChecked == true,
|
||||||
digitsBox?.isChecked == true,
|
digitsBox?.isChecked == true,
|
||||||
@@ -174,9 +179,9 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
bracketsBox?.isChecked == true,
|
bracketsBox?.isChecked == true,
|
||||||
extendedBox?.isChecked == true)
|
extendedBox?.isChecked == true)
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
Toast.makeText(context, R.string.error_wrong_length, Toast.LENGTH_LONG).show()
|
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
|
passwordInputLayoutView?.error = e.message
|
||||||
}
|
}
|
||||||
|
|
||||||
return password
|
return password
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ import android.app.Dialog
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.design.widget.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import android.support.v4.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import android.support.v7.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@@ -62,15 +62,15 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttach(context: Context?) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
// Verify that the host activity implements the callback interface
|
// Verify that the host activity implements the callback interface
|
||||||
try {
|
try {
|
||||||
// Instantiate the NoticeDialogListener so we can send events to the host
|
// Instantiate the NoticeDialogListener so we can send events to the host
|
||||||
editGroupListener = context as EditGroupListener?
|
editGroupListener = context as EditGroupListener
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
// The activity doesn't implement the interface, throw exception
|
// The activity doesn't implement the interface, throw exception
|
||||||
throw ClassCastException(context?.toString()
|
throw ClassCastException(context.toString()
|
||||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.setView(root)
|
builder.setView(root)
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
editGroupListener?.cancelEditGroup(
|
editGroupListener?.cancelEditGroup(
|
||||||
editGroupDialogAction,
|
editGroupDialogAction,
|
||||||
nameTextView?.text?.toString(),
|
nameTextView?.text?.toString(),
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ import android.content.Context
|
|||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import android.support.v4.widget.ImageViewCompat
|
import androidx.core.widget.ImageViewCompat
|
||||||
import android.support.v7.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -45,13 +45,13 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
private var iconPickerListener: IconPickerListener? = null
|
private var iconPickerListener: IconPickerListener? = null
|
||||||
private var iconPack: IconPack? = null
|
private var iconPack: IconPack? = null
|
||||||
|
|
||||||
override fun onAttach(context: Context?) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
try {
|
try {
|
||||||
iconPickerListener = context as IconPickerListener?
|
iconPickerListener = context as IconPickerListener
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
// The activity doesn't implement the interface, throw exception
|
// The activity doesn't implement the interface, throw exception
|
||||||
throw ClassCastException(context!!.toString()
|
throw ClassCastException(context.toString()
|
||||||
+ " must implement " + IconPickerListener::class.java.name)
|
+ " must implement " + IconPickerListener::class.java.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.setNegativeButton(R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog.cancel() }
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ import android.content.Intent
|
|||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.support.v4.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import android.support.v7.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.Util
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class KeyboardExplanationDialogFragment : DialogFragment() {
|
class KeyboardExplanationDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -40,16 +40,14 @@ class KeyboardExplanationDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
val rootView = inflater.inflate(R.layout.fragment_keyboard_explanation, null)
|
val rootView = inflater.inflate(R.layout.fragment_keyboard_explanation, null)
|
||||||
|
|
||||||
rootView.findViewById<View>(R.id.keyboards_activate_setting_path1_text)
|
rootView.findViewById<View>(R.id.keyboards_activate_device_setting_button)
|
||||||
.setOnClickListener { launchActivateKeyboardSetting() }
|
|
||||||
rootView.findViewById<View>(R.id.keyboards_activate_setting_path2_text)
|
|
||||||
.setOnClickListener { launchActivateKeyboardSetting() }
|
.setOnClickListener { launchActivateKeyboardSetting() }
|
||||||
|
|
||||||
val containerKeyboardSwitcher = rootView.findViewById<View>(R.id.container_keyboard_switcher)
|
val containerKeyboardSwitcher = rootView.findViewById<View>(R.id.container_keyboard_switcher)
|
||||||
if (BuildConfig.CLOSED_STORE) {
|
if (BuildConfig.CLOSED_STORE) {
|
||||||
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
|
containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
|
||||||
} else {
|
} else {
|
||||||
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context!!, R.string.keyboard_switcher_f_droid) }
|
containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_f_droid) }
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.setView(rootView)
|
builder.setView(rootView)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import android.app.AlertDialog
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||||
@@ -35,7 +35,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
||||||
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
|
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
|
||||||
builder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.cancel() }
|
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,17 +20,15 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.os.Bundle
|
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.SpannableStringBuilder
|
||||||
import android.widget.Toast
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
|
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.Util
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Dialog that asks the user to download the pro version or make a donation.
|
* Custom Dialog that asks the user to download the pro version or make a donation.
|
||||||
@@ -44,25 +42,16 @@ class ProFeatureDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
val stringBuilder = SpannableStringBuilder()
|
val stringBuilder = SpannableStringBuilder()
|
||||||
if (BuildConfig.CLOSED_STORE) {
|
if (BuildConfig.CLOSED_STORE) {
|
||||||
// TODO HtmlCompat with androidX
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_ad_free))).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
|
||||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_buy_pro)))
|
|
||||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||||
try {
|
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
|
||||||
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 {
|
} else {
|
||||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_feature_generosity))).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_donation)))
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||||
try {
|
UriUtil.gotoUrl(context!!, R.string.contribution_url)
|
||||||
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.setMessage(stringBuilder)
|
||||||
|
|||||||
@@ -19,33 +19,38 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.preference.PreferenceManager
|
import android.preference.PreferenceManager
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
class ReadOnlyDialog(context: Context) : AlertDialog(context) {
|
class ReadOnlyDialog : DialogFragment() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle) {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val ctx = context
|
activity?.let { activity ->
|
||||||
var warning = ctx.getString(R.string.read_only_warning)
|
// Use the Builder class for convenient dialog construction
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
val builder = androidx.appcompat.app.AlertDialog.Builder(activity)
|
||||||
warning = warning + "\n\n" + context.getString(R.string.read_only_kitkat_warning)
|
|
||||||
|
var warning = getString(R.string.read_only_warning)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
warning = warning + "\n\n" + getString(R.string.read_only_kitkat_warning)
|
||||||
|
}
|
||||||
|
builder.setMessage(warning)
|
||||||
|
|
||||||
|
builder.setPositiveButton(getString(android.R.string.ok)) { _, _ -> dismiss() }
|
||||||
|
builder.setNegativeButton(getString(R.string.beta_dontask)) { _, _ ->
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val edit = prefs.edit()
|
||||||
|
edit.putBoolean(getString(R.string.show_read_only_warning), false)
|
||||||
|
edit.apply()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the AlertDialog object and return it
|
||||||
|
return builder.create()
|
||||||
}
|
}
|
||||||
setMessage(warning)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import android.support.v4.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import android.support.v7.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.RadioGroup
|
import android.widget.RadioGroup
|
||||||
@@ -42,12 +42,14 @@ class SortDialogFragment : DialogFragment() {
|
|||||||
private var mAscending: Boolean = true
|
private var mAscending: Boolean = true
|
||||||
private var mRecycleBinBottom: Boolean = true
|
private var mRecycleBinBottom: Boolean = true
|
||||||
|
|
||||||
override fun onAttach(context: Context?) {
|
private var recycleBinBottomView: CompoundButton? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
try {
|
try {
|
||||||
mListener = context as SortSelectionListener?
|
mListener = context as SortSelectionListener
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
throw ClassCastException(context!!.toString()
|
throw ClassCastException(context.toString()
|
||||||
+ " must implement " + SortSelectionListener::class.java.name)
|
+ " must implement " + SortSelectionListener::class.java.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,15 +75,15 @@ class SortDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mCheckedId = retrieveViewFromEnum(mSortNodeEnum!!)
|
mCheckedId = retrieveViewFromEnum(mSortNodeEnum)
|
||||||
|
|
||||||
val rootView = activity.layoutInflater.inflate(R.layout.fragment_sort_selection, null)
|
val rootView = activity.layoutInflater.inflate(R.layout.fragment_sort_selection, null)
|
||||||
builder.setTitle(R.string.sort_menu)
|
builder.setTitle(R.string.sort_menu)
|
||||||
builder.setView(rootView)
|
builder.setView(rootView)
|
||||||
// Add action buttons
|
// Add action buttons
|
||||||
.setPositiveButton(android.R.string.ok
|
.setPositiveButton(android.R.string.ok
|
||||||
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum!!, mAscending, mGroupsBefore, mRecycleBinBottom) }
|
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
|
||||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
|
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
|
||||||
// Check if is ascending or descending
|
// Check if is ascending or descending
|
||||||
@@ -93,25 +95,35 @@ class SortDialogFragment : DialogFragment() {
|
|||||||
groupsBeforeView.isChecked = mGroupsBefore
|
groupsBeforeView.isChecked = mGroupsBefore
|
||||||
groupsBeforeView.setOnCheckedChangeListener { _, isChecked -> mGroupsBefore = isChecked }
|
groupsBeforeView.setOnCheckedChangeListener { _, isChecked -> mGroupsBefore = isChecked }
|
||||||
|
|
||||||
val recycleBinBottomView = rootView.findViewById<CompoundButton>(R.id.sort_selection_recycle_bin_bottom)
|
recycleBinBottomView = rootView.findViewById(R.id.sort_selection_recycle_bin_bottom)
|
||||||
if (!recycleBinAllowed) {
|
if (!recycleBinAllowed) {
|
||||||
recycleBinBottomView.visibility = View.GONE
|
recycleBinBottomView?.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
// Check if recycle bin at the bottom
|
// Check if recycle bin at the bottom
|
||||||
recycleBinBottomView.isChecked = mRecycleBinBottom
|
recycleBinBottomView?.isChecked = mRecycleBinBottom
|
||||||
recycleBinBottomView.setOnCheckedChangeListener { _, isChecked -> mRecycleBinBottom = isChecked }
|
recycleBinBottomView?.setOnCheckedChangeListener { _, isChecked -> mRecycleBinBottom = isChecked }
|
||||||
|
|
||||||
|
disableRecycleBinBottomOptionIfNaturalOrder()
|
||||||
}
|
}
|
||||||
|
|
||||||
val sortSelectionRadioGroupView = rootView.findViewById<RadioGroup>(R.id.sort_selection_radio_group)
|
val sortSelectionRadioGroupView = rootView.findViewById<RadioGroup>(R.id.sort_selection_radio_group)
|
||||||
// Check value by default
|
// Check value by default
|
||||||
sortSelectionRadioGroupView.check(mCheckedId)
|
sortSelectionRadioGroupView.check(mCheckedId)
|
||||||
sortSelectionRadioGroupView.setOnCheckedChangeListener { _, checkedId -> mSortNodeEnum = retrieveSortEnumFromViewId(checkedId) }
|
sortSelectionRadioGroupView.setOnCheckedChangeListener { _, checkedId ->
|
||||||
|
mSortNodeEnum = retrieveSortEnumFromViewId(checkedId)
|
||||||
|
disableRecycleBinBottomOptionIfNaturalOrder()
|
||||||
|
}
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun disableRecycleBinBottomOptionIfNaturalOrder() {
|
||||||
|
// Disable recycle bin if natural order
|
||||||
|
recycleBinBottomView?.isEnabled = mSortNodeEnum != SortNodeEnum.DB
|
||||||
|
}
|
||||||
|
|
||||||
@IdRes
|
@IdRes
|
||||||
private fun retrieveViewFromEnum(sortNodeEnum: SortNodeEnum): Int {
|
private fun retrieveViewFromEnum(sortNodeEnum: SortNodeEnum): Int {
|
||||||
return when (sortNodeEnum) {
|
return when (sortNodeEnum) {
|
||||||
|
|||||||
@@ -22,12 +22,13 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import android.support.v7.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
class UnavailableFeatureDialogFragment : DialogFragment() {
|
class UnavailableFeatureDialogFragment : DialogFragment() {
|
||||||
@@ -53,7 +54,7 @@ class UnavailableFeatureDialogFragment : DialogFragment() {
|
|||||||
androidNameFromApiNumber(Build.VERSION.SDK_INT, Build.VERSION.RELEASE),
|
androidNameFromApiNumber(Build.VERSION.SDK_INT, Build.VERSION.RELEASE),
|
||||||
androidNameFromApiNumber(minVersionRequired)))
|
androidNameFromApiNumber(minVersionRequired)))
|
||||||
message.append("\n\n")
|
message.append("\n\n")
|
||||||
.append(Html.fromHtml("<a href=\"https://source.android.com/setup/build-numbers\">CodeNames</a>"))
|
.append(HtmlCompat.fromHtml("<a href=\"https://source.android.com/setup/build-numbers\">CodeNames</a>", HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||||
} else
|
} else
|
||||||
message.append(getString(R.string.unavailable_feature_hardware))
|
message.append(getString(R.string.unavailable_feature_hardware))
|
||||||
|
|
||||||
|
|||||||
@@ -20,17 +20,14 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.os.Bundle
|
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.SpannableStringBuilder
|
||||||
import android.widget.Toast
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.Util
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Dialog that asks the user to download the pro version or make a donation.
|
* Custom Dialog that asks the user to download the pro version or make a donation.
|
||||||
@@ -45,34 +42,26 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
|||||||
val stringBuilder = SpannableStringBuilder()
|
val stringBuilder = SpannableStringBuilder()
|
||||||
if (BuildConfig.CLOSED_STORE) {
|
if (BuildConfig.CLOSED_STORE) {
|
||||||
if (BuildConfig.FULL_VERSION) {
|
if (BuildConfig.FULL_VERSION) {
|
||||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature_thanks))).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
.append(Html.fromHtml(getString(R.string.html_rose))).append("\n\n")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_work_hard))).append("\n")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_work_hard), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
||||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_upgrade))).append(" ")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
||||||
} else {
|
} else {
|
||||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_buy_pro))).append("\n")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
||||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||||
try {
|
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
|
||||||
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() }
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_contibute))).append(" ")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||||
try {
|
UriUtil.gotoUrl(context!!, R.string.contribution_url)
|
||||||
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.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +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.helpers;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
public class ClipDataCompat {
|
|
||||||
private static Method getClipDataFromIntent;
|
|
||||||
private static Method getDescription;
|
|
||||||
private static Method getItemCount;
|
|
||||||
private static Method getLabel;
|
|
||||||
private static Method getItemAt;
|
|
||||||
private static Method getUri;
|
|
||||||
|
|
||||||
private static boolean initSucceded;
|
|
||||||
|
|
||||||
static {
|
|
||||||
try {
|
|
||||||
Class clipData = Class.forName("android.content.ClipData");
|
|
||||||
getDescription = clipData.getMethod("getDescription", (Class[])null);
|
|
||||||
getItemCount = clipData.getMethod("getItemCount", (Class[])null);
|
|
||||||
getItemAt = clipData.getMethod("getItemAt", new Class[]{int.class});
|
|
||||||
Class clipDescription = Class.forName("android.content.ClipDescription");
|
|
||||||
getLabel = clipDescription.getMethod("getLabel", (Class[])null);
|
|
||||||
|
|
||||||
Class clipDataItem = Class.forName("android.content.ClipData$Item");
|
|
||||||
getUri = clipDataItem.getMethod("getUri", (Class[])null);
|
|
||||||
|
|
||||||
getClipDataFromIntent = Intent.class.getMethod("getClipData", (Class[])null);
|
|
||||||
|
|
||||||
initSucceded = true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
initSucceded = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Uri getUriFromIntent(Intent i, String key) {
|
|
||||||
if (initSucceded) {
|
|
||||||
try {
|
|
||||||
Object clip = getClipDataFromIntent.invoke(i);
|
|
||||||
|
|
||||||
if (clip != null) {
|
|
||||||
Object clipDescription = getDescription.invoke(clip);
|
|
||||||
CharSequence label = (CharSequence)getLabel.invoke(clipDescription);
|
|
||||||
if (label.equals(key)) {
|
|
||||||
int itemCount = (int) getItemCount.invoke(clip);
|
|
||||||
if (itemCount == 1) {
|
|
||||||
Object clipItem = getItemAt.invoke(clip,0);
|
|
||||||
if (clipItem != null) {
|
|
||||||
return (Uri)getUri.invoke(clipItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Fall through below to backup method if reflection fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return i.getParcelableExtra(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
@@ -10,6 +11,12 @@ object EntrySelectionHelper {
|
|||||||
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
|
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
|
||||||
private const val DEFAULT_ENTRY_SELECTION_MODE = false
|
private const val DEFAULT_ENTRY_SELECTION_MODE = false
|
||||||
|
|
||||||
|
fun startActivityForEntrySelection(context: Context, intent: Intent) {
|
||||||
|
addEntrySelectionModeExtraInIntent(intent)
|
||||||
|
// only to avoid visible flickering when redirecting
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
fun addEntrySelectionModeExtraInIntent(intent: Intent) {
|
fun addEntrySelectionModeExtraInIntent(intent: Intent) {
|
||||||
intent.putExtra(EXTRA_ENTRY_SELECTION_MODE, true)
|
intent.putExtra(EXTRA_ENTRY_SELECTION_MODE, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Activity.RESULT_OK
|
import android.app.Activity.RESULT_OK
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -26,21 +27,20 @@ import android.content.Intent
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.support.v4.app.Fragment
|
|
||||||
import android.support.v4.app.FragmentActivity
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
||||||
import com.kunzisoft.keepass.fileselect.StorageAF
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class KeyFileHelper {
|
class OpenFileHelper {
|
||||||
|
|
||||||
private var activity: Activity? = null
|
private var activity: Activity? = null
|
||||||
private var fragment: Fragment? = null
|
private var fragment: Fragment? = null
|
||||||
|
|
||||||
val openFileOnClickViewListener: OpenFileOnClickViewListener
|
val openFileOnClickViewListener: OpenFileOnClickViewListener
|
||||||
get() = OpenFileOnClickViewListener(null)
|
get() = OpenFileOnClickViewListener()
|
||||||
|
|
||||||
constructor(context: Activity) {
|
constructor(context: Activity) {
|
||||||
this.activity = context
|
this.activity = context
|
||||||
@@ -52,69 +52,55 @@ class KeyFileHelper {
|
|||||||
this.fragment = context
|
this.fragment = context
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class OpenFileOnClickViewListener(private val dataUri: (() -> Uri)?) : View.OnClickListener {
|
inner class OpenFileOnClickViewListener : View.OnClickListener {
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
try {
|
try {
|
||||||
if (activity != null && StorageAF.useStorageFramework(activity!!)) {
|
try {
|
||||||
openActivityWithActionOpenDocument()
|
openActivityWithActionOpenDocument()
|
||||||
} else {
|
} catch(e: Exception) {
|
||||||
openActivityWithActionGetContent()
|
openActivityWithActionGetContent()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Enable to start the file picker activity", e)
|
Log.e(TAG, "Enable to start the file picker activity", e)
|
||||||
|
// Open browser dialog
|
||||||
// Open File picker if can't open activity
|
if (lookForOpenIntentsFilePicker())
|
||||||
if (lookForOpenIntentsFilePicker(dataUri?.invoke()))
|
|
||||||
showBrowserDialog()
|
showBrowserDialog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
private fun openActivityWithActionOpenDocument() {
|
private fun openActivityWithActionOpenDocument() {
|
||||||
val i = Intent(StorageAF.ACTION_OPEN_DOCUMENT)
|
val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply {
|
||||||
i.addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
i.type = "*/*"
|
type = "*/*"
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
|
||||||
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
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)
|
if (fragment != null)
|
||||||
fragment?.startActivityForResult(i, OPEN_DOC)
|
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||||
else
|
else
|
||||||
activity?.startActivityForResult(i, OPEN_DOC)
|
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openActivityWithActionGetContent() {
|
private fun openActivityWithActionGetContent() {
|
||||||
val i = Intent(Intent.ACTION_GET_CONTENT)
|
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
i.addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
i.type = "*/*"
|
type = "*/*"
|
||||||
|
}
|
||||||
if (fragment != null)
|
if (fragment != null)
|
||||||
fragment?.startActivityForResult(i, GET_CONTENT)
|
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||||
else
|
else
|
||||||
activity?.startActivityForResult(i, GET_CONTENT)
|
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOpenFileOnClickViewListener(dataUri: () -> Uri): OpenFileOnClickViewListener {
|
private fun lookForOpenIntentsFilePicker(): Boolean {
|
||||||
return OpenFileOnClickViewListener(dataUri)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean {
|
|
||||||
var showBrowser = false
|
var showBrowser = false
|
||||||
try {
|
try {
|
||||||
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
||||||
val intent = Intent(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)
|
if (fragment != null)
|
||||||
fragment?.startActivityForResult(intent, FILE_BROWSE)
|
fragment?.startActivityForResult(intent, FILE_BROWSE)
|
||||||
else
|
else
|
||||||
@@ -158,7 +144,7 @@ class KeyFileHelper {
|
|||||||
val browserDialogFragment = BrowserDialogFragment()
|
val browserDialogFragment = BrowserDialogFragment()
|
||||||
if (fragment != null && fragment!!.fragmentManager != null)
|
if (fragment != null && fragment!!.fragmentManager != null)
|
||||||
browserDialogFragment.show(fragment!!.fragmentManager!!, "browserDialog")
|
browserDialogFragment.show(fragment!!.fragmentManager!!, "browserDialog")
|
||||||
else if (activity!!.fragmentManager != null)
|
else
|
||||||
browserDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
|
browserDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Can't open BrowserDialog", e)
|
Log.e(TAG, "Can't open BrowserDialog", e)
|
||||||
@@ -182,7 +168,7 @@ class KeyFileHelper {
|
|||||||
val filename = data?.dataString
|
val filename = data?.dataString
|
||||||
var keyUri: Uri? = null
|
var keyUri: Uri? = null
|
||||||
if (filename != null) {
|
if (filename != null) {
|
||||||
keyUri = UriUtil.parseUriFile(filename)
|
keyUri = UriUtil.parse(filename)
|
||||||
}
|
}
|
||||||
keyFileCallback?.invoke(keyUri)
|
keyFileCallback?.invoke(keyUri)
|
||||||
}
|
}
|
||||||
@@ -191,23 +177,18 @@ class KeyFileHelper {
|
|||||||
GET_CONTENT, OPEN_DOC -> {
|
GET_CONTENT, OPEN_DOC -> {
|
||||||
if (resultCode == RESULT_OK) {
|
if (resultCode == RESULT_OK) {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
var uri = data.data
|
val uri = data.data
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
if (StorageAF.useStorageFramework(activity!!)) {
|
try {
|
||||||
try {
|
// try to persist read and write permissions
|
||||||
// try to persist read and write permissions
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
activity?.contentResolver?.apply {
|
||||||
activity?.contentResolver?.apply {
|
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
takePersistableUriPermission(uri!!, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
takePersistableUriPermission(uri!!, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
// nop
|
|
||||||
}
|
}
|
||||||
}
|
} catch (e: Exception) {
|
||||||
if (requestCode == GET_CONTENT) {
|
// nop
|
||||||
uri = UriUtil.translateUri(activity!!, uri)
|
|
||||||
}
|
}
|
||||||
keyFileCallback?.invoke(uri)
|
keyFileCallback?.invoke(uri)
|
||||||
}
|
}
|
||||||
@@ -221,7 +202,13 @@ class KeyFileHelper {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "KeyFileHelper"
|
private const val TAG = "OpenFileHelper"
|
||||||
|
|
||||||
|
private var APP_ACTION_OPEN_DOCUMENT: String = try {
|
||||||
|
Intent::class.java.getField("ACTION_OPEN_DOCUMENT").get(null) as String
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"android.intent.action.OPEN_DOCUMENT"
|
||||||
|
}
|
||||||
|
|
||||||
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
||||||
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.net.Uri
|
|
||||||
|
|
||||||
interface UriIntentInitTaskCallback {
|
|
||||||
fun onPostInitTask(databaseFileUri: Uri?, keyFileUri: Uri?, errorStringId: Int?)
|
|
||||||
}
|
|
||||||
@@ -51,46 +51,46 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected var timeoutEnable: Boolean = true
|
protected var mTimeoutEnable: Boolean = true
|
||||||
|
|
||||||
private var lockReceiver: LockReceiver? = null
|
private var mLockReceiver: LockReceiver? = null
|
||||||
private var exitLock: Boolean = false
|
private var mExitLock: Boolean = false
|
||||||
|
|
||||||
// Force readOnly if Entry Selection mode
|
// Force readOnly if Entry Selection mode
|
||||||
protected var readOnly: Boolean = false
|
protected var mReadOnly: Boolean = false
|
||||||
get() {
|
get() {
|
||||||
return field || selectionMode
|
return field || mSelectionMode
|
||||||
}
|
}
|
||||||
protected var selectionMode: Boolean = false
|
protected var mSelectionMode: Boolean = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)) {
|
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)) {
|
||||||
timeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
|
mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
|
||||||
} else {
|
} else {
|
||||||
if (intent != null)
|
if (intent != null)
|
||||||
timeoutEnable = intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
|
mTimeoutEnable = intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
lockReceiver = LockReceiver()
|
mLockReceiver = LockReceiver()
|
||||||
val intentFilter = IntentFilter().apply {
|
val intentFilter = IntentFilter().apply {
|
||||||
addAction(Intent.ACTION_SCREEN_OFF)
|
addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
addAction(LOCK_ACTION)
|
addAction(LOCK_ACTION)
|
||||||
}
|
}
|
||||||
registerReceiver(lockReceiver, intentFilter)
|
registerReceiver(mLockReceiver, intentFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
exitLock = false
|
mExitLock = false
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
|
mReadOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
if (resultCode == RESULT_EXIT_LOCK) {
|
if (resultCode == RESULT_EXIT_LOCK) {
|
||||||
exitLock = true
|
mExitLock = true
|
||||||
if (Database.getInstance().loaded) {
|
if (Database.getInstance().loaded) {
|
||||||
lockAndExit()
|
lockAndExit()
|
||||||
}
|
}
|
||||||
@@ -101,9 +101,9 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// To refresh when back to normal workflow from selection workflow
|
// To refresh when back to normal workflow from selection workflow
|
||||||
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
|
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
|
||||||
|
|
||||||
if (timeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
// End activity if database not loaded
|
// End activity if database not loaded
|
||||||
if (!Database.getInstance().loaded) {
|
if (!Database.getInstance().loaded) {
|
||||||
finish()
|
finish()
|
||||||
@@ -115,7 +115,7 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
// If the time is out -> close the Activity
|
// If the time is out -> close the Activity
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||||
// If onCreate already record time
|
// If onCreate already record time
|
||||||
if (!exitLock)
|
if (!mExitLock)
|
||||||
TimeoutHelper.recordTime(this)
|
TimeoutHelper.recordTime(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,15 +123,15 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
ReadOnlyHelper.onSaveInstanceState(outState, mReadOnly)
|
||||||
outState.putBoolean(TIMEOUT_ENABLE_KEY, timeoutEnable)
|
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
if (timeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
// If the time is out during our navigation in activity -> close the Activity
|
// If the time is out during our navigation in activity -> close the Activity
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||||
}
|
}
|
||||||
@@ -139,8 +139,8 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
if (lockReceiver != null)
|
if (mLockReceiver != null)
|
||||||
unregisterReceiver(lockReceiver)
|
unregisterReceiver(mLockReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class LockReceiver : BroadcastReceiver() {
|
inner class LockReceiver : BroadcastReceiver() {
|
||||||
@@ -184,7 +184,7 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (timeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
package com.kunzisoft.keepass.activities.stylish
|
package com.kunzisoft.keepass.activities.stylish
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.support.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import android.support.v7.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -68,15 +68,4 @@ object Stylish {
|
|||||||
else -> R.style.KeepassDXStyle_Light
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
package com.kunzisoft.keepass.activities.stylish
|
package com.kunzisoft.keepass.activities.stylish
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import android.support.v7.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
abstract class StylishActivity : AppCompatActivity() {
|
abstract class StylishActivity : AppCompatActivity() {
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ import android.content.Context
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import android.support.v4.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import android.support.v7.view.ContextThemeWrapper
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -36,11 +36,9 @@ abstract class StylishFragment : Fragment() {
|
|||||||
protected var themeId: Int = 0
|
protected var themeId: Int = 0
|
||||||
protected var contextThemed: Context? = null
|
protected var contextThemed: Context? = null
|
||||||
|
|
||||||
override fun onAttach(context: Context?) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
if (context != null) {
|
this.themeId = Stylish.getThemeId(context)
|
||||||
this.themeId = Stylish.getThemeId(context)
|
|
||||||
}
|
|
||||||
contextThemed = ContextThemeWrapper(context, themeId)
|
contextThemed = ContextThemeWrapper(context, themeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||||
|
|
||||||
|
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
|
||||||
|
|
||||||
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
var entryHistoryList: MutableList<EntryVersioned> = ArrayList()
|
||||||
|
var onItemClickListener: ((item: EntryVersioned, position: Int)->Unit)? = null
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder {
|
||||||
|
return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: EntryHistoryViewHolder, position: Int) {
|
||||||
|
val entryHistory = entryHistoryList[position]
|
||||||
|
|
||||||
|
holder.lastModifiedView.text = entryHistory.lastModificationTime.getDateTimeString(context.resources)
|
||||||
|
holder.titleView.text = entryHistory.title
|
||||||
|
holder.usernameView.text = entryHistory.username
|
||||||
|
holder.urlView.text = entryHistory.url
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
onItemClickListener?.invoke(entryHistory, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return entryHistoryList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
entryHistoryList.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class EntryHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var lastModifiedView: TextView = itemView.findViewById(R.id.entry_history_last_modified)
|
||||||
|
var titleView: TextView = itemView.findViewById(R.id.entry_history_title)
|
||||||
|
var usernameView: TextView = itemView.findViewById(R.id.entry_history_username)
|
||||||
|
var urlView: TextView = itemView.findViewById(R.id.entry_history_url)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.kunzisoft.keepass.magikeyboard.adapter
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.support.v7.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -20,24 +20,31 @@
|
|||||||
package com.kunzisoft.keepass.adapters
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import androidx.annotation.ColorInt
|
||||||
import android.support.annotation.ColorInt
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
import android.widget.EditText
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.ViewSwitcher
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryEntity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class FileDatabaseHistoryAdapter(private val context: Context, private val listFiles: List<String>)
|
class FileDatabaseHistoryAdapter(private val context: Context)
|
||||||
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
|
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
|
||||||
|
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
private var fileItemOpenListener: FileItemOpenListener? = null
|
private var fileItemOpenListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
||||||
private var fileSelectClearListener: FileSelectClearListener? = null
|
private var fileSelectClearListener: ((FileDatabaseHistoryEntity)->Boolean)? = null
|
||||||
private var fileInformationShowListener: FileInformationShowListener? = null
|
private var saveAliasListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
||||||
|
|
||||||
|
private val listDatabaseFiles = ArrayList<FileDatabaseHistoryEntity>()
|
||||||
|
|
||||||
|
private var mExpandedPosition = -1
|
||||||
|
private var mPreviousExpandedPosition = -1
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val defaultColor: Int
|
private val defaultColor: Int
|
||||||
@@ -45,7 +52,6 @@ class FileDatabaseHistoryAdapter(private val context: Context, private val listF
|
|||||||
private val warningColor: Int
|
private val warningColor: Int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
val theme = context.theme
|
val theme = context.theme
|
||||||
theme.resolveAttribute(R.attr.colorAccent, typedValue, true)
|
theme.resolveAttribute(R.attr.colorAccent, typedValue, true)
|
||||||
@@ -60,91 +66,120 @@ class FileDatabaseHistoryAdapter(private val context: Context, private val listF
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
||||||
val fileDatabaseModel = FileDatabaseModel(context, listFiles[position])
|
// Get info from position
|
||||||
// Context menu creation
|
val fileHistoryEntity = listDatabaseFiles[position]
|
||||||
holder.fileContainer.setOnCreateContextMenuListener(ContextMenuBuilder(fileDatabaseModel))
|
val fileDatabaseInfo = FileDatabaseInfo(context, fileHistoryEntity.databaseUri)
|
||||||
|
|
||||||
// Click item to open file
|
// Click item to open file
|
||||||
if (fileItemOpenListener != null)
|
if (fileItemOpenListener != null)
|
||||||
holder.fileContainer.setOnClickListener(FileItemClickListener(position))
|
holder.fileContainer.setOnClickListener {
|
||||||
// Assign file name
|
fileItemOpenListener?.invoke(fileHistoryEntity)
|
||||||
if (PreferencesUtil.isFullFilePathEnable(context))
|
}
|
||||||
holder.fileName.text = Uri.decode(fileDatabaseModel.fileUri.toString())
|
|
||||||
else
|
// File alias
|
||||||
holder.fileName.text = fileDatabaseModel.fileName
|
holder.fileAlias.text = fileDatabaseInfo.retrieveDatabaseAlias(fileHistoryEntity.databaseAlias)
|
||||||
holder.fileName.textSize = PreferencesUtil.getListTextSize(context)
|
|
||||||
|
// File path
|
||||||
|
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
|
||||||
|
|
||||||
|
holder.filePreciseInfoContainer.visibility = if (fileDatabaseInfo.found()) {
|
||||||
|
// Modification
|
||||||
|
holder.fileModification.text = fileDatabaseInfo.getModificationString()
|
||||||
|
// Size
|
||||||
|
holder.fileSize.text = fileDatabaseInfo.getSizeString()
|
||||||
|
|
||||||
|
View.VISIBLE
|
||||||
|
} else
|
||||||
|
View.GONE
|
||||||
|
|
||||||
// Click on information
|
// Click on information
|
||||||
if (fileInformationShowListener != null)
|
val isExpanded = position == mExpandedPosition
|
||||||
holder.fileInformation.setOnClickListener(FileInformationClickListener(fileDatabaseModel))
|
//This line hides or shows the layout in question
|
||||||
|
holder.fileExpandContainer.visibility = if (isExpanded) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
// Save alias modification
|
||||||
|
holder.fileAliasCloseButton.setOnClickListener {
|
||||||
|
// Change the alias
|
||||||
|
fileHistoryEntity.databaseAlias = holder.fileAliasEdit.text.toString()
|
||||||
|
saveAliasListener?.invoke(fileHistoryEntity)
|
||||||
|
|
||||||
|
// Finish save mode
|
||||||
|
holder.fileMainSwitcher.showPrevious()
|
||||||
|
// Refresh current position to show alias
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open alias modification
|
||||||
|
holder.fileModifyButton.setOnClickListener {
|
||||||
|
holder.fileAliasEdit.setText(holder.fileAlias.text)
|
||||||
|
holder.fileMainSwitcher.showNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.fileDeleteButton.setOnClickListener {
|
||||||
|
fileSelectClearListener?.invoke(fileHistoryEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExpanded) {
|
||||||
|
mPreviousExpandedPosition = position
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.fileInformation.setOnClickListener {
|
||||||
|
mExpandedPosition = if (isExpanded) -1 else position
|
||||||
|
|
||||||
|
// Notify change
|
||||||
|
if (mPreviousExpandedPosition < itemCount)
|
||||||
|
notifyItemChanged(mPreviousExpandedPosition)
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh View / Close alias modification if not contains fileAlias
|
||||||
|
if (holder.fileMainSwitcher.currentView.findViewById<View>(R.id.file_alias)
|
||||||
|
!= holder.fileAlias)
|
||||||
|
holder.fileMainSwitcher.showPrevious()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return listFiles.size
|
return listDatabaseFiles.size
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnItemClickListener(fileItemOpenListener: FileItemOpenListener) {
|
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
|
||||||
this.fileItemOpenListener = fileItemOpenListener
|
listDatabaseFiles.clear()
|
||||||
|
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFileSelectClearListener(fileSelectClearListener: FileSelectClearListener) {
|
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: FileDatabaseHistoryEntity) {
|
||||||
this.fileSelectClearListener = fileSelectClearListener
|
listDatabaseFiles.remove(fileDatabaseHistoryToDelete)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFileInformationShowListener(fileInformationShowListener: FileInformationShowListener) {
|
fun setOnFileDatabaseHistoryOpenListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
||||||
this.fileInformationShowListener = fileInformationShowListener
|
this.fileItemOpenListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileItemOpenListener {
|
fun setOnFileDatabaseHistoryDeleteListener(listener : ((FileDatabaseHistoryEntity)->Boolean)?) {
|
||||||
fun onFileItemOpenListener(itemPosition: Int)
|
this.fileSelectClearListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileSelectClearListener {
|
fun setOnSaveAliasListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
||||||
fun onFileSelectClearListener(fileDatabaseModel: FileDatabaseModel): Boolean
|
this.saveAliasListener = listener
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
var fileContainer: View = itemView.findViewById(R.id.file_container)
|
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
|
||||||
var fileName: TextView = itemView.findViewById(R.id.file_filename)
|
|
||||||
|
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
|
||||||
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
|
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
var fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
|
||||||
|
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
|
||||||
|
var fileAliasCloseButton: ImageView = itemView.findViewById(R.id.file_alias_save)
|
||||||
|
|
||||||
private const val MENU_CLEAR = 1
|
var fileExpandContainer: ViewGroup = itemView.findViewById(R.id.file_expand_container)
|
||||||
|
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
|
||||||
|
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
||||||
|
var filePath: TextView = itemView.findViewById(R.id.file_path)
|
||||||
|
var filePreciseInfoContainer: ViewGroup = itemView.findViewById(R.id.file_precise_info_container)
|
||||||
|
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
|
||||||
|
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,34 +21,43 @@ package com.kunzisoft.keepass.adapters
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.support.v7.util.SortedList
|
import android.graphics.Paint
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.support.v7.widget.util.SortedListAdapterCallback
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SortedList
|
||||||
|
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class NodeAdapter
|
class NodeAdapter
|
||||||
/**
|
/**
|
||||||
* Create node list adapter with contextMenu or not
|
* Create node list adapter with contextMenu or not
|
||||||
* @param context Context to use
|
* @param context Context to use
|
||||||
*/
|
*/
|
||||||
(private val context: Context, private val menuInflater: MenuInflater)
|
(private val context: Context)
|
||||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||||
|
|
||||||
private val nodeSortedList: SortedList<NodeVersioned>
|
private val nodeSortedList: SortedList<NodeVersioned>
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
private var textSize: Float = 0.toFloat()
|
|
||||||
private var subtextSize: Float = 0.toFloat()
|
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
|
||||||
private var infoTextSize: Float = 0.toFloat()
|
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||||
private var iconSize: Float = 0.toFloat()
|
private var prefTextSize: Float = 0F
|
||||||
|
private var subtextSize: Float = 0F
|
||||||
|
private var infoTextSize: Float = 0F
|
||||||
|
private var numberChildrenTextSize: Float = 0F
|
||||||
|
private var iconSize: Float = 0F
|
||||||
private var listSort: SortNodeEnum = SortNodeEnum.DB
|
private var listSort: SortNodeEnum = SortNodeEnum.DB
|
||||||
private var ascendingSort: Boolean = true
|
private var ascendingSort: Boolean = true
|
||||||
private var groupsBeforeSort: Boolean = true
|
private var groupsBeforeSort: Boolean = true
|
||||||
@@ -56,11 +65,8 @@ class NodeAdapter
|
|||||||
private var showUserNames: Boolean = true
|
private var showUserNames: Boolean = true
|
||||||
private var showNumberEntries: Boolean = true
|
private var showNumberEntries: Boolean = true
|
||||||
|
|
||||||
|
private var actionNodesList = LinkedList<NodeVersioned>()
|
||||||
private var nodeClickCallback: NodeClickCallback? = null
|
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 mDatabase: Database
|
||||||
|
|
||||||
@@ -76,9 +82,6 @@ class NodeAdapter
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
assignPreferences()
|
assignPreferences()
|
||||||
this.activateContextMenu = false
|
|
||||||
this.readOnly = false
|
|
||||||
this.isASearchResult = false
|
|
||||||
|
|
||||||
this.nodeSortedList = SortedList(NodeVersioned::class.java, object : SortedListAdapterCallback<NodeVersioned>(this) {
|
this.nodeSortedList = SortedList(NodeVersioned::class.java, object : SortedListAdapterCallback<NodeVersioned>(this) {
|
||||||
override fun compare(item1: NodeVersioned, item2: NodeVersioned): Int {
|
override fun compare(item1: NodeVersioned, item2: NodeVersioned): Int {
|
||||||
@@ -86,7 +89,9 @@ class NodeAdapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: NodeVersioned, newItem: NodeVersioned): Boolean {
|
override fun areContentsTheSame(oldItem: NodeVersioned, newItem: NodeVersioned): Boolean {
|
||||||
return oldItem.title == newItem.title && oldItem.icon == newItem.icon
|
return oldItem.type == newItem.type
|
||||||
|
&& oldItem.title == newItem.title
|
||||||
|
&& oldItem.icon == newItem.icon
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areItemsTheSame(item1: NodeVersioned, item2: NodeVersioned): Boolean {
|
override fun areItemsTheSame(item1: NodeVersioned, item2: NodeVersioned): Boolean {
|
||||||
@@ -107,32 +112,22 @@ class NodeAdapter
|
|||||||
taTextColor.recycle()
|
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() {
|
private fun assignPreferences() {
|
||||||
val textSizeDefault = java.lang.Float.parseFloat(context.getString(R.string.list_size_default))
|
this.prefTextSize = PreferencesUtil.getListTextSize(context)
|
||||||
this.textSize = PreferencesUtil.getListTextSize(context)
|
this.infoTextSize = context.resources.getDimension(R.dimen.list_medium_size_default) * prefTextSize
|
||||||
this.subtextSize = context.resources.getInteger(R.integer.list_small_size_default) * textSize / textSizeDefault
|
this.subtextSize = context.resources.getDimension(R.dimen.list_small_size_default) * prefTextSize
|
||||||
this.infoTextSize = context.resources.getInteger(R.integer.list_tiny_size_default) * textSize / textSizeDefault
|
this.numberChildrenTextSize = context.resources.getDimension(R.dimen.list_tiny_size_default) * prefTextSize
|
||||||
// Retrieve the icon size
|
this.iconSize = context.resources.getDimension(R.dimen.list_icon_size_default) * prefTextSize
|
||||||
val iconDefaultSize = context.resources.getDimension(R.dimen.list_icon_size_default)
|
|
||||||
this.iconSize = iconDefaultSize * textSize / textSizeDefault
|
|
||||||
this.listSort = PreferencesUtil.getListSort(context)
|
this.listSort = PreferencesUtil.getListSort(context)
|
||||||
this.ascendingSort = PreferencesUtil.getAscendingSort(context)
|
this.ascendingSort = PreferencesUtil.getAscendingSort(context)
|
||||||
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
|
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
|
||||||
this.recycleBinBottomSort = PreferencesUtil.getRecycleBinBottomSort(context)
|
this.recycleBinBottomSort = PreferencesUtil.getRecycleBinBottomSort(context)
|
||||||
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||||
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
|
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||||
|
|
||||||
|
// Reinit textSize for all view type
|
||||||
|
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -147,6 +142,11 @@ class NodeAdapter
|
|||||||
Log.e(TAG, "Can't add node elements to the list", 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.message, Toast.LENGTH_LONG).show()
|
Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contains(node: NodeVersioned): Boolean {
|
||||||
|
return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,6 +157,14 @@ class NodeAdapter
|
|||||||
nodeSortedList.add(node)
|
nodeSortedList.add(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add nodes to the list
|
||||||
|
* @param nodes Nodes to add
|
||||||
|
*/
|
||||||
|
fun addNodes(nodes: List<NodeVersioned>) {
|
||||||
|
nodeSortedList.addAll(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a node in the list
|
* Remove a node in the list
|
||||||
* @param node Node to delete
|
* @param node Node to delete
|
||||||
@@ -165,6 +173,37 @@ class NodeAdapter
|
|||||||
nodeSortedList.remove(node)
|
nodeSortedList.remove(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove nodes in the list
|
||||||
|
* @param nodes Nodes to delete
|
||||||
|
*/
|
||||||
|
fun removeNodes(nodes: List<NodeVersioned>) {
|
||||||
|
nodes.forEach { node ->
|
||||||
|
nodeSortedList.remove(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a node at [position] in the list
|
||||||
|
*/
|
||||||
|
fun removeNodeAt(position: Int) {
|
||||||
|
nodeSortedList.removeItemAt(position)
|
||||||
|
// Refresh all the next items
|
||||||
|
notifyItemRangeChanged(position, nodeSortedList.size() - position)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove nodes in the list by [positions]
|
||||||
|
* Note : algorithm remove the higher position at each iteration
|
||||||
|
*/
|
||||||
|
fun removeNodesAt(positions: IntArray) {
|
||||||
|
val positionsSortDescending = positions.toMutableList()
|
||||||
|
positionsSortDescending.sortDescending()
|
||||||
|
positionsSortDescending.forEach {
|
||||||
|
removeNodeAt(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a node in the list
|
* Update a node in the list
|
||||||
* @param oldNode Node before the update
|
* @param oldNode Node before the update
|
||||||
@@ -177,6 +216,40 @@ class NodeAdapter
|
|||||||
nodeSortedList.endBatchedUpdates()
|
nodeSortedList.endBatchedUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update nodes in the list
|
||||||
|
* @param oldNodes Nodes before the update
|
||||||
|
* @param newNodes Node after the update
|
||||||
|
*/
|
||||||
|
fun updateNodes(oldNodes: List<NodeVersioned>, newNodes: List<NodeVersioned>) {
|
||||||
|
nodeSortedList.beginBatchedUpdates()
|
||||||
|
oldNodes.forEach { oldNode ->
|
||||||
|
nodeSortedList.remove(oldNode)
|
||||||
|
}
|
||||||
|
nodeSortedList.addAll(newNodes)
|
||||||
|
nodeSortedList.endBatchedUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyNodeChanged(node: NodeVersioned) {
|
||||||
|
notifyItemChanged(nodeSortedList.indexOf(node))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setActionNodes(actionNodes: List<NodeVersioned>) {
|
||||||
|
this.actionNodesList.apply {
|
||||||
|
clear()
|
||||||
|
addAll(actionNodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unselectActionNodes() {
|
||||||
|
actionNodesList.forEach {
|
||||||
|
notifyItemChanged(nodeSortedList.indexOf(it))
|
||||||
|
}
|
||||||
|
this.actionNodesList.apply {
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify a change sort of the list
|
* Notify a change sort of the list
|
||||||
*/
|
*/
|
||||||
@@ -206,47 +279,65 @@ class NodeAdapter
|
|||||||
Type.GROUP -> iconGroupColor
|
Type.GROUP -> iconGroupColor
|
||||||
Type.ENTRY -> iconEntryColor
|
Type.ENTRY -> iconEntryColor
|
||||||
}
|
}
|
||||||
holder.icon.assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
holder.icon.apply {
|
||||||
// Assign text
|
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||||
holder.text.text = subNode.title
|
// Relative size of the icon
|
||||||
// Assign click
|
layoutParams?.apply {
|
||||||
holder.container.setOnClickListener { nodeClickCallback?.onNodeClick(subNode) }
|
height = iconSize.toInt()
|
||||||
// Context menu
|
width = iconSize.toInt()
|
||||||
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 text
|
||||||
|
holder.text.apply {
|
||||||
|
text = subNode.title
|
||||||
|
setTextSize(textSizeUnit, infoTextSize)
|
||||||
|
paintFlags = if (subNode.isCurrentlyExpires)
|
||||||
|
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
||||||
|
else
|
||||||
|
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
|
||||||
|
}
|
||||||
|
// Assign click
|
||||||
|
holder.container.setOnClickListener {
|
||||||
|
nodeClickCallback?.onNodeClick(subNode)
|
||||||
|
}
|
||||||
|
holder.container.setOnLongClickListener {
|
||||||
|
nodeClickCallback?.onNodeLongClick(subNode) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign image and text size
|
holder.container.isSelected = actionNodesList.contains(subNode)
|
||||||
// Relative size of the icon
|
|
||||||
holder.icon.layoutParams?.height = iconSize.toInt()
|
// Add subText with username
|
||||||
holder.icon.layoutParams?.width = iconSize.toInt()
|
holder.subText.apply {
|
||||||
holder.text.textSize = textSize
|
text = ""
|
||||||
holder.subText.textSize = subtextSize
|
paintFlags = if (subNode.isCurrentlyExpires)
|
||||||
|
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
||||||
|
else
|
||||||
|
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
|
||||||
|
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()) {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
text = username
|
||||||
|
setTextSize(textSizeUnit, subtextSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabase.stopManageEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add number of entries in groups
|
||||||
if (subNode.type == Type.GROUP) {
|
if (subNode.type == Type.GROUP) {
|
||||||
if (showNumberEntries) {
|
if (showNumberEntries) {
|
||||||
holder.numberChildren?.apply {
|
holder.numberChildren?.apply {
|
||||||
text = (subNode as GroupVersioned).getChildEntries().size.toString()
|
text = (subNode as GroupVersioned).getChildEntries(true).size.toString()
|
||||||
textSize = infoTextSize
|
setTextSize(textSizeUnit, numberChildrenTextSize)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -266,103 +357,12 @@ class NodeAdapter
|
|||||||
this.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
|
* Callback listener to redefine to do an action when a node is click
|
||||||
*/
|
*/
|
||||||
interface NodeClickCallback {
|
interface NodeClickCallback {
|
||||||
fun onNodeClick(node: NodeVersioned)
|
fun onNodeClick(node: NodeVersioned)
|
||||||
}
|
fun onNodeLongClick(node: NodeVersioned): Boolean
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.adapters
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.support.v4.widget.CursorAdapter
|
import androidx.cursoradapter.widget.CursorAdapter
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -38,7 +38,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
||||||
: CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||||
|
|
||||||
private val cursorInflater: LayoutInflater = context.getSystemService(
|
private val cursorInflater: LayoutInflater = context.getSystemService(
|
||||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.app
|
package com.kunzisoft.keepass.app
|
||||||
|
|
||||||
import android.support.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
|
import android.os.AsyncTask
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private class to invoke each method in a separate thread
|
||||||
|
*/
|
||||||
|
class ActionDatabaseAsyncTask<T>(
|
||||||
|
private val action: () -> T ,
|
||||||
|
private val afterActionDatabaseListener: ((T?) -> Unit)? = null
|
||||||
|
) : AsyncTask<Void, Void, T>() {
|
||||||
|
|
||||||
|
override fun doInBackground(vararg args: Void?): T? {
|
||||||
|
return action.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute(result: T?) {
|
||||||
|
afterActionDatabaseListener?.invoke(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
@Database(version = 1, entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class])
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao
|
||||||
|
abstract fun cipherDatabaseDao(): CipherDatabaseDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getDatabase(applicationContext: Context): AppDatabase {
|
||||||
|
return Room.databaseBuilder(
|
||||||
|
applicationContext,
|
||||||
|
AppDatabase::class.java, "com.kunzisoft.keepass.database"
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
|
|
||||||
|
class CipherDatabaseAction(applicationContext: Context) {
|
||||||
|
|
||||||
|
private val cipherDatabaseDao =
|
||||||
|
AppDatabase
|
||||||
|
.getDatabase(applicationContext)
|
||||||
|
.cipherDatabaseDao()
|
||||||
|
|
||||||
|
fun getCipherDatabase(databaseUri: Uri,
|
||||||
|
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
||||||
|
ActionDatabaseAsyncTask(
|
||||||
|
{
|
||||||
|
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cipherDatabaseResultListener.invoke(it)
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsCipherDatabase(databaseUri: Uri,
|
||||||
|
contains: (Boolean) -> Unit) {
|
||||||
|
getCipherDatabase(databaseUri) {
|
||||||
|
contains.invoke(it != null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
||||||
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
|
ActionDatabaseAsyncTask(
|
||||||
|
{
|
||||||
|
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
||||||
|
|
||||||
|
// Update values if element not yet in the database
|
||||||
|
if (cipherDatabaseRetrieve == null) {
|
||||||
|
cipherDatabaseDao.add(cipherDatabaseEntity)
|
||||||
|
} else {
|
||||||
|
cipherDatabaseDao.update(cipherDatabaseEntity)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cipherDatabaseResultListener?.invoke()
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteByDatabaseUri(databaseUri: Uri,
|
||||||
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
|
ActionDatabaseAsyncTask(
|
||||||
|
{
|
||||||
|
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cipherDatabaseResultListener?.invoke()
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteAll() {
|
||||||
|
ActionDatabaseAsyncTask(
|
||||||
|
{
|
||||||
|
cipherDatabaseDao.deleteAll()
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction)
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface CipherDatabaseDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM cipher_database WHERE database_uri = :databaseUriString")
|
||||||
|
fun getByDatabaseUri(databaseUriString: String): CipherDatabaseEntity?
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun add(vararg fileDatabaseHistory: CipherDatabaseEntity)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun update(vararg fileDatabaseHistory: CipherDatabaseEntity)
|
||||||
|
|
||||||
|
@Query("DELETE FROM cipher_database WHERE database_uri = :databaseUriString")
|
||||||
|
fun deleteByDatabaseUri(databaseUriString: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM cipher_database")
|
||||||
|
fun deleteAll()
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "cipher_database")
|
||||||
|
data class CipherDatabaseEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "database_uri")
|
||||||
|
val databaseUri: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "encrypted_value")
|
||||||
|
var encryptedValue: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "specs_parameters")
|
||||||
|
var specParameters: String
|
||||||
|
): Parcelable {
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this(
|
||||||
|
parcel.readString()!!,
|
||||||
|
parcel.readString()!!,
|
||||||
|
parcel.readString()!!)
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(databaseUri)
|
||||||
|
parcel.writeString(encryptedValue)
|
||||||
|
parcel.writeString(specParameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<CipherDatabaseEntity> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): CipherDatabaseEntity {
|
||||||
|
return CipherDatabaseEntity(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<CipherDatabaseEntity?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as CipherDatabaseEntity
|
||||||
|
|
||||||
|
if (databaseUri != other.databaseUri) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return databaseUri.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* 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.app.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
|
class FileDatabaseHistoryAction(applicationContext: Context) {
|
||||||
|
|
||||||
|
private val databaseFileHistoryDao =
|
||||||
|
AppDatabase
|
||||||
|
.getDatabase(applicationContext)
|
||||||
|
.fileDatabaseHistoryDao()
|
||||||
|
|
||||||
|
fun getFileDatabaseHistory(databaseUri: Uri,
|
||||||
|
fileHistoryResultListener: (fileDatabaseHistoryResult: FileDatabaseHistoryEntity?) -> Unit) {
|
||||||
|
ActionDatabaseAsyncTask(
|
||||||
|
{
|
||||||
|
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileHistoryResultListener.invoke(it)
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
|
||||||
|
keyFileUriResultListener: (Uri?) -> Unit) {
|
||||||
|
ActionDatabaseAsyncTask(
|
||||||
|
{
|
||||||
|
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
|
},
|
||||||
|
{
|
||||||
|
it?.let { fileHistoryEntity ->
|
||||||
|
fileHistoryEntity.keyFileUri?.let { keyFileUri ->
|
||||||
|
keyFileUriResultListener.invoke(UriUtil.parse(keyFileUri))
|
||||||
|
}
|
||||||
|
} ?: keyFileUriResultListener.invoke(null)
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllFileDatabaseHistories(fileHistoryResultListener: (fileDatabaseHistoryResult: List<FileDatabaseHistoryEntity>?) -> Unit) {
|
||||||
|
ActionDatabaseAsyncTask(
|
||||||
|
{
|
||||||
|
databaseFileHistoryDao.getAll()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileHistoryResultListener.invoke(it)
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null) {
|
||||||
|
addOrUpdateFileDatabaseHistory(FileDatabaseHistoryEntity(
|
||||||
|
databaseUri.toString(),
|
||||||
|
"",
|
||||||
|
keyFileUri?.toString(),
|
||||||
|
System.currentTimeMillis()
|
||||||
|
), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addOrUpdateFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity, unmodifiedAlias: Boolean = false) {
|
||||||
|
ActionDatabaseAsyncTask(
|
||||||
|
{
|
||||||
|
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(fileDatabaseHistory.databaseUri)
|
||||||
|
|
||||||
|
if (unmodifiedAlias) {
|
||||||
|
fileDatabaseHistory.databaseAlias = fileDatabaseHistoryRetrieve?.databaseAlias ?: ""
|
||||||
|
}
|
||||||
|
// Update values if history element not yet in the database
|
||||||
|
if (fileDatabaseHistoryRetrieve == null) {
|
||||||
|
databaseFileHistoryDao.add(fileDatabaseHistory)
|
||||||
|
} else {
|
||||||
|
databaseFileHistoryDao.update(fileDatabaseHistory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity,
|
||||||
|
fileHistoryDeletedResult: (FileDatabaseHistoryEntity?) -> Unit) {
|
||||||
|
ActionDatabaseAsyncTask(
|
||||||
|
{
|
||||||
|
databaseFileHistoryDao.delete(fileDatabaseHistory)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
if (it != null && it > 0)
|
||||||
|
fileHistoryDeletedResult.invoke(fileDatabaseHistory)
|
||||||
|
else
|
||||||
|
fileHistoryDeletedResult.invoke(null)
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteAllKeyFiles() {
|
||||||
|
ActionDatabaseAsyncTask(
|
||||||
|
{
|
||||||
|
databaseFileHistoryDao.deleteAllKeyFiles()
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteAll() {
|
||||||
|
ActionDatabaseAsyncTask(
|
||||||
|
{
|
||||||
|
databaseFileHistoryDao.deleteAll()
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : SingletonHolderParameter<FileDatabaseHistoryAction, Context>(::FileDatabaseHistoryAction)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface FileDatabaseHistoryDao {
|
||||||
|
@Query("SELECT * FROM file_database_history ORDER BY updated DESC")
|
||||||
|
fun getAll(): List<FileDatabaseHistoryEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM file_database_history WHERE database_uri = :databaseUriString")
|
||||||
|
fun getByDatabaseUri(databaseUriString: String): FileDatabaseHistoryEntity?
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun add(vararg fileDatabaseHistory: FileDatabaseHistoryEntity)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun update(vararg fileDatabaseHistory: FileDatabaseHistoryEntity)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
|
||||||
|
|
||||||
|
@Query("UPDATE file_database_history SET keyfile_uri=null")
|
||||||
|
fun deleteAllKeyFiles()
|
||||||
|
|
||||||
|
@Query("DELETE FROM file_database_history")
|
||||||
|
fun deleteAll()
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "file_database_history")
|
||||||
|
data class FileDatabaseHistoryEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "database_uri")
|
||||||
|
val databaseUri: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "database_alias")
|
||||||
|
var databaseAlias: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "keyfile_uri")
|
||||||
|
var keyFileUri: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "updated")
|
||||||
|
val updated: Long
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as FileDatabaseHistoryEntity
|
||||||
|
|
||||||
|
if (databaseUri != other.databaseUri) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return databaseUri.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.autofill.Dataset
|
import android.service.autofill.Dataset
|
||||||
import android.service.autofill.FillResponse
|
import android.service.autofill.FillResponse
|
||||||
import android.support.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.autofill.AutofillManager
|
import android.view.autofill.AutofillManager
|
||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import android.content.Intent
|
|||||||
import android.content.IntentSender
|
import android.content.IntentSender
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import android.support.v7.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
@@ -56,6 +56,7 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.autofill
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
import android.service.autofill.*
|
import android.service.autofill.*
|
||||||
import android.support.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user