mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
326 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 | ||
|
|
c2781af38d | ||
|
|
a1cd2683d4 | ||
|
|
6d671f53c2 | ||
|
|
4efb81f597 | ||
|
|
faddeb6e2d | ||
|
|
e36629968d | ||
|
|
0b756797f6 | ||
|
|
110c7bbbc7 | ||
|
|
639c6dc4ac | ||
|
|
f605d36adf | ||
|
|
f6d6c134f4 | ||
|
|
a0c07654df | ||
|
|
03ab688abe | ||
|
|
4a28802b02 | ||
|
|
18f8fe7cc3 | ||
|
|
459606f5d5 | ||
|
|
3d53c31680 | ||
|
|
173dd5b59b | ||
|
|
8536de3555 | ||
|
|
722ba5c34d | ||
|
|
3ccfd7226b | ||
|
|
578da7bae5 | ||
|
|
6916389657 | ||
|
|
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.
|
||||
34
CHANGELOG
34
CHANGELOG
@@ -1,3 +1,37 @@
|
||||
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)
|
||||
* Fix nested groups no longer visible in V1 databases
|
||||
* Improved data import algorithm for V1 databases
|
||||
* Add natural database sort
|
||||
* Add username database sort
|
||||
* Fix button disabled with only KeyFile
|
||||
* Show the number of entries in a group
|
||||
|
||||
KeepassDX (2.5.0.0beta20)
|
||||
* Fix a major bug that displays an entry history
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -4,15 +4,16 @@ apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion '28.0.3'
|
||||
ndkVersion "20.0.5594570"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 27
|
||||
versionCode = 20
|
||||
versionName = "2.5.0.0beta20"
|
||||
targetSdkVersion 28
|
||||
versionCode = 24
|
||||
versionName = "2.5.0.0beta24"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -79,42 +80,35 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
def supportVersion = "27.1.1"
|
||||
def spongycastleVersion = "1.58.0.0"
|
||||
def permissionDispatcherVersion = "3.3.1"
|
||||
def room_version = "2.2.0"
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "com.android.support:appcompat-v7:$supportVersion"
|
||||
implementation "com.android.support:design:$supportVersion"
|
||||
implementation "com.android.support:preference-v7:$supportVersion"
|
||||
implementation "com.android.support:preference-v14:$supportVersion"
|
||||
implementation "com.android.support:cardview-v7:$supportVersion"
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.biometric:biometric:1.0.0-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:prov:$spongycastleVersion"
|
||||
// Expandable view
|
||||
implementation 'net.cachapa.expandablelayout:expandablelayout:2.9.2'
|
||||
// Time
|
||||
implementation 'joda-time:joda-time:2.9.9'
|
||||
implementation 'org.sufficientlysecure:html-textview:3.5'
|
||||
implementation 'com.nononsenseapps:filepicker:4.1.0'
|
||||
// Color
|
||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
|
||||
// Education
|
||||
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
|
||||
implementation 'commons-collections:commons-collections:3.2.1'
|
||||
implementation 'org.apache.commons:commons-io:1.3.2'
|
||||
// Base64
|
||||
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
|
||||
implementation project(path: ':icon-pack-classic')
|
||||
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"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.kunzisoft.keepass"
|
||||
android:installLocation="auto">
|
||||
<supports-screens
|
||||
@@ -8,7 +7,8 @@
|
||||
android:normalScreens="true"
|
||||
android:largeScreens="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.VIBRATE"/>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\.kdb" />
|
||||
@@ -71,29 +71,28 @@
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
</intent-filter>
|
||||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Folder picker -->
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/nnf_provider_paths" />
|
||||
</provider>
|
||||
<activity
|
||||
android:name=".activities.stylish.FilePickerStylishActivity"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.GET_CONTENT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/x-kdb" />
|
||||
<data android:mimeType="application/x-kdbx" />
|
||||
<data android:mimeType="application/x-keepass" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*" />
|
||||
<data android:pathPattern=".*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Main Activity -->
|
||||
@@ -129,9 +128,11 @@
|
||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
||||
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.magikeyboard.KeyboardLauncherActivity"
|
||||
android:label="@string/keyboard_name">
|
||||
android:label="@string/keyboard_name"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
<activity android:name="com.kunzisoft.keepass.settings.MagikIMESettings"
|
||||
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.os.Bundle
|
||||
import android.support.v7.widget.Toolbar
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.widget.TextView
|
||||
|
||||
@@ -19,15 +19,14 @@
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.support.design.widget.CollapsingToolbarLayout
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.widget.Toolbar
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -45,22 +44,27 @@ import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields
|
||||
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.Util
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.EntryContentsView
|
||||
import java.util.*
|
||||
|
||||
class EntryActivity : LockingHideActivity() {
|
||||
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var titleIconView: ImageView? = null
|
||||
private var historyView: View? = null
|
||||
private var entryContentsView: EntryContentsView? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
private var mEntry: EntryVersioned? = null
|
||||
private var mIsHistory: Boolean = false
|
||||
|
||||
private var mShowPassword: Boolean = false
|
||||
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
@@ -78,31 +82,14 @@ class EntryActivity : LockingHideActivity() {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
val currentDatabase = Database.getInstance()
|
||||
readOnly = currentDatabase.isReadOnly || readOnly
|
||||
mDatabase = Database.getInstance()
|
||||
mReadOnly = mDatabase!!.isReadOnly || mReadOnly
|
||||
|
||||
mShowPassword = !PreferencesUtil.isPasswordMask(this)
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
val keyEntry: PwNodeId<*> = intent.getParcelableExtra(KEY_ENTRY)
|
||||
mEntry = currentDatabase.getEntryById(keyEntry)
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
}
|
||||
|
||||
if (mEntry == null) {
|
||||
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Update last access time.
|
||||
mEntry?.touch(modified = false, touchParents = false)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||
iconColor = taIconColor.getColor(0, Color.WHITE)
|
||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
iconColor = taIconColor.getColor(0, Color.BLACK)
|
||||
taIconColor.recycle()
|
||||
|
||||
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
|
||||
@@ -111,6 +98,7 @@ class EntryActivity : LockingHideActivity() {
|
||||
// Get views
|
||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||
titleIconView = findViewById(R.id.entry_icon)
|
||||
historyView = findViewById(R.id.history_container)
|
||||
entryContentsView = findViewById(R.id.entry_contents)
|
||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||
|
||||
@@ -122,6 +110,29 @@ class EntryActivity : LockingHideActivity() {
|
||||
override fun 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 ->
|
||||
// Fill data in resume to update from EntryEditActivity
|
||||
fillEntryDataInContentsView(entry)
|
||||
@@ -152,90 +163,126 @@ class EntryActivity : LockingHideActivity() {
|
||||
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor)
|
||||
|
||||
// Assign title text
|
||||
val entryTitle = entry.getVisualTitle()
|
||||
val entryTitle = entry.title
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
|
||||
// Assign basic fields
|
||||
entryContentsView?.assignUserName(entry.username)
|
||||
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
|
||||
database.startManageEntry(entry)
|
||||
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_user_name)))
|
||||
database.stopManageEntry(entry)
|
||||
})
|
||||
|
||||
val allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
|
||||
entryContentsView?.assignPassword(entry.password, allowCopyPassword)
|
||||
if (allowCopyPassword) {
|
||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
||||
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
})
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)) {
|
||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
||||
val message = getString(R.string.allow_copy_password_warning) +
|
||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||
val allowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
|
||||
|
||||
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
|
||||
AlertDialog.Builder(this@EntryActivity)
|
||||
.setMessage(getString(R.string.allow_copy_password_warning) +
|
||||
"\n\n" +
|
||||
getString(R.string.clipboard_warning)
|
||||
val warningDialog = AlertDialog.Builder(this@EntryActivity)
|
||||
.setMessage(message).create()
|
||||
warningDialog.setButton(AlertDialog.BUTTON_POSITIVE, getText(android.R.string.ok)
|
||||
) { dialog, _ ->
|
||||
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)
|
||||
}
|
||||
warningDialog.setButton(AlertDialog.BUTTON_NEGATIVE, getText(android.R.string.cancel)
|
||||
) { dialog, _ ->
|
||||
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
|
||||
dialog.dismiss()
|
||||
fillEntryDataInContentsView(entry)
|
||||
}
|
||||
warningDialog.show()
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
|
||||
if (allowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
||||
database.startManageEntry(entry)
|
||||
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
database.stopManageEntry(entry)
|
||||
})
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
|
||||
} else {
|
||||
entryContentsView?.assignPasswordCopyListener(null)
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView?.assignURL(entry.url)
|
||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
||||
entryContentsView?.assignComment(entry.notes)
|
||||
|
||||
// Assign custom fields
|
||||
if (entry.allowExtraFields()) {
|
||||
if (entry.allowCustomFields()) {
|
||||
entryContentsView?.clearExtraFields()
|
||||
|
||||
entry.fields.doActionToAllCustomProtectedField { label, value ->
|
||||
val showAction = !value.isProtected || PreferencesUtil.allowCopyPasswordAndProtectedFields(this@EntryActivity)
|
||||
entryContentsView?.addExtraField(label, value, showAction, View.OnClickListener {
|
||||
for (element in entry.customFields.entries) {
|
||||
val label = element.key
|
||||
val value = element.value
|
||||
|
||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||
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
|
||||
entry.creationTime.date?.let {
|
||||
entryContentsView?.assignCreationDate(it)
|
||||
}
|
||||
entry.lastModificationTime.date?.let {
|
||||
entryContentsView?.assignModificationDate(it)
|
||||
}
|
||||
entry.lastAccessTime.date?.let {
|
||||
entryContentsView?.assignLastAccessDate(it)
|
||||
}
|
||||
val expires = entry.expiryTime.date
|
||||
if (entry.isExpires && expires != null) {
|
||||
entryContentsView?.assignExpiresDate(expires)
|
||||
entryContentsView?.assignCreationDate(entry.creationTime)
|
||||
entryContentsView?.assignModificationDate(entry.lastModificationTime)
|
||||
entryContentsView?.assignLastAccessDate(entry.lastAccessTime)
|
||||
entryContentsView?.setExpires(entry.isCurrentlyExpires)
|
||||
if (entry.expires) {
|
||||
entryContentsView?.assignExpiresDate(entry.expiryTime)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -268,7 +315,7 @@ class EntryActivity : LockingHideActivity() {
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database_lock, menu)
|
||||
|
||||
if (readOnly) {
|
||||
if (mReadOnly) {
|
||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
|
||||
@@ -303,7 +350,7 @@ class EntryActivity : LockingHideActivity() {
|
||||
|
||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||
menu: Menu) {
|
||||
if (entryContentsView?.isUserNamePresent == true
|
||||
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
|
||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||
findViewById(R.id.entry_user_name_action_image),
|
||||
{
|
||||
@@ -314,8 +361,11 @@ class EntryActivity : LockingHideActivity() {
|
||||
{
|
||||
// Launch autofill settings
|
||||
startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java))
|
||||
}))
|
||||
else if (toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||
})
|
||||
|
||||
if (!entryCopyEducationPerformed) {
|
||||
// entryEditEducationPerformed
|
||||
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||
toolbar!!.findViewById(R.id.menu_edit),
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||
@@ -323,14 +373,17 @@ class EntryActivity : LockingHideActivity() {
|
||||
{
|
||||
// Open Keepass doc to create field references
|
||||
startActivity(Intent(Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.field_references_url))))
|
||||
}))
|
||||
;
|
||||
UriUtil.parse(getString(R.string.field_references_url))))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this)
|
||||
R.id.menu_contribute -> {
|
||||
MenuUtil.onContributionItemSelected(this)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_toggle_pass -> {
|
||||
mShowPassword = !mShowPassword
|
||||
@@ -354,11 +407,7 @@ class EntryActivity : LockingHideActivity() {
|
||||
url = "http://$url"
|
||||
}
|
||||
|
||||
try {
|
||||
Util.gotoUrl(this, url)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
UriUtil.gotoUrl(this, url)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -389,13 +438,16 @@ class EntryActivity : LockingHideActivity() {
|
||||
companion object {
|
||||
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)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, pw.nodeId)
|
||||
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||
if (historyPosition != null)
|
||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,30 +22,30 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
|
||||
import com.kunzisoft.keepass.database.action.node.ActionNodeValues
|
||||
import com.kunzisoft.keepass.database.action.node.AddEntryRunnable
|
||||
import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable
|
||||
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
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.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
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
|
||||
|
||||
@@ -58,11 +58,12 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
|
||||
// Views
|
||||
private var scrollView: ScrollView? = null
|
||||
|
||||
private var entryEditContentsView: EntryEditContentsView? = null
|
||||
|
||||
private var saveView: View? = null
|
||||
|
||||
// Dialog thread
|
||||
private var progressDialogThread: ProgressDialogThread? = null
|
||||
|
||||
// Education
|
||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||
|
||||
@@ -88,7 +89,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
// Entry is retrieve, it's an entry to update
|
||||
intent.getParcelableExtra<PwNodeId<*>>(KEY_ENTRY)?.let {
|
||||
intent.getParcelableExtra<PwNodeId<UUID>>(KEY_ENTRY)?.let {
|
||||
mIsNew = false
|
||||
// Create an Entry copy to modify from the database entry
|
||||
mEntry = mDatabase?.getEntryById(it)
|
||||
@@ -103,16 +104,14 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the icon after an orientation change
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY) as EntryVersioned
|
||||
} else {
|
||||
// Create the new entry from the current one
|
||||
if (savedInstanceState == null
|
||||
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||
mEntry?.let { entry ->
|
||||
// Create a copy to modify
|
||||
mNewEntry = EntryVersioned(entry).also { newEntry ->
|
||||
|
||||
// WARNING Remove the parent to keep memory with parcelable
|
||||
newEntry.parent = null
|
||||
newEntry.removeParent()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,7 +120,11 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
// Parent is retrieve, it's a new entry to create
|
||||
intent.getParcelableExtra<PwNodeId<*>>(KEY_PARENT)?.let {
|
||||
mIsNew = true
|
||||
// Create an empty new entry
|
||||
if (savedInstanceState == null
|
||||
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||
mNewEntry = mDatabase?.createEntry()
|
||||
}
|
||||
mParent = mDatabase?.getGroupById(it)
|
||||
// Add the default icon
|
||||
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
|
||||
if (mNewEntry == null || mParent == null) {
|
||||
finish()
|
||||
@@ -150,37 +159,21 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
saveView = findViewById(R.id.entry_edit_save)
|
||||
saveView?.setOnClickListener { saveEntry() }
|
||||
|
||||
entryEditContentsView?.allowCustomField(mNewEntry?.allowExtraFields() == true) { addNewCustomField() }
|
||||
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) { addNewCustomField() }
|
||||
|
||||
// Verify the education views
|
||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||
entryEditActivityEducation?.let {
|
||||
Handler().post { performedNextEducation(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
val passwordView = entryEditContentsView?.generatePasswordView
|
||||
val addNewFieldView = entryEditContentsView?.addNewFieldView
|
||||
|
||||
if (passwordView != null
|
||||
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||
passwordView,
|
||||
{
|
||||
openPasswordGenerator()
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryEditActivityEducation)
|
||||
// Create progress dialog
|
||||
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||
if (result.isSuccess)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
))
|
||||
else if (mNewEntry != null && mNewEntry!!.allowExtraFields() && !mNewEntry!!.containsCustomFields()
|
||||
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
|
||||
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||
addNewFieldView,
|
||||
{
|
||||
addNewCustomField()
|
||||
}))
|
||||
;
|
||||
}
|
||||
|
||||
private fun populateViewsWithEntry(newEntry: EntryVersioned) {
|
||||
@@ -193,12 +186,14 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
// Set info in view
|
||||
entryEditContentsView?.apply {
|
||||
title = newEntry.title
|
||||
username = newEntry.username
|
||||
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
|
||||
url = newEntry.url
|
||||
password = newEntry.password
|
||||
notes = newEntry.notes
|
||||
newEntry.fields.doActionToAllCustomProtectedField { key, value ->
|
||||
addNewCustomField(key, value)
|
||||
for (entry in newEntry.customFields.entries) {
|
||||
post {
|
||||
addNewCustomField(entry.key, entry.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,6 +205,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
newEntry.apply {
|
||||
// Build info from view
|
||||
entryEditContentsView?.let { entryView ->
|
||||
removeAllFields()
|
||||
title = entryView.title
|
||||
username = entryView.username
|
||||
url = entryView.url
|
||||
@@ -243,8 +239,6 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
*/
|
||||
private fun addNewCustomField() {
|
||||
entryEditContentsView?.addNewCustomField()
|
||||
// Scroll bottom
|
||||
scrollView?.post { scrollView?.fullScroll(ScrollView.FOCUS_DOWN) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,7 +249,6 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
// Launch a validation and show the error if present
|
||||
if (entryEditContentsView?.isValid() == true) {
|
||||
// Clone the entry
|
||||
mDatabase?.let { database ->
|
||||
mNewEntry?.let { newEntry ->
|
||||
|
||||
// WARNING Add the parent previously deleted
|
||||
@@ -267,41 +260,39 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
populateEntryWithViews(newEntry)
|
||||
|
||||
// Open a progress dialog and save entry
|
||||
var actionRunnable: ActionRunnable? = null
|
||||
val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
if (actionNodeValues.result.isSuccess)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
if (mIsNew) {
|
||||
mParent?.let { parent ->
|
||||
actionRunnable = AddEntryRunnable(this@EntryEditActivity,
|
||||
database,
|
||||
progressDialogThread?.startDatabaseCreateEntry(
|
||||
newEntry,
|
||||
parent,
|
||||
afterActionNodeFinishRunnable,
|
||||
!readOnly)
|
||||
!mReadOnly
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
mEntry?.let { oldEntry ->
|
||||
actionRunnable = UpdateEntryRunnable(this@EntryEditActivity,
|
||||
database,
|
||||
progressDialogThread?.startDatabaseUpdateEntry(
|
||||
oldEntry,
|
||||
newEntry,
|
||||
afterActionNodeFinishRunnable,
|
||||
!readOnly)
|
||||
}
|
||||
}
|
||||
actionRunnable?.let { runnable ->
|
||||
ProgressDialogSaveDatabaseThread(this@EntryEditActivity) { runnable }.start()
|
||||
!mReadOnly
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
progressDialogThread?.registerProgressTask()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
progressDialogThread?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
@@ -309,9 +300,39 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
inflater.inflate(R.menu.database_lock, menu)
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
|
||||
entryEditActivityEducation?.let {
|
||||
Handler().post { performedNextEducation(it) }
|
||||
}
|
||||
|
||||
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 {
|
||||
when (item.itemId) {
|
||||
R.id.menu_lock -> {
|
||||
@@ -319,7 +340,10 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this)
|
||||
R.id.menu_contribute -> {
|
||||
MenuUtil.onContributionItemSelected(this)
|
||||
return true
|
||||
}
|
||||
|
||||
android.R.id.home -> finish()
|
||||
}
|
||||
@@ -334,7 +358,10 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putParcelable(KEY_NEW_ENTRY, mNewEntry)
|
||||
mNewEntry?.let {
|
||||
populateEntryWithViews(it)
|
||||
outState.putParcelable(KEY_NEW_ENTRY, it)
|
||||
}
|
||||
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Intent
|
||||
@@ -29,81 +29,59 @@ import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import 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.activities.dialogs.AssignMasterKeyDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.CreateFileDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.FileInformationDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
||||
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.adapters.FileDatabaseHistoryAdapter
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
import com.kunzisoft.keepass.fileselect.DeleteFileHistoryAsyncTask
|
||||
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
|
||||
import com.kunzisoft.keepass.fileselect.OpenFileHistoryAsyncTask
|
||||
import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory
|
||||
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import net.cachapa.expandablelayout.ExpandableLayout
|
||||
import permissions.dispatcher.*
|
||||
import java.io.File
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.lang.ref.WeakReference
|
||||
import java.net.URLDecoder
|
||||
import java.util.*
|
||||
|
||||
@RuntimePermissions
|
||||
class FileDatabaseSelectActivity : StylishActivity(),
|
||||
CreateFileDialogFragment.DefinePathDialogListener,
|
||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener,
|
||||
FileDatabaseHistoryAdapter.FileItemOpenListener,
|
||||
FileDatabaseHistoryAdapter.FileSelectClearListener,
|
||||
FileDatabaseHistoryAdapter.FileInformationShowListener {
|
||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||
|
||||
// Views
|
||||
private var fileListContainer: View? = null
|
||||
private var createButtonView: View? = null
|
||||
private var browseButtonView: View? = null
|
||||
private var openButtonView: View? = null
|
||||
private var fileSelectExpandableButtonView: View? = null
|
||||
private var fileSelectExpandableLayout: ExpandableLayout? = null
|
||||
private var openFileNameView: EditText? = null
|
||||
private var openDatabaseButtonView: View? = null
|
||||
|
||||
// Adapter to manage database history list
|
||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||
|
||||
private var mFileDatabaseHistory: FileDatabaseHistory? = null
|
||||
private var mFileDatabaseHistoryAction: FileDatabaseHistoryAction? = 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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mFileDatabaseHistory = FileDatabaseHistory.getInstance(WeakReference(applicationContext))
|
||||
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||
|
||||
setContentView(R.layout.activity_file_selection)
|
||||
fileListContainer = findViewById(R.id.container_file_list)
|
||||
@@ -112,132 +90,122 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
toolbar.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
openFileNameView = findViewById(R.id.file_filename)
|
||||
|
||||
// Set the initial value of the filename
|
||||
mDefaultPath = (Environment.getExternalStorageDirectory().absolutePath
|
||||
+ getString(R.string.database_file_path_default)
|
||||
+ getString(R.string.database_file_name_default)
|
||||
+ getString(R.string.database_file_extension_default))
|
||||
openFileNameView?.setHint(R.string.open_link_database)
|
||||
|
||||
// Button to expand file selection
|
||||
fileSelectExpandableButtonView = findViewById(R.id.file_select_expandable_button)
|
||||
fileSelectExpandableLayout = findViewById(R.id.file_select_expandable)
|
||||
fileSelectExpandableButtonView?.setOnClickListener { _ ->
|
||||
if (fileSelectExpandableLayout?.isExpanded == true)
|
||||
fileSelectExpandableLayout?.collapse()
|
||||
else
|
||||
fileSelectExpandableLayout?.expand()
|
||||
// Create button
|
||||
createButtonView = findViewById(R.id.create_database_button)
|
||||
if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/x-keepass"
|
||||
}.resolveActivity(packageManager) == null) {
|
||||
// No Activity found that can handle this intent.
|
||||
createButtonView?.visibility = View.GONE
|
||||
}
|
||||
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
|
||||
val databaseFileListView = findViewById<RecyclerView>(R.id.file_list)
|
||||
databaseFileListView.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
// Open button
|
||||
openButtonView = findViewById(R.id.open_database)
|
||||
openButtonView?.setOnClickListener { _ ->
|
||||
var fileName = openFileNameView?.text?.toString() ?: ""
|
||||
mDefaultPath?.let {
|
||||
if (fileName.isEmpty())
|
||||
fileName = it
|
||||
}
|
||||
launchPasswordActivityWithPath(fileName)
|
||||
}
|
||||
|
||||
// Create button
|
||||
createButtonView = findViewById(R.id.create_database)
|
||||
createButtonView?.setOnClickListener { openCreateFileDialogFragmentWithPermissionCheck() }
|
||||
|
||||
mKeyFileHelper = KeyFileHelper(this)
|
||||
browseButtonView = findViewById(R.id.browse_button)
|
||||
browseButtonView?.setOnClickListener(mKeyFileHelper!!.getOpenFileOnClickViewListener {
|
||||
Uri.parse("file://" + openFileNameView!!.text.toString())
|
||||
})
|
||||
|
||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||
fileDatabaseHistoryRecyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
|
||||
// Removes blinks
|
||||
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
// Construct adapter with listeners
|
||||
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this@FileDatabaseSelectActivity,
|
||||
mFileDatabaseHistory?.databaseUriList ?: ArrayList())
|
||||
mAdapterDatabaseHistory?.setOnItemClickListener(this)
|
||||
mAdapterDatabaseHistory?.setFileSelectClearListener(this)
|
||||
mAdapterDatabaseHistory?.setFileInformationShowListener(this)
|
||||
databaseFileListView.adapter = mAdapterDatabaseHistory
|
||||
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.databaseUri)?.let { databaseFileUri ->
|
||||
launchPasswordActivity(
|
||||
databaseFileUri,
|
||||
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
|
||||
if (!(savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(EXTRA_STAY)
|
||||
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val fileName = prefs.getString(PasswordActivity.KEY_DEFAULT_FILENAME, "")
|
||||
val databasePath = prefs.getString(PasswordActivity.KEY_DEFAULT_DATABASE_PATH, "")
|
||||
|
||||
if (fileName != null && fileName.isNotEmpty()) {
|
||||
val dbUri = UriUtil.parseUriFile(fileName)
|
||||
var scheme: String? = null
|
||||
if (dbUri != null)
|
||||
scheme = dbUri.scheme
|
||||
|
||||
if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) {
|
||||
val path = dbUri!!.path
|
||||
val db = File(path!!)
|
||||
|
||||
if (db.exists()) {
|
||||
launchPasswordActivityWithPath(path)
|
||||
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
||||
launchPasswordActivityWithPath(databaseFileUri)
|
||||
} ?: run {
|
||||
Log.i(TAG, "Unable to launch Password Activity")
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
} else {
|
||||
if (dbUri != null)
|
||||
launchPasswordActivityWithPath(dbUri.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||
}
|
||||
|
||||
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
||||
// If no recent files
|
||||
if (createButtonView != null
|
||||
&& mFileDatabaseHistory != null
|
||||
&& !mFileDatabaseHistory!!.hasRecentFiles() && fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||
createButtonView!!,
|
||||
{
|
||||
openCreateFileDialogFragmentWithPermissionCheck()
|
||||
/**
|
||||
* Create a new file by calling the content provider
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun createNewFile() {
|
||||
try {
|
||||
startActivityForResult(Intent(
|
||||
Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/x-keepass"
|
||||
putExtra(Intent.EXTRA_TITLE, getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default))
|
||||
},
|
||||
{
|
||||
// But if the user cancel, it can also select a database
|
||||
performedNextEducation(fileDatabaseSelectActivityEducation)
|
||||
}))
|
||||
else if (browseButtonView != null
|
||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||
browseButtonView!!,
|
||||
{tapTargetView ->
|
||||
tapTargetView?.let {
|
||||
mKeyFileHelper?.openFileOnClickViewListener?.onClick(it)
|
||||
CREATE_FILE_REQUEST_CODE)
|
||||
} catch (e: Exception) {
|
||||
BrowserDialogFragment().show(supportFragmentManager, "browserDialog")
|
||||
}
|
||||
},
|
||||
{
|
||||
fileSelectExpandableButtonView?.let {
|
||||
fileDatabaseSelectActivityEducation
|
||||
.checkAndPerformedOpenLinkDatabaseEducation(it)
|
||||
}
|
||||
}
|
||||
))
|
||||
;
|
||||
}
|
||||
|
||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||
val error = getString(R.string.file_not_found_content)
|
||||
Toast.makeText(this@FileDatabaseSelectActivity,
|
||||
error, Toast.LENGTH_LONG).show()
|
||||
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
Log.e(TAG, error, e)
|
||||
}
|
||||
|
||||
private fun launchPasswordActivity(fileName: String, keyFile: String) {
|
||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
try {
|
||||
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
||||
fileName, keyFile)
|
||||
databaseUri, keyFile)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
@@ -245,7 +213,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
{
|
||||
try {
|
||||
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
||||
fileName, keyFile)
|
||||
databaseUri, keyFile)
|
||||
finish()
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
@@ -255,7 +223,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||
fileName, keyFile,
|
||||
databaseUri, keyFile,
|
||||
assistStructure)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
@@ -265,8 +233,25 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
})
|
||||
}
|
||||
|
||||
private fun launchPasswordActivityWithPath(path: String) {
|
||||
launchPasswordActivity(path, "")
|
||||
private fun launchGroupActivity(readOnly: Boolean) {
|
||||
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 <=
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||
overridePendingTransition(0, 0)
|
||||
@@ -292,29 +277,41 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val database = Database.getInstance()
|
||||
if (database.loaded) {
|
||||
launchGroupActivity(database.isReadOnly)
|
||||
}
|
||||
|
||||
super.onResume()
|
||||
|
||||
updateExternalStorageWarning()
|
||||
|
||||
// Construct adapter with listeners
|
||||
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
||||
databaseFileHistoryList?.let {
|
||||
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(it)
|
||||
updateFileListVisibility()
|
||||
mAdapterDatabaseHistory!!.notifyDataSetChanged()
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
// Register progress task
|
||||
progressDialogThread?.registerProgressTask()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
// Unregister progress task
|
||||
progressDialogThread?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
// only to keep the current activity
|
||||
outState.putBoolean(EXTRA_STAY, true)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
// NOTE: delegate the permission handling to generated method
|
||||
onRequestPermissionsResult(requestCode, grantResults)
|
||||
}
|
||||
|
||||
@NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
fun openCreateFileDialogFragment() {
|
||||
val createFileDialogFragment = CreateFileDialogFragment()
|
||||
createFileDialogFragment.show(supportFragmentManager, "createFileDialogFragment")
|
||||
// to retrieve the URI of a created database after an orientation change
|
||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||
}
|
||||
|
||||
private fun updateFileListVisibility() {
|
||||
@@ -324,133 +321,26 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
fileListContainer?.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Create file for database
|
||||
* @return If not created, return false
|
||||
*/
|
||||
private fun createDatabaseFile(path: Uri): Boolean {
|
||||
|
||||
val pathString = URLDecoder.decode(path.path, "UTF-8")
|
||||
// Make sure file name exists
|
||||
if (pathString.isEmpty()) {
|
||||
Log.e(TAG, getString(R.string.error_filename_required))
|
||||
Toast.makeText(this@FileDatabaseSelectActivity,
|
||||
R.string.error_filename_required,
|
||||
Toast.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
|
||||
// Try to create the file
|
||||
val file = File(pathString)
|
||||
try {
|
||||
if (file.exists()) {
|
||||
Log.e(TAG, getString(R.string.error_database_exists) + " " + file)
|
||||
Toast.makeText(this@FileDatabaseSelectActivity,
|
||||
R.string.error_database_exists,
|
||||
Toast.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
val parent = file.parentFile
|
||||
|
||||
if (parent == null || parent.exists() && !parent.isDirectory) {
|
||||
Log.e(TAG, getString(R.string.error_invalid_path) + " " + file)
|
||||
Toast.makeText(this@FileDatabaseSelectActivity,
|
||||
R.string.error_invalid_path,
|
||||
Toast.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
|
||||
if (!parent.exists()) {
|
||||
// Create parent directory
|
||||
if (!parent.mkdirs()) {
|
||||
Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + parent)
|
||||
Toast.makeText(this@FileDatabaseSelectActivity,
|
||||
R.string.error_could_not_create_parent,
|
||||
Toast.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return file.createNewFile()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + e.localizedMessage)
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
this@FileDatabaseSelectActivity,
|
||||
getText(R.string.error_file_not_create).toString() + " "
|
||||
+ e.localizedMessage,
|
||||
Toast.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onDefinePathDialogPositiveClick(pathFile: Uri?): Boolean {
|
||||
mDatabaseFileUri = pathFile
|
||||
if (pathFile == null)
|
||||
return false
|
||||
return if (createDatabaseFile(pathFile)) {
|
||||
AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog")
|
||||
true
|
||||
} else
|
||||
false
|
||||
}
|
||||
|
||||
override fun onDefinePathDialogNegativeClick(pathFile: Uri?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogPositiveClick(
|
||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
||||
|
||||
try {
|
||||
UriUtil.parseUriFile(mDatabaseFileUri)?.let { databaseUri ->
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
|
||||
// Create the new database
|
||||
ProgressDialogThread(this@FileDatabaseSelectActivity,
|
||||
{
|
||||
CreateDatabaseRunnable(this@FileDatabaseSelectActivity,
|
||||
progressDialogThread?.startDatabaseCreate(
|
||||
databaseUri,
|
||||
Database.getInstance(),
|
||||
masterPasswordChecked,
|
||||
masterPassword,
|
||||
keyFileChecked,
|
||||
keyFile,
|
||||
true, // TODO get readonly
|
||||
LaunchGroupActivityFinish(databaseUri)
|
||||
keyFile
|
||||
)
|
||||
},
|
||||
R.string.progress_create)
|
||||
.start()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
val error = "Unable to create database with this password and key file"
|
||||
Toast.makeText(this, error, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, error + " " + e.message)
|
||||
// TODO remove
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class LaunchGroupActivityFinish internal constructor(private val fileURI: Uri) : ActionRunnable() {
|
||||
|
||||
override fun run() {
|
||||
finishRun(true, null)
|
||||
}
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
runOnUiThread {
|
||||
if (result.isSuccess) {
|
||||
// Add database to recent files
|
||||
mFileDatabaseHistory?.addDatabaseUri(fileURI)
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
updateFileListVisibility()
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity)
|
||||
} else {
|
||||
Log.e(TAG, "Unable to open the database")
|
||||
}
|
||||
}
|
||||
val error = getString(R.string.error_create_database_file)
|
||||
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
Log.e(TAG, error, e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
@@ -490,44 +357,64 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) {
|
||||
launchPasswordActivityWithPath(uri.toString())
|
||||
} else {
|
||||
fileSelectExpandableLayout?.expand(false)
|
||||
openFileNameView?.setText(uri.toString())
|
||||
}
|
||||
}
|
||||
launchPasswordActivityWithPath(uri)
|
||||
}
|
||||
}
|
||||
|
||||
@OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
internal fun showRationaleForExternalStorage(request: PermissionRequest) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.permission_external_storage_rationale_write_database)
|
||||
.setPositiveButton(R.string.allow) { _, _ -> request.proceed() }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> request.cancel() }
|
||||
.show()
|
||||
// Retrieve the created URI from the file manager
|
||||
if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
||||
mDatabaseFileUri = data?.data
|
||||
if (mDatabaseFileUri != null) {
|
||||
AssignMasterKeyDialogFragment.getInstance(true)
|
||||
.show(supportFragmentManager, "passwordDialog")
|
||||
}
|
||||
|
||||
@OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
internal fun showDeniedForExternalStorage() {
|
||||
Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show()
|
||||
// else {
|
||||
// TODO Show error
|
||||
// }
|
||||
}
|
||||
|
||||
@OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
internal fun showNeverAskForExternalStorage() {
|
||||
Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||
|
||||
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||
|
||||
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 {
|
||||
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
||||
}
|
||||
@@ -536,6 +423,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
|
||||
private const val TAG = "FileDbSelectActivity"
|
||||
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) {
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.SearchManager
|
||||
import android.app.assist.AssistStructure
|
||||
@@ -29,17 +28,19 @@ import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v7.widget.SearchView
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import 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.activities.dialogs.GroupEditDialogFragment
|
||||
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.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
|
||||
import com.kunzisoft.keepass.database.action.node.*
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.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.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.view.AddNodeButtonView
|
||||
import net.cachapa.expandablelayout.ExpandableLayout
|
||||
import com.kunzisoft.keepass.view.ToolbarAction
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
|
||||
class GroupActivity : LockingActivity(),
|
||||
GroupEditDialogFragment.EditGroupListener,
|
||||
IconPickerDialogFragment.IconPickerListener,
|
||||
NodeAdapter.NodeMenuListener,
|
||||
ListNodesFragment.NodeClickListener,
|
||||
ListNodesFragment.NodesActionMenuListener,
|
||||
ListNodesFragment.OnScrollListener,
|
||||
NodeAdapter.NodeClickCallback,
|
||||
SortDialogFragment.SortSelectionListener {
|
||||
|
||||
// Views
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
private var searchTitleView: View? = null
|
||||
private var toolbarPasteExpandableLayout: ExpandableLayout? = null
|
||||
private var toolbarPaste: Toolbar? = null
|
||||
private var toolbarAction: ToolbarAction? = null
|
||||
private var iconView: ImageView? = null
|
||||
private var numberChildrenView: TextView? = null
|
||||
private var modeTitleView: TextView? = null
|
||||
private var addNodeButtonView: AddNodeButtonView? = null
|
||||
private var groupNameView: TextView? = null
|
||||
@@ -89,12 +96,14 @@ class GroupActivity : LockingActivity(),
|
||||
private var mListNodesFragment: ListNodesFragment? = null
|
||||
private var mCurrentGroupIsASearch: Boolean = false
|
||||
|
||||
private var progressDialogThread: ProgressDialogThread? = null
|
||||
|
||||
// Nodes
|
||||
private var mRootGroup: GroupVersioned? = null
|
||||
private var mCurrentGroup: GroupVersioned? = null
|
||||
private var mOldGroupToUpdate: GroupVersioned? = null
|
||||
private var mNodeToCopy: NodeVersioned? = null
|
||||
private var mNodeToMove: NodeVersioned? = null
|
||||
// TODO private var mNodeToCopy: NodeVersioned? = null
|
||||
// TODO private var mNodeToMove: NodeVersioned? = null
|
||||
|
||||
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
|
||||
|
||||
@@ -112,15 +121,27 @@ class GroupActivity : LockingActivity(),
|
||||
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
|
||||
|
||||
// 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)
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
searchTitleView = findViewById(R.id.search_title)
|
||||
groupNameView = findViewById(R.id.group_name)
|
||||
toolbarPasteExpandableLayout = findViewById(R.id.expandable_toolbar_paste_layout)
|
||||
toolbarPaste = findViewById(R.id.toolbar_paste)
|
||||
toolbarAction = findViewById(R.id.toolbar_action)
|
||||
modeTitleView = findViewById(R.id.mode_title_view)
|
||||
|
||||
toolbar?.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
/*
|
||||
toolbarAction?.setNavigationOnClickListener {
|
||||
toolbarAction?.collapse()
|
||||
mNodeToCopy = null
|
||||
mNodeToMove = null
|
||||
}
|
||||
*/
|
||||
|
||||
// Focus view to reinitialize timeout
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
|
||||
|
||||
@@ -128,13 +149,6 @@ class GroupActivity : LockingActivity(),
|
||||
if (savedInstanceState != null) {
|
||||
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY))
|
||||
mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY)
|
||||
if (savedInstanceState.containsKey(NODE_TO_COPY_KEY)) {
|
||||
mNodeToCopy = savedInstanceState.getParcelable(NODE_TO_COPY_KEY)
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
|
||||
} else if (savedInstanceState.containsKey(NODE_TO_MOVE_KEY)) {
|
||||
mNodeToMove = savedInstanceState.getParcelable(NODE_TO_MOVE_KEY)
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -155,17 +169,6 @@ class GroupActivity : LockingActivity(),
|
||||
// Update last access time.
|
||||
mCurrentGroup?.touch(modified = false, touchParents = false)
|
||||
|
||||
toolbar?.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
toolbarPaste?.inflateMenu(R.menu.node_paste_menu)
|
||||
toolbarPaste?.setNavigationIcon(R.drawable.ic_arrow_left_white_24dp)
|
||||
toolbarPaste?.setNavigationOnClickListener {
|
||||
toolbarPasteExpandableLayout?.collapse()
|
||||
mNodeToCopy = null
|
||||
mNodeToMove = null
|
||||
}
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
||||
@@ -178,12 +181,12 @@ class GroupActivity : LockingActivity(),
|
||||
// Initialize the fragment with the list
|
||||
mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment?
|
||||
if (mListNodesFragment == null)
|
||||
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, readOnly, mCurrentGroupIsASearch)
|
||||
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, mCurrentGroupIsASearch)
|
||||
|
||||
// Attach fragment to content view
|
||||
supportFragmentManager.beginTransaction().replace(
|
||||
R.id.nodes_list_fragment_container,
|
||||
mListNodesFragment,
|
||||
mListNodesFragment!!,
|
||||
fragmentTag)
|
||||
.commit()
|
||||
|
||||
@@ -199,25 +202,92 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
})
|
||||
|
||||
// Search suggestion
|
||||
mDatabase?.let { database ->
|
||||
// Search suggestion
|
||||
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")
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
Log.d(TAG, "setNewIntent: $intent")
|
||||
setIntent(intent)
|
||||
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intent.action) {
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
intent?.let { intentNotNull ->
|
||||
Log.d(TAG, "setNewIntent: $intentNotNull")
|
||||
setIntent(intentNotNull)
|
||||
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intentNotNull.action) {
|
||||
// only one instance of search in backstack
|
||||
openSearchGroup(retrieveCurrentGroup(intent, null))
|
||||
openSearchGroup(retrieveCurrentGroup(intentNotNull, null))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openSearchGroup(group: GroupVersioned?) {
|
||||
// Delete the previous search fragment
|
||||
@@ -239,7 +309,7 @@ class GroupActivity : LockingActivity(),
|
||||
// Check TimeoutHelper
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
||||
// 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()
|
||||
// Different animation
|
||||
val fragmentTag: String
|
||||
@@ -272,17 +342,14 @@ class GroupActivity : LockingActivity(),
|
||||
mOldGroupToUpdate?.let {
|
||||
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it)
|
||||
}
|
||||
mNodeToCopy?.let {
|
||||
outState.putParcelable(NODE_TO_COPY_KEY, it)
|
||||
}
|
||||
mNodeToMove?.let {
|
||||
outState.putParcelable(NODE_TO_MOVE_KEY, it)
|
||||
}
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): GroupVersioned? {
|
||||
|
||||
// Force read only if the database is like that
|
||||
mReadOnly = mDatabase?.isReadOnly == true || mReadOnly
|
||||
|
||||
// If it's a search
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' })
|
||||
@@ -297,8 +364,6 @@ class GroupActivity : LockingActivity(),
|
||||
pwGroupId = intent.getParcelableExtra(GROUP_ID_KEY)
|
||||
}
|
||||
|
||||
readOnly = mDatabase?.isReadOnly == true || readOnly // Force read only if the database is like that
|
||||
|
||||
Log.w(TAG, "Creating tree view")
|
||||
val currentGroup: GroupVersioned?
|
||||
currentGroup = if (pwGroupId == null) {
|
||||
@@ -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
|
||||
if (selectionMode) {
|
||||
if (mSelectionMode) {
|
||||
modeTitleView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
modeTitleView?.visibility = View.GONE
|
||||
@@ -367,8 +442,8 @@ class GroupActivity : LockingActivity(),
|
||||
addNodeButtonView?.apply {
|
||||
|
||||
// To enable add button
|
||||
val addGroupEnabled = !readOnly && !mCurrentGroupIsASearch
|
||||
var addEntryEnabled = !readOnly && !mCurrentGroupIsASearch
|
||||
val addGroupEnabled = !mReadOnly && !mCurrentGroupIsASearch
|
||||
var addEntryEnabled = !mReadOnly && !mCurrentGroupIsASearch
|
||||
mCurrentGroup?.let {
|
||||
val isRoot = it == mRootGroup
|
||||
if (!it.allowAddEntryIfIsRoot())
|
||||
@@ -401,7 +476,7 @@ class GroupActivity : LockingActivity(),
|
||||
val entryVersioned = node as EntryVersioned
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
EntryActivity.launch(this@GroupActivity, entryVersioned, readOnly)
|
||||
EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly)
|
||||
},
|
||||
{
|
||||
// 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 {
|
||||
finishNodeAction()
|
||||
onNodeClick(node)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onEditMenuClick(node: NodeVersioned): Boolean {
|
||||
finishNodeAction()
|
||||
when (node.type) {
|
||||
Type.GROUP -> {
|
||||
mOldGroupToUpdate = node as GroupVersioned
|
||||
@@ -445,132 +544,56 @@ class GroupActivity : LockingActivity(),
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCopyMenuClick(node: NodeVersioned): Boolean {
|
||||
toolbarPasteExpandableLayout?.expand()
|
||||
mNodeToCopy = node
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
|
||||
return false
|
||||
}
|
||||
override fun onCopyMenuClick(nodes: List<NodeVersioned>): Boolean {
|
||||
actionNodeMode?.invalidate()
|
||||
|
||||
private inner class OnCopyMenuItemClickListener : Toolbar.OnMenuItemClickListener {
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
toolbarPasteExpandableLayout?.collapse()
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.menu_paste -> {
|
||||
when (mNodeToCopy?.type) {
|
||||
Type.GROUP -> Log.e(TAG, "Copy not allowed for group")
|
||||
Type.ENTRY -> {
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
copyEntry(mNodeToCopy as EntryVersioned, currentGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
mNodeToCopy = null
|
||||
// Nothing here fragment calls onPasteMenuClick internally
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMoveMenuClick(nodes: List<NodeVersioned>): Boolean {
|
||||
actionNodeMode?.invalidate()
|
||||
|
||||
// Nothing here fragment calls onPasteMenuClick internally
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyEntry(entryToCopy: EntryVersioned, newParent: GroupVersioned) {
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
CopyEntryRunnable(this,
|
||||
Database.getInstance(),
|
||||
entryToCopy,
|
||||
override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?,
|
||||
nodes: List<NodeVersioned>): Boolean {
|
||||
when (pasteMode) {
|
||||
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
|
||||
// Copy
|
||||
mCurrentGroup?.let { newParent ->
|
||||
progressDialogThread?.startDatabaseCopyNodes(
|
||||
nodes,
|
||||
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)
|
||||
!mReadOnly
|
||||
)
|
||||
}
|
||||
}
|
||||
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,
|
||||
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
|
||||
// Move
|
||||
mCurrentGroup?.let { newParent ->
|
||||
progressDialogThread?.startDatabaseMoveNodes(
|
||||
nodes,
|
||||
newParent,
|
||||
AfterAddNodeRunnable(),
|
||||
!readOnly)
|
||||
}.start()
|
||||
!mReadOnly
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
else -> {}
|
||||
}
|
||||
finishNodeAction()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun deleteGroup(group: GroupVersioned) {
|
||||
//TODO Verify trash recycle bin
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
DeleteGroupRunnable(
|
||||
this,
|
||||
Database.getInstance(),
|
||||
group,
|
||||
AfterDeleteNodeRunnable(),
|
||||
!readOnly)
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun deleteEntry(entry: EntryVersioned) {
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
DeleteEntryRunnable(
|
||||
this,
|
||||
Database.getInstance(),
|
||||
entry,
|
||||
AfterDeleteNodeRunnable(),
|
||||
!readOnly)
|
||||
}.start()
|
||||
override fun onDeleteMenuClick(nodes: List<NodeVersioned>): Boolean {
|
||||
progressDialogThread?.startDatabaseDeleteNodes(
|
||||
nodes,
|
||||
!mReadOnly
|
||||
)
|
||||
finishNodeAction()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -579,6 +602,16 @@ class GroupActivity : LockingActivity(),
|
||||
assignGroupViewElements()
|
||||
// Refresh suggestions to change preferences
|
||||
mSearchSuggestionAdapter?.reInit(this)
|
||||
|
||||
progressDialogThread?.registerProgressTask()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
progressDialogThread?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
|
||||
finishNodeAction()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@@ -586,7 +619,7 @@ class GroupActivity : LockingActivity(),
|
||||
val inflater = menuInflater
|
||||
inflater.inflate(R.menu.search, menu)
|
||||
inflater.inflate(R.menu.database_lock, menu)
|
||||
if (!selectionMode) {
|
||||
if (!mSelectionMode) {
|
||||
inflater.inflate(R.menu.default_menu, menu)
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
}
|
||||
@@ -628,8 +661,9 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
private fun performedNextEducation(groupActivityEducation: GroupActivityEducation,
|
||||
menu: Menu) {
|
||||
|
||||
// If no node, show education to add new one
|
||||
if (mListNodesFragment != null
|
||||
val addNodeButtonEducationPerformed = mListNodesFragment != null
|
||||
&& mListNodesFragment!!.isEmpty
|
||||
&& addNodeButtonView?.addButtonView != null
|
||||
&& addNodeButtonView!!.isEnable
|
||||
@@ -641,8 +675,10 @@ class GroupActivity : LockingActivity(),
|
||||
{
|
||||
performedNextEducation(groupActivityEducation, menu)
|
||||
}
|
||||
))
|
||||
else if (toolbar != null
|
||||
)
|
||||
if (!addNodeButtonEducationPerformed) {
|
||||
|
||||
val searchMenuEducationPerformed = toolbar != null
|
||||
&& toolbar!!.findViewById<View>(R.id.menu_search) != null
|
||||
&& groupActivityEducation.checkAndPerformedSearchMenuEducation(
|
||||
toolbar!!.findViewById(R.id.menu_search),
|
||||
@@ -651,8 +687,11 @@ class GroupActivity : LockingActivity(),
|
||||
},
|
||||
{
|
||||
performedNextEducation(groupActivityEducation, menu)
|
||||
}))
|
||||
else if (toolbar != null
|
||||
})
|
||||
|
||||
if (!searchMenuEducationPerformed) {
|
||||
|
||||
val sortMenuEducationPerformed = toolbar != null
|
||||
&& toolbar!!.findViewById<View>(R.id.menu_sort) != null
|
||||
&& groupActivityEducation.checkAndPerformedSortMenuEducation(
|
||||
toolbar!!.findViewById(R.id.menu_sort),
|
||||
@@ -661,8 +700,11 @@ class GroupActivity : LockingActivity(),
|
||||
},
|
||||
{
|
||||
performedNextEducation(groupActivityEducation, menu)
|
||||
}))
|
||||
else if (toolbar != null
|
||||
})
|
||||
|
||||
if (!sortMenuEducationPerformed) {
|
||||
// lockMenuEducationPerformed
|
||||
toolbar != null
|
||||
&& toolbar!!.findViewById<View>(R.id.menu_lock) != null
|
||||
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
|
||||
toolbar!!.findViewById(R.id.menu_lock),
|
||||
@@ -671,41 +713,9 @@ class GroupActivity : LockingActivity(),
|
||||
},
|
||||
{
|
||||
performedNextEducation(groupActivityEducation, menu)
|
||||
}))
|
||||
;
|
||||
}
|
||||
|
||||
override fun startActivity(intent: Intent) {
|
||||
|
||||
// Get the intent, verify the action and get the query
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
val query = intent.getStringExtra(SearchManager.QUERY)
|
||||
// manually launch the real search activity
|
||||
val searchIntent = Intent(applicationContext, GroupActivity::class.java)
|
||||
// add query to the Intent Extras
|
||||
searchIntent.action = Intent.ACTION_SEARCH
|
||||
searchIntent.putExtra(SearchManager.QUERY, query)
|
||||
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
super@GroupActivity.startActivity(intent)
|
||||
},
|
||||
{
|
||||
KeyboardHelper.startActivityForKeyboardSelection(
|
||||
this@GroupActivity,
|
||||
searchIntent)
|
||||
finish()
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
this@GroupActivity,
|
||||
searchIntent,
|
||||
assistStructure)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
super.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,7 +734,7 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
else -> {
|
||||
// Check the time lock before launching settings
|
||||
MenuUtil.onDefaultMenuOptionsItemSelected(this, item, readOnly, true)
|
||||
MenuUtil.onDefaultMenuOptionsItemSelected(this, item, mReadOnly, true)
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -733,7 +743,6 @@ class GroupActivity : LockingActivity(),
|
||||
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
|
||||
name: String?,
|
||||
icon: PwIcon?) {
|
||||
val database = Database.getInstance()
|
||||
|
||||
if (name != null && name.isNotEmpty() && icon != null) {
|
||||
when (action) {
|
||||
@@ -741,94 +750,37 @@ class GroupActivity : LockingActivity(),
|
||||
// If group creation
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
// Build the group
|
||||
database.createGroup()?.let { newGroup ->
|
||||
mDatabase?.createGroup()?.let { newGroup ->
|
||||
newGroup.title = name
|
||||
newGroup.icon = icon
|
||||
// Not really needed here because added in runnable but safe
|
||||
newGroup.parent = currentGroup
|
||||
|
||||
// If group created save it in the database
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
AddGroupRunnable(this,
|
||||
Database.getInstance(),
|
||||
newGroup,
|
||||
currentGroup,
|
||||
AfterAddNodeRunnable(),
|
||||
!readOnly)
|
||||
}.start()
|
||||
progressDialogThread?.startDatabaseCreateGroup(
|
||||
newGroup, currentGroup, !mReadOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
GroupEditDialogFragment.EditGroupDialogAction.UPDATE ->
|
||||
GroupEditDialogFragment.EditGroupDialogAction.UPDATE -> {
|
||||
// If update add new elements
|
||||
mOldGroupToUpdate?.let { oldGroupToUpdate ->
|
||||
GroupVersioned(oldGroupToUpdate).let { updateGroup ->
|
||||
updateGroup.title = name
|
||||
// TODO custom icon
|
||||
updateGroup.icon = icon
|
||||
updateGroup.apply {
|
||||
// WARNING remove parent and children to keep memory
|
||||
removeParent()
|
||||
removeChildren() // TODO concurrent exception
|
||||
|
||||
mListNodesFragment?.removeNode(oldGroupToUpdate)
|
||||
title = name
|
||||
this.icon = icon // TODO custom icon
|
||||
}
|
||||
|
||||
// If group updated save it in the database
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
UpdateGroupRunnable(this,
|
||||
Database.getInstance(),
|
||||
oldGroupToUpdate,
|
||||
updateGroup,
|
||||
AfterUpdateNodeRunnable(),
|
||||
!readOnly)
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class AfterAddNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
runOnUiThread {
|
||||
if (actionNodeValues.result.isSuccess) {
|
||||
if (actionNodeValues.newNode != null)
|
||||
mListNodesFragment?.addNode(actionNodeValues.newNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class AfterUpdateNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
runOnUiThread {
|
||||
if (actionNodeValues.result.isSuccess) {
|
||||
if (actionNodeValues.oldNode!= null && actionNodeValues.newNode != null)
|
||||
mListNodesFragment?.updateNode(actionNodeValues.oldNode, actionNodeValues.newNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class AfterDeleteNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
runOnUiThread {
|
||||
if (actionNodeValues.result.isSuccess) {
|
||||
actionNodeValues.oldNode?.let { oldNode ->
|
||||
|
||||
mListNodesFragment?.removeNode(oldNode)
|
||||
|
||||
// TODO Move trash view
|
||||
// Add trash in views list if it doesn't exists
|
||||
val database = Database.getInstance()
|
||||
if (database.isRecycleBinEnabled) {
|
||||
val recycleBin = database.recycleBin
|
||||
if (mCurrentGroup != null && recycleBin != null
|
||||
&& mCurrentGroup!!.parent == null
|
||||
&& mCurrentGroup != recycleBin) {
|
||||
mListNodesFragment?.addNode(recycleBin)
|
||||
}
|
||||
progressDialogThread?.startDatabaseUpdateGroup(
|
||||
oldGroupToUpdate, updateGroup, !mReadOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -847,11 +799,9 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
private fun showWarnings() {
|
||||
// TODO Preferences
|
||||
if (Database.getInstance().isReadOnly) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
if (prefs.getBoolean(getString(R.string.show_read_only_warning), true)) {
|
||||
ReadOnlyDialog(this).show()
|
||||
if (PreferencesUtil.showReadOnlyWarning(this)) {
|
||||
ReadOnlyDialog().show(supportFragmentManager, "readOnlyDialog")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -860,6 +810,40 @@ class GroupActivity : LockingActivity(),
|
||||
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?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
@@ -871,24 +855,6 @@ class GroupActivity : LockingActivity(),
|
||||
mListNodesFragment?.rebuildList()
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
|
||||
/*
|
||||
* ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in
|
||||
* another app such as Files or GoogleDrive and then Search for an entry. Here we remove the
|
||||
* FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task.
|
||||
*/
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
var flags = intent.flags
|
||||
flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
|
||||
intent.flags = flags
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
super.startActivityForResult(intent, requestCode, options)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeSearchInIntent(intent: Intent) {
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
mCurrentGroupIsASearch = false
|
||||
@@ -898,6 +864,9 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (mListNodesFragment?.nodeActionSelectionMode == true) {
|
||||
finishNodeAction()
|
||||
} else {
|
||||
// Normal way when we are not in root
|
||||
if (mRootGroup != null && mRootGroup != mCurrentGroup)
|
||||
super.onBackPressed()
|
||||
@@ -918,6 +887,7 @@ class GroupActivity : LockingActivity(),
|
||||
removeSearchInIntent(intent)
|
||||
assignGroupViewElements()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -927,13 +897,15 @@ class GroupActivity : LockingActivity(),
|
||||
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
|
||||
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
|
||||
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
|
||||
private const val NODE_TO_COPY_KEY = "NODE_TO_COPY_KEY"
|
||||
private const val NODE_TO_MOVE_KEY = "NODE_TO_MOVE_KEY"
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity, group: GroupVersioned?, readOnly: Boolean,
|
||||
private fun buildAndLaunchIntent(context: Context, group: GroupVersioned?, readOnly: Boolean,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, GroupActivity::class.java)
|
||||
val checkTime = if (context is Activity)
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(context)
|
||||
else
|
||||
TimeoutHelper.checkTime(context)
|
||||
if (checkTime) {
|
||||
val intent = Intent(context, GroupActivity::class.java)
|
||||
if (group != null) {
|
||||
intent.putExtra(GROUP_ID_KEY, group.nodeId)
|
||||
}
|
||||
@@ -949,10 +921,10 @@ class GroupActivity : LockingActivity(),
|
||||
*/
|
||||
|
||||
@JvmOverloads
|
||||
fun launch(activity: Activity, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
|
||||
TimeoutHelper.recordTime(activity)
|
||||
buildAndLaunchIntent(activity, null, readOnly) { intent ->
|
||||
activity.startActivity(intent)
|
||||
fun launch(context: Context, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||
TimeoutHelper.recordTime(context)
|
||||
buildAndLaunchIntent(context, null, readOnly) { intent ->
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -963,10 +935,10 @@ class GroupActivity : LockingActivity(),
|
||||
*/
|
||||
// TODO implement pre search to directly open the direct group
|
||||
|
||||
fun launchForKeyboardSelection(activity: Activity, readOnly: Boolean) {
|
||||
TimeoutHelper.recordTime(activity)
|
||||
buildAndLaunchIntent(activity, null, readOnly) { intent ->
|
||||
KeyboardHelper.startActivityForKeyboardSelection(activity, intent)
|
||||
fun launchForKeyboardSelection(context: Context, readOnly: Boolean) {
|
||||
TimeoutHelper.recordTime(context)
|
||||
buildAndLaunchIntent(context, null, readOnly) { intent ->
|
||||
EntrySelectionHelper.startActivityForEntrySelection(context, intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
@@ -14,6 +14,7 @@ import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ActionMode
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||
@@ -25,11 +26,13 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Type
|
||||
import java.util.*
|
||||
|
||||
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
||||
|
||||
private var nodeClickCallback: NodeAdapter.NodeClickCallback? = null
|
||||
private var nodeMenuListener: NodeAdapter.NodeMenuListener? = null
|
||||
private var nodeClickListener: NodeClickListener? = null
|
||||
private var onScrollListener: OnScrollListener? = null
|
||||
|
||||
private var listView: RecyclerView? = null
|
||||
@@ -37,6 +40,13 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
private set
|
||||
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 isASearchResult: Boolean = false
|
||||
|
||||
@@ -52,31 +62,22 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
val isEmpty: Boolean
|
||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
nodeClickCallback = context as NodeAdapter.NodeClickCallback?
|
||||
nodeClickListener = context as NodeClickListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context?.toString()
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
|
||||
}
|
||||
|
||||
try {
|
||||
nodeMenuListener = context as NodeAdapter.NodeMenuListener?
|
||||
} catch (e: ClassCastException) {
|
||||
nodeMenuListener = null
|
||||
// Context menu can be omit
|
||||
Log.w(TAG, context?.toString()
|
||||
+ " must implement " + NodeAdapter.NodeMenuListener::class.java.name)
|
||||
}
|
||||
|
||||
try {
|
||||
onScrollListener = context as OnScrollListener?
|
||||
onScrollListener = context as OnScrollListener
|
||||
} catch (e: ClassCastException) {
|
||||
onScrollListener = null
|
||||
// Context menu can be omit
|
||||
Log.w(TAG, context?.toString()
|
||||
Log.w(TAG, context.toString()
|
||||
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
|
||||
}
|
||||
}
|
||||
@@ -84,7 +85,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
activity?.let { currentActivity ->
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
|
||||
@@ -100,18 +100,44 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
|
||||
contextThemed?.let { context ->
|
||||
mAdapter = NodeAdapter(context, currentActivity.menuInflater)
|
||||
mAdapter = NodeAdapter(context)
|
||||
mAdapter?.apply {
|
||||
setReadOnly(readOnly)
|
||||
setIsASearchResult(isASearchResult)
|
||||
setOnNodeClickListener(nodeClickCallback)
|
||||
setActivateContextMenu(true)
|
||||
setNodeMenuListener(nodeMenuListener)
|
||||
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) {
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||
@@ -129,7 +155,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
|
||||
onScrollListener?.let { 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)
|
||||
onScrollListener.onScrolled(dy)
|
||||
}
|
||||
@@ -147,10 +173,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
activity?.intent?.let {
|
||||
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it)
|
||||
}
|
||||
// Force read only mode if selection mode
|
||||
mAdapter?.apply {
|
||||
setReadOnly(readOnly)
|
||||
}
|
||||
|
||||
// Refresh data
|
||||
mAdapter?.notifyDataSetChanged()
|
||||
@@ -194,35 +216,31 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
|
||||
inflater?.inflate(R.menu.tree, menu)
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.tree, menu)
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||
when (item?.itemId) {
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
|
||||
R.id.menu_sort -> {
|
||||
context?.let { context ->
|
||||
val sortDialogFragment: SortDialogFragment
|
||||
|
||||
/*
|
||||
// TODO Recycle bin bottom
|
||||
if (database.isRecycleBinAvailable() && database.isRecycleBinEnabled()) {
|
||||
sortDialogFragment =
|
||||
val sortDialogFragment: SortDialogFragment =
|
||||
if (Database.getInstance().allowRecycleBin
|
||||
&& Database.getInstance().isRecycleBinEnabled) {
|
||||
SortDialogFragment.getInstance(
|
||||
PrefsUtil.getListSort(this),
|
||||
PrefsUtil.getAscendingSort(this),
|
||||
PrefsUtil.getGroupsBeforeSort(this),
|
||||
PrefsUtil.getRecycleBinBottomSort(this));
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
PreferencesUtil.getRecycleBinBottomSort(context))
|
||||
} else {
|
||||
*/
|
||||
sortDialogFragment = SortDialogFragment.getInstance(
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context))
|
||||
//}
|
||||
}
|
||||
|
||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||
}
|
||||
@@ -233,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?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
@@ -255,18 +369,66 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
}
|
||||
|
||||
fun contains(node: NodeVersioned): Boolean {
|
||||
return mAdapter?.contains(node) ?: false
|
||||
}
|
||||
|
||||
fun addNode(newNode: NodeVersioned) {
|
||||
mAdapter?.addNode(newNode)
|
||||
}
|
||||
|
||||
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) {
|
||||
mAdapter?.updateNode(oldNode, newNode)
|
||||
fun addNodes(newNodes: List<NodeVersioned>) {
|
||||
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) {
|
||||
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 {
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,61 +19,69 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.assist.AssistStructure
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.hardware.fingerprint.FingerprintManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.widget.*
|
||||
import 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.activities.dialogs.PasswordEncodingDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.*
|
||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||
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.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.app.App
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
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.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.fingerprint.FingerPrintHelper
|
||||
import com.kunzisoft.keepass.fingerprint.FingerPrintViewsManager
|
||||
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.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.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.FingerPrintInfoView
|
||||
import permissions.dispatcher.*
|
||||
import java.io.File
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import kotlinx.android.synthetic.main.activity_password.*
|
||||
import java.io.FileNotFoundException
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
@RuntimePermissions
|
||||
class PasswordActivity : StylishActivity(),
|
||||
UriIntentInitTaskCallback {
|
||||
class PasswordActivity : StylishActivity() {
|
||||
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
private var containerView: View? = null
|
||||
private var filenameView: TextView? = null
|
||||
private var passwordView: EditText? = null
|
||||
private var keyFileView: EditText? = null
|
||||
@@ -81,26 +89,29 @@ class PasswordActivity : StylishActivity(),
|
||||
private var checkboxPasswordView: CompoundButton? = null
|
||||
private var checkboxKeyFileView: 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 mDatabaseFileUri: Uri? = null
|
||||
private var mDatabaseKeyFileUri: Uri? = null
|
||||
|
||||
private var prefs: SharedPreferences? = null
|
||||
|
||||
private var mRememberKeyFile: Boolean = false
|
||||
private var mKeyFileHelper: KeyFileHelper? = null
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
mRememberKeyFile = prefs!!.getBoolean(getString(R.string.keyfile_key),
|
||||
resources.getBoolean(R.bool.keyfile_default))
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this)
|
||||
|
||||
setContentView(R.layout.activity_password)
|
||||
|
||||
@@ -110,6 +121,7 @@ class PasswordActivity : StylishActivity(),
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
containerView = findViewById(R.id.container)
|
||||
confirmButtonView = findViewById(R.id.pass_ok)
|
||||
filenameView = findViewById(R.id.filename)
|
||||
passwordView = findViewById(R.id.password)
|
||||
@@ -117,13 +129,13 @@ class PasswordActivity : StylishActivity(),
|
||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
||||
fingerPrintInfoView = findViewById(R.id.fingerprint_info)
|
||||
advancedUnlockInfoView = findViewById(R.id.fingerprint_info)
|
||||
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||
|
||||
val browseView = findViewById<View>(R.id.browse_button)
|
||||
mKeyFileHelper = KeyFileHelper(this@PasswordActivity)
|
||||
browseView.setOnClickListener(mKeyFileHelper!!.openFileOnClickViewListener)
|
||||
val browseView = findViewById<View>(R.id.open_database_button)
|
||||
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
||||
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
|
||||
|
||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
||||
@@ -148,11 +160,91 @@ class PasswordActivity : StylishActivity(),
|
||||
}
|
||||
})
|
||||
|
||||
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
|
||||
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
|
||||
confirmButtonView?.isEnabled = isChecked
|
||||
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||
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 {
|
||||
@@ -166,6 +258,9 @@ class PasswordActivity : StylishActivity(),
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
if (Database.getInstance().loaded)
|
||||
launchGroupActivity()
|
||||
|
||||
// If the database isn't accessible make sure to clear the password field, if it
|
||||
// was saved in the instance state
|
||||
if (Database.getInstance().loaded) {
|
||||
@@ -175,17 +270,9 @@ class PasswordActivity : StylishActivity(),
|
||||
// For check shutdown
|
||||
super.onResume()
|
||||
|
||||
// Enable or not the open button
|
||||
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
|
||||
checkboxPasswordView?.let {
|
||||
confirmButtonView?.isEnabled = it.isChecked
|
||||
}
|
||||
} else {
|
||||
confirmButtonView?.isEnabled = true
|
||||
}
|
||||
progressDialogThread?.registerProgressTask()
|
||||
|
||||
UriIntentInitTask(WeakReference(this), this, mRememberKeyFile)
|
||||
.execute(intent)
|
||||
initUriFromIntent()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -193,26 +280,45 @@ class PasswordActivity : StylishActivity(),
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onPostInitTask(databaseFileUri: Uri?, keyFileUri: Uri?, errorStringId: Int?) {
|
||||
mDatabaseFileUri = databaseFileUri
|
||||
private fun initUriFromIntent() {
|
||||
|
||||
if (errorStringId != null) {
|
||||
Toast.makeText(this@PasswordActivity, errorStringId, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
return
|
||||
val databaseUri: Uri?
|
||||
val keyFileUri: Uri?
|
||||
|
||||
// If is a view intent
|
||||
val action = intent.action
|
||||
if (action != null
|
||||
&& action == VIEW_INTENT) {
|
||||
databaseUri = intent.data
|
||||
keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
||||
} else {
|
||||
databaseUri = intent.getParcelableExtra(KEY_FILENAME)
|
||||
keyFileUri = intent.getParcelableExtra(KEY_KEYFILE)
|
||||
}
|
||||
|
||||
// Verify permission to read file
|
||||
if (databaseFileUri != null && !databaseFileUri.scheme!!.contains("content"))
|
||||
doNothingWithPermissionCheck()
|
||||
// Post init uri with KeyFile if needed
|
||||
if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) {
|
||||
// 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
|
||||
val dbUriString = databaseFileUri?.toString() ?: ""
|
||||
if (dbUriString.isNotEmpty()) {
|
||||
if (PreferencesUtil.isFullFilePathEnable(this))
|
||||
filenameView?.text = dbUriString
|
||||
else
|
||||
filenameView?.text = File(databaseFileUri!!.path!!).name // TODO Encapsulate
|
||||
databaseFileUri?.let {
|
||||
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
|
||||
filenameView?.text = title
|
||||
}
|
||||
}
|
||||
|
||||
// Define Key File text
|
||||
@@ -223,13 +329,17 @@ class PasswordActivity : StylishActivity(),
|
||||
|
||||
// Define listeners for default database checkbox and validate button
|
||||
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
|
||||
var newDefaultFileName = ""
|
||||
var newDefaultFileName: Uri? = null
|
||||
if (isChecked) {
|
||||
newDefaultFileName = databaseFileUri?.toString() ?: newDefaultFileName
|
||||
newDefaultFileName = databaseFileUri ?: newDefaultFileName
|
||||
}
|
||||
|
||||
prefs?.edit()?.apply() {
|
||||
putString(KEY_DEFAULT_FILENAME, newDefaultFileName)
|
||||
prefs?.edit()?.apply {
|
||||
newDefaultFileName?.let {
|
||||
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
|
||||
} ?: kotlin.run {
|
||||
remove(KEY_DEFAULT_DATABASE_PATH)
|
||||
}
|
||||
apply()
|
||||
}
|
||||
|
||||
@@ -239,10 +349,10 @@ class PasswordActivity : StylishActivity(),
|
||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
||||
|
||||
// Retrieve settings for default database
|
||||
val defaultFilename = prefs?.getString(KEY_DEFAULT_FILENAME, "")
|
||||
val defaultFilename = prefs?.getString(KEY_DEFAULT_DATABASE_PATH, "")
|
||||
if (databaseFileUri != null
|
||||
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
|
||||
&& databaseFileUri == UriUtil.parseUriFile(defaultFilename)) {
|
||||
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
|
||||
checkboxDefaultDatabaseView?.isChecked = true
|
||||
}
|
||||
|
||||
@@ -259,35 +369,62 @@ class PasswordActivity : StylishActivity(),
|
||||
// Init FingerPrint elements
|
||||
var fingerPrintInit = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (PreferencesUtil.isFingerprintEnable(this)) {
|
||||
if (fingerPrintViewsManager == null) {
|
||||
fingerPrintViewsManager = FingerPrintViewsManager(this,
|
||||
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
||||
|
||||
advancedUnlockInfoView?.setOnClickListener {
|
||||
FingerPrintExplanationDialog().show(supportFragmentManager, "fingerPrintExplanationDialog")
|
||||
}
|
||||
|
||||
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
||||
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
||||
databaseFileUri,
|
||||
fingerPrintInfoView,
|
||||
advancedUnlockInfoView,
|
||||
checkboxPasswordView,
|
||||
enableButtonOnCheckedChangeListener,
|
||||
passwordView) { passwordRetrieve ->
|
||||
// Load the database if password is registered or retrieve
|
||||
passwordRetrieve?.let {
|
||||
passwordView,
|
||||
{ passwordEncrypted, ivSpec ->
|
||||
// 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
|
||||
verifyKeyFileCheckboxAndLoadDatabase(it)
|
||||
} ?: run {
|
||||
// Register with fingerprint
|
||||
verifyCheckboxesAndLoadDatabase()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
fingerPrintViewsManager?.initFingerprint()
|
||||
// checks if fingerprint is available, will also start listening for fingerprints when available
|
||||
fingerPrintViewsManager?.checkFingerprintAvailability()
|
||||
advancedUnlockedManager?.initBiometric()
|
||||
fingerPrintInit = true
|
||||
} else {
|
||||
fingerPrintViewsManager?.destroy()
|
||||
advancedUnlockedManager?.destroy()
|
||||
}
|
||||
}
|
||||
if (!fingerPrintInit) {
|
||||
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||
}
|
||||
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||
}
|
||||
|
||||
enableOrNotTheConfirmationButton()
|
||||
}
|
||||
|
||||
private fun enableOrNotTheConfirmationButton() {
|
||||
// Enable or not the open button if setting is checked
|
||||
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
|
||||
checkboxPasswordView?.let {
|
||||
confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true
|
||||
|| checkboxKeyFileView?.isChecked == true)
|
||||
}
|
||||
} else {
|
||||
confirmButtonView?.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,29 +462,43 @@ class PasswordActivity : StylishActivity(),
|
||||
|
||||
override fun onPause() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
fingerPrintViewsManager?.stopListening()
|
||||
advancedUnlockedManager?.pause()
|
||||
}
|
||||
|
||||
progressDialogThread?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
fingerPrintViewsManager?.destroy()
|
||||
advancedUnlockedManager?.destroy()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun verifyCheckboxesAndLoadDatabase(password: String? = passwordView?.text?.toString(),
|
||||
keyFile: Uri? = UriUtil.parseUriFile(keyFileView?.text?.toString())) {
|
||||
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
|
||||
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||
loadDatabase(keyPassword, keyFileUri)
|
||||
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
val password: String? = passwordView?.text?.toString()
|
||||
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
|
||||
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
||||
}
|
||||
|
||||
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String? = passwordView?.text?.toString(),
|
||||
keyFile: Uri? = UriUtil.parseUriFile(keyFileView?.text?.toString())) {
|
||||
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||
loadDatabase(password, keyFileUri)
|
||||
private fun verifyCheckboxesAndLoadDatabase(password: String?,
|
||||
keyFile: Uri?,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
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() {
|
||||
@@ -355,89 +506,51 @@ class PasswordActivity : StylishActivity(),
|
||||
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)) {
|
||||
removePassword()
|
||||
}
|
||||
|
||||
// Clear before we load
|
||||
val database = Database.getInstance()
|
||||
database.closeAndClear(applicationContext.filesDir)
|
||||
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
// Show the progress dialog and load the database
|
||||
ProgressDialogThread(this,
|
||||
{ progressTaskUpdater ->
|
||||
LoadDatabaseRunnable(
|
||||
WeakReference(this@PasswordActivity),
|
||||
database,
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseUri,
|
||||
password,
|
||||
keyFileUri,
|
||||
readOnly,
|
||||
cipherDatabaseEntity,
|
||||
false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
||||
password: String?,
|
||||
keyFile: Uri?,
|
||||
readOnly: Boolean,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||
fixDuplicateUUID: Boolean) {
|
||||
progressDialogThread?.startDatabaseLoad(
|
||||
databaseUri,
|
||||
password,
|
||||
keyFile,
|
||||
progressTaskUpdater,
|
||||
AfterLoadingDatabase(database, password))
|
||||
},
|
||||
R.string.loading_database).start()
|
||||
}
|
||||
readOnly,
|
||||
cipherDatabaseEntity,
|
||||
fixDuplicateUUID
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after verify and try to opening the database
|
||||
*/
|
||||
private inner class AfterLoadingDatabase internal constructor(var database: Database,
|
||||
val password: String?)
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
runOnUiThread {
|
||||
// Recheck fingerprint if error
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (PreferencesUtil.isFingerprintEnable(this@PasswordActivity)) {
|
||||
// Stay with the same mode
|
||||
fingerPrintViewsManager?.reInitWithFingerprintMode()
|
||||
}
|
||||
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
|
||||
DuplicateUuidDialog().apply {
|
||||
positiveAction = loadDatabaseWithFix
|
||||
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
||||
}
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Remove the password in view in all cases
|
||||
removePassword()
|
||||
|
||||
if (database.validatePasswordEncoding(password)) {
|
||||
launchGroupActivity()
|
||||
} else {
|
||||
PasswordEncodingDialogFragment().apply {
|
||||
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
|
||||
launchGroupActivity()
|
||||
}
|
||||
show(supportFragmentManager, "passwordEncodingTag")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (result.message != null && result.message!!.isNotEmpty()) {
|
||||
Toast.makeText(this@PasswordActivity, result.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchGroupActivity() {
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
GroupActivity.launch(this@PasswordActivity, readOnly)
|
||||
},
|
||||
{
|
||||
GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly)
|
||||
// Do not keep history
|
||||
finish()
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly)
|
||||
}
|
||||
})
|
||||
}
|
||||
// To fix multiple view education
|
||||
private var performedEductionInProgress = false
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val inflater = menuInflater
|
||||
@@ -449,45 +562,58 @@ class PasswordActivity : StylishActivity(),
|
||||
|
||||
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// Fingerprint menu
|
||||
fingerPrintViewsManager?.inflateOptionsMenu(inflater, menu)
|
||||
advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
|
||||
}
|
||||
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
if (!performedEductionInProgress) {
|
||||
performedEductionInProgress = true
|
||||
// Show education views
|
||||
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||
menu: Menu) {
|
||||
if (toolbar != null
|
||||
&& passwordActivityEducation.checkAndPerformedFingerprintUnlockEducation(
|
||||
toolbar!!,
|
||||
val educationContainerView = containerView
|
||||
val unlockEducationPerformed = educationContainerView != null
|
||||
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
||||
educationContainerView,
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
}))
|
||||
else if (toolbar != null
|
||||
&& toolbar!!.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||
})
|
||||
if (!unlockEducationPerformed) {
|
||||
val educationToolbar = toolbar
|
||||
val readOnlyEducationPerformed =
|
||||
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||
toolbar!!.findViewById(R.id.menu_open_file_read_mode_key),
|
||||
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
}))
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& PreferencesUtil.isFingerprintEnable(applicationContext)
|
||||
&& FingerPrintHelper.isFingerprintSupported(getSystemService(FingerprintManager::class.java))
|
||||
&& fingerPrintInfoView != null && fingerPrintInfoView?.fingerPrintImageView != null
|
||||
&& passwordActivityEducation.checkAndPerformedFingerprintEducation(fingerPrintInfoView?.fingerPrintImageView!!))
|
||||
;
|
||||
})
|
||||
|
||||
if (!readOnlyEducationPerformed) {
|
||||
|
||||
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) {
|
||||
@@ -509,7 +635,7 @@ class PasswordActivity : StylishActivity(),
|
||||
changeOpenFileReadIcon(item)
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -517,12 +643,6 @@ class PasswordActivity : StylishActivity(),
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
// NOTE: delegate the permission handling to generated method
|
||||
onRequestPermissionsResult(requestCode, grantResults)
|
||||
}
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
@@ -535,7 +655,7 @@ class PasswordActivity : StylishActivity(),
|
||||
}
|
||||
|
||||
var keyFileResult = false
|
||||
mKeyFileHelper?.let {
|
||||
mOpenFileHelper?.let {
|
||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
@@ -554,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 {
|
||||
|
||||
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_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity, fileName: String, keyFile: String,
|
||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
val intent = Intent(activity, PasswordActivity::class.java)
|
||||
intent.putExtra(UriIntentInitTask.KEY_FILENAME, fileName)
|
||||
intent.putExtra(UriIntentInitTask.KEY_KEYFILE, keyFile)
|
||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||
if (keyFile != null)
|
||||
intent.putExtra(KEY_KEYFILE, keyFile)
|
||||
intentBuildLauncher.invoke(intent)
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
private fun verifyFileNameUriFromLaunch(fileName: String) {
|
||||
if (fileName.isEmpty()) {
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
|
||||
val uri = UriUtil.parseUriFile(fileName)
|
||||
val scheme = uri?.scheme
|
||||
if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) {
|
||||
val dbFile = File(uri.path!!)
|
||||
if (!dbFile.exists()) {
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Standard Launch
|
||||
@@ -621,10 +705,9 @@ class PasswordActivity : StylishActivity(),
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
fileName: String,
|
||||
keyFile: String) {
|
||||
verifyFileNameUriFromLaunch(fileName)
|
||||
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -638,12 +721,10 @@ class PasswordActivity : StylishActivity(),
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForKeyboardResult(
|
||||
activity: Activity,
|
||||
fileName: String,
|
||||
keyFile: String) {
|
||||
verifyFileNameUriFromLaunch(fileName)
|
||||
|
||||
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
|
||||
KeyboardHelper.startActivityForKeyboardSelection(activity, intent)
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
EntrySelectionHelper.startActivityForEntrySelection(activity, intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,20 +738,18 @@ class PasswordActivity : StylishActivity(),
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForAutofillResult(
|
||||
activity: Activity,
|
||||
fileName: String,
|
||||
keyFile: String,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
assistStructure: AssistStructure?) {
|
||||
verifyFileNameUriFromLaunch(fileName)
|
||||
|
||||
if (assistStructure != null) {
|
||||
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
intent,
|
||||
assistStructure)
|
||||
}
|
||||
} else {
|
||||
launch(activity, fileName, keyFile)
|
||||
launch(activity, databaseFile, keyFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,16 +25,16 @@ import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
@@ -43,15 +43,41 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
private var mKeyFile: Uri? = null
|
||||
|
||||
private var rootView: View? = null
|
||||
|
||||
private var passwordCheckBox: CompoundButton? = null
|
||||
private var passView: TextView? = null
|
||||
private var passConfView: TextView? = null
|
||||
|
||||
private var 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 keyFileView: TextView? = 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 {
|
||||
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
||||
@@ -60,18 +86,25 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
keyFileChecked: Boolean, keyFile: Uri?)
|
||||
}
|
||||
|
||||
override fun onAttach(activity: Context?) {
|
||||
override fun onAttach(activity: Context) {
|
||||
super.onAttach(activity)
|
||||
try {
|
||||
mListener = activity as AssignPasswordDialogListener?
|
||||
mListener = activity as AssignPasswordDialogListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(activity?.toString()
|
||||
throw ClassCastException(activity.toString()
|
||||
+ " must implement " + AssignPasswordDialogListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
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 inflater = activity.layoutInflater
|
||||
|
||||
@@ -80,36 +113,21 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
.setTitle(R.string.assign_master_key)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||
passView = rootView?.findViewById(R.id.pass_password)
|
||||
passView?.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
passwordCheckBox?.isChecked = true
|
||||
}
|
||||
})
|
||||
passConfView = rootView?.findViewById(R.id.pass_conf_password)
|
||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
||||
|
||||
keyFileTextInputLayout = rootView?.findViewById(R.id.keyfile_input_layout)
|
||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
|
||||
keyFileView?.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
keyFileCheckBox?.isChecked = true
|
||||
}
|
||||
})
|
||||
|
||||
mKeyFileHelper = KeyFileHelper(this)
|
||||
rootView?.findViewById<View>(R.id.browse_button)?.setOnClickListener { view ->
|
||||
mKeyFileHelper?.openFileOnClickViewListener?.onClick(view) }
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
rootView?.findViewById<View>(R.id.open_database_button)?.setOnClickListener { view ->
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
|
||||
|
||||
val dialog = builder.create()
|
||||
|
||||
@@ -124,7 +142,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
var error = verifyPassword() || verifyFile()
|
||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||
error = true
|
||||
if (allowNoMasterKey)
|
||||
showNoKeyConfirmationDialog()
|
||||
else {
|
||||
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
|
||||
}
|
||||
}
|
||||
if (!error) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(
|
||||
@@ -149,20 +171,35 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
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 {
|
||||
var error = false
|
||||
if (passwordCheckBox != null
|
||||
&& passwordCheckBox!!.isChecked
|
||||
&& passView != null
|
||||
&& passConfView != null) {
|
||||
mMasterPassword = passView!!.text.toString()
|
||||
val confPassword = passConfView!!.text.toString()
|
||||
&& passwordView != null
|
||||
&& passwordRepeatView != null) {
|
||||
mMasterPassword = passwordView!!.text.toString()
|
||||
val confPassword = passwordRepeatView!!.text.toString()
|
||||
|
||||
// Verify that passwords match
|
||||
if (mMasterPassword != confPassword) {
|
||||
error = true
|
||||
// Passwords do not match
|
||||
Toast.makeText(context, R.string.error_pass_match, Toast.LENGTH_LONG).show()
|
||||
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
|
||||
}
|
||||
|
||||
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
|
||||
@@ -170,6 +207,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
showEmptyPasswordConfirmationDialog()
|
||||
}
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
@@ -177,13 +215,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
var error = false
|
||||
if (keyFileCheckBox != null
|
||||
&& keyFileCheckBox!!.isChecked) {
|
||||
val keyFile = UriUtil.parseUriFile(keyFileView?.text?.toString())
|
||||
mKeyFile = keyFile
|
||||
|
||||
// Verify that a keyfile is set
|
||||
if (keyFile == null || keyFile.toString().isEmpty()) {
|
||||
UriUtil.parse(keyFileView?.text?.toString())?.let { uri ->
|
||||
mKeyFile = uri
|
||||
} ?: run {
|
||||
error = true
|
||||
Toast.makeText(context, R.string.error_nokeyfile, Toast.LENGTH_LONG).show()
|
||||
keyFileTextInputLayout?.error = getString(R.string.error_nokeyfile)
|
||||
}
|
||||
}
|
||||
return error
|
||||
@@ -201,7 +238,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
}
|
||||
}
|
||||
@@ -216,7 +253,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
}
|
||||
}
|
||||
@@ -224,13 +261,26 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
UriUtil.parseUriFile(uri)?.let { pathUri ->
|
||||
uri?.let { pathUri ->
|
||||
keyFileCheckBox?.isChecked = true
|
||||
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.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.utils.Util
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class BrowserDialogFragment : DialogFragment() {
|
||||
|
||||
@@ -35,17 +36,20 @@ class BrowserDialogFragment : DialogFragment() {
|
||||
// Get the layout inflater
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_browser_install, null)
|
||||
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 {
|
||||
Util.gotoUrl(context!!, R.string.filemanager_play_store)
|
||||
UriUtil.gotoUrl(context!!, R.string.file_manager_play_store)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
val web = root.findViewById<Button>(R.id.install_web)
|
||||
val web = root.findViewById<Button>(R.id.file_manager_install_f_droid)
|
||||
web.setOnClickListener {
|
||||
Util.gotoUrl(context!!, R.string.filemanager_f_droid)
|
||||
UriUtil.gotoUrl(context!!, R.string.file_manager_f_droid)
|
||||
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/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.fingerprint
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.biometric.FingerPrintAnimatedVector
|
||||
import com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
class FingerPrintExplanationDialog : DialogFragment() {
|
||||
@@ -41,11 +43,16 @@ class FingerPrintExplanationDialog : DialogFragment() {
|
||||
|
||||
val rootView = inflater.inflate(R.layout.fragment_fingerprint_explanation, null)
|
||||
|
||||
val fingerprintSettingWayTextView = rootView.findViewById<View>(R.id.fingerprint_setting_way_text)
|
||||
fingerprintSettingWayTextView.setOnClickListener { startActivity(Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS)) }
|
||||
rootView.findViewById<View>(R.id.fingerprint_setting_link_text).setOnClickListener {
|
||||
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,
|
||||
rootView.findViewById(R.id.fingerprint_image))
|
||||
rootView.findViewById(R.id.biometric_image))
|
||||
|
||||
builder.setView(rootView)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
@@ -22,14 +22,18 @@ package com.kunzisoft.keepass.activities.dialogs
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
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.password.PasswordGenerator
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.applyFontVisibility
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
|
||||
class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
|
||||
@@ -37,6 +41,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
|
||||
private var root: View? = null
|
||||
private var lengthTextView: EditText? = null
|
||||
private var passwordInputLayoutView: TextInputLayout? = null
|
||||
private var passwordView: EditText? = null
|
||||
|
||||
private var uppercaseBox: CompoundButton? = null
|
||||
@@ -49,12 +54,12 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
private var bracketsBox: CompoundButton? = null
|
||||
private var extendedBox: CompoundButton? = null
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as GeneratePasswordListener?
|
||||
mListener = context as GeneratePasswordListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context?.toString()
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + GeneratePasswordListener::class.java.name)
|
||||
}
|
||||
}
|
||||
@@ -65,6 +70,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
val inflater = activity.layoutInflater
|
||||
root = inflater.inflate(R.layout.fragment_generate_password, null)
|
||||
|
||||
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
|
||||
passwordView = root?.findViewById(R.id.password)
|
||||
passwordView?.applyFontVisibility()
|
||||
|
||||
@@ -108,7 +114,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
|
||||
dismiss()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
val bundle = Bundle()
|
||||
mListener?.cancelPassword(bundle)
|
||||
|
||||
@@ -162,8 +168,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
try {
|
||||
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
||||
|
||||
val generator = PasswordGenerator(resources)
|
||||
password = generator.generatePassword(length,
|
||||
password = PasswordGenerator(resources).generatePassword(length,
|
||||
uppercaseBox?.isChecked == true,
|
||||
lowercaseBox?.isChecked == true,
|
||||
digitsBox?.isChecked == true,
|
||||
@@ -174,9 +179,9 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
bracketsBox?.isChecked == true,
|
||||
extendedBox?.isChecked == true)
|
||||
} 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) {
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
|
||||
passwordInputLayoutView?.error = e.message
|
||||
}
|
||||
|
||||
return password
|
||||
|
||||
@@ -23,9 +23,9 @@ import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.TextInputLayout
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
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)
|
||||
// Verify that the host activity implements the callback interface
|
||||
try {
|
||||
// Instantiate the NoticeDialogListener so we can send events to the host
|
||||
editGroupListener = context as EditGroupListener?
|
||||
editGroupListener = context as EditGroupListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context?.toString()
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
editGroupListener?.cancelEditGroup(
|
||||
editGroupDialogAction,
|
||||
nameTextView?.text?.toString(),
|
||||
|
||||
@@ -24,9 +24,9 @@ import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v4.widget.ImageViewCompat
|
||||
import android.support.v7.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -45,13 +45,13 @@ class IconPickerDialogFragment : DialogFragment() {
|
||||
private var iconPickerListener: IconPickerListener? = null
|
||||
private var iconPack: IconPack? = null
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
iconPickerListener = context as IconPickerListener?
|
||||
iconPickerListener = context as IconPickerListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context!!.toString()
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + IconPickerListener::class.java.name)
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
builder.setNegativeButton(R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog.cancel() }
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@ import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.utils.Util
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class KeyboardExplanationDialogFragment : DialogFragment() {
|
||||
|
||||
@@ -40,16 +40,14 @@ class KeyboardExplanationDialogFragment : DialogFragment() {
|
||||
|
||||
val rootView = inflater.inflate(R.layout.fragment_keyboard_explanation, null)
|
||||
|
||||
rootView.findViewById<View>(R.id.keyboards_activate_setting_path1_text)
|
||||
.setOnClickListener { launchActivateKeyboardSetting() }
|
||||
rootView.findViewById<View>(R.id.keyboards_activate_setting_path2_text)
|
||||
rootView.findViewById<View>(R.id.keyboards_activate_device_setting_button)
|
||||
.setOnClickListener { launchActivateKeyboardSetting() }
|
||||
|
||||
val containerKeyboardSwitcher = rootView.findViewById<View>(R.id.container_keyboard_switcher)
|
||||
if (BuildConfig.CLOSED_STORE) {
|
||||
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
|
||||
containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
|
||||
} 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)
|
||||
|
||||
@@ -23,7 +23,7 @@ import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
@@ -35,7 +35,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
||||
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
|
||||
builder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
@@ -20,17 +20,15 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.text.Html
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.widget.Toast
|
||||
|
||||
import 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.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.
|
||||
@@ -44,25 +42,16 @@ class ProFeatureDialogFragment : DialogFragment() {
|
||||
|
||||
val stringBuilder = SpannableStringBuilder()
|
||||
if (BuildConfig.CLOSED_STORE) {
|
||||
// TODO HtmlCompat with androidX
|
||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_ad_free))).append("\n\n")
|
||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_buy_pro)))
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||
try {
|
||||
Util.gotoUrl(context!!, R.string.app_pro_url)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
|
||||
}
|
||||
} else {
|
||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_feature_generosity))).append("\n\n")
|
||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_donation)))
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||
try {
|
||||
Util.gotoUrl(context!!, R.string.contribution_url)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
UriUtil.gotoUrl(context!!, R.string.contribution_url)
|
||||
}
|
||||
}
|
||||
builder.setMessage(stringBuilder)
|
||||
|
||||
@@ -19,33 +19,38 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.app.Dialog
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class ReadOnlyDialog(context: Context) : AlertDialog(context) {
|
||||
class ReadOnlyDialog : DialogFragment() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle) {
|
||||
val ctx = context
|
||||
var warning = ctx.getString(R.string.read_only_warning)
|
||||
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)
|
||||
|
||||
var warning = getString(R.string.read_only_warning)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
warning = warning + "\n\n" + context.getString(R.string.read_only_kitkat_warning)
|
||||
warning = warning + "\n\n" + getString(R.string.read_only_kitkat_warning)
|
||||
}
|
||||
setMessage(warning)
|
||||
builder.setMessage(warning)
|
||||
|
||||
setButton(BUTTON_POSITIVE, ctx.getText(android.R.string.ok)) { _, _ -> dismiss() }
|
||||
setButton(BUTTON_NEGATIVE, ctx.getText(R.string.beta_dontask)) { _, _ ->
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
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(ctx.getString(R.string.show_read_only_warning), false)
|
||||
edit.putBoolean(getString(R.string.show_read_only_warning), false)
|
||||
edit.apply()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ package com.kunzisoft.keepass.activities.dialogs
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.IdRes
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.RadioGroup
|
||||
@@ -35,33 +35,30 @@ class SortDialogFragment : DialogFragment() {
|
||||
|
||||
private var mListener: SortSelectionListener? = null
|
||||
|
||||
private var mSortNodeEnum: SortNodeEnum? = null
|
||||
private var mSortNodeEnum: SortNodeEnum = SortNodeEnum.DB
|
||||
@IdRes
|
||||
private var mCheckedId: Int = 0
|
||||
private var mGroupsBefore: Boolean = false
|
||||
private var mAscending: Boolean = false
|
||||
private var mRecycleBinBottom: Boolean = false
|
||||
private var mGroupsBefore: Boolean = true
|
||||
private var mAscending: 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)
|
||||
try {
|
||||
mListener = context as SortSelectionListener?
|
||||
mListener = context as SortSelectionListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context!!.toString()
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + SortSelectionListener::class.java.name)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
mSortNodeEnum = SortNodeEnum.TITLE
|
||||
mAscending = true
|
||||
mGroupsBefore = true
|
||||
var recycleBinAllowed = false
|
||||
mRecycleBinBottom = true
|
||||
|
||||
arguments?.apply {
|
||||
if (containsKey(SORT_NODE_ENUM_BUNDLE_KEY))
|
||||
@@ -78,15 +75,15 @@ class SortDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
mCheckedId = retrieveViewFromEnum(mSortNodeEnum!!)
|
||||
mCheckedId = retrieveViewFromEnum(mSortNodeEnum)
|
||||
|
||||
val rootView = activity.layoutInflater.inflate(R.layout.fragment_sort_selection, null)
|
||||
builder.setTitle(R.string.sort_menu)
|
||||
builder.setView(rootView)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok
|
||||
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum!!, mAscending, mGroupsBefore, mRecycleBinBottom) }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
|
||||
// Check if is ascending or descending
|
||||
@@ -98,25 +95,35 @@ class SortDialogFragment : DialogFragment() {
|
||||
groupsBeforeView.isChecked = mGroupsBefore
|
||||
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) {
|
||||
recycleBinBottomView.visibility = View.GONE
|
||||
recycleBinBottomView?.visibility = View.GONE
|
||||
} else {
|
||||
// Check if recycle bin at the bottom
|
||||
recycleBinBottomView.isChecked = mRecycleBinBottom
|
||||
recycleBinBottomView.setOnCheckedChangeListener { _, isChecked -> mRecycleBinBottom = isChecked }
|
||||
recycleBinBottomView?.isChecked = mRecycleBinBottom
|
||||
recycleBinBottomView?.setOnCheckedChangeListener { _, isChecked -> mRecycleBinBottom = isChecked }
|
||||
|
||||
disableRecycleBinBottomOptionIfNaturalOrder()
|
||||
}
|
||||
|
||||
val sortSelectionRadioGroupView = rootView.findViewById<RadioGroup>(R.id.sort_selection_radio_group)
|
||||
// Check value by default
|
||||
sortSelectionRadioGroupView.check(mCheckedId)
|
||||
sortSelectionRadioGroupView.setOnCheckedChangeListener { _, checkedId -> mSortNodeEnum = retrieveSortEnumFromViewId(checkedId) }
|
||||
sortSelectionRadioGroupView.setOnCheckedChangeListener { _, checkedId ->
|
||||
mSortNodeEnum = retrieveSortEnumFromViewId(checkedId)
|
||||
disableRecycleBinBottomOptionIfNaturalOrder()
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun disableRecycleBinBottomOptionIfNaturalOrder() {
|
||||
// Disable recycle bin if natural order
|
||||
recycleBinBottomView?.isEnabled = mSortNodeEnum != SortNodeEnum.DB
|
||||
}
|
||||
|
||||
@IdRes
|
||||
private fun retrieveViewFromEnum(sortNodeEnum: SortNodeEnum): Int {
|
||||
return when (sortNodeEnum) {
|
||||
|
||||
@@ -22,12 +22,13 @@ package com.kunzisoft.keepass.activities.dialogs
|
||||
import android.app.Dialog
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.text.Html
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.widget.TextView
|
||||
import androidx.core.text.HtmlCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class UnavailableFeatureDialogFragment : DialogFragment() {
|
||||
@@ -53,7 +54,7 @@ class UnavailableFeatureDialogFragment : DialogFragment() {
|
||||
androidNameFromApiNumber(Build.VERSION.SDK_INT, Build.VERSION.RELEASE),
|
||||
androidNameFromApiNumber(minVersionRequired)))
|
||||
message.append("\n\n")
|
||||
.append(Html.fromHtml("<a href=\"https://source.android.com/setup/build-numbers\">CodeNames</a>"))
|
||||
.append(HtmlCompat.fromHtml("<a href=\"https://source.android.com/setup/build-numbers\">CodeNames</a>", HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
} else
|
||||
message.append(getString(R.string.unavailable_feature_hardware))
|
||||
|
||||
|
||||
@@ -20,17 +20,14 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.text.Html
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.widget.Toast
|
||||
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
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.
|
||||
@@ -45,34 +42,26 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
||||
val stringBuilder = SpannableStringBuilder()
|
||||
if (BuildConfig.CLOSED_STORE) {
|
||||
if (BuildConfig.FULL_VERSION) {
|
||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature_thanks))).append("\n\n")
|
||||
.append(Html.fromHtml(getString(R.string.html_rose))).append("\n\n")
|
||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_work_hard))).append("\n")
|
||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_upgrade))).append(" ")
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_work_hard), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
||||
} else {
|
||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
|
||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_buy_pro))).append("\n")
|
||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||
try {
|
||||
Util.gotoUrl(context!!, R.string.app_pro_url)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
}
|
||||
} else {
|
||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
|
||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_contibute))).append(" ")
|
||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||
try {
|
||||
Util.gotoUrl(context!!, R.string.contribution_url)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
UriUtil.gotoUrl(context!!, R.string.contribution_url)
|
||||
}
|
||||
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
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
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 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) {
|
||||
intent.putExtra(EXTRA_ENTRY_SELECTION_MODE, true)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
@@ -26,21 +27,20 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentActivity
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
||||
import com.kunzisoft.keepass.fileselect.StorageAF
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class KeyFileHelper {
|
||||
class OpenFileHelper {
|
||||
|
||||
private var activity: Activity? = null
|
||||
private var fragment: Fragment? = null
|
||||
|
||||
val openFileOnClickViewListener: OpenFileOnClickViewListener
|
||||
get() = OpenFileOnClickViewListener(null)
|
||||
get() = OpenFileOnClickViewListener()
|
||||
|
||||
constructor(context: Activity) {
|
||||
this.activity = context
|
||||
@@ -52,69 +52,55 @@ class KeyFileHelper {
|
||||
this.fragment = context
|
||||
}
|
||||
|
||||
inner class OpenFileOnClickViewListener(private val dataUri: (() -> Uri)?) : View.OnClickListener {
|
||||
inner class OpenFileOnClickViewListener : View.OnClickListener {
|
||||
|
||||
override fun onClick(v: View) {
|
||||
try {
|
||||
if (activity != null && StorageAF.useStorageFramework(activity!!)) {
|
||||
try {
|
||||
openActivityWithActionOpenDocument()
|
||||
} else {
|
||||
} catch(e: Exception) {
|
||||
openActivityWithActionGetContent()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Enable to start the file picker activity", e)
|
||||
|
||||
// Open File picker if can't open activity
|
||||
if (lookForOpenIntentsFilePicker(dataUri?.invoke()))
|
||||
// Open browser dialog
|
||||
if (lookForOpenIntentsFilePicker())
|
||||
showBrowserDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun openActivityWithActionOpenDocument() {
|
||||
val i = Intent(StorageAF.ACTION_OPEN_DOCUMENT)
|
||||
i.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
i.type = "*/*"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
} else {
|
||||
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(i, OPEN_DOC)
|
||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
else
|
||||
activity?.startActivityForResult(i, OPEN_DOC)
|
||||
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
}
|
||||
|
||||
private fun openActivityWithActionGetContent() {
|
||||
val i = Intent(Intent.ACTION_GET_CONTENT)
|
||||
i.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
i.type = "*/*"
|
||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(i, GET_CONTENT)
|
||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
else
|
||||
activity?.startActivityForResult(i, GET_CONTENT)
|
||||
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
}
|
||||
|
||||
fun getOpenFileOnClickViewListener(dataUri: () -> Uri): OpenFileOnClickViewListener {
|
||||
return OpenFileOnClickViewListener(dataUri)
|
||||
}
|
||||
|
||||
private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean {
|
||||
private fun lookForOpenIntentsFilePicker(): Boolean {
|
||||
var showBrowser = false
|
||||
try {
|
||||
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
||||
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
|
||||
// Get file path parent if possible
|
||||
if (dataUri != null
|
||||
&& dataUri.toString().isNotEmpty()
|
||||
&& dataUri.scheme == "file") {
|
||||
intent.data = dataUri
|
||||
} else {
|
||||
Log.w(javaClass.name, "Unable to read the URI")
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intent, FILE_BROWSE)
|
||||
else
|
||||
@@ -158,7 +144,7 @@ class KeyFileHelper {
|
||||
val browserDialogFragment = BrowserDialogFragment()
|
||||
if (fragment != null && fragment!!.fragmentManager != null)
|
||||
browserDialogFragment.show(fragment!!.fragmentManager!!, "browserDialog")
|
||||
else if (activity!!.fragmentManager != null)
|
||||
else
|
||||
browserDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Can't open BrowserDialog", e)
|
||||
@@ -182,7 +168,7 @@ class KeyFileHelper {
|
||||
val filename = data?.dataString
|
||||
var keyUri: Uri? = null
|
||||
if (filename != null) {
|
||||
keyUri = UriUtil.parseUriFile(filename)
|
||||
keyUri = UriUtil.parse(filename)
|
||||
}
|
||||
keyFileCallback?.invoke(keyUri)
|
||||
}
|
||||
@@ -191,24 +177,19 @@ class KeyFileHelper {
|
||||
GET_CONTENT, OPEN_DOC -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (data != null) {
|
||||
var uri = data.data
|
||||
val uri = data.data
|
||||
if (uri != null) {
|
||||
if (StorageAF.useStorageFramework(activity!!)) {
|
||||
try {
|
||||
// try to persist read and write permissions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
activity?.contentResolver?.apply {
|
||||
takePersistableUriPermission(uri!!, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
takePersistableUriPermission(uri!!, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
if (requestCode == GET_CONTENT) {
|
||||
uri = UriUtil.translateUri(activity!!, uri)
|
||||
}
|
||||
keyFileCallback?.invoke(uri)
|
||||
}
|
||||
}
|
||||
@@ -221,7 +202,13 @@ class KeyFileHelper {
|
||||
|
||||
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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
protected var timeoutEnable: Boolean = true
|
||||
protected var mTimeoutEnable: Boolean = true
|
||||
|
||||
private var lockReceiver: LockReceiver? = null
|
||||
private var exitLock: Boolean = false
|
||||
private var mLockReceiver: LockReceiver? = null
|
||||
private var mExitLock: Boolean = false
|
||||
|
||||
// Force readOnly if Entry Selection mode
|
||||
protected var readOnly: Boolean = false
|
||||
protected var mReadOnly: Boolean = false
|
||||
get() {
|
||||
return field || selectionMode
|
||||
return field || mSelectionMode
|
||||
}
|
||||
protected var selectionMode: Boolean = false
|
||||
protected var mSelectionMode: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)) {
|
||||
timeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
|
||||
mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
|
||||
} else {
|
||||
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) {
|
||||
lockReceiver = LockReceiver()
|
||||
if (mTimeoutEnable) {
|
||||
mLockReceiver = LockReceiver()
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
addAction(LOCK_ACTION)
|
||||
}
|
||||
registerReceiver(lockReceiver, intentFilter)
|
||||
registerReceiver(mLockReceiver, intentFilter)
|
||||
}
|
||||
|
||||
exitLock = false
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
|
||||
mExitLock = false
|
||||
mReadOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (resultCode == RESULT_EXIT_LOCK) {
|
||||
exitLock = true
|
||||
mExitLock = true
|
||||
if (Database.getInstance().loaded) {
|
||||
lockAndExit()
|
||||
}
|
||||
@@ -101,9 +101,9 @@ abstract class LockingActivity : StylishActivity() {
|
||||
super.onResume()
|
||||
|
||||
// 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
|
||||
if (!Database.getInstance().loaded) {
|
||||
finish()
|
||||
@@ -115,7 +115,7 @@ abstract class LockingActivity : StylishActivity() {
|
||||
// If the time is out -> close the Activity
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||
// If onCreate already record time
|
||||
if (!exitLock)
|
||||
if (!mExitLock)
|
||||
TimeoutHelper.recordTime(this)
|
||||
}
|
||||
|
||||
@@ -123,15 +123,15 @@ abstract class LockingActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||
outState.putBoolean(TIMEOUT_ENABLE_KEY, timeoutEnable)
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, mReadOnly)
|
||||
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
if (timeoutEnable) {
|
||||
if (mTimeoutEnable) {
|
||||
// If the time is out during our navigation in activity -> close the Activity
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||
}
|
||||
@@ -139,8 +139,8 @@ abstract class LockingActivity : StylishActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (lockReceiver != null)
|
||||
unregisterReceiver(lockReceiver)
|
||||
if (mLockReceiver != null)
|
||||
unregisterReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
inner class LockReceiver : BroadcastReceiver() {
|
||||
@@ -184,7 +184,7 @@ abstract class LockingActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (timeoutEnable) {
|
||||
if (mTimeoutEnable) {
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.support.annotation.StyleRes
|
||||
import android.support.v7.preference.PreferenceManager
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -68,15 +68,4 @@ object Stylish {
|
||||
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
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.StyleRes
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.util.Log
|
||||
|
||||
abstract class StylishActivity : AppCompatActivity() {
|
||||
|
||||
@@ -23,9 +23,9 @@ import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.StyleRes
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.view.ContextThemeWrapper
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -36,11 +36,9 @@ abstract class StylishFragment : Fragment() {
|
||||
protected var themeId: Int = 0
|
||||
protected var contextThemed: Context? = null
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
if (context != null) {
|
||||
this.themeId = Stylish.getThemeId(context)
|
||||
}
|
||||
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.support.v7.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -20,24 +20,31 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.support.annotation.ColorInt
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.ViewSwitcher
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryEntity
|
||||
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>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var fileItemOpenListener: FileItemOpenListener? = null
|
||||
private var fileSelectClearListener: FileSelectClearListener? = null
|
||||
private var fileInformationShowListener: FileInformationShowListener? = null
|
||||
private var fileItemOpenListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
||||
private var fileSelectClearListener: ((FileDatabaseHistoryEntity)->Boolean)? = null
|
||||
private var saveAliasListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
||||
|
||||
private val listDatabaseFiles = ArrayList<FileDatabaseHistoryEntity>()
|
||||
|
||||
private var mExpandedPosition = -1
|
||||
private var mPreviousExpandedPosition = -1
|
||||
|
||||
@ColorInt
|
||||
private val defaultColor: Int
|
||||
@@ -45,7 +52,6 @@ class FileDatabaseHistoryAdapter(private val context: Context, private val listF
|
||||
private val warningColor: Int
|
||||
|
||||
init {
|
||||
|
||||
val typedValue = TypedValue()
|
||||
val theme = context.theme
|
||||
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) {
|
||||
val fileDatabaseModel = FileDatabaseModel(context, listFiles[position])
|
||||
// Context menu creation
|
||||
holder.fileContainer.setOnCreateContextMenuListener(ContextMenuBuilder(fileDatabaseModel))
|
||||
// Get info from position
|
||||
val fileHistoryEntity = listDatabaseFiles[position]
|
||||
val fileDatabaseInfo = FileDatabaseInfo(context, fileHistoryEntity.databaseUri)
|
||||
|
||||
// Click item to open file
|
||||
if (fileItemOpenListener != null)
|
||||
holder.fileContainer.setOnClickListener(FileItemClickListener(position))
|
||||
// Assign file name
|
||||
if (PreferencesUtil.isFullFilePathEnable(context))
|
||||
holder.fileName.text = Uri.decode(fileDatabaseModel.fileUri.toString())
|
||||
else
|
||||
holder.fileName.text = fileDatabaseModel.fileName
|
||||
holder.fileName.textSize = PreferencesUtil.getListTextSize(context)
|
||||
holder.fileContainer.setOnClickListener {
|
||||
fileItemOpenListener?.invoke(fileHistoryEntity)
|
||||
}
|
||||
|
||||
// File alias
|
||||
holder.fileAlias.text = fileDatabaseInfo.retrieveDatabaseAlias(fileHistoryEntity.databaseAlias)
|
||||
|
||||
// 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
|
||||
if (fileInformationShowListener != null)
|
||||
holder.fileInformation.setOnClickListener(FileInformationClickListener(fileDatabaseModel))
|
||||
val isExpanded = position == mExpandedPosition
|
||||
//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 {
|
||||
return listFiles.size
|
||||
return listDatabaseFiles.size
|
||||
}
|
||||
|
||||
fun setOnItemClickListener(fileItemOpenListener: FileItemOpenListener) {
|
||||
this.fileItemOpenListener = fileItemOpenListener
|
||||
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
|
||||
listDatabaseFiles.clear()
|
||||
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
||||
}
|
||||
|
||||
fun setFileSelectClearListener(fileSelectClearListener: FileSelectClearListener) {
|
||||
this.fileSelectClearListener = fileSelectClearListener
|
||||
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: FileDatabaseHistoryEntity) {
|
||||
listDatabaseFiles.remove(fileDatabaseHistoryToDelete)
|
||||
}
|
||||
|
||||
fun setFileInformationShowListener(fileInformationShowListener: FileInformationShowListener) {
|
||||
this.fileInformationShowListener = fileInformationShowListener
|
||||
fun setOnFileDatabaseHistoryOpenListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
||||
this.fileItemOpenListener = listener
|
||||
}
|
||||
|
||||
interface FileItemOpenListener {
|
||||
fun onFileItemOpenListener(itemPosition: Int)
|
||||
fun setOnFileDatabaseHistoryDeleteListener(listener : ((FileDatabaseHistoryEntity)->Boolean)?) {
|
||||
this.fileSelectClearListener = listener
|
||||
}
|
||||
|
||||
interface FileSelectClearListener {
|
||||
fun onFileSelectClearListener(fileDatabaseModel: FileDatabaseModel): Boolean
|
||||
}
|
||||
|
||||
interface FileInformationShowListener {
|
||||
fun onClickFileInformation(fileDatabaseModel: FileDatabaseModel)
|
||||
}
|
||||
|
||||
private inner class FileItemClickListener(private val position: Int) : View.OnClickListener {
|
||||
|
||||
override fun onClick(v: View) {
|
||||
fileItemOpenListener?.onFileItemOpenListener(position)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FileInformationClickListener(private val fileDatabaseModel: FileDatabaseModel) : View.OnClickListener {
|
||||
|
||||
override fun onClick(view: View) {
|
||||
fileInformationShowListener?.onClickFileInformation(fileDatabaseModel)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ContextMenuBuilder(private val fileDatabaseModel: FileDatabaseModel) : View.OnCreateContextMenuListener {
|
||||
|
||||
private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item ->
|
||||
if (fileSelectClearListener == null)
|
||||
return@OnMenuItemClickListener false
|
||||
when (item.itemId) {
|
||||
MENU_CLEAR -> fileSelectClearListener!!.onFileSelectClearListener(fileDatabaseModel)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(contextMenu: ContextMenu?, view: View?, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
|
||||
contextMenu?.add(Menu.NONE, MENU_CLEAR, Menu.NONE, R.string.remove_from_filelist)
|
||||
?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
}
|
||||
fun setOnSaveAliasListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
||||
this.saveAliasListener = listener
|
||||
}
|
||||
|
||||
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var fileContainer: View = itemView.findViewById(R.id.file_container)
|
||||
var fileName: TextView = itemView.findViewById(R.id.file_filename)
|
||||
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
|
||||
|
||||
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
|
||||
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,44 +21,52 @@ package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.support.v7.util.SortedList
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.support.v7.widget.util.SortedListAdapterCallback
|
||||
import android.graphics.Paint
|
||||
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.TextView
|
||||
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.app.App
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
|
||||
class NodeAdapter
|
||||
/**
|
||||
* Create node list adapter with contextMenu or not
|
||||
* @param context Context to use
|
||||
*/
|
||||
(private val context: Context, private val menuInflater: MenuInflater)
|
||||
(private val context: Context)
|
||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||
|
||||
private val nodeSortedList: SortedList<NodeVersioned>
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var textSize: Float = 0.toFloat()
|
||||
private var subtextSize: Float = 0.toFloat()
|
||||
private var iconSize: Float = 0.toFloat()
|
||||
private var listSort: SortNodeEnum? = null
|
||||
private var groupsBeforeSort: Boolean = false
|
||||
private var ascendingSort: Boolean = false
|
||||
private var showUserNames: Boolean = false
|
||||
|
||||
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
|
||||
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||
private var prefTextSize: Float = 0F
|
||||
private var subtextSize: Float = 0F
|
||||
private var infoTextSize: Float = 0F
|
||||
private var numberChildrenTextSize: Float = 0F
|
||||
private var iconSize: Float = 0F
|
||||
private var listSort: SortNodeEnum = SortNodeEnum.DB
|
||||
private var ascendingSort: Boolean = true
|
||||
private var groupsBeforeSort: Boolean = true
|
||||
private var recycleBinBottomSort: Boolean = true
|
||||
private var showUserNames: Boolean = true
|
||||
private var showNumberEntries: Boolean = true
|
||||
|
||||
private var actionNodesList = LinkedList<NodeVersioned>()
|
||||
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
|
||||
|
||||
@@ -74,17 +82,16 @@ class NodeAdapter
|
||||
|
||||
init {
|
||||
assignPreferences()
|
||||
this.activateContextMenu = false
|
||||
this.readOnly = false
|
||||
this.isASearchResult = false
|
||||
|
||||
this.nodeSortedList = SortedList(NodeVersioned::class.java, object : SortedListAdapterCallback<NodeVersioned>(this) {
|
||||
override fun compare(item1: NodeVersioned, item2: NodeVersioned): Int {
|
||||
return listSort?.getNodeComparator(ascendingSort, groupsBeforeSort)?.compare(item1, item2) ?: 0
|
||||
return listSort.getNodeComparator(ascendingSort, groupsBeforeSort, recycleBinBottomSort).compare(item1, item2)
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -105,29 +112,22 @@ class NodeAdapter
|
||||
taTextColor.recycle()
|
||||
}
|
||||
|
||||
fun setReadOnly(readOnly: Boolean) {
|
||||
this.readOnly = readOnly
|
||||
}
|
||||
|
||||
fun setIsASearchResult(isASearchResult: Boolean) {
|
||||
this.isASearchResult = isASearchResult
|
||||
}
|
||||
|
||||
fun setActivateContextMenu(activate: Boolean) {
|
||||
this.activateContextMenu = activate
|
||||
}
|
||||
|
||||
private fun assignPreferences() {
|
||||
val textSizeDefault = java.lang.Float.parseFloat(context.getString(R.string.list_size_default))
|
||||
this.textSize = PreferencesUtil.getListTextSize(context)
|
||||
this.subtextSize = context.resources.getInteger(R.integer.list_small_size_default) * textSize / textSizeDefault
|
||||
// Retrieve the icon size
|
||||
val iconDefaultSize = context.resources.getDimension(R.dimen.list_icon_size_default)
|
||||
this.iconSize = iconDefaultSize * textSize / textSizeDefault
|
||||
this.prefTextSize = PreferencesUtil.getListTextSize(context)
|
||||
this.infoTextSize = context.resources.getDimension(R.dimen.list_medium_size_default) * prefTextSize
|
||||
this.subtextSize = context.resources.getDimension(R.dimen.list_small_size_default) * prefTextSize
|
||||
this.numberChildrenTextSize = context.resources.getDimension(R.dimen.list_tiny_size_default) * prefTextSize
|
||||
this.iconSize = context.resources.getDimension(R.dimen.list_icon_size_default) * prefTextSize
|
||||
|
||||
this.listSort = PreferencesUtil.getListSort(context)
|
||||
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
|
||||
this.ascendingSort = PreferencesUtil.getAscendingSort(context)
|
||||
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
|
||||
this.recycleBinBottomSort = PreferencesUtil.getRecycleBinBottomSort(context)
|
||||
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||
|
||||
// Reinit textSize for all view type
|
||||
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,14 +136,17 @@ class NodeAdapter
|
||||
fun rebuildList(group: GroupVersioned) {
|
||||
this.nodeSortedList.clear()
|
||||
assignPreferences()
|
||||
// TODO verify sort
|
||||
try {
|
||||
this.nodeSortedList.addAll(group.getChildrenWithoutMetaStream())
|
||||
this.nodeSortedList.addAll(group.getChildren())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Can't add node elements to the list", e)
|
||||
Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun contains(node: NodeVersioned): Boolean {
|
||||
return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,6 +157,14 @@ class NodeAdapter
|
||||
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
|
||||
* @param node Node to delete
|
||||
@@ -162,6 +173,37 @@ class NodeAdapter
|
||||
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
|
||||
* @param oldNode Node before the update
|
||||
@@ -174,6 +216,40 @@ class NodeAdapter
|
||||
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
|
||||
*/
|
||||
@@ -203,20 +279,41 @@ class NodeAdapter
|
||||
Type.GROUP -> iconGroupColor
|
||||
Type.ENTRY -> iconEntryColor
|
||||
}
|
||||
holder.icon.assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||
holder.icon.apply {
|
||||
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||
// Relative size of the icon
|
||||
layoutParams?.apply {
|
||||
height = iconSize.toInt()
|
||||
width = iconSize.toInt()
|
||||
}
|
||||
}
|
||||
// Assign text
|
||||
holder.text.text = subNode.title
|
||||
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) }
|
||||
// Context menu
|
||||
if (activateContextMenu) {
|
||||
holder.container.setOnCreateContextMenuListener(
|
||||
ContextMenuBuilder(menuInflater, subNode, readOnly, isASearchResult, nodeMenuListener))
|
||||
holder.container.setOnClickListener {
|
||||
nodeClickCallback?.onNodeClick(subNode)
|
||||
}
|
||||
holder.container.setOnLongClickListener {
|
||||
nodeClickCallback?.onNodeLongClick(subNode) ?: false
|
||||
}
|
||||
|
||||
// Add username
|
||||
holder.subText.text = ""
|
||||
holder.subText.visibility = View.GONE
|
||||
holder.container.isSelected = actionNodesList.contains(subNode)
|
||||
|
||||
// Add subText with username
|
||||
holder.subText.apply {
|
||||
text = ""
|
||||
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
|
||||
|
||||
@@ -226,19 +323,27 @@ class NodeAdapter
|
||||
|
||||
val username = entry.username
|
||||
if (showUserNames && username.isNotEmpty()) {
|
||||
holder.subText.visibility = View.VISIBLE
|
||||
holder.subText.text = username
|
||||
visibility = View.VISIBLE
|
||||
text = username
|
||||
setTextSize(textSizeUnit, subtextSize)
|
||||
}
|
||||
|
||||
mDatabase.stopManageEntry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign image and text size
|
||||
// Relative size of the icon
|
||||
holder.icon.layoutParams?.height = iconSize.toInt()
|
||||
holder.icon.layoutParams?.width = iconSize.toInt()
|
||||
holder.text.textSize = textSize
|
||||
holder.subText.textSize = subtextSize
|
||||
// Add number of entries in groups
|
||||
if (subNode.type == Type.GROUP) {
|
||||
if (showNumberEntries) {
|
||||
holder.numberChildren?.apply {
|
||||
text = (subNode as GroupVersioned).getChildEntries(true).size.toString()
|
||||
setTextSize(textSizeUnit, numberChildrenTextSize)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
holder.numberChildren?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
@@ -252,103 +357,12 @@ class NodeAdapter
|
||||
this.nodeClickCallback = nodeClickCallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a listener when an element of menu is clicked
|
||||
*/
|
||||
fun setNodeMenuListener(nodeMenuListener: NodeMenuListener?) {
|
||||
this.nodeMenuListener = nodeMenuListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
interface NodeClickCallback {
|
||||
fun onNodeClick(node: NodeVersioned)
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu listener to redefine to do an action in menu
|
||||
*/
|
||||
interface NodeMenuListener {
|
||||
fun onOpenMenuClick(node: NodeVersioned): Boolean
|
||||
fun onEditMenuClick(node: NodeVersioned): Boolean
|
||||
fun onCopyMenuClick(node: NodeVersioned): Boolean
|
||||
fun onMoveMenuClick(node: NodeVersioned): Boolean
|
||||
fun onDeleteMenuClick(node: NodeVersioned): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class for menu listener
|
||||
*/
|
||||
private class ContextMenuBuilder(val menuInflater: MenuInflater,
|
||||
val node: NodeVersioned,
|
||||
val readOnly: Boolean,
|
||||
val isASearchResult: Boolean,
|
||||
val menuListener: NodeMenuListener?)
|
||||
: View.OnCreateContextMenuListener {
|
||||
|
||||
private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item ->
|
||||
if (menuListener == null)
|
||||
return@OnMenuItemClickListener false
|
||||
when (item.itemId) {
|
||||
R.id.menu_open -> menuListener.onOpenMenuClick(node)
|
||||
R.id.menu_edit -> menuListener.onEditMenuClick(node)
|
||||
R.id.menu_copy -> menuListener.onCopyMenuClick(node)
|
||||
R.id.menu_move -> menuListener.onMoveMenuClick(node)
|
||||
R.id.menu_delete -> menuListener.onDeleteMenuClick(node)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(contextMenu: ContextMenu?,
|
||||
view: View?,
|
||||
contextMenuInfo: ContextMenu.ContextMenuInfo?) {
|
||||
menuInflater.inflate(R.menu.node_menu, contextMenu)
|
||||
|
||||
// Opening
|
||||
var menuItem = contextMenu?.findItem(R.id.menu_open)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
|
||||
val database = Database.getInstance()
|
||||
|
||||
// Edition
|
||||
if (readOnly || node == database.recycleBin) {
|
||||
contextMenu?.removeItem(R.id.menu_edit)
|
||||
} else {
|
||||
menuItem = contextMenu?.findItem(R.id.menu_edit)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
}
|
||||
|
||||
// Copy (not for group)
|
||||
if (readOnly
|
||||
|| isASearchResult
|
||||
|| node == database.recycleBin
|
||||
|| node.type == Type.GROUP) {
|
||||
// TODO COPY For Group
|
||||
contextMenu?.removeItem(R.id.menu_copy)
|
||||
} else {
|
||||
menuItem = contextMenu?.findItem(R.id.menu_copy)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
}
|
||||
|
||||
// Move
|
||||
if (readOnly
|
||||
|| isASearchResult
|
||||
|| node == database.recycleBin) {
|
||||
contextMenu?.removeItem(R.id.menu_move)
|
||||
} else {
|
||||
menuItem = contextMenu?.findItem(R.id.menu_move)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
}
|
||||
|
||||
// Deletion
|
||||
if (readOnly || node == database.recycleBin) {
|
||||
contextMenu?.removeItem(R.id.menu_delete)
|
||||
} else {
|
||||
menuItem = contextMenu?.findItem(R.id.menu_delete)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
}
|
||||
}
|
||||
fun onNodeLongClick(node: NodeVersioned): Boolean
|
||||
}
|
||||
|
||||
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
@@ -356,6 +370,7 @@ class NodeAdapter
|
||||
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.adapters
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.graphics.Color
|
||||
import android.support.v4.widget.CursorAdapter
|
||||
import androidx.cursoradapter.widget.CursorAdapter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -38,7 +38,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
|
||||
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(
|
||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
*/
|
||||
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.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.service.autofill.Dataset
|
||||
import android.service.autofill.FillResponse
|
||||
import android.support.annotation.RequiresApi
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.util.Log
|
||||
import android.view.autofill.AutofillManager
|
||||
import android.view.autofill.AutofillValue
|
||||
|
||||
@@ -26,8 +26,8 @@ import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
@@ -56,6 +56,7 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.autofill
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.service.autofill.*
|
||||
import android.support.annotation.RequiresApi
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.util.Log
|
||||
import android.widget.RemoteViews
|
||||
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