mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
205 Commits
2.5.0.0bet
...
2.5beta29
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fc5e6751b | ||
|
|
4c1630312b | ||
|
|
d397c5c996 | ||
|
|
f6c41b5a60 | ||
|
|
06eb5c01c3 | ||
|
|
7df49f91e8 | ||
|
|
96dcbb0ce7 | ||
|
|
5f828fb986 | ||
|
|
533d663938 | ||
|
|
ae788503a9 | ||
|
|
cf0acd9c73 | ||
|
|
0857f2f1cf | ||
|
|
c05d412bdb | ||
|
|
c8e0ce717d | ||
|
|
cc3485b201 | ||
|
|
81ea7080c2 | ||
|
|
76ff6f5ae0 | ||
|
|
0c0d0b7a6f | ||
|
|
ec8cf1f6b7 | ||
|
|
da44310d1b | ||
|
|
4bd9c84bb0 | ||
|
|
3b1269a770 | ||
|
|
7c986ccee8 | ||
|
|
903bad8f36 | ||
|
|
4b9577437c | ||
|
|
c316011fbc | ||
|
|
3fb1f18c22 | ||
|
|
53935058f5 | ||
|
|
a3860c9581 | ||
|
|
dc20899d26 | ||
|
|
62ac3ddb75 | ||
|
|
b792a61bf9 | ||
|
|
aae9f9e1cb | ||
|
|
d098bf5e6a | ||
|
|
b0e8a3ecd9 | ||
|
|
4efa684022 | ||
|
|
f2c8082990 | ||
|
|
1abba80045 | ||
|
|
68564a2b75 | ||
|
|
385b701b38 | ||
|
|
11c9a1d707 | ||
|
|
c5aef6b561 | ||
|
|
dfcf73cfd0 | ||
|
|
7fc9389700 | ||
|
|
d1af7349bc | ||
|
|
0fd955197d | ||
|
|
cbf33507d1 | ||
|
|
bc60a5d97e | ||
|
|
57596b2991 | ||
|
|
43b3602a52 | ||
|
|
c09ec961b8 | ||
|
|
d140b453b2 | ||
|
|
9eb42636ec | ||
|
|
ee2d663fce | ||
|
|
8e83615a22 | ||
|
|
0ff129c5ca | ||
|
|
e49858439f | ||
|
|
3df07f7f47 | ||
|
|
ff9e179593 | ||
|
|
7539fee04b | ||
|
|
71a339a58f | ||
|
|
9ef2ea016b | ||
|
|
de4936a16d | ||
|
|
574d2b8904 | ||
|
|
1f3f7634e7 | ||
|
|
3c0725baff | ||
|
|
b0e1411012 | ||
|
|
39daf4714d | ||
|
|
4706afa823 | ||
|
|
25977d389d | ||
|
|
f760110569 | ||
|
|
133e78fe97 | ||
|
|
d92e0c8620 | ||
|
|
62fdb69d6b | ||
|
|
def57c9fb2 | ||
|
|
21c9c898c3 | ||
|
|
1f03c922c2 | ||
|
|
3f6ae6bdac | ||
|
|
60615ee1eb | ||
|
|
92b0d1bfa9 | ||
|
|
237988dc1f | ||
|
|
a846ec29ca | ||
|
|
4533e96bff | ||
|
|
0a401c3ac9 | ||
|
|
468abaf077 | ||
|
|
4ccf2f641c | ||
|
|
34eb2785cf | ||
|
|
09dbfe323e | ||
|
|
1f06c5b425 | ||
|
|
b98e089f7a | ||
|
|
a0ad06ed0a | ||
|
|
ec63365429 | ||
|
|
2cb85e4346 | ||
|
|
0d7c479c51 | ||
|
|
5a6c21e662 | ||
|
|
d6cadac98f | ||
|
|
dac2fc2c37 | ||
|
|
0fb45cef0d | ||
|
|
5ebdbd4003 | ||
|
|
b30f1023cb | ||
|
|
e5f65a4d1e | ||
|
|
ab42a65aa4 | ||
|
|
e351456bfe | ||
|
|
452e68b08f | ||
|
|
d65beed7a1 | ||
|
|
f5a5a0e8cb | ||
|
|
98380a0906 | ||
|
|
22fe7508f3 | ||
|
|
c8e241fc76 | ||
|
|
2c943e00d0 | ||
|
|
7ddb83b72d | ||
|
|
50bac01699 | ||
|
|
e0a92dfadd | ||
|
|
b50c951091 | ||
|
|
2f589a95a9 | ||
|
|
53eac86a95 | ||
|
|
9412f8955e | ||
|
|
71c98d82b1 | ||
|
|
42c1a925b4 | ||
|
|
1e84534ffd | ||
|
|
b087733e37 | ||
|
|
675efe29ac | ||
|
|
9833af8225 | ||
|
|
790c624571 | ||
|
|
67a70a8453 | ||
|
|
64bb05e2dd | ||
|
|
c111db6e73 | ||
|
|
24c5915bd3 | ||
|
|
04c3717618 | ||
|
|
78275a0984 | ||
|
|
bd908ed10d | ||
|
|
5473deec95 | ||
|
|
653258afd2 | ||
|
|
00a2210eea | ||
|
|
ec5688a013 | ||
|
|
fc0c7b5708 | ||
|
|
464f7ac486 | ||
|
|
fb2457146c | ||
|
|
d90870838e | ||
|
|
8c45f23291 | ||
|
|
315a3efa52 | ||
|
|
28abb5ae6f | ||
|
|
bb78d89682 | ||
|
|
fd36e19168 | ||
|
|
58db516e44 | ||
|
|
2b875e94dc | ||
|
|
0ba2447f55 | ||
|
|
8e684d0d3a | ||
|
|
ce3c1d4685 | ||
|
|
2cd77d47eb | ||
|
|
ac9366e351 | ||
|
|
af356586f8 | ||
|
|
7650db81a4 | ||
|
|
8925c86afd | ||
|
|
8f1836009e | ||
|
|
5fcf3f9b95 | ||
|
|
1c466d9e40 | ||
|
|
b97c2d9cbc | ||
|
|
3d970e4967 | ||
|
|
765c8f53dd | ||
|
|
4e81caeadf | ||
|
|
08b10e4d58 | ||
|
|
f8594f72e8 | ||
|
|
24ebee07cd | ||
|
|
a06ebb0991 | ||
|
|
cd5d4498e7 | ||
|
|
0bd62780c6 | ||
|
|
896f9327d6 | ||
|
|
cc83a99efe | ||
|
|
b6da20fef7 | ||
|
|
2912678559 | ||
|
|
409e870ef0 | ||
|
|
3d84074a0a | ||
|
|
a717fdfed4 | ||
|
|
be1f68015b | ||
|
|
2c29dcf1f6 | ||
|
|
2b1173177f | ||
|
|
3c0f7dc79c | ||
|
|
39d813bf3a | ||
|
|
5e2bc0d05b | ||
|
|
ec751159ae | ||
|
|
dda8b95f83 | ||
|
|
286012fe2a | ||
|
|
ab27299789 | ||
|
|
dfcdd5aa88 | ||
|
|
a05ea52689 | ||
|
|
de12b5de5b | ||
|
|
57b03eaca4 | ||
|
|
36bd00b760 | ||
|
|
4578a9974a | ||
|
|
d0371f58c6 | ||
|
|
9f80457351 | ||
|
|
56daf6f676 | ||
|
|
7f7b8d423b | ||
|
|
031afc80cb | ||
|
|
b77e28b04d | ||
|
|
2f8c3fdcfe | ||
|
|
f501a87099 | ||
|
|
d0ec5f26dd | ||
|
|
97776e9329 | ||
|
|
a19356c49e | ||
|
|
4f16918cf0 | ||
|
|
1af9761144 | ||
|
|
d74e814c79 | ||
|
|
16d09bca6c |
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -20,9 +20,6 @@ Steps to reproduce the behavior:
|
||||
**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]
|
||||
|
||||
81
CHANGELOG
81
CHANGELOG
@@ -1,4 +1,25 @@
|
||||
KeepassDX (2.5.0.0beta26)
|
||||
KeePassDX(2.5beta29)
|
||||
* Upgrade autofill algorithm
|
||||
* Delete registered KeyFile after save new credentials
|
||||
* Fix title and username entry view refresh after an update
|
||||
* Fix database lock request (open notification always active)
|
||||
* Allow empty title in entries
|
||||
* Add expiration datetime
|
||||
|
||||
KeePassDX(2.5beta28)
|
||||
* Fix read only database
|
||||
* Upgrade to Android SDK 29
|
||||
|
||||
KeePassDX (2.5beta27)
|
||||
* New setting to hide broken links
|
||||
* Show URL when title is empty
|
||||
* Setting to open search field at database opening
|
||||
* Fix settings for database locations
|
||||
* Fix error message when database file not writable
|
||||
* Fix appearance refresh settings
|
||||
* Sort optimization
|
||||
|
||||
KeePassDX (2.5.0.0beta26)
|
||||
* Download attachments
|
||||
* Change file size string format
|
||||
* Prevent screenshot for all screen
|
||||
@@ -11,7 +32,7 @@ KeepassDX (2.5.0.0beta26)
|
||||
* Fix dates
|
||||
* Fix UUID message for Database v1
|
||||
|
||||
KeepassDX (2.5.0.0beta25)
|
||||
KeePassDX (2.5.0.0beta25)
|
||||
* Setting for Recycle Bin
|
||||
* Fix Recycle bin issues
|
||||
* Fix TOTP
|
||||
@@ -19,7 +40,7 @@ KeepassDX (2.5.0.0beta25)
|
||||
* Fix update group
|
||||
* Fix OOM
|
||||
|
||||
KeepassDX (2.5.0.0beta24)
|
||||
KeePassDX (2.5.0.0beta24)
|
||||
* Add OTP (HOTP / TOTP)
|
||||
* Add settings (Color, Security, Master Key)
|
||||
* Show history of each entry
|
||||
@@ -29,7 +50,7 @@ KeepassDX (2.5.0.0beta24)
|
||||
* Open/Save database as service / Add persistent notification
|
||||
* Fix settings / edit group / small bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta23)
|
||||
KeePassDX (2.5.0.0beta23)
|
||||
* New, more secure database creation workflow
|
||||
* Recognize more database files
|
||||
* Add alias for history files (WARNING: history is erased)
|
||||
@@ -38,14 +59,14 @@ KeepassDX (2.5.0.0beta23)
|
||||
* Fix OOM with KeyFile
|
||||
* Fix small issues
|
||||
|
||||
KeepassDX (2.5.0.0beta22)
|
||||
KeePassDX (2.5.0.0beta22)
|
||||
* Rebuild code for actions
|
||||
* Add UUID as entry view
|
||||
* Fix bug with natural order
|
||||
* Fix number of entries in databaseV1
|
||||
* New entry views
|
||||
|
||||
KeepassDX (2.5.0.0beta21)
|
||||
KeePassDX (2.5.0.0beta21)
|
||||
* Fix nested groups no longer visible in V1 databases
|
||||
* Improved data import algorithm for V1 databases
|
||||
* Add natural database sort
|
||||
@@ -53,10 +74,10 @@ KeepassDX (2.5.0.0beta21)
|
||||
* Fix button disabled with only KeyFile
|
||||
* Show the number of entries in a group
|
||||
|
||||
KeepassDX (2.5.0.0beta20)
|
||||
KeePassDX (2.5.0.0beta20)
|
||||
* Fix a major bug that displays an entry history
|
||||
|
||||
KeepassDX (2.5.0.0beta19)
|
||||
KeePassDX (2.5.0.0beta19)
|
||||
* Add lock button always visible
|
||||
* New connection workflow
|
||||
* Code refactored in Kotlin
|
||||
@@ -67,7 +88,7 @@ KeepassDX (2.5.0.0beta19)
|
||||
* Fix memory when load database
|
||||
* Fix small bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta18)
|
||||
KeePassDX (2.5.0.0beta18)
|
||||
* New recent databases views
|
||||
* New information dialog
|
||||
* Custom fields for the Magikeyboard
|
||||
@@ -76,10 +97,10 @@ KeepassDX (2.5.0.0beta18)
|
||||
* Fix memory when opening the database
|
||||
* Memory management for attachments
|
||||
|
||||
KeepassDX (2.5.0.0beta17)
|
||||
KeePassDX (2.5.0.0beta17)
|
||||
* Fix font and search
|
||||
|
||||
KeepassDX (2.5.0.0beta16)
|
||||
KeePassDX (2.5.0.0beta16)
|
||||
* New search in a single fragment
|
||||
* Search suggestions
|
||||
* Added the display of usernames
|
||||
@@ -87,20 +108,20 @@ KeepassDX (2.5.0.0beta16)
|
||||
* Fix read-only mode
|
||||
* Fix parcelable / toolbar / back
|
||||
|
||||
KeepassDX (2.5.0.0beta15)
|
||||
KeePassDX (2.5.0.0beta15)
|
||||
* Read only mode
|
||||
* Best group recovery for the navigation fragment
|
||||
* Fix copies in notifications
|
||||
* Fix orientation
|
||||
* Added translations
|
||||
|
||||
KeepassDX (2.5.0.0beta14)
|
||||
KeePassDX (2.5.0.0beta14)
|
||||
* Optimize all the memory with parcelables / fix search
|
||||
|
||||
KeepassDX (2.5.0.0beta13)
|
||||
KeePassDX (2.5.0.0beta13)
|
||||
* Fix memory issue with parcelable (crash in beta12 version)
|
||||
|
||||
KeepassDX (2.5.0.0beta12)
|
||||
KeePassDX (2.5.0.0beta12)
|
||||
* Added the Magikeyboard to fill the forms (settings still in development)
|
||||
* Added move and copy for groups and entries
|
||||
* New navigation in a single screen / new animations between activities
|
||||
@@ -113,10 +134,10 @@ KeepassDX (2.5.0.0beta12)
|
||||
* Fix the fingerprint recognition (WARNING : The keystore is reinit, you must delete the old keys)
|
||||
* Fix small bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta11)
|
||||
KeePassDX (2.5.0.0beta11)
|
||||
* Fix crash in beta10 version
|
||||
|
||||
KeepassDX (2.5.0.0beta10)
|
||||
KeePassDX (2.5.0.0beta10)
|
||||
* Dynamically change Algorithm and Key Derivation Function in settings
|
||||
* Upgrade translations
|
||||
* New red volcano theme, fix classic dark theme
|
||||
@@ -124,7 +145,7 @@ KeepassDX (2.5.0.0beta10)
|
||||
* Update fingerprint state with checkbox
|
||||
* Fix bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta9)
|
||||
KeePassDX (2.5.0.0beta9)
|
||||
* Education Screens to learn how to use the app
|
||||
* New designs
|
||||
* New custom font for character visibility
|
||||
@@ -133,9 +154,9 @@ KeepassDX (2.5.0.0beta9)
|
||||
* Change setting organisation
|
||||
* Pro version
|
||||
|
||||
KeepassDX (2.5.0.0beta8)
|
||||
KeePassDX (2.5.0.0beta8)
|
||||
* Hide custom entries protected
|
||||
* Best management of field references (https://keepass.info/help/base/fieldrefs.html)
|
||||
* Best management of field references (https://KeePass.info/help/base/fieldrefs.html)
|
||||
* Change database / default settings
|
||||
* Add Autofill for search
|
||||
* Add sorting by last access and by creation time
|
||||
@@ -143,7 +164,7 @@ KeepassDX (2.5.0.0beta8)
|
||||
* Refactor old code
|
||||
* Fix bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta7)
|
||||
KeePassDX (2.5.0.0beta7)
|
||||
* Rebuild Notifications
|
||||
* Change links to https
|
||||
* Add extended Ascii (ñæËÌÂÝÜ...)
|
||||
@@ -152,10 +173,10 @@ KeepassDX (2.5.0.0beta7)
|
||||
* Add setting to prevent the password copy
|
||||
* Fix bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta6)
|
||||
KeePassDX (2.5.0.0beta6)
|
||||
* Fix crash
|
||||
|
||||
KeepassDX (2.5.0.0beta5)
|
||||
KeePassDX (2.5.0.0beta5)
|
||||
* Autofill (Android O)
|
||||
* Deletion for group
|
||||
* New sorts with (Asc/Dsc, Groups before or after)
|
||||
@@ -176,7 +197,7 @@ KeepassDX (2.5.0.0beta5)
|
||||
* Fix many small bugs
|
||||
* Add recycle bin setting (not yet accessible)
|
||||
|
||||
KeepassDX (2.5.0.0beta4)
|
||||
KeePassDX (2.5.0.0beta4)
|
||||
* Show only file name
|
||||
* Setting for full path
|
||||
* Add information for each database file
|
||||
@@ -185,7 +206,7 @@ KeepassDX (2.5.0.0beta4)
|
||||
* Delete view assignment for fingerprint opening
|
||||
* Merge KeePassDroid 2.2.1
|
||||
|
||||
KeepassDX (2.5.0.0beta3)
|
||||
KeePassDX (2.5.0.0beta3)
|
||||
* New database workflow with new screens and folder selection
|
||||
* Settings for default password generation
|
||||
* Fingerprint dialog for explanations
|
||||
@@ -196,17 +217,17 @@ KeepassDX (2.5.0.0beta3)
|
||||
* Merge KeePassDroid 2.2.0.9
|
||||
* Add corruption fix mode
|
||||
|
||||
KeepassDX (2.5.0.0beta2)
|
||||
KeePassDX (2.5.0.0beta2)
|
||||
* Remove libs for F-Droid
|
||||
|
||||
KeepassDX (2.5.0.0beta1)
|
||||
* Fork KeepassDroid
|
||||
KeePassDX (2.5.0.0beta1)
|
||||
* Fork KeePassDroid
|
||||
* Add Material Design
|
||||
* Add Light and Night theme
|
||||
* Min API is 14
|
||||
* Solve bug for fingerprint
|
||||
* Update French translation
|
||||
* Change donation (see KeepassDroid to contribute on both projects)
|
||||
* Change donation (see KeePassDroid to contribute on both projects)
|
||||
|
||||
KeePassDroid (2.2.1)
|
||||
* Fix kdbx4 date corruption
|
||||
@@ -467,7 +488,7 @@ KeePassDroid (1.9.10)
|
||||
|
||||
KeePassDroid (1.9.9)
|
||||
* Go back to explicitly storing blank fields in the database
|
||||
(works around bug in keepassx)
|
||||
(works around bug in KeePassx)
|
||||
* Add support for native code on MIPS architectures
|
||||
* Adding Vibrate permission. On some devices notifications fail
|
||||
without the vibrate permission.
|
||||
|
||||
@@ -30,7 +30,7 @@ KeePassDX is a **free open source password manager for Android**, which helps yo
|
||||
|
||||
Yes, KeePassDX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
|
||||
|
||||
*Note : If you access the application from a store, visual features may not be available to incentivize the contribution to the work of open source projects. These optional visuals are accessible after a donation (and a small congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
|
||||
*Note : If you access the application from a store, visual styles may not be available to encourage contribution to the work of open source projects. These optional styles are accessible after a contribution (and a congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -59,9 +59,11 @@ Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassD
|
||||
|
||||
## Other devices
|
||||
|
||||
- [KeePass XC](https://keepassxc.org/) (https://keepassxc.org/) works with **GNU/Linux**, **Mac** and **Windows**, is updated regularly and under the terms of the GNU General Public License. This is the recommended version for computers.
|
||||
- [KeePass](https://keepass.info/) (https://keepass.info/) is the original and official project for desktop, with technical documentation for standardized database files. It is updated regularly with active maintenance (written in C#).
|
||||
|
||||
- [KeePass](https://keepass.info/) (https://keepass.info/) is the historical project, with good technical documentation for standardized database files but only running on **Windows**.
|
||||
- [KeePassXC](https://keepassxc.org/) (https://keepassxc.org/) is an alternative integration to KeePass written in C++.
|
||||
|
||||
- [KeeWeb](https://keeweb.info/) (https://keeweb.info/) is a web version also compatible with KeePass files.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -4,21 +4,28 @@ apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion '28.0.3'
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion '29.0.3'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 28
|
||||
versionCode = 26
|
||||
versionName = "2.5.0.0beta26"
|
||||
targetSdkVersion 29
|
||||
versionCode = 29
|
||||
versionName = "2.5beta29"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
||||
|
||||
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
arg("room.incremental", "true")
|
||||
arg("room.schemaLocation", "$projectDir/schemas".toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
@@ -79,7 +86,7 @@ android {
|
||||
}
|
||||
|
||||
def spongycastleVersion = "1.58.0.0"
|
||||
def room_version = "2.2.1"
|
||||
def room_version = "2.2.5"
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
@@ -88,7 +95,9 @@ dependencies {
|
||||
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'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.biometric:biometric:1.0.1'
|
||||
// To upgrade with style
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "56438e5f7372ef3e36e33b782aed245d",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "file_database_history",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "databaseUri",
|
||||
"columnName": "database_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "databaseAlias",
|
||||
"columnName": "database_alias",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "keyFileUri",
|
||||
"columnName": "keyfile_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "updated",
|
||||
"columnName": "updated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"database_uri"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "cipher_database",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "databaseUri",
|
||||
"columnName": "database_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "encryptedValue",
|
||||
"columnName": "encrypted_value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "specParameters",
|
||||
"columnName": "specs_parameters",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"database_uri"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '56438e5f7372ef3e36e33b782aed245d')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,15 @@
|
||||
android:normalScreens="true"
|
||||
android:largeScreens="true"
|
||||
android:anyDensity="true" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission
|
||||
android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission
|
||||
android:maxSdkVersion="18"
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
@@ -128,7 +133,7 @@
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/menu_about" />
|
||||
android:label="@string/about" />
|
||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
||||
android:configChanges="keyboardHidden" />
|
||||
|
||||
@@ -21,15 +21,15 @@ package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.widget.TextView
|
||||
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.text.HtmlCompat
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
|
||||
import org.joda.time.DateTime
|
||||
|
||||
class AboutActivity : StylishActivity() {
|
||||
@@ -40,7 +40,7 @@ class AboutActivity : StylishActivity() {
|
||||
setContentView(R.layout.activity_about)
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.title = getString(R.string.menu_about)
|
||||
toolbar.title = getString(R.string.about)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
@@ -64,9 +64,17 @@ class AboutActivity : StylishActivity() {
|
||||
val buildTextView = findViewById<TextView>(R.id.activity_about_build)
|
||||
buildTextView.text = build
|
||||
|
||||
findViewById<TextView>(R.id.activity_about_licence_text).apply {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
|
||||
val disclaimerText = findViewById<TextView>(R.id.disclaimer)
|
||||
disclaimerText.text = getString(R.string.disclaimer_formal, DateTime().year)
|
||||
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
@@ -34,6 +34,7 @@ import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
@@ -60,11 +61,13 @@ import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.createDocument
|
||||
import com.kunzisoft.keepass.utils.onCreateDocumentResult
|
||||
import com.kunzisoft.keepass.view.EntryContentsView
|
||||
import com.kunzisoft.keepass.view.showActionError
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
class EntryActivity : LockingActivity() {
|
||||
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var titleIconView: ImageView? = null
|
||||
private var historyView: View? = null
|
||||
@@ -114,6 +117,7 @@ class EntryActivity : LockingActivity() {
|
||||
invalidateOptionsMenu()
|
||||
|
||||
// Get views
|
||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||
titleIconView = findViewById(R.id.entry_icon)
|
||||
historyView = findViewById(R.id.history_container)
|
||||
@@ -137,7 +141,7 @@ class EntryActivity : LockingActivity() {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
// TODO Visual error for entry history
|
||||
coordinatorLayout?.showActionError(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,9 +150,11 @@ class EntryActivity : LockingActivity() {
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
val keyEntry: NodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
|
||||
mEntry = mDatabase?.getEntryById(keyEntry)
|
||||
mEntryLastVersion = mEntry
|
||||
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
|
||||
if (keyEntry != null) {
|
||||
mEntry = mDatabase?.getEntryById(keyEntry)
|
||||
mEntryLastVersion = mEntry
|
||||
}
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
}
|
||||
@@ -535,12 +541,10 @@ class EntryActivity : LockingActivity() {
|
||||
|
||||
override fun finish() {
|
||||
// Transit data in previous Activity after an update
|
||||
/*
|
||||
TODO Slowdown when add entry as result
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
|
||||
onFinish(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
|
||||
*/
|
||||
Intent().apply {
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
|
||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
|
||||
}
|
||||
super.finish()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.TimePickerDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
@@ -26,16 +28,20 @@ import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
import android.widget.DatePicker
|
||||
import android.widget.TimePicker
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.ActionMenuView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.*
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
@@ -49,13 +55,16 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.view.EntryEditContentsView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.showActionError
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
|
||||
class EntryEditActivity : LockingActivity(),
|
||||
IconPickerDialogFragment.IconPickerListener,
|
||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||
SetOTPDialogFragment.CreateOtpListener {
|
||||
SetOTPDialogFragment.CreateOtpListener,
|
||||
DatePickerDialog.OnDateSetListener,
|
||||
TimePickerDialog.OnTimeSetListener {
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
@@ -68,8 +77,9 @@ class EntryEditActivity : LockingActivity(),
|
||||
|
||||
// Views
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var scrollView: ScrollView? = null
|
||||
private var scrollView: NestedScrollView? = null
|
||||
private var entryEditContentsView: EntryEditContentsView? = null
|
||||
private var entryEditAddToolBar: ActionMenuView? = null
|
||||
private var saveView: View? = null
|
||||
|
||||
// Education
|
||||
@@ -92,6 +102,16 @@ class EntryEditActivity : LockingActivity(),
|
||||
|
||||
entryEditContentsView = findViewById(R.id.entry_edit_contents)
|
||||
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||
entryEditContentsView?.onDateClickListener = View.OnClickListener {
|
||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||
val dateTime = DateTime(expiresDate)
|
||||
val defaultYear = dateTime.year
|
||||
val defaultMonth = dateTime.monthOfYear-1
|
||||
val defaultDay = dateTime.dayOfMonth
|
||||
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
|
||||
.show(supportFragmentManager, "DatePickerFragment")
|
||||
}
|
||||
}
|
||||
// Focus view to reinitialize timeout
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
|
||||
|
||||
@@ -165,17 +185,46 @@ class EntryEditActivity : LockingActivity(),
|
||||
// Add listener to the icon
|
||||
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
|
||||
|
||||
// Generate password button
|
||||
entryEditContentsView?.setOnPasswordGeneratorClickListener { openPasswordGenerator() }
|
||||
// Bottom Bar
|
||||
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
||||
entryEditAddToolBar?.apply {
|
||||
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||
|
||||
menu.findItem(R.id.menu_add_field).apply {
|
||||
val allowCustomField = mNewEntry?.allowCustomFields() == true
|
||||
isEnabled = allowCustomField
|
||||
isVisible = allowCustomField
|
||||
}
|
||||
|
||||
menu.findItem(R.id.menu_add_otp).apply {
|
||||
val allowOTP = mDatabase?.allowOTP == true
|
||||
isEnabled = allowOTP
|
||||
isVisible = allowOTP
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.menu_generate_password -> {
|
||||
openPasswordGenerator()
|
||||
true
|
||||
}
|
||||
R.id.menu_add_field -> {
|
||||
addNewCustomField()
|
||||
true
|
||||
}
|
||||
R.id.menu_add_otp -> {
|
||||
setupOTP()
|
||||
true
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save button
|
||||
saveView = findViewById(R.id.entry_edit_save)
|
||||
saveView = findViewById(R.id.entry_edit_validate)
|
||||
saveView?.setOnClickListener { saveEntry() }
|
||||
|
||||
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) {
|
||||
addNewCustomField()
|
||||
}
|
||||
|
||||
// Verify the education views
|
||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||
|
||||
@@ -188,15 +237,7 @@ class EntryEditActivity : LockingActivity(),
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
// Show error
|
||||
if (!result.isSuccess) {
|
||||
result.message?.let { resultMessage ->
|
||||
Snackbar.make(coordinatorLayout!!,
|
||||
resultMessage,
|
||||
Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionError(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +254,9 @@ class EntryEditActivity : LockingActivity(),
|
||||
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
|
||||
url = newEntry.url
|
||||
password = newEntry.password
|
||||
expires = newEntry.expires
|
||||
if (expires)
|
||||
expiresDate = newEntry.expiryTime
|
||||
notes = newEntry.notes
|
||||
for (entry in newEntry.customFields.entries) {
|
||||
post {
|
||||
@@ -234,7 +278,11 @@ class EntryEditActivity : LockingActivity(),
|
||||
username = entryView.username
|
||||
url = entryView.url
|
||||
password = entryView.password
|
||||
notes = entryView.notes
|
||||
expires = entryView.expires
|
||||
if (entryView.expires) {
|
||||
expiryTime = entryView.expiresDate
|
||||
}
|
||||
notes = entryView. notes
|
||||
entryView.customFields.forEach { customField ->
|
||||
putExtraField(customField.name, customField.protectedValue)
|
||||
}
|
||||
@@ -265,6 +313,13 @@ class EntryEditActivity : LockingActivity(),
|
||||
entryEditContentsView?.addEmptyCustomField()
|
||||
}
|
||||
|
||||
private fun setupOTP() {
|
||||
// Retrieve the current otpElement if exists
|
||||
// and open the dialog to set up the OTP
|
||||
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
|
||||
.show(supportFragmentManager, "addOTPDialog")
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the new entry or update an existing entry in the database
|
||||
*/
|
||||
@@ -313,8 +368,6 @@ class EntryEditActivity : LockingActivity(),
|
||||
// Save database not needed here
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
if (mDatabase?.allowOTP == true)
|
||||
inflater.inflate(R.menu.entry_otp, menu)
|
||||
|
||||
entryEditActivityEducation?.let {
|
||||
Handler().post { performedNextEducation(it) }
|
||||
@@ -324,12 +377,10 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
val passwordView = entryEditContentsView?.generatePasswordView
|
||||
val addNewFieldView = entryEditContentsView?.addNewFieldButton
|
||||
|
||||
val generatePasswordEducationPerformed = passwordView != null
|
||||
val passwordGeneratorView: View? = entryEditAddToolBar?.findViewById(R.id.menu_generate_password)
|
||||
val generatePasswordEducationPerformed = passwordGeneratorView != null
|
||||
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||
passwordView,
|
||||
passwordGeneratorView,
|
||||
{
|
||||
openPasswordGenerator()
|
||||
},
|
||||
@@ -338,14 +389,28 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
)
|
||||
if (!generatePasswordEducationPerformed) {
|
||||
// entryNewFieldEducationPerformed
|
||||
mNewEntry != null && mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
|
||||
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
|
||||
val addNewFieldEducationPerformed = mNewEntry != null
|
||||
&& mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
|
||||
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
|
||||
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||
addNewFieldView,
|
||||
{
|
||||
addNewCustomField()
|
||||
})
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryEditActivityEducation)
|
||||
}
|
||||
)
|
||||
if (!addNewFieldEducationPerformed) {
|
||||
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
||||
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
|
||||
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||
setupOtpView,
|
||||
{
|
||||
setupOTP()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,14 +427,9 @@ class EntryEditActivity : LockingActivity(),
|
||||
MenuUtil.onContributionItemSelected(this)
|
||||
return true
|
||||
}
|
||||
R.id.menu_add_otp -> {
|
||||
// Retrieve the current otpElement if exists
|
||||
// and open the dialog to set up the OTP
|
||||
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
|
||||
.show(supportFragmentManager, "addOTPDialog")
|
||||
return true
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
}
|
||||
android.R.id.home -> finish()
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
@@ -389,6 +449,39 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
||||
// To fix android 4.4 issue
|
||||
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
||||
if (datePicker?.isShown == true) {
|
||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||
// Save the date
|
||||
entryEditContentsView?.expiresDate =
|
||||
DateInstant(DateTime(expiresDate)
|
||||
.withYear(year)
|
||||
.withMonthOfYear(month + 1)
|
||||
.withDayOfMonth(day)
|
||||
.toDate())
|
||||
// Launch the time picker
|
||||
val dateTime = DateTime(expiresDate)
|
||||
val defaultHour = dateTime.hourOfDay
|
||||
val defaultMinute = dateTime.minuteOfHour
|
||||
TimePickerFragment.getInstance(defaultHour, defaultMinute)
|
||||
.show(supportFragmentManager, "TimePickerFragment")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
|
||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||
// Save the date
|
||||
entryEditContentsView?.expiresDate =
|
||||
DateInstant(DateTime(expiresDate)
|
||||
.withHourOfDay(hours)
|
||||
.withMinuteOfHour(minutes)
|
||||
.toDate())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
mNewEntry?.let {
|
||||
populateEntryWithViews(it)
|
||||
@@ -412,6 +505,15 @@ class EntryEditActivity : LockingActivity(),
|
||||
// Do nothing here
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.discard_changes)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
super@EntryEditActivity.onBackPressed()
|
||||
}.create().show()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
// Assign entry callback as a result in all case
|
||||
try {
|
||||
|
||||
@@ -28,7 +28,6 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -52,6 +51,7 @@ import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
||||
@@ -141,8 +141,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
if (!(savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(EXTRA_STAY)
|
||||
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val databasePath = prefs.getString(PasswordActivity.KEY_DEFAULT_DATABASE_PATH, "")
|
||||
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
|
||||
|
||||
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
||||
launchPasswordActivityWithPath(databaseFileUri)
|
||||
@@ -162,9 +161,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
onActionFinish = { actionTask, _ ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK -> {
|
||||
// TODO Check
|
||||
// mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
// updateFileListVisibility()
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity)
|
||||
}
|
||||
}
|
||||
@@ -274,12 +270,28 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
updateExternalStorageWarning()
|
||||
|
||||
// Construct adapter with listeners
|
||||
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
||||
databaseFileHistoryList?.let {
|
||||
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(it)
|
||||
updateFileListVisibility()
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
if (PreferencesUtil.showRecentFiles(this)) {
|
||||
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
||||
databaseFileHistoryList?.let { historyList ->
|
||||
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(this@FileDatabaseSelectActivity)
|
||||
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(
|
||||
// Show only uri accessible
|
||||
historyList.filter {
|
||||
if (hideBrokenLocations) {
|
||||
UriUtil.parse(it.databaseUri)?.let { historyUri ->
|
||||
UriUtil.isUriAccessible(contentResolver, historyUri)
|
||||
} ?: false
|
||||
} else
|
||||
true
|
||||
})
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
updateFileListVisibility()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
updateFileListVisibility()
|
||||
}
|
||||
|
||||
// Register progress task
|
||||
|
||||
@@ -76,6 +76,7 @@ import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.view.AddNodeButtonView
|
||||
import com.kunzisoft.keepass.view.ToolbarAction
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.showActionError
|
||||
|
||||
class GroupActivity : LockingActivity(),
|
||||
GroupEditDialogFragment.EditGroupListener,
|
||||
@@ -101,6 +102,7 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
private var mListNodesFragment: ListNodesFragment? = null
|
||||
private var mCurrentGroupIsASearch: Boolean = false
|
||||
private var mRequestStartupSearch = true
|
||||
|
||||
// Nodes
|
||||
private var mRootGroup: Group? = null
|
||||
@@ -141,6 +143,8 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
// Retrieve elements after an orientation change
|
||||
if (savedInstanceState != null) {
|
||||
if (savedInstanceState.containsKey(REQUEST_STARTUP_SEARCH_KEY))
|
||||
mRequestStartupSearch = savedInstanceState.getBoolean(REQUEST_STARTUP_SEARCH_KEY)
|
||||
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY))
|
||||
mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY)
|
||||
}
|
||||
@@ -251,15 +255,7 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.isSuccess) {
|
||||
coordinatorLayout?.let { coordinatorLayout ->
|
||||
result.exception?.errorId?.let { errorId ->
|
||||
Snackbar.make(coordinatorLayout, errorId, Snackbar.LENGTH_LONG).asError().show()
|
||||
} ?: result.message?.let { message ->
|
||||
Snackbar.make(coordinatorLayout, message, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionError(result)
|
||||
|
||||
finishNodeAction()
|
||||
|
||||
@@ -339,6 +335,7 @@ class GroupActivity : LockingActivity(),
|
||||
mOldGroupToUpdate?.let {
|
||||
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it)
|
||||
}
|
||||
outState.putBoolean(REQUEST_STARTUP_SEARCH_KEY, mRequestStartupSearch)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
@@ -349,7 +346,8 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
// If it's a search
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' })
|
||||
val searchString = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
||||
return mDatabase?.search(searchString)
|
||||
}
|
||||
// else a real group
|
||||
else {
|
||||
@@ -441,15 +439,14 @@ class GroupActivity : LockingActivity(),
|
||||
enableAddGroup(addGroupEnabled)
|
||||
enableAddEntry(addEntryEnabled)
|
||||
|
||||
if (isEnable)
|
||||
showButton()
|
||||
showButton()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshNumberOfChildren() {
|
||||
numberChildrenView?.apply {
|
||||
if (PreferencesUtil.showNumberEntries(context)) {
|
||||
text = mCurrentGroup?.getChildEntries(*Group.ChildFilter.getDefaults(context))?.size?.toString() ?: ""
|
||||
text = mCurrentGroup?.getNumberOfChildEntries(*Group.ChildFilter.getDefaults(context))?.toString() ?: ""
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
@@ -506,6 +503,7 @@ class GroupActivity : LockingActivity(),
|
||||
private fun finishNodeAction() {
|
||||
actionNodeMode?.finish()
|
||||
actionNodeMode = null
|
||||
addNodeButtonView?.showButton()
|
||||
}
|
||||
|
||||
override fun onNodeSelected(nodes: List<Node>): Boolean {
|
||||
@@ -517,6 +515,7 @@ class GroupActivity : LockingActivity(),
|
||||
} else {
|
||||
actionNodeMode?.invalidate()
|
||||
}
|
||||
addNodeButtonView?.hideButton()
|
||||
} else {
|
||||
finishNodeAction()
|
||||
}
|
||||
@@ -666,13 +665,15 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
// Get the SearchView and set the searchable configuration
|
||||
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
|
||||
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
|
||||
|
||||
menu.findItem(R.id.menu_search)?.let {
|
||||
val searchView = it.actionView as SearchView?
|
||||
searchView?.apply {
|
||||
setSearchableInfo(searchManager.getSearchableInfo(
|
||||
ComponentName(this@GroupActivity, GroupActivity::class.java)))
|
||||
(searchManager?.getSearchableInfo(
|
||||
ComponentName(this@GroupActivity, GroupActivity::class.java)))?.let { searchableInfo ->
|
||||
setSearchableInfo(searchableInfo)
|
||||
}
|
||||
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
|
||||
suggestionsAdapter = mSearchSuggestionAdapter
|
||||
setOnSuggestionListener(object : SearchView.OnSuggestionListener {
|
||||
@@ -690,6 +691,13 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
})
|
||||
}
|
||||
// Expand the search view if defined in settings
|
||||
if (mRequestStartupSearch
|
||||
&& PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) {
|
||||
// To request search only one time
|
||||
mRequestStartupSearch = false
|
||||
it.expandActionView()
|
||||
}
|
||||
}
|
||||
|
||||
super.onCreateOptionsMenu(menu)
|
||||
@@ -855,8 +863,8 @@ class GroupActivity : LockingActivity(),
|
||||
.iconPicked(bundle)
|
||||
}
|
||||
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
|
||||
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||
mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
|
||||
}
|
||||
|
||||
override fun startActivity(intent: Intent) {
|
||||
@@ -900,8 +908,8 @@ class GroupActivity : LockingActivity(),
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
// Not directly get the entry from intent data but from database
|
||||
mListNodesFragment?.rebuildList()
|
||||
// Directly used the onActivityResult in fragment
|
||||
mListNodesFragment?.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private fun removeSearchInIntent(intent: Intent) {
|
||||
@@ -942,24 +950,33 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
private val TAG = GroupActivity::class.java.name
|
||||
|
||||
private const val REQUEST_STARTUP_SEARCH_KEY = "REQUEST_STARTUP_SEARCH_KEY"
|
||||
private const val GROUP_ID_KEY = "GROUP_ID_KEY"
|
||||
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
|
||||
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
|
||||
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
|
||||
|
||||
private fun buildAndLaunchIntent(context: Context, group: Group?, readOnly: Boolean,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
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)
|
||||
}
|
||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||
intentBuildLauncher.invoke(intent)
|
||||
private fun buildIntent(context: Context, group: Group?, readOnly: Boolean,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
val intent = Intent(context, GroupActivity::class.java)
|
||||
if (group != null) {
|
||||
intent.putExtra(GROUP_ID_KEY, group.nodeId)
|
||||
}
|
||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||
intentBuildLauncher.invoke(intent)
|
||||
}
|
||||
|
||||
private fun checkTimeAndBuildIntent(activity: Activity, group: Group?, readOnly: Boolean,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
buildIntent(activity, group, readOnly, intentBuildLauncher)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkTimeAndBuildIntent(context: Context, group: Group?, readOnly: Boolean,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
if (TimeoutHelper.checkTime(context)) {
|
||||
buildIntent(context, group, readOnly, intentBuildLauncher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -970,9 +987,9 @@ class GroupActivity : LockingActivity(),
|
||||
*/
|
||||
|
||||
@JvmOverloads
|
||||
fun launch(context: Context, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||
TimeoutHelper.recordTime(context)
|
||||
buildAndLaunchIntent(context, null, readOnly) { intent ->
|
||||
fun launch(context: Context,
|
||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -984,9 +1001,9 @@ class GroupActivity : LockingActivity(),
|
||||
*/
|
||||
// TODO implement pre search to directly open the direct group
|
||||
|
||||
fun launchForKeyboardSelection(context: Context, readOnly: Boolean) {
|
||||
TimeoutHelper.recordTime(context)
|
||||
buildAndLaunchIntent(context, null, readOnly) { intent ->
|
||||
fun launchForKeyboardSelection(context: Context,
|
||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
|
||||
EntrySelectionHelper.startActivityForEntrySelection(context, intent)
|
||||
}
|
||||
}
|
||||
@@ -999,9 +1016,10 @@ class GroupActivity : LockingActivity(),
|
||||
// TODO implement pre search to directly open the direct group
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure, readOnly: Boolean) {
|
||||
TimeoutHelper.recordTime(activity)
|
||||
buildAndLaunchIntent(activity, null, readOnly) { intent ->
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
assistStructure: AssistStructure,
|
||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
|
||||
checkTimeAndBuildIntent(activity, null, readOnly) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,7 @@ package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.util.Log
|
||||
@@ -54,7 +52,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
private var nodeClickListener: NodeClickListener? = null
|
||||
private var onScrollListener: OnScrollListener? = null
|
||||
|
||||
private var listView: RecyclerView? = null
|
||||
private var mNodesRecyclerView: RecyclerView? = null
|
||||
var mainGroup: Group? = null
|
||||
private set
|
||||
private var mAdapter: NodeAdapter? = null
|
||||
@@ -69,8 +67,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
private var notFoundView: View? = null
|
||||
private var isASearchResult: Boolean = false
|
||||
|
||||
// Preferences for sorting
|
||||
private var prefs: SharedPreferences? = null
|
||||
|
||||
private var readOnly: Boolean = false
|
||||
get() {
|
||||
@@ -155,7 +151,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
})
|
||||
}
|
||||
}
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -169,11 +164,17 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
// To apply theme
|
||||
val rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_list_nodes, container, false)
|
||||
listView = rootView.findViewById(R.id.nodes_list)
|
||||
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list)
|
||||
notFoundView = rootView.findViewById(R.id.not_found_container)
|
||||
|
||||
mNodesRecyclerView?.apply {
|
||||
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = mAdapter
|
||||
}
|
||||
|
||||
onScrollListener?.let { onScrollListener ->
|
||||
listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
onScrollListener.onScrolled(dy)
|
||||
@@ -181,8 +182,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
})
|
||||
}
|
||||
|
||||
rebuildList()
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
@@ -194,14 +193,14 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
|
||||
// Refresh data
|
||||
mAdapter?.notifyDataSetChanged()
|
||||
rebuildList()
|
||||
|
||||
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
||||
// To show the " no search entry found "
|
||||
listView?.visibility = View.GONE
|
||||
mNodesRecyclerView?.visibility = View.GONE
|
||||
notFoundView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
listView?.visibility = View.VISIBLE
|
||||
mNodesRecyclerView?.visibility = View.VISIBLE
|
||||
notFoundView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
@@ -209,30 +208,27 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
fun rebuildList() {
|
||||
// Add elements to the list
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
}
|
||||
listView?.apply {
|
||||
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = mAdapter
|
||||
mAdapter?.apply {
|
||||
rebuildList(mainGroup)
|
||||
// To visually change the elements
|
||||
if (PreferencesUtil.APPEARANCE_CHANGED) {
|
||||
notifyDataSetChanged()
|
||||
PreferencesUtil.APPEARANCE_CHANGED = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
|
||||
// Toggle setting
|
||||
prefs?.edit()?.apply {
|
||||
putString(getString(R.string.sort_node_key), sortNodeEnum.name)
|
||||
putBoolean(getString(R.string.sort_ascending_key), ascending)
|
||||
putBoolean(getString(R.string.sort_group_before_key), groupsBefore)
|
||||
putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom)
|
||||
apply()
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||
// Save setting
|
||||
context?.let {
|
||||
PreferencesUtil.saveNodeSort(it, sortNodeEnum, sortNodeParameters)
|
||||
}
|
||||
|
||||
// Tell the adapter to refresh it's list
|
||||
mAdapter?.notifyChangeSort(sortNodeEnum, ascending, groupsBefore)
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
}
|
||||
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
|
||||
rebuildList()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
@@ -373,15 +369,11 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|
||||
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode ->
|
||||
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
||||
mAdapter?.addNode(newNode)
|
||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||
//mAdapter.updateLastNodeRegister(newNode);
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
}
|
||||
}
|
||||
addNode(changedNode)
|
||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
|
||||
mAdapter?.notifyDataSetChanged()
|
||||
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,26 +23,22 @@ import android.app.Activity
|
||||
import android.app.assist.AssistStructure
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
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.*
|
||||
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 androidx.core.app.ActivityCompat
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||
@@ -74,11 +70,10 @@ import com.kunzisoft.keepass.view.asError
|
||||
import kotlinx.android.synthetic.main.activity_password.*
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class PasswordActivity : StylishActivity() {
|
||||
open class PasswordActivity : StylishActivity() {
|
||||
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
private var containerView: View? = null
|
||||
private var filenameView: TextView? = null
|
||||
private var passwordView: EditText? = null
|
||||
@@ -88,18 +83,26 @@ class PasswordActivity : StylishActivity() {
|
||||
private var checkboxKeyFileView: CompoundButton? = null
|
||||
private var checkboxDefaultDatabaseView: CompoundButton? = null
|
||||
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
||||
private var infoContainerView: ViewGroup? = 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 mOpenFileHelper: OpenFileHelper? = null
|
||||
|
||||
private var mPermissionAsked = false
|
||||
private var readOnly: Boolean = false
|
||||
private var mForceReadOnly: Boolean = false
|
||||
set(value) {
|
||||
infoContainerView?.visibility = if (value) {
|
||||
readOnly = true
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
private var mProgressDialogThread: ProgressDialogThread? = null
|
||||
|
||||
@@ -108,10 +111,6 @@ class PasswordActivity : StylishActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this)
|
||||
|
||||
setContentView(R.layout.activity_password)
|
||||
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
@@ -121,7 +120,7 @@ class PasswordActivity : StylishActivity() {
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
containerView = findViewById(R.id.container)
|
||||
confirmButtonView = findViewById(R.id.pass_ok)
|
||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||
filenameView = findViewById(R.id.filename)
|
||||
passwordView = findViewById(R.id.password)
|
||||
keyFileView = findViewById(R.id.pass_keyfile)
|
||||
@@ -129,8 +128,8 @@ class PasswordActivity : StylishActivity() {
|
||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
||||
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||
|
||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||
|
||||
val browseView = findViewById<View>(R.id.open_database_button)
|
||||
@@ -164,6 +163,12 @@ class PasswordActivity : StylishActivity() {
|
||||
enableOrNotTheConfirmationButton()
|
||||
}
|
||||
|
||||
// If is a view intent
|
||||
getUriFromIntent(intent)
|
||||
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
||||
}
|
||||
|
||||
mProgressDialogThread = ProgressDialogThread(this).apply {
|
||||
onActionFinish = { actionTask, result ->
|
||||
when (actionTask) {
|
||||
@@ -176,10 +181,9 @@ class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the password in view in all cases
|
||||
removePassword()
|
||||
|
||||
if (result.isSuccess) {
|
||||
mDatabaseKeyFileUri = null
|
||||
clearCredentialsViews(true)
|
||||
launchGroupActivity()
|
||||
} else {
|
||||
var resultError = ""
|
||||
@@ -235,6 +239,24 @@ class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUriFromIntent(intent: Intent?) {
|
||||
// If is a view intent
|
||||
val action = intent?.action
|
||||
if (action != null
|
||||
&& action == VIEW_INTENT) {
|
||||
mDatabaseFileUri = intent.data
|
||||
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
||||
} else {
|
||||
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
||||
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
getUriFromIntent(intent)
|
||||
}
|
||||
|
||||
private fun launchGroupActivity() {
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
@@ -263,13 +285,15 @@ class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
|
||||
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) {
|
||||
setEmptyViews()
|
||||
clearCredentialsViews()
|
||||
}
|
||||
|
||||
// For check shutdown
|
||||
@@ -281,45 +305,31 @@ class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
||||
mDatabaseKeyFileUri?.let {
|
||||
outState.putString(KEY_KEYFILE, it.toString())
|
||||
}
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun initUriFromIntent() {
|
||||
|
||||
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)
|
||||
}
|
||||
mForceReadOnly = !UriUtil.isUriWritable(mDatabaseFileUri)
|
||||
|
||||
// Post init uri with KeyFile if needed
|
||||
if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) {
|
||||
if (mRememberKeyFile && (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
||||
// Retrieve KeyFile in a thread
|
||||
databaseUri?.let { databaseUriNotNull ->
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||
.getKeyFileUriByDatabaseUri(databaseUriNotNull) {
|
||||
.getKeyFileUriByDatabaseUri(databaseUri) {
|
||||
onPostInitUri(databaseUri, it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onPostInitUri(databaseUri, keyFileUri)
|
||||
onPostInitUri(mDatabaseFileUri, mDatabaseKeyFileUri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||
mDatabaseFileUri = databaseFileUri
|
||||
mDatabaseKeyFileUri = keyFileUri
|
||||
|
||||
// Define title
|
||||
databaseFileUri?.let {
|
||||
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
|
||||
@@ -328,26 +338,18 @@ class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
// Define Key File text
|
||||
val keyUriString = keyFileUri?.toString() ?: ""
|
||||
if (keyUriString.isNotEmpty() && mRememberKeyFile) { // Bug KeepassDX #18
|
||||
populateKeyFileTextView(keyUriString)
|
||||
if (mRememberKeyFile) {
|
||||
populateKeyFileTextView(keyFileUri?.toString())
|
||||
}
|
||||
|
||||
// Define listeners for default database checkbox and validate button
|
||||
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
|
||||
var newDefaultFileName: Uri? = null
|
||||
var newDefaultFileUri: Uri? = null
|
||||
if (isChecked) {
|
||||
newDefaultFileName = databaseFileUri ?: newDefaultFileName
|
||||
newDefaultFileUri = databaseFileUri ?: newDefaultFileUri
|
||||
}
|
||||
|
||||
prefs?.edit()?.apply {
|
||||
newDefaultFileName?.let {
|
||||
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
|
||||
} ?: kotlin.run {
|
||||
remove(KEY_DEFAULT_DATABASE_PATH)
|
||||
}
|
||||
apply()
|
||||
}
|
||||
PreferencesUtil.saveDefaultDatabasePath(this, newDefaultFileUri)
|
||||
|
||||
val backupManager = BackupManager(this@PasswordActivity)
|
||||
backupManager.dataChanged()
|
||||
@@ -355,7 +357,7 @@ class PasswordActivity : StylishActivity() {
|
||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
||||
|
||||
// Retrieve settings for default database
|
||||
val defaultFilename = prefs?.getString(KEY_DEFAULT_DATABASE_PATH, "")
|
||||
val defaultFilename = PreferencesUtil.getDefaultDatabasePath(this)
|
||||
if (databaseFileUri != null
|
||||
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
|
||||
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
|
||||
@@ -365,6 +367,8 @@ class PasswordActivity : StylishActivity() {
|
||||
// If Activity is launch with a password and want to open directly
|
||||
val intent = intent
|
||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||
// Consume the intent extra password
|
||||
intent.removeExtra(KEY_PASSWORD)
|
||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||
if (password != null) {
|
||||
populatePasswordTextView(password)
|
||||
@@ -430,10 +434,9 @@ class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEmptyViews() {
|
||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||
populatePasswordTextView(null)
|
||||
// Bug KeepassDX #18
|
||||
if (!mRememberKeyFile) {
|
||||
if (clearKeyFile) {
|
||||
populateKeyFileTextView(null)
|
||||
}
|
||||
}
|
||||
@@ -499,18 +502,13 @@ class PasswordActivity : StylishActivity() {
|
||||
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||
}
|
||||
|
||||
private fun removePassword() {
|
||||
passwordView?.setText("")
|
||||
checkboxPasswordView?.isChecked = false
|
||||
}
|
||||
|
||||
private fun loadDatabase(databaseFileUri: Uri?,
|
||||
password: String?,
|
||||
keyFileUri: Uri?,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
|
||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||
removePassword()
|
||||
clearCredentialsViews()
|
||||
}
|
||||
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
@@ -551,7 +549,12 @@ class PasswordActivity : StylishActivity() {
|
||||
val inflater = menuInflater
|
||||
// Read menu
|
||||
inflater.inflate(R.menu.open_file, menu)
|
||||
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||
|
||||
if (mForceReadOnly) {
|
||||
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
||||
} else {
|
||||
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||
}
|
||||
|
||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||
|
||||
@@ -562,45 +565,14 @@ class PasswordActivity : StylishActivity() {
|
||||
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
launchEducation(menu) {
|
||||
launchCheckPermission()
|
||||
}
|
||||
launchEducation(menu)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Check permission
|
||||
private fun launchCheckPermission() {
|
||||
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
val permissions = arrayOf(writePermission)
|
||||
if (Build.VERSION.SDK_INT >= 23
|
||||
&& !readOnly
|
||||
&& !mPermissionAsked) {
|
||||
mPermissionAsked = true
|
||||
// Check self permission to show or not the dialog
|
||||
if (toolbar != null
|
||||
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
when (requestCode) {
|
||||
WRITE_EXTERNAL_STORAGE_REQUEST -> {
|
||||
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
||||
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To fix multiple view education
|
||||
private var performedEductionInProgress = false
|
||||
private fun launchEducation(menu: Menu, onEducationFinished: ()-> Unit) {
|
||||
private fun launchEducation(menu: Menu, onEducationFinished: (()-> Unit)? = null) {
|
||||
if (!performedEductionInProgress) {
|
||||
performedEductionInProgress = true
|
||||
// Show education views
|
||||
@@ -610,7 +582,7 @@ class PasswordActivity : StylishActivity() {
|
||||
|
||||
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||
menu: Menu,
|
||||
onEducationFinished: ()-> Unit) {
|
||||
onEducationFinished: (()-> Unit)? = null) {
|
||||
val educationToolbar = toolbar
|
||||
val unlockEducationPerformed = educationToolbar != null
|
||||
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
||||
@@ -650,7 +622,7 @@ class PasswordActivity : StylishActivity() {
|
||||
})
|
||||
|
||||
if (!biometricEducationPerformed) {
|
||||
onEducationFinished.invoke()
|
||||
onEducationFinished?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -699,6 +671,7 @@ class PasswordActivity : StylishActivity() {
|
||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
mDatabaseKeyFileUri = uri
|
||||
populateKeyFileTextView(uri.toString())
|
||||
}
|
||||
}
|
||||
@@ -707,7 +680,7 @@ class PasswordActivity : StylishActivity() {
|
||||
// this block if not a key file response
|
||||
when (resultCode) {
|
||||
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
|
||||
setEmptyViews()
|
||||
clearCredentialsViews()
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
}
|
||||
}
|
||||
@@ -718,8 +691,6 @@ class PasswordActivity : StylishActivity() {
|
||||
|
||||
private val TAG = PasswordActivity::class.java.name
|
||||
|
||||
const val KEY_DEFAULT_DATABASE_PATH = "KEY_DEFAULT_DATABASE_PATH"
|
||||
|
||||
private const val KEY_FILENAME = "fileName"
|
||||
private const val KEY_KEYFILE = "keyFile"
|
||||
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||
@@ -727,10 +698,6 @@ class PasswordActivity : StylishActivity() {
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||
|
||||
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
||||
|
||||
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
val intent = Intent(activity, PasswordActivity::class.java)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
||||
class DatePickerFragment : DialogFragment() {
|
||||
|
||||
private var mDefaultYear: Int = 2000
|
||||
private var mDefaultMonth: Int = 1
|
||||
private var mDefaultDay: Int = 1
|
||||
|
||||
private var mListener: DatePickerDialog.OnDateSetListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as DatePickerDialog.OnDateSetListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
// Create a new instance of DatePickerDialog and return it
|
||||
return context?.let {
|
||||
arguments?.apply {
|
||||
if (containsKey(DEFAULT_YEAR_BUNDLE_KEY))
|
||||
mDefaultYear = getInt(DEFAULT_YEAR_BUNDLE_KEY)
|
||||
if (containsKey(DEFAULT_MONTH_BUNDLE_KEY))
|
||||
mDefaultMonth = getInt(DEFAULT_MONTH_BUNDLE_KEY)
|
||||
if (containsKey(DEFAULT_DAY_BUNDLE_KEY))
|
||||
mDefaultDay = getInt(DEFAULT_DAY_BUNDLE_KEY)
|
||||
}
|
||||
|
||||
DatePickerDialog(it, mListener, mDefaultYear, mDefaultMonth, mDefaultDay)
|
||||
} ?: super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DEFAULT_YEAR_BUNDLE_KEY = "DEFAULT_YEAR_BUNDLE_KEY"
|
||||
private const val DEFAULT_MONTH_BUNDLE_KEY = "DEFAULT_MONTH_BUNDLE_KEY"
|
||||
private const val DEFAULT_DAY_BUNDLE_KEY = "DEFAULT_DAY_BUNDLE_KEY"
|
||||
|
||||
fun getInstance(defaultYear: Int,
|
||||
defaultMonth: Int,
|
||||
defaultDay: Int): DatePickerFragment {
|
||||
return DatePickerFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putInt(DEFAULT_YEAR_BUNDLE_KEY, defaultYear)
|
||||
putInt(DEFAULT_MONTH_BUNDLE_KEY, defaultMonth)
|
||||
putInt(DEFAULT_DAY_BUNDLE_KEY, defaultDay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
|
||||
ta?.recycle()
|
||||
ta.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,9 @@ class SortDialogFragment : DialogFragment() {
|
||||
builder.setView(rootView)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok
|
||||
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
|
||||
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum,
|
||||
SortNodeEnum.SortNodeParameters(mAscending, mGroupsBefore, mRecycleBinBottom))
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
|
||||
@@ -150,10 +152,7 @@ class SortDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
interface SortSelectionListener {
|
||||
fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||
ascending: Boolean,
|
||||
groupsBefore: Boolean,
|
||||
recycleBinBottom: Boolean)
|
||||
fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.Dialog
|
||||
import android.app.TimePickerDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateFormat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
||||
class TimePickerFragment : DialogFragment() {
|
||||
|
||||
private var defaultHour: Int = 0
|
||||
private var defaultMinute: Int = 0
|
||||
|
||||
private var mListener: TimePickerDialog.OnTimeSetListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as TimePickerDialog.OnTimeSetListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
// Create a new instance of DatePickerDialog and return it
|
||||
return context?.let {
|
||||
arguments?.apply {
|
||||
if (containsKey(DEFAULT_HOUR_BUNDLE_KEY))
|
||||
defaultHour = getInt(DEFAULT_HOUR_BUNDLE_KEY)
|
||||
if (containsKey(DEFAULT_MINUTE_BUNDLE_KEY))
|
||||
defaultMinute = getInt(DEFAULT_MINUTE_BUNDLE_KEY)
|
||||
}
|
||||
|
||||
TimePickerDialog(it, mListener, defaultHour, defaultMinute, DateFormat.is24HourFormat(activity))
|
||||
} ?: super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DEFAULT_HOUR_BUNDLE_KEY = "DEFAULT_HOUR_BUNDLE_KEY"
|
||||
private const val DEFAULT_MINUTE_BUNDLE_KEY = "DEFAULT_MINUTE_BUNDLE_KEY"
|
||||
|
||||
fun getInstance(defaultHour: Int,
|
||||
defaultMinute: Int): TimePickerFragment {
|
||||
return TimePickerFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putInt(DEFAULT_HOUR_BUNDLE_KEY, defaultHour)
|
||||
putInt(DEFAULT_MINUTE_BUNDLE_KEY, defaultMinute)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,7 @@
|
||||
package com.kunzisoft.keepass.activities.lock
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
@@ -34,12 +30,9 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
|
||||
abstract class LockingActivity : StylishActivity() {
|
||||
|
||||
@@ -81,12 +74,10 @@ abstract class LockingActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
if (mTimeoutEnable) {
|
||||
mLockReceiver = LockReceiver()
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
addAction(LOCK_ACTION)
|
||||
mLockReceiver = LockReceiver {
|
||||
lockAndExit()
|
||||
}
|
||||
registerReceiver(mLockReceiver, intentFilter)
|
||||
registerLockReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
mExitLock = false
|
||||
@@ -151,29 +142,12 @@ abstract class LockingActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterLockReceiver(mLockReceiver)
|
||||
super.onDestroy()
|
||||
if (mLockReceiver != null)
|
||||
unregisterReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
inner class LockReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
// If allowed, lock and exit
|
||||
if (!TimeoutHelper.temporarilyDisableTimeout) {
|
||||
intent.action?.let {
|
||||
when (it) {
|
||||
Intent.ACTION_SCREEN_OFF -> if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this@LockingActivity)) {
|
||||
lockAndExit()
|
||||
}
|
||||
LOCK_ACTION -> lockAndExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun lockAndExit() {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
lock()
|
||||
}
|
||||
|
||||
@@ -208,20 +182,8 @@ abstract class LockingActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
fun Activity.lock() {
|
||||
// Stop the Magikeyboard service
|
||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||
MagikIME.removeEntry(this)
|
||||
closeDatabase()
|
||||
|
||||
// Stop the notification service
|
||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||
|
||||
Log.i(Activity::class.java.name, "Shutdown " + localClassName +
|
||||
" after inactivity or manual lock")
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
|
||||
cancelAll()
|
||||
}
|
||||
// Clear data
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
// Add onActivityForResult response
|
||||
setResult(LockingActivity.RESULT_EXIT_LOCK)
|
||||
finish()
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
@@ -38,6 +39,8 @@ class EntryAttachmentsAdapter(val context: Context) : RecyclerView.Adapter<Entry
|
||||
var entryAttachmentsList: MutableList<EntryAttachment> = ArrayList()
|
||||
var onItemClickListener: ((item: EntryAttachment, position: Int)->Unit)? = null
|
||||
|
||||
private val mDatabase = Database.getInstance()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
|
||||
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
|
||||
}
|
||||
@@ -49,7 +52,8 @@ class EntryAttachmentsAdapter(val context: Context) : RecyclerView.Adapter<Entry
|
||||
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
||||
entryAttachment.binaryAttachment.length())
|
||||
holder.binaryFileCompression.apply {
|
||||
if (entryAttachment.binaryAttachment.isCompressed == true) {
|
||||
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||
|| entryAttachment.binaryAttachment.isCompressed == true) {
|
||||
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.util.TypedValue
|
||||
@@ -82,15 +84,26 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
// File path
|
||||
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
|
||||
|
||||
holder.filePreciseInfoContainer.visibility = if (fileDatabaseInfo.found()) {
|
||||
// Modification
|
||||
holder.fileModification.text = fileDatabaseInfo.getModificationString()
|
||||
// Size
|
||||
holder.fileSize.text = fileDatabaseInfo.getSizeString()
|
||||
if (fileDatabaseInfo.dataAccessible()) {
|
||||
holder.fileInformation.clearColorFilter()
|
||||
} else {
|
||||
holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
|
||||
}
|
||||
|
||||
View.VISIBLE
|
||||
} else
|
||||
View.GONE
|
||||
// Modification
|
||||
if (fileDatabaseInfo.lastModificationAccessible()) {
|
||||
holder.fileModification.text = fileDatabaseInfo.getModificationString()
|
||||
holder.fileModification.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.fileModification.visibility = View.GONE
|
||||
}
|
||||
// Size
|
||||
if (fileDatabaseInfo.sizeAccessible()) {
|
||||
holder.fileSize.text = fileDatabaseInfo.getSizeString()
|
||||
holder.fileSize.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.fileSize.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Click on information
|
||||
val isExpanded = position == mExpandedPosition
|
||||
@@ -142,6 +155,10 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
return listDatabaseFiles.size
|
||||
}
|
||||
|
||||
fun clearDatabaseFileHistoryList() {
|
||||
listDatabaseFiles.clear()
|
||||
}
|
||||
|
||||
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
|
||||
listDatabaseFiles.clear()
|
||||
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
||||
@@ -178,7 +195,6 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
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,15 +21,12 @@ package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.util.Log
|
||||
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
|
||||
@@ -39,34 +36,34 @@ import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.setTextSize
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
import java.util.*
|
||||
|
||||
class NodeAdapter
|
||||
/**
|
||||
* Create node list adapter with contextMenu or not
|
||||
* @param context Context to use
|
||||
*/
|
||||
(private val context: Context)
|
||||
class NodeAdapter (private val context: Context)
|
||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||
|
||||
private var nodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||
private val nodeSortedListCallback: NodeSortedListCallback
|
||||
private val nodeSortedList: SortedList<Node>
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
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 prefSizeMultiplier: Float = 0F
|
||||
private var subtextDefaultDimension: Float = 0F
|
||||
private var infoTextDefaultDimension: Float = 0F
|
||||
private var numberChildrenTextDefaultDimension: Float = 0F
|
||||
private var iconDefaultDimension: Float = 0F
|
||||
|
||||
private var showUserNames: Boolean = true
|
||||
private var showNumberEntries: Boolean = true
|
||||
private var entryFilters = arrayOf<Group.ChildFilter>()
|
||||
@@ -87,23 +84,15 @@ class NodeAdapter
|
||||
get() = nodeSortedList.size() <= 0
|
||||
|
||||
init {
|
||||
this.infoTextDefaultDimension = context.resources.getDimension(R.dimen.list_medium_size_default)
|
||||
this.subtextDefaultDimension = context.resources.getDimension(R.dimen.list_small_size_default)
|
||||
this.numberChildrenTextDefaultDimension = context.resources.getDimension(R.dimen.list_tiny_size_default)
|
||||
this.iconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_size_default)
|
||||
|
||||
assignPreferences()
|
||||
|
||||
this.nodeSortedList = SortedList(Node::class.java, object : SortedListAdapterCallback<Node>(this) {
|
||||
override fun compare(item1: Node, item2: Node): Int {
|
||||
return listSort.getNodeComparator(ascendingSort, groupsBeforeSort, recycleBinBottomSort).compare(item1, item2)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||
return oldItem.type == newItem.type
|
||||
&& oldItem.title == newItem.title
|
||||
&& oldItem.icon == newItem.icon
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
|
||||
return item1 == item2
|
||||
}
|
||||
})
|
||||
this.nodeSortedListCallback = NodeSortedListCallback()
|
||||
this.nodeSortedList = SortedList(Node::class.java, nodeSortedListCallback)
|
||||
|
||||
// Database
|
||||
this.mDatabase = Database.getInstance()
|
||||
@@ -118,17 +107,18 @@ class NodeAdapter
|
||||
taTextColor.recycle()
|
||||
}
|
||||
|
||||
private fun assignPreferences() {
|
||||
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
|
||||
fun assignPreferences() {
|
||||
this.prefSizeMultiplier = PreferencesUtil.getListTextSize(context)
|
||||
|
||||
notifyChangeSort(
|
||||
PreferencesUtil.getListSort(context),
|
||||
SortNodeEnum.SortNodeParameters(
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||
)
|
||||
)
|
||||
|
||||
this.listSort = PreferencesUtil.getListSort(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)
|
||||
|
||||
@@ -142,15 +132,25 @@ class NodeAdapter
|
||||
* Rebuild the list by clear and build children from the group
|
||||
*/
|
||||
fun rebuildList(group: Group) {
|
||||
this.nodeSortedList.clear()
|
||||
assignPreferences()
|
||||
try {
|
||||
this.nodeSortedList.addAll(group.getChildren(*entryFilters))
|
||||
} 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()
|
||||
nodeSortedList.replaceAll(group.getFilteredChildren(*entryFilters)
|
||||
)
|
||||
}
|
||||
|
||||
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
||||
override fun compare(item1: Node, item2: Node): Int {
|
||||
return nodeComparator!!.compare(item1, item2)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||
return oldItem.type == newItem.type
|
||||
&& oldItem.title == newItem.title
|
||||
&& oldItem.icon == newItem.icon
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
|
||||
return item1 == item2
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun contains(node: Node): Boolean {
|
||||
@@ -261,10 +261,9 @@ class NodeAdapter
|
||||
/**
|
||||
* Notify a change sort of the list
|
||||
*/
|
||||
fun notifyChangeSort(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean) {
|
||||
this.listSort = sortNodeEnum
|
||||
this.ascendingSort = ascending
|
||||
this.groupsBeforeSort = groupsBefore
|
||||
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
|
||||
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||
this.nodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
@@ -291,15 +290,15 @@ class NodeAdapter
|
||||
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||
// Relative size of the icon
|
||||
layoutParams?.apply {
|
||||
height = iconSize.toInt()
|
||||
width = iconSize.toInt()
|
||||
height = (iconDefaultDimension * prefSizeMultiplier).toInt()
|
||||
width = (iconDefaultDimension * prefSizeMultiplier).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
// Assign text
|
||||
holder.text.apply {
|
||||
text = subNode.title
|
||||
setTextSize(textSizeUnit, infoTextSize)
|
||||
setTextSize(textSizeUnit, infoTextDefaultDimension, prefSizeMultiplier)
|
||||
strikeOut(subNode.isCurrentlyExpires)
|
||||
}
|
||||
// Add subText with username
|
||||
@@ -320,7 +319,7 @@ class NodeAdapter
|
||||
if (showUserNames && username.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
text = username
|
||||
setTextSize(textSizeUnit, subtextSize)
|
||||
setTextSize(textSizeUnit, subtextDefaultDimension, prefSizeMultiplier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,9 +331,9 @@ class NodeAdapter
|
||||
if (showNumberEntries) {
|
||||
holder.numberChildren?.apply {
|
||||
text = (subNode as Group)
|
||||
.getChildEntries(*entryFilters)
|
||||
.size.toString()
|
||||
setTextSize(textSizeUnit, numberChildrenTextSize)
|
||||
.getNumberOfChildEntries(*entryFilters)
|
||||
.toString()
|
||||
setTextSize(textSizeUnit, numberChildrenTextDefaultDimension, prefSizeMultiplier)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
@@ -352,7 +351,6 @@ class NodeAdapter
|
||||
|
||||
holder.container.isSelected = actionNodesList.contains(subNode)
|
||||
}
|
||||
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return nodeSortedList.size()
|
||||
|
||||
@@ -38,8 +38,8 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
private val database: Database)
|
||||
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||
|
||||
private val cursorInflater: LayoutInflater = context.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
private val cursorInflater: LayoutInflater? = context.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
private var displayUsername: Boolean = false
|
||||
private val iconColor: Int
|
||||
|
||||
@@ -58,7 +58,7 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
|
||||
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
|
||||
|
||||
val view = cursorInflater.inflate(R.layout.item_search_entry, parent, false)
|
||||
val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false)
|
||||
val viewHolder = ViewHolder()
|
||||
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
|
||||
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
|
||||
|
||||
@@ -112,6 +112,14 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
||||
ActionDatabaseAsyncTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteAllKeyFiles() {
|
||||
ActionDatabaseAsyncTask(
|
||||
{
|
||||
|
||||
@@ -38,6 +38,9 @@ interface FileDatabaseHistoryDao {
|
||||
@Delete
|
||||
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
|
||||
|
||||
@Query("UPDATE file_database_history SET keyfile_uri=null WHERE database_uri = :databaseUriString")
|
||||
fun deleteKeyFileByDatabaseUri(databaseUriString: String)
|
||||
|
||||
@Query("UPDATE file_database_history SET keyfile_uri=null")
|
||||
fun deleteAllKeyFiles()
|
||||
|
||||
|
||||
@@ -26,15 +26,14 @@ import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.autofill.Dataset
|
||||
import android.service.autofill.FillResponse
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.util.Log
|
||||
import android.view.autofill.AutofillManager
|
||||
import android.view.autofill.AutofillValue
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import java.util.*
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@@ -56,10 +55,10 @@ object AutofillHelper {
|
||||
return String.format("%s (%s)", entryInfo.title, entryInfo.username)
|
||||
if (entryInfo.title.isNotEmpty())
|
||||
return entryInfo.title
|
||||
if (entryInfo.username.isNotEmpty())
|
||||
return entryInfo.username
|
||||
if (entryInfo.url.isNotEmpty())
|
||||
return entryInfo.url
|
||||
if (entryInfo.username.isNotEmpty())
|
||||
return entryInfo.username
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -71,12 +70,12 @@ object AutofillHelper {
|
||||
val builder = Dataset.Builder(views)
|
||||
builder.setId(entryInfo.id)
|
||||
|
||||
struct.password.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.password)) }
|
||||
|
||||
val ids = ArrayList(struct.username)
|
||||
if (entryInfo.username.contains("@") || struct.username.isEmpty())
|
||||
ids.addAll(struct.email)
|
||||
ids.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.username)) }
|
||||
struct.usernameId?.let { usernameId ->
|
||||
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
|
||||
}
|
||||
struct.passwordId?.let { password ->
|
||||
builder.setValue(password, AutofillValue.forText(entryInfo.password))
|
||||
}
|
||||
|
||||
return try {
|
||||
builder.build()
|
||||
|
||||
@@ -31,7 +31,6 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@@ -42,9 +41,11 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
|
||||
if (assistStructure != null) {
|
||||
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
|
||||
GroupActivity.launchForAutofillResult(this, assistStructure, PreferencesUtil.enableReadOnlyDatabase(this))
|
||||
GroupActivity.launchForAutofillResult(this,
|
||||
assistStructure)
|
||||
else {
|
||||
FileDatabaseSelectActivity.launchForAutofillResult(this, assistStructure)
|
||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||
assistStructure)
|
||||
}
|
||||
} else {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
|
||||
@@ -35,13 +35,13 @@ class KeeAutofillService : AutofillService() {
|
||||
val fillContexts = request.fillContexts
|
||||
val latestStructure = fillContexts[fillContexts.size - 1].structure
|
||||
|
||||
cancellationSignal.setOnCancelListener { Log.e(TAG, "Cancel autofill not implemented in this sample.") }
|
||||
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
// Check user's settings for authenticating Responses and Datasets.
|
||||
val parseResult = StructureParser(latestStructure).parse()
|
||||
parseResult?.allAutofillIds()?.let { autofillIds ->
|
||||
if (listOf(*autofillIds).isNotEmpty()) {
|
||||
if (autofillIds.isNotEmpty()) {
|
||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||
// to generate Response.
|
||||
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this)
|
||||
|
||||
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.autofill
|
||||
import android.app.assist.AssistStructure
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.autofill.AutofillId
|
||||
@@ -35,74 +34,140 @@ import java.util.*
|
||||
internal class StructureParser(private val structure: AssistStructure) {
|
||||
private var result: Result? = null
|
||||
private var usernameCandidate: AutofillId? = null
|
||||
private var lockHint: Boolean = false
|
||||
|
||||
fun parse(): Result? {
|
||||
result = Result()
|
||||
result?.apply {
|
||||
usernameCandidate = null
|
||||
for (i in 0 until structure.windowNodeCount) {
|
||||
mainLoop@ for (i in 0 until structure.windowNodeCount) {
|
||||
val windowNode = structure.getWindowNodeAt(i)
|
||||
/*
|
||||
title.add(windowNode.title)
|
||||
windowNode.rootViewNode.webDomain?.let {
|
||||
webDomain.add(it)
|
||||
}
|
||||
parseViewNode(windowNode.rootViewNode)
|
||||
*/
|
||||
if (parseViewNode(windowNode.rootViewNode))
|
||||
break@mainLoop
|
||||
}
|
||||
// If not explicit username field found, add the field just before password field.
|
||||
if (username.isEmpty() && email.isEmpty()
|
||||
&& password.isNotEmpty() && usernameCandidate != null)
|
||||
username.add(usernameCandidate!!)
|
||||
if (usernameId == null && passwordId != null && usernameCandidate != null)
|
||||
usernameId = usernameCandidate
|
||||
}
|
||||
|
||||
return result
|
||||
// Return the result only if password field is retrieved
|
||||
return if (result?.passwordId != null)
|
||||
result
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
private fun parseViewNode(node: AssistStructure.ViewNode) {
|
||||
val hints = node.autofillHints
|
||||
val autofillId = node.autofillId
|
||||
if (autofillId != null) {
|
||||
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
||||
if (node.autofillId != null) {
|
||||
val hints = node.autofillHints
|
||||
if (hints != null && hints.isNotEmpty()) {
|
||||
when {
|
||||
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_USERNAME == it } -> result?.username?.add(autofillId)
|
||||
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_EMAIL_ADDRESS == it } -> result?.email?.add(autofillId)
|
||||
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_PASSWORD == it } -> result?.password?.add(autofillId)
|
||||
else -> Log.d(TAG, "unsupported hints")
|
||||
if (parseNodeByAutofillHint(node))
|
||||
return true
|
||||
} else {
|
||||
if (parseNodeByHtmlAttributes(node))
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Recursive method to process each node
|
||||
for (i in 0 until node.childCount) {
|
||||
if (parseViewNode(node.getChildAt(i)))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
|
||||
val autofillId = node.autofillId
|
||||
node.autofillHints?.forEach {
|
||||
when {
|
||||
it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_USERNAME
|
||||
|| it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_EMAIL_ADDRESS
|
||||
|| it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_PHONE -> {
|
||||
result?.usernameId = autofillId
|
||||
Log.d(TAG, "Autofill username hint")
|
||||
}
|
||||
} else if (node.autofillType == View.AUTOFILL_TYPE_TEXT) {
|
||||
val inputType = node.inputType
|
||||
when {
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 -> result?.email?.add(autofillId)
|
||||
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD > 0 -> result?.password?.add(autofillId)
|
||||
result?.password?.isEmpty() == true -> usernameCandidate = autofillId
|
||||
it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_PASSWORD
|
||||
|| it.toLowerCase(Locale.ENGLISH).contains("password") -> {
|
||||
result?.passwordId = autofillId
|
||||
Log.d(TAG, "Autofill password hint")
|
||||
return true
|
||||
}
|
||||
it.toLowerCase(Locale.ENGLISH) == "off" -> {
|
||||
Log.d(TAG, "Autofill OFF hint")
|
||||
lockHint = true
|
||||
return false
|
||||
}
|
||||
it.toLowerCase(Locale.ENGLISH) == "on" -> {
|
||||
Log.d(TAG, "Autofill ON hint")
|
||||
if (parseNodeByHtmlAttributes(node))
|
||||
return true
|
||||
}
|
||||
else -> Log.d(TAG, "Autofill unsupported hint $it")
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
|
||||
if (lockHint)
|
||||
return false
|
||||
val autofillId = node.autofillId
|
||||
val nodHtml = node.htmlInfo
|
||||
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
|
||||
"input" -> {
|
||||
nodHtml.attributes?.forEach { pairAttribute ->
|
||||
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
|
||||
"type" -> {
|
||||
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
|
||||
"tel", "email" -> {
|
||||
result?.usernameId = autofillId
|
||||
Log.d(TAG, "Autofill username type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||
}
|
||||
"text" -> {
|
||||
usernameCandidate = autofillId
|
||||
Log.d(TAG, "Autofill type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||
}
|
||||
"password" -> {
|
||||
result?.passwordId = autofillId
|
||||
Log.d(TAG, "Autofill password type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i in 0 until node.childCount)
|
||||
parseViewNode(node.getChildAt(i))
|
||||
return false
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
internal class Result {
|
||||
val title: MutableList<CharSequence>
|
||||
val webDomain: MutableList<String>
|
||||
val username: MutableList<AutofillId>
|
||||
val email: MutableList<AutofillId>
|
||||
val password: MutableList<AutofillId>
|
||||
var usernameId: AutofillId? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
init {
|
||||
title = ArrayList()
|
||||
webDomain = ArrayList()
|
||||
username = ArrayList()
|
||||
email = ArrayList()
|
||||
password = ArrayList()
|
||||
}
|
||||
var passwordId: AutofillId? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
fun allAutofillIds(): Array<AutofillId> {
|
||||
val all = ArrayList<AutofillId>()
|
||||
all.addAll(username)
|
||||
all.addAll(email)
|
||||
all.addAll(password)
|
||||
usernameId?.let {
|
||||
all.add(it)
|
||||
}
|
||||
passwordId?.let {
|
||||
all.add(it)
|
||||
}
|
||||
return all.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,8 +313,9 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
}
|
||||
|
||||
override fun onBiometricException(e: Exception) {
|
||||
if (e.localizedMessage != null)
|
||||
setAdvancedUnlockedMessageView(e.localizedMessage)
|
||||
e.localizedMessage?.let {
|
||||
setAdvancedUnlockedMessageView(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFingerPrintViews(show: Boolean) {
|
||||
|
||||
@@ -95,7 +95,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
// really not much to do when no fingerprint support found
|
||||
isKeyManagerInit = false
|
||||
} else {
|
||||
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
|
||||
|
||||
try {
|
||||
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.action
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
@@ -70,6 +71,9 @@ open class AssignPasswordInDatabaseRunnable (
|
||||
// Erase the biometric
|
||||
CipherDatabaseAction.getInstance(context)
|
||||
.deleteByDatabaseUri(mDatabaseUri)
|
||||
// Erase the register keyfile
|
||||
FileDatabaseHistoryAction.getInstance(context)
|
||||
.deleteKeyFileByDatabaseUri(mDatabaseUri)
|
||||
|
||||
if (!result.isSuccess) {
|
||||
// Erase the current master key
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
class CreateDatabaseRunnable(context: Context,
|
||||
private val mDatabase: Database,
|
||||
@@ -57,8 +58,11 @@ class CreateDatabaseRunnable(context: Context,
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Add database to recent files
|
||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||
.addOrUpdateDatabaseUri(mDatabaseUri, mKeyFile)
|
||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||
.addOrUpdateDatabaseUri(mDatabaseUri,
|
||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFile else null)
|
||||
}
|
||||
} else {
|
||||
Log.e("CreateDatabaseRunnable", "Unable to create the database")
|
||||
}
|
||||
|
||||
@@ -74,14 +74,10 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
override fun onFinishRun() {
|
||||
if (result.isSuccess) {
|
||||
// Save keyFile in app database
|
||||
val rememberKeyFile = PreferencesUtil.rememberKeyFiles(context)
|
||||
if (rememberKeyFile) {
|
||||
var keyUri = mKey
|
||||
if (!rememberKeyFile) {
|
||||
keyUri = null
|
||||
}
|
||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||
FileDatabaseHistoryAction.getInstance(context)
|
||||
.addOrUpdateDatabaseUri(mUri, keyUri)
|
||||
.addOrUpdateDatabaseUri(mUri,
|
||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mKey else null)
|
||||
}
|
||||
|
||||
// Register the biometric
|
||||
@@ -90,8 +86,11 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called
|
||||
}
|
||||
|
||||
// Register the current time to init the lock timer
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
|
||||
// Start the opening notification
|
||||
DatabaseOpenNotificationService.startIfAllowed(context)
|
||||
DatabaseOpenNotificationService.start(context)
|
||||
} else {
|
||||
mDatabase.closeAndClear(cacheDirectory)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
@@ -35,6 +36,7 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||
@@ -85,12 +87,17 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
||||
|
||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
TimeoutHelper.temporarilyDisableTimeout(activity)
|
||||
TimeoutHelper.temporarilyDisableTimeout()
|
||||
// Stop the opening notification
|
||||
DatabaseOpenNotificationService.stop(activity)
|
||||
startOrUpdateDialog(titleId, messageId, warningId)
|
||||
|
||||
}
|
||||
|
||||
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
TimeoutHelper.temporarilyDisableTimeout(activity)
|
||||
TimeoutHelper.temporarilyDisableTimeout()
|
||||
// Stop the opening notification
|
||||
DatabaseOpenNotificationService.stop(activity)
|
||||
startOrUpdateDialog(titleId, messageId, warningId)
|
||||
}
|
||||
|
||||
@@ -98,7 +105,18 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
||||
onActionFinish?.invoke(actionTask, result)
|
||||
// Remove the progress task
|
||||
ProgressTaskDialogFragment.stop(activity)
|
||||
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity)
|
||||
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
||||
|
||||
val inTime = if (activity is LockingActivity) {
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(activity)
|
||||
} else {
|
||||
TimeoutHelper.checkTime(activity)
|
||||
}
|
||||
// Start the opening notification if in time
|
||||
// (databaseOpenService is open manually in Action Open Task)
|
||||
if (actionTask != ACTION_DATABASE_LOAD_TASK && inTime) {
|
||||
DatabaseOpenNotificationService.start(activity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +144,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
||||
if (serviceConnection == null) {
|
||||
serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder).apply {
|
||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||
addActionTaskListener(actionTaskListener)
|
||||
getService().checkAction()
|
||||
}
|
||||
|
||||
@@ -415,7 +415,7 @@ class Database {
|
||||
val searchResult = search(query, SearchHelper.MAX_SEARCH_ENTRY)
|
||||
if (searchResult != null) {
|
||||
// Search in hide entries but not meta-stream
|
||||
for (entry in searchResult.getChildEntries(*Group.ChildFilter.getDefaults(context))) {
|
||||
for (entry in searchResult.getFilteredChildEntries(*Group.ChildFilter.getDefaults(context))) {
|
||||
entry.entryKDB?.let {
|
||||
cursorKDB?.addEntry(it)
|
||||
}
|
||||
@@ -493,10 +493,11 @@ class Database {
|
||||
var outputStream: OutputStream? = null
|
||||
try {
|
||||
outputStream = contentResolver.openOutputStream(uri)
|
||||
val pmo =
|
||||
mDatabaseKDB?.let { DatabaseOutputKDB(it, outputStream) }
|
||||
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, outputStream) }
|
||||
pmo?.output()
|
||||
outputStream?.let { definedOutputStream ->
|
||||
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
||||
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
|
||||
databaseOutput?.output()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw IOException(e)
|
||||
} finally {
|
||||
@@ -723,7 +724,7 @@ class Database {
|
||||
fun canRecycle(entry: Entry): Boolean {
|
||||
var canRecycle: Boolean? = null
|
||||
entry.entryKDB?.let {
|
||||
canRecycle = mDatabaseKDB?.canRecycle(it)
|
||||
canRecycle = mDatabaseKDB?.canRecycle()
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||
@@ -734,7 +735,7 @@ class Database {
|
||||
fun canRecycle(group: Group): Boolean {
|
||||
var canRecycle: Boolean? = null
|
||||
group.groupKDB?.let {
|
||||
canRecycle = mDatabaseKDB?.canRecycle(it)
|
||||
canRecycle = mDatabaseKDB?.canRecycle()
|
||||
}
|
||||
group.groupKDBX?.let {
|
||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||
|
||||
@@ -46,7 +46,7 @@ class DateInstant : Parcelable {
|
||||
}
|
||||
|
||||
constructor(string: String) {
|
||||
jDate = dateFormat.parse(string)
|
||||
jDate = dateFormat.parse(string) ?: jDate
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@@ -121,7 +121,7 @@ class DateInstant : Parcelable {
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSameDate(d1: Date?, d2: Date?): Boolean {
|
||||
private fun isSameDate(d1: Date, d2: Date): Boolean {
|
||||
val cal1 = Calendar.getInstance()
|
||||
cal1.time = d1
|
||||
cal1.set(Calendar.MILLISECOND, 0)
|
||||
@@ -142,7 +142,7 @@ class DateInstant : Parcelable {
|
||||
fun getDateTimeString(resources: Resources, date: Date): String {
|
||||
return java.text.DateFormat.getDateTimeInstance(
|
||||
java.text.DateFormat.MEDIUM,
|
||||
java.text.DateFormat.MEDIUM,
|
||||
java.text.DateFormat.SHORT,
|
||||
ConfigurationCompat.getLocales(resources.configuration)[0])
|
||||
.format(date)
|
||||
}
|
||||
|
||||
@@ -26,17 +26,25 @@ import java.util.UUID
|
||||
class DeletedObject {
|
||||
|
||||
var uuid: UUID = DatabaseVersioned.UUID_ZERO
|
||||
var deletionTime: Date? = null
|
||||
get() = if (field == null) {
|
||||
Date(System.currentTimeMillis())
|
||||
} else field
|
||||
private var mDeletionTime: Date? = null
|
||||
|
||||
fun getDeletionTime(): Date {
|
||||
if (mDeletionTime == null) {
|
||||
mDeletionTime = Date(System.currentTimeMillis())
|
||||
}
|
||||
return mDeletionTime!!
|
||||
}
|
||||
|
||||
fun setDeletionTime(deletionTime: Date) {
|
||||
this.mDeletionTime = deletionTime
|
||||
}
|
||||
|
||||
constructor()
|
||||
|
||||
@JvmOverloads
|
||||
constructor(uuid: UUID, deletionTime: Date = Date()) {
|
||||
this.uuid = uuid
|
||||
this.deletionTime = deletionTime
|
||||
this.mDeletionTime = deletionTime
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
@@ -160,6 +160,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
return contained ?: false
|
||||
}
|
||||
|
||||
override fun nodeIndexInParentForNaturalOrder(): Int {
|
||||
return entryKDB?.nodeIndexInParentForNaturalOrder()
|
||||
?: entryKDBX?.nodeIndexInParentForNaturalOrder()
|
||||
?: -1
|
||||
}
|
||||
|
||||
override var creationTime: DateInstant
|
||||
get() = entryKDB?.creationTime ?: entryKDBX?.creationTime ?: DateInstant()
|
||||
set(value) {
|
||||
@@ -241,13 +247,13 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
"$PMS_TAN_ENTRY $username"
|
||||
} else {
|
||||
if (title.isEmpty())
|
||||
if (username.isEmpty())
|
||||
if (url.isEmpty())
|
||||
nodeId.toString()
|
||||
if (url.isEmpty())
|
||||
if (username.isEmpty())
|
||||
nodeId.toString()
|
||||
else
|
||||
url
|
||||
username
|
||||
else
|
||||
username
|
||||
url
|
||||
else
|
||||
title
|
||||
}
|
||||
|
||||
@@ -187,6 +187,12 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
return contained ?: false
|
||||
}
|
||||
|
||||
override fun nodeIndexInParentForNaturalOrder(): Int {
|
||||
return groupKDB?.nodeIndexInParentForNaturalOrder()
|
||||
?: groupKDBX?.nodeIndexInParentForNaturalOrder()
|
||||
?: -1
|
||||
}
|
||||
|
||||
override var creationTime: DateInstant
|
||||
get() = groupKDB?.creationTime ?: groupKDBX?.creationTime ?: DateInstant()
|
||||
set(value) {
|
||||
@@ -225,62 +231,58 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
override val isCurrentlyExpires: Boolean
|
||||
get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false
|
||||
|
||||
override fun getChildGroups(): MutableList<Group> {
|
||||
val children = ArrayList<Group>()
|
||||
|
||||
groupKDB?.getChildGroups()?.forEach {
|
||||
children.add(Group(it))
|
||||
}
|
||||
groupKDBX?.getChildGroups()?.forEach {
|
||||
children.add(Group(it))
|
||||
}
|
||||
|
||||
return children
|
||||
override fun getChildGroups(): List<Group> {
|
||||
return groupKDB?.getChildGroups()?.map {
|
||||
Group(it)
|
||||
} ?:
|
||||
groupKDBX?.getChildGroups()?.map {
|
||||
Group(it)
|
||||
} ?:
|
||||
ArrayList()
|
||||
}
|
||||
|
||||
override fun getChildEntries(): MutableList<Entry> {
|
||||
// To cal function with vararg
|
||||
return getChildEntries(*emptyArray<ChildFilter>())
|
||||
override fun getChildEntries(): List<Entry> {
|
||||
return groupKDB?.getChildEntries()?.map {
|
||||
Entry(it)
|
||||
} ?:
|
||||
groupKDBX?.getChildEntries()?.map {
|
||||
Entry(it)
|
||||
} ?:
|
||||
ArrayList()
|
||||
}
|
||||
|
||||
fun getChildEntries(vararg filter: ChildFilter): MutableList<Entry> {
|
||||
val children = ArrayList<Entry>()
|
||||
|
||||
fun getFilteredChildEntries(vararg filter: ChildFilter): List<Entry> {
|
||||
val withoutMetaStream = filter.contains(ChildFilter.META_STREAM)
|
||||
val showExpiredEntries = !filter.contains(ChildFilter.EXPIRED)
|
||||
|
||||
groupKDB?.getChildEntries()?.forEach {
|
||||
val entryToAddAsChild = Entry(it)
|
||||
if ((!withoutMetaStream || (withoutMetaStream && !entryToAddAsChild.isMetaStream))
|
||||
&& (!entryToAddAsChild.isCurrentlyExpires or showExpiredEntries))
|
||||
children.add(entryToAddAsChild)
|
||||
}
|
||||
groupKDBX?.getChildEntries()?.forEach {
|
||||
val entryToAddAsChild = Entry(it)
|
||||
if (!entryToAddAsChild.isCurrentlyExpires or showExpiredEntries)
|
||||
children.add(entryToAddAsChild)
|
||||
}
|
||||
return groupKDB?.getChildEntries()?.filter {
|
||||
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream))
|
||||
&& (!it.isCurrentlyExpires or showExpiredEntries)
|
||||
}?.map {
|
||||
Entry(it)
|
||||
} ?:
|
||||
groupKDBX?.getChildEntries()?.filter {
|
||||
!it.isCurrentlyExpires or showExpiredEntries
|
||||
}?.map {
|
||||
Entry(it)
|
||||
} ?:
|
||||
ArrayList()
|
||||
}
|
||||
|
||||
return children
|
||||
fun getNumberOfChildEntries(vararg filter: ChildFilter): Int {
|
||||
return getFilteredChildEntries(*filter).size
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter entries and return children
|
||||
* @return List of direct children (one level below) as NodeVersioned
|
||||
*/
|
||||
fun getChildren(vararg filter: ChildFilter): List<Node> {
|
||||
val children = ArrayList<Node>()
|
||||
children.addAll(getChildGroups())
|
||||
fun getChildren(): List<Node> {
|
||||
return getChildGroups() + getChildEntries()
|
||||
}
|
||||
|
||||
groupKDB?.let {
|
||||
children.addAll(getChildEntries(*filter))
|
||||
}
|
||||
groupKDBX?.let {
|
||||
// No MetasStream in V4
|
||||
children.addAll(getChildEntries(*filter))
|
||||
}
|
||||
|
||||
return children
|
||||
fun getFilteredChildren(vararg filter: ChildFilter): List<Node> {
|
||||
return getChildGroups() + getFilteredChildEntries(*filter)
|
||||
}
|
||||
|
||||
override fun addChildGroup(group: Group) {
|
||||
|
||||
@@ -20,149 +20,198 @@
|
||||
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import java.util.*
|
||||
|
||||
enum class SortNodeEnum {
|
||||
DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME;
|
||||
|
||||
fun getNodeComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean): Comparator<Node> {
|
||||
fun <G: GroupVersionedInterface<G, *>> getNodeComparator(sortNodeParameters: SortNodeParameters)
|
||||
: Comparator<NodeVersionedInterface<G>> {
|
||||
return when (this) {
|
||||
DB -> NodeNaturalComparator(ascending, groupsBefore, false) // Force false because natural order contains recycle bin
|
||||
TITLE -> NodeTitleComparator(ascending, groupsBefore, recycleBinBottom)
|
||||
USERNAME -> NodeUsernameComparator(ascending, groupsBefore, recycleBinBottom)
|
||||
CREATION_TIME -> NodeCreationComparator(ascending, groupsBefore, recycleBinBottom)
|
||||
LAST_MODIFY_TIME -> NodeLastModificationComparator(ascending, groupsBefore, recycleBinBottom)
|
||||
LAST_ACCESS_TIME -> NodeLastAccessComparator(ascending, groupsBefore, recycleBinBottom)
|
||||
DB -> NodeNaturalComparator(sortNodeParameters) // Force false because natural order contains recycle bin
|
||||
TITLE -> NodeTitleComparator(sortNodeParameters)
|
||||
USERNAME -> NodeUsernameComparator(sortNodeParameters)
|
||||
CREATION_TIME -> NodeCreationComparator(sortNodeParameters)
|
||||
LAST_MODIFY_TIME -> NodeLastModificationComparator(sortNodeParameters)
|
||||
LAST_ACCESS_TIME -> NodeLastAccessComparator(sortNodeParameters)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class NodeComparator(var ascending: Boolean, var groupsBefore: Boolean, var recycleBinBottom: Boolean) : Comparator<Node> {
|
||||
data class SortNodeParameters(var ascending: Boolean = true,
|
||||
var groupsBefore: Boolean = true,
|
||||
var recycleBinBottom: Boolean = true)
|
||||
|
||||
abstract fun compareBySpecificOrder(object1: Node, object2: Node): Int
|
||||
abstract class NodeComparator
|
||||
<
|
||||
G: GroupVersionedInterface<*, *>,
|
||||
T: NodeVersionedInterface<G>
|
||||
>(var sortNodeParameters: SortNodeParameters)
|
||||
: Comparator<T> {
|
||||
|
||||
private fun specificOrderOrHashIfEquals(object1: Node, object2: Node): Int {
|
||||
val database = Database.getInstance()
|
||||
|
||||
abstract fun compareBySpecificOrder(object1: T, object2: T): Int
|
||||
|
||||
private fun specificOrderOrHashIfEquals(object1: T, object2: T): Int {
|
||||
val specificOrderComp = compareBySpecificOrder(object1, object2)
|
||||
|
||||
return if (specificOrderComp == 0) {
|
||||
object1.hashCode() - object2.hashCode()
|
||||
} else if (!ascending) -specificOrderComp else specificOrderComp // If descending, revert
|
||||
return when {
|
||||
specificOrderComp == 0 -> object1.hashCode() - object2.hashCode()
|
||||
sortNodeParameters.ascending -> specificOrderComp
|
||||
else -> -specificOrderComp
|
||||
}
|
||||
}
|
||||
|
||||
override fun compare(object1: Node, object2: Node): Int {
|
||||
override fun compare(object1: T, object2: T): Int {
|
||||
if (object1 == object2)
|
||||
return 0
|
||||
|
||||
if (object1.type == Type.GROUP) {
|
||||
return if (object2.type == Type.GROUP) {
|
||||
// RecycleBin at end of groups
|
||||
val database = Database.getInstance()
|
||||
if (database.isRecycleBinEnabled && recycleBinBottom) {
|
||||
if (database.recycleBin == object1)
|
||||
return 1
|
||||
if (database.recycleBin == object2)
|
||||
return -1
|
||||
when (object1.type) {
|
||||
Type.GROUP -> {
|
||||
when (object2.type) {
|
||||
Type.GROUP -> {
|
||||
// RecycleBin at end of groups
|
||||
if (database.isRecycleBinEnabled && sortNodeParameters.recycleBinBottom) {
|
||||
if (database.recycleBin == object1)
|
||||
return 1
|
||||
if (database.recycleBin == object2)
|
||||
return -1
|
||||
}
|
||||
return specificOrderOrHashIfEquals(object1, object2)
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
return if (sortNodeParameters.groupsBefore)
|
||||
-1
|
||||
else
|
||||
1
|
||||
}
|
||||
}
|
||||
specificOrderOrHashIfEquals(object1, object2)
|
||||
} else if (object2.type == Type.ENTRY) {
|
||||
if (groupsBefore)
|
||||
-1
|
||||
else
|
||||
1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
} else if (object1.type == Type.ENTRY) {
|
||||
return if (object2.type == Type.ENTRY) {
|
||||
specificOrderOrHashIfEquals(object1, object2)
|
||||
} else if (object2.type == Type.GROUP) {
|
||||
if (groupsBefore)
|
||||
1
|
||||
else
|
||||
-1
|
||||
} else {
|
||||
-1
|
||||
Type.ENTRY -> {
|
||||
return when (object2.type) {
|
||||
Type.GROUP -> {
|
||||
if (sortNodeParameters.groupsBefore)
|
||||
1
|
||||
else
|
||||
-1
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
specificOrderOrHashIfEquals(object1, object2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type not known
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator of node by natural database placement
|
||||
*/
|
||||
class NodeNaturalComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
|
||||
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
|
||||
class NodeNaturalComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
|
||||
return object1.nodePositionInParent.compareTo(object2.nodePositionInParent)
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
return object1.nodeIndexInParentForNaturalOrder()
|
||||
.compareTo(object2.nodeIndexInParentForNaturalOrder())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator of Node by Title
|
||||
*/
|
||||
class NodeTitleComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
|
||||
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
|
||||
class NodeTitleComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
|
||||
return object1.title.compareTo(object2.title, ignoreCase = true)
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val titleCompare = object1.title.compareTo(object2.title, ignoreCase = true)
|
||||
return if (titleCompare == 0)
|
||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
titleCompare
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator of Node by Username, Groups by title
|
||||
*/
|
||||
class NodeUsernameComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
|
||||
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
|
||||
class NodeUsernameComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
|
||||
if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
return if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
|
||||
// To get username if it's a ref
|
||||
return (object1 as Entry).getEntryInfo(Database.getInstance()).username
|
||||
.compareTo((object2 as Entry).getEntryInfo(Database.getInstance()).username,
|
||||
val usernameCompare = (object1 as Entry).getEntryInfo(database).username
|
||||
.compareTo((object2 as Entry).getEntryInfo(database).username,
|
||||
ignoreCase = true)
|
||||
if (usernameCompare == 0)
|
||||
NodeTitleComparator<G, T>(sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
usernameCompare
|
||||
} else {
|
||||
NodeTitleComparator<G, T>(sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
}
|
||||
return NodeTitleComparator(ascending, groupsBefore, recycleBinBottom).compare(object1, object2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator of node by creation
|
||||
*/
|
||||
class NodeCreationComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
|
||||
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
|
||||
class NodeCreationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
|
||||
return object1.creationTime.date
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val creationCompare = object1.creationTime.date
|
||||
.compareTo(object2.creationTime.date)
|
||||
return if (creationCompare == 0)
|
||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
creationCompare
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator of node by last modification
|
||||
*/
|
||||
class NodeLastModificationComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
|
||||
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
|
||||
class NodeLastModificationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
|
||||
return object1.lastModificationTime.date
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val lastModificationCompare = object1.lastModificationTime.date
|
||||
.compareTo(object2.lastModificationTime.date)
|
||||
return if (lastModificationCompare == 0)
|
||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
lastModificationCompare
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator of node by last access
|
||||
*/
|
||||
class NodeLastAccessComparator(ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean)
|
||||
: NodeComparator(ascending, groupsBefore, recycleBinBottom) {
|
||||
class NodeLastAccessComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: Node, object2: Node): Int {
|
||||
return object1.lastAccessTime.date
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val lastAccessCompare = object1.lastAccessTime.date
|
||||
.compareTo(object2.lastAccessTime.date)
|
||||
return if (lastAccessCompare == 0)
|
||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
lastAccessCompare
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,8 +231,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
* @param node Node to remove
|
||||
* @return true if node can be recycle, false elsewhere
|
||||
*/
|
||||
fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
|
||||
// TODO #394 Backup pw3
|
||||
// TODO #394 Backup KDB
|
||||
// fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
|
||||
fun canRecycle(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -36,4 +36,14 @@ abstract class EntryVersioned
|
||||
|
||||
constructor(parcel: Parcel) : super(parcel)
|
||||
|
||||
override fun nodeIndexInParentForNaturalOrder(): Int {
|
||||
if (nodeIndexInParentForNaturalOrder == -1) {
|
||||
val numberOfGroups = parent?.getChildGroups()?.size
|
||||
val indexInEntries = parent?.getChildEntries()?.indexOf(this)
|
||||
if (numberOfGroups != null && indexInEntries != null)
|
||||
return numberOfGroups + indexInEntries
|
||||
}
|
||||
return nodeIndexInParentForNaturalOrder
|
||||
}
|
||||
|
||||
}
|
||||
@@ -90,6 +90,7 @@ class FieldReferencesEngine {
|
||||
|
||||
if (result != null) {
|
||||
val found = result.entry
|
||||
found?.stopToManageFieldReferences()
|
||||
val wanted = result.wanted
|
||||
|
||||
var data: String? = null
|
||||
@@ -145,22 +146,15 @@ class FieldReferencesEngine {
|
||||
searchParametersV4.setupNone()
|
||||
|
||||
searchParametersV4.searchString = ref.substring(4)
|
||||
if (scan == 'T') {
|
||||
searchParametersV4.searchInTitles = true
|
||||
} else if (scan == 'U') {
|
||||
searchParametersV4.searchInUserNames = true
|
||||
} else if (scan == 'A') {
|
||||
searchParametersV4.searchInUrls = true
|
||||
} else if (scan == 'P') {
|
||||
searchParametersV4.searchInPasswords = true
|
||||
} else if (scan == 'N') {
|
||||
searchParametersV4.searchInNotes = true
|
||||
} else if (scan == 'I') {
|
||||
searchParametersV4.searchInUUIDs = true
|
||||
} else if (scan == 'O') {
|
||||
searchParametersV4.searchInOther = true
|
||||
} else {
|
||||
return null
|
||||
when (scan) {
|
||||
'T' -> searchParametersV4.searchInTitles = true
|
||||
'U' -> searchParametersV4.searchInUserNames = true
|
||||
'A' -> searchParametersV4.searchInUrls = true
|
||||
'P' -> searchParametersV4.searchInPasswords = true
|
||||
'N' -> searchParametersV4.searchInNotes = true
|
||||
'I' -> searchParametersV4.searchInUUIDs = true
|
||||
'O' -> searchParametersV4.searchInOther = true
|
||||
else -> return null
|
||||
}
|
||||
|
||||
val list = ArrayList<EntryKDBX>()
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.element.group
|
||||
import android.os.Parcel
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import java.util.*
|
||||
|
||||
abstract class GroupVersioned
|
||||
<
|
||||
@@ -34,9 +35,10 @@ abstract class GroupVersioned
|
||||
|
||||
private var titleGroup = ""
|
||||
@Transient
|
||||
private val childGroups = ArrayList<Group>()
|
||||
private val childGroups = LinkedList<Group>()
|
||||
@Transient
|
||||
private val childEntries = ArrayList<Entry>()
|
||||
private val childEntries = LinkedList<Entry>()
|
||||
private var positionIndexChildren = 0
|
||||
|
||||
constructor() : super()
|
||||
|
||||
@@ -52,9 +54,8 @@ abstract class GroupVersioned
|
||||
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>) {
|
||||
super.updateWith(source)
|
||||
titleGroup = source.titleGroup
|
||||
childGroups.clear()
|
||||
removeChildren()
|
||||
childGroups.addAll(source.childGroups)
|
||||
childEntries.clear()
|
||||
childEntries.addAll(source.childEntries)
|
||||
}
|
||||
|
||||
@@ -62,23 +63,27 @@ abstract class GroupVersioned
|
||||
get() = titleGroup
|
||||
set(value) { titleGroup = value }
|
||||
|
||||
override fun getChildGroups(): MutableList<Group> {
|
||||
override fun getChildGroups(): List<Group> {
|
||||
return childGroups
|
||||
}
|
||||
|
||||
override fun getChildEntries(): MutableList<Entry> {
|
||||
override fun getChildEntries(): List<Entry> {
|
||||
return childEntries
|
||||
}
|
||||
|
||||
override fun addChildGroup(group: Group) {
|
||||
if (childGroups.contains(group))
|
||||
removeChildGroup(group)
|
||||
positionIndexChildren++
|
||||
group.nodeIndexInParentForNaturalOrder = positionIndexChildren
|
||||
this.childGroups.add(group)
|
||||
}
|
||||
|
||||
override fun addChildEntry(entry: Entry) {
|
||||
if (childEntries.contains(entry))
|
||||
removeChildEntry(entry)
|
||||
positionIndexChildren++
|
||||
entry.nodeIndexInParentForNaturalOrder = positionIndexChildren
|
||||
this.childEntries.add(entry)
|
||||
}
|
||||
|
||||
@@ -95,6 +100,13 @@ abstract class GroupVersioned
|
||||
this.childEntries.clear()
|
||||
}
|
||||
|
||||
override fun nodeIndexInParentForNaturalOrder(): Int {
|
||||
return if (nodeIndexInParentForNaturalOrder == -1)
|
||||
childGroups.indexOf(this)
|
||||
else
|
||||
nodeIndexInParentForNaturalOrder
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return titleGroup
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||
|
||||
interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>, Entry> : NodeVersionedInterface<Group> {
|
||||
|
||||
fun getChildGroups(): MutableList<Group>
|
||||
fun getChildGroups(): List<Group>
|
||||
|
||||
fun getChildEntries(): MutableList<Entry>
|
||||
fun getChildEntries(): List<Entry>
|
||||
|
||||
fun addChildGroup(group: Group)
|
||||
|
||||
@@ -40,6 +40,7 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
|
||||
|
||||
fun allowAddEntryIfIsRoot(): Boolean
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun doForEachChildAndForIt(entryHandler: NodeHandler<Entry>,
|
||||
groupHandler: NodeHandler<Group>) {
|
||||
doForEachChild(entryHandler, groupHandler)
|
||||
|
||||
@@ -25,17 +25,6 @@ interface Node: NodeVersionedInterface<Group> {
|
||||
|
||||
val nodeId: NodeId<*>?
|
||||
|
||||
val nodePositionInParent: Int
|
||||
get() {
|
||||
parent?.getChildren(Group.ChildFilter.META_STREAM)?.let { children ->
|
||||
children.forEachIndexed { index, nodeVersioned ->
|
||||
if (nodeVersioned.nodeId == this.nodeId)
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun addParentFrom(node: Node) {
|
||||
parent = node.parent
|
||||
}
|
||||
|
||||
@@ -21,8 +21,7 @@ package com.kunzisoft.keepass.database.element.node
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
|
||||
class NodeIdUUID : NodeId<UUID> {
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
|
||||
val id: IdType
|
||||
get() = nodeId.id
|
||||
|
||||
var nodeIndexInParentForNaturalOrder = -1
|
||||
|
||||
protected constructor()
|
||||
|
||||
protected constructor(parcel: Parcel) {
|
||||
|
||||
@@ -47,5 +47,10 @@ interface NodeVersionedInterface<ParentGroup> : NodeTimeInterface, Parcelable {
|
||||
|
||||
fun isContainedIn(container: ParentGroup): Boolean
|
||||
|
||||
/**
|
||||
* Groups are always before in natural order (DB order)
|
||||
*/
|
||||
fun nodeIndexInParentForNaturalOrder(): Int
|
||||
|
||||
fun touch(modified: Boolean, touchParents: Boolean)
|
||||
}
|
||||
@@ -61,7 +61,9 @@ class BinaryAttachment : Parcelable {
|
||||
val compressedByte = parcel.readByte().toInt()
|
||||
isCompressed = if (compressedByte == 2) null else compressedByte != 0
|
||||
isProtected = parcel.readByte().toInt() != 0
|
||||
dataFile = File(parcel.readString())
|
||||
parcel.readString()?.let {
|
||||
dataFile = File(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
@@ -74,10 +76,10 @@ class BinaryAttachment : Parcelable {
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||
if (dataFile != null) {
|
||||
dataFile?.let { concreteDataFile ->
|
||||
// To compress, create a new binary with file
|
||||
if (isCompressed != true) {
|
||||
val fileBinaryCompress = File(dataFile!!.parent, dataFile!!.name + "_temp")
|
||||
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||
var outputStream: GZIPOutputStream? = null
|
||||
var inputStream: InputStream? = null
|
||||
try {
|
||||
@@ -91,8 +93,8 @@ class BinaryAttachment : Parcelable {
|
||||
outputStream?.close()
|
||||
|
||||
// Remove unGzip file
|
||||
if (dataFile!!.delete()) {
|
||||
if (fileBinaryCompress.renameTo(dataFile)) {
|
||||
if (concreteDataFile.delete()) {
|
||||
if (fileBinaryCompress.renameTo(concreteDataFile)) {
|
||||
// Harmonize with database compression
|
||||
isCompressed = true
|
||||
}
|
||||
@@ -104,9 +106,9 @@ class BinaryAttachment : Parcelable {
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||
if (dataFile != null) {
|
||||
dataFile?.let { concreteDataFile ->
|
||||
if (isCompressed != false) {
|
||||
val fileBinaryDecompress = File(dataFile!!.parent, dataFile!!.name + "_temp")
|
||||
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||
var outputStream: FileOutputStream? = null
|
||||
var inputStream: GZIPInputStream? = null
|
||||
try {
|
||||
@@ -120,8 +122,8 @@ class BinaryAttachment : Parcelable {
|
||||
outputStream?.close()
|
||||
|
||||
// Remove gzip file
|
||||
if (dataFile!!.delete()) {
|
||||
if (fileBinaryDecompress.renameTo(dataFile)) {
|
||||
if (concreteDataFile.delete()) {
|
||||
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
|
||||
// Harmonize with database compression
|
||||
isCompressed = false
|
||||
}
|
||||
|
||||
@@ -127,11 +127,7 @@ object DatabaseKDBXXML {
|
||||
const val ElemCustomData = "CustomData"
|
||||
const val ElemStringDictExItem = "Item"
|
||||
|
||||
val dateFormatter: ThreadLocal<SimpleDateFormat> = object : ThreadLocal<SimpleDateFormat>() {
|
||||
override fun initialValue(): SimpleDateFormat {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
return dateFormat
|
||||
}
|
||||
val DateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,7 +660,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
|
||||
ctxDeletedObject?.uuid = readUuid(xpp)
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
|
||||
ctxDeletedObject?.deletionTime = readTime(xpp)
|
||||
ctxDeletedObject?.setDeletionTime(readTime(xpp))
|
||||
} else {
|
||||
readUnknown(xpp)
|
||||
}
|
||||
@@ -829,7 +829,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
} else {
|
||||
|
||||
try {
|
||||
utcDate = DatabaseKDBXXML.dateFormatter.get()?.parse(sDate)
|
||||
utcDate = DatabaseKDBXXML.DateFormatter.parse(sDate)
|
||||
} catch (e: ParseException) {
|
||||
// Catch with null test below
|
||||
}
|
||||
|
||||
@@ -384,9 +384,9 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeObject(name: String, value: Date?) {
|
||||
private fun writeObject(name: String, value: Date) {
|
||||
if (header!!.version < DatabaseHeaderKDBX.FILE_VERSION_32_4) {
|
||||
writeObject(name, DatabaseKDBXXML.dateFormatter.get().format(value))
|
||||
writeObject(name, DatabaseKDBXXML.DateFormatter.format(value))
|
||||
} else {
|
||||
val dt = DateTime(value)
|
||||
val seconds = DateKDBXUtil.convertDateToKDBX4Time(dt)
|
||||
@@ -553,7 +553,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
||||
|
||||
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
|
||||
writeObject(DatabaseKDBXXML.ElemDeletionTime, value.deletionTime)
|
||||
writeObject(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime())
|
||||
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ package com.kunzisoft.keepass.education
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.getkeepsafe.taptargetview.TapTarget
|
||||
import com.getkeepsafe.taptargetview.TapTargetView
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -94,7 +94,8 @@ open class Education(val activity: Activity) {
|
||||
R.string.education_copy_username_key,
|
||||
R.string.education_entry_edit_key,
|
||||
R.string.education_password_generator_key,
|
||||
R.string.education_entry_new_field_key)
|
||||
R.string.education_entry_new_field_key,
|
||||
R.string.education_setup_OTP_key)
|
||||
|
||||
|
||||
/**
|
||||
@@ -271,6 +272,18 @@ open class Education(val activity: Activity) {
|
||||
context.resources.getBoolean(R.bool.education_entry_new_field_default))
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the explanatory view to setup OTP has already been displayed.
|
||||
*
|
||||
* @param context The context to open the SharedPreferences
|
||||
* @return boolean value of education_setup_OTP_key key
|
||||
*/
|
||||
fun isEducationSetupOTPPerformed(context: Context): Boolean {
|
||||
val prefs = getEducationSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.education_setup_OTP_key),
|
||||
context.resources.getBoolean(R.bool.education_setup_OTP_default))
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines if the reset education preference has been reclicked
|
||||
*
|
||||
|
||||
@@ -37,7 +37,7 @@ class EntryEditActivityEducation(activity: Activity)
|
||||
activity.getString(R.string.education_generate_password_title),
|
||||
activity.getString(R.string.education_generate_password_summary))
|
||||
.textColorInt(Color.WHITE)
|
||||
.tintTarget(false)
|
||||
.tintTarget(true)
|
||||
.cancelable(true),
|
||||
object : TapTargetView.Listener() {
|
||||
override fun onTargetClick(view: TapTargetView) {
|
||||
@@ -66,7 +66,7 @@ class EntryEditActivityEducation(activity: Activity)
|
||||
activity.getString(R.string.education_entry_new_field_title),
|
||||
activity.getString(R.string.education_entry_new_field_summary))
|
||||
.textColorInt(Color.WHITE)
|
||||
.tintTarget(false)
|
||||
.tintTarget(true)
|
||||
.cancelable(true),
|
||||
object : TapTargetView.Listener() {
|
||||
override fun onTargetClick(view: TapTargetView) {
|
||||
@@ -82,4 +82,33 @@ class EntryEditActivityEducation(activity: Activity)
|
||||
},
|
||||
R.string.education_entry_new_field_key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and display learning views
|
||||
* Displays the explanation to setup OTP
|
||||
*/
|
||||
fun checkAndPerformedSetUpOTPEducation(educationView: View,
|
||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
||||
return checkAndPerformedEducation(!isEducationSetupOTPPerformed(activity),
|
||||
TapTarget.forView(educationView,
|
||||
activity.getString(R.string.education_setup_OTP_title),
|
||||
activity.getString(R.string.education_setup_OTP_summary))
|
||||
.textColorInt(Color.WHITE)
|
||||
.tintTarget(true)
|
||||
.cancelable(true),
|
||||
object : TapTargetView.Listener() {
|
||||
override fun onTargetClick(view: TapTargetView) {
|
||||
super.onTargetClick(view)
|
||||
onEducationViewClick?.invoke(view)
|
||||
}
|
||||
|
||||
override fun onOuterCircleClick(view: TapTargetView?) {
|
||||
super.onOuterCircleClick(view)
|
||||
view?.dismiss(false)
|
||||
onOuterViewClick?.invoke(view)
|
||||
}
|
||||
},
|
||||
R.string.education_setup_OTP_key)
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,13 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
|
||||
class KeyboardLauncherActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
|
||||
GroupActivity.launchForKeyboardSelection(this, PreferencesUtil.enableReadOnlyDatabase(this))
|
||||
GroupActivity.launchForKeyboardSelection(this)
|
||||
else {
|
||||
// Pass extra to get entry
|
||||
FileDatabaseSelectActivity.launchForKeyboardSelection(this)
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.kunzisoft.keepass.magikeyboard
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.inputmethodservice.InputMethodService
|
||||
import android.inputmethodservice.Keyboard
|
||||
import android.inputmethodservice.KeyboardView
|
||||
@@ -42,8 +42,7 @@ import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.REMOVE_ENTRY_MAGIKEYBOARD_ACTION
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
|
||||
class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
|
||||
@@ -55,29 +54,18 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
private var fieldsAdapter: FieldsAdapter? = null
|
||||
private var playSoundDuringCLick: Boolean = false
|
||||
|
||||
private var lockBroadcastReceiver: BroadcastReceiver? = null
|
||||
private var lockReceiver: LockReceiver? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Remove the entry and lock the keyboard when the lock signal is receive
|
||||
lockBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
REMOVE_ENTRY_MAGIKEYBOARD_ACTION, LOCK_ACTION -> {
|
||||
lockReceiver = LockReceiver {
|
||||
removeEntryInfo()
|
||||
assignKeyboardView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerReceiver(lockBroadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(LOCK_ACTION)
|
||||
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
|
||||
}
|
||||
)
|
||||
registerLockReceiver(lockReceiver, true)
|
||||
}
|
||||
|
||||
override fun onCreateInputView(): View {
|
||||
@@ -187,12 +175,12 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
private fun switchToPreviousKeyboard() {
|
||||
var imeManager: InputMethodManager? = null
|
||||
try {
|
||||
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
switchToPreviousInputMethod()
|
||||
} else {
|
||||
window.window?.let { window ->
|
||||
imeManager.switchToLastInputMethod(window.attributes.token)
|
||||
imeManager?.switchToLastInputMethod(window.attributes.token)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -214,8 +202,8 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
KEY_BACK_KEYBOARD -> switchToPreviousKeyboard()
|
||||
|
||||
KEY_CHANGE_KEYBOARD -> {
|
||||
val imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imeManager.showInputMethodPicker()
|
||||
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?)
|
||||
?.showInputMethodPicker()
|
||||
}
|
||||
KEY_UNLOCK -> {
|
||||
}
|
||||
@@ -301,7 +289,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
|
||||
override fun onDestroy() {
|
||||
dismissCustomKeys()
|
||||
unregisterReceiver(lockBroadcastReceiver)
|
||||
unregisterLockReceiver(lockReceiver)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class EntryInfo : Parcelable {
|
||||
password = parcel.readString() ?: password
|
||||
url = parcel.readString() ?: url
|
||||
notes = parcel.readString() ?: notes
|
||||
parcel.readList(customFields, Field::class.java.classLoader)
|
||||
parcel.readList(customFields as List<Field>, Field::class.java.classLoader)
|
||||
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
|
||||
}
|
||||
|
||||
|
||||
@@ -94,20 +94,21 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
val nextNotificationId = (downloadFileUris.values.maxBy { it.notificationId }
|
||||
?.notificationId ?: notificationId) + 1
|
||||
|
||||
val entryAttachment: EntryAttachment = intent.getParcelableExtra(ATTACHMENT_KEY)
|
||||
val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
|
||||
downloadFileUris[downloadFileUri] = attachmentNotification
|
||||
try {
|
||||
AttachmentFileAsyncTask(downloadFileUri,
|
||||
attachmentNotification,
|
||||
contentResolver).apply {
|
||||
onUpdate = { uri, attachment, notificationIdAttach ->
|
||||
newNotification(uri, attachment, notificationIdAttach)
|
||||
mActionTaskListeners.forEach { actionListener ->
|
||||
actionListener.onAttachmentProgress(downloadFileUri, attachment)
|
||||
intent.getParcelableExtra<EntryAttachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
|
||||
val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
|
||||
downloadFileUris[downloadFileUri] = attachmentNotification
|
||||
AttachmentFileAsyncTask(downloadFileUri,
|
||||
attachmentNotification,
|
||||
contentResolver).apply {
|
||||
onUpdate = { uri, attachment, notificationIdAttach ->
|
||||
newNotification(uri, attachment, notificationIdAttach)
|
||||
mActionTaskListeners.forEach { actionListener ->
|
||||
actionListener.onAttachmentProgress(downloadFileUri, attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.execute()
|
||||
}.execute()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to download $downloadFileUri", e)
|
||||
}
|
||||
|
||||
@@ -22,14 +22,11 @@ package com.kunzisoft.keepass.notifications
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.exception.ClipboardException
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import java.util.*
|
||||
@@ -62,9 +59,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
|
||||
|
||||
//Get settings
|
||||
notificationTimeoutMilliSecs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getString(getString(R.string.clipboard_timeout_key),
|
||||
getString(R.string.clipboard_timeout_default))?.toLong() ?: TimeoutHelper.DEFAULT_TIMEOUT
|
||||
notificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
|
||||
|
||||
when {
|
||||
intent == null -> Log.w(TAG, "null intent")
|
||||
@@ -78,12 +73,14 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
}
|
||||
else -> for (actionKey in ClipboardEntryNotificationField.allActionKeys) {
|
||||
if (actionKey == intent.action) {
|
||||
val fieldToCopy = intent.getParcelableExtra<ClipboardEntryNotificationField>(
|
||||
ClipboardEntryNotificationField.getExtraKeyLinkToActionKey(actionKey))
|
||||
val nextFields = constructListOfField(intent)
|
||||
// Remove the current field from the next fields
|
||||
nextFields.remove(fieldToCopy)
|
||||
copyField(fieldToCopy, nextFields)
|
||||
intent.getParcelableExtra<ClipboardEntryNotificationField>(
|
||||
ClipboardEntryNotificationField.getExtraKeyLinkToActionKey(actionKey))?.let {
|
||||
fieldToCopy ->
|
||||
val nextFields = constructListOfField(intent)
|
||||
// Remove the current field from the next fields
|
||||
nextFields.remove(fieldToCopy)
|
||||
copyField(fieldToCopy, nextFields)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,10 +88,12 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
}
|
||||
|
||||
private fun constructListOfField(intent: Intent?): ArrayList<ClipboardEntryNotificationField> {
|
||||
var fieldList = ArrayList<ClipboardEntryNotificationField>()
|
||||
if (intent != null && intent.extras != null) {
|
||||
if (intent.extras!!.containsKey(EXTRA_CLIPBOARD_FIELDS))
|
||||
fieldList = intent.getParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS)
|
||||
val fieldList = ArrayList<ClipboardEntryNotificationField>()
|
||||
if (intent?.extras?.containsKey(EXTRA_CLIPBOARD_FIELDS) == true) {
|
||||
intent.getParcelableArrayListExtra<ClipboardEntryNotificationField>(EXTRA_CLIPBOARD_FIELDS)?.let { retrieveFields ->
|
||||
fieldList.clear()
|
||||
fieldList.addAll(retrieveFields)
|
||||
}
|
||||
}
|
||||
return fieldList
|
||||
}
|
||||
@@ -208,7 +207,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
private fun cleanClipboard() {
|
||||
try {
|
||||
clipboardHelper?.cleanClipboard()
|
||||
} catch (e: ClipboardException) {
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Clipboard can't be cleaned", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ import android.os.Build
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.closeDatabase
|
||||
|
||||
class DatabaseOpenNotificationService: LockNotificationService() {
|
||||
|
||||
@@ -36,8 +37,14 @@ class DatabaseOpenNotificationService: LockNotificationService() {
|
||||
private fun stopNotificationAndSendLock() {
|
||||
// Send lock action
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
// Stop the service
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun actionOnLock() {
|
||||
closeDatabase()
|
||||
// Remove the lock timer (no more needed if it exists)
|
||||
TimeoutHelper.cancelLockTimer(this)
|
||||
// Service is stopped after receive the broadcast
|
||||
super.actionOnLock()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@@ -60,13 +67,16 @@ class DatabaseOpenNotificationService: LockNotificationService() {
|
||||
|
||||
val database = Database.getInstance()
|
||||
if (database.loaded) {
|
||||
notificationManager?.notify(notificationId, buildNewNotification().apply {
|
||||
startForeground(notificationId, buildNewNotification().apply {
|
||||
setSmallIcon(R.drawable.notification_ic_database_open)
|
||||
setContentTitle(getString(R.string.database_opened))
|
||||
setContentText(database.name + " (" + database.version + ")")
|
||||
setAutoCancel(false)
|
||||
setContentIntent(pendingDatabaseIntent)
|
||||
// Unfortunately swipe is disabled in lollipop+
|
||||
setDeleteIntent(pendingDeleteIntent)
|
||||
addAction(R.drawable.ic_lock_white_24dp, getString(R.string.lock),
|
||||
pendingDeleteIntent)
|
||||
}.build())
|
||||
} else {
|
||||
stopSelf()
|
||||
@@ -80,9 +90,11 @@ class DatabaseOpenNotificationService: LockNotificationService() {
|
||||
companion object {
|
||||
const val ACTION_CLOSE_DATABASE = "ACTION_CLOSE_DATABASE"
|
||||
|
||||
fun startIfAllowed(context: Context) {
|
||||
if (PreferencesUtil.isPersistentNotificationEnable(context)) {
|
||||
// Start the opening notification
|
||||
fun start(context: Context) {
|
||||
// Start the opening notification, keep it active to receive lock
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(Intent(context, DatabaseOpenNotificationService::class.java))
|
||||
} else {
|
||||
context.startService(Intent(context, DatabaseOpenNotificationService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseR
|
||||
import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable
|
||||
import com.kunzisoft.keepass.database.action.node.*
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
@@ -209,8 +210,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
|
||||
&& intent.hasExtra(KEY_FILE_KEY)
|
||||
) {
|
||||
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY)
|
||||
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
|
||||
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
|
||||
|
||||
if (databaseUri == null)
|
||||
return null
|
||||
|
||||
return CreateDatabaseRunnable(this,
|
||||
Database.getInstance(),
|
||||
databaseUri,
|
||||
@@ -236,12 +241,15 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
|
||||
) {
|
||||
val database = Database.getInstance()
|
||||
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY)
|
||||
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
|
||||
val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY)
|
||||
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
|
||||
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
|
||||
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
|
||||
|
||||
if (databaseUri == null)
|
||||
return null
|
||||
|
||||
return LoadDatabaseRunnable(
|
||||
this,
|
||||
database,
|
||||
@@ -275,9 +283,10 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
|
||||
&& intent.hasExtra(KEY_FILE_KEY)
|
||||
) {
|
||||
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
|
||||
AssignPasswordInDatabaseRunnable(this,
|
||||
Database.getInstance(),
|
||||
intent.getParcelableExtra(DATABASE_URI_KEY),
|
||||
databaseUri,
|
||||
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
|
||||
intent.getStringExtra(MASTER_PASSWORD_KEY),
|
||||
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
|
||||
@@ -304,10 +313,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||
) {
|
||||
val database = Database.getInstance()
|
||||
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { parent ->
|
||||
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
|
||||
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
|
||||
|
||||
if (parentId == null
|
||||
|| newGroup == null)
|
||||
return null
|
||||
|
||||
database.getGroupById(parentId)?.let { parent ->
|
||||
AddGroupRunnable(this,
|
||||
database,
|
||||
intent.getParcelableExtra(GROUP_KEY),
|
||||
newGroup,
|
||||
parent,
|
||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
@@ -323,8 +339,14 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||
) {
|
||||
val database = Database.getInstance()
|
||||
database.getGroupById(intent.getParcelableExtra(GROUP_ID_KEY))?.let { oldGroup ->
|
||||
val newGroup: Group = intent.getParcelableExtra(GROUP_KEY)
|
||||
val groupId: NodeId<*>? = intent.getParcelableExtra(GROUP_ID_KEY)
|
||||
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
|
||||
|
||||
if (groupId == null
|
||||
|| newGroup == null)
|
||||
return null
|
||||
|
||||
database.getGroupById(groupId)?.let { oldGroup ->
|
||||
UpdateGroupRunnable(this,
|
||||
database,
|
||||
oldGroup,
|
||||
@@ -343,10 +365,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||
) {
|
||||
val database = Database.getInstance()
|
||||
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { parent ->
|
||||
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
|
||||
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
|
||||
|
||||
if (parentId == null
|
||||
|| newEntry == null)
|
||||
return null
|
||||
|
||||
database.getGroupById(parentId)?.let { parent ->
|
||||
AddEntryRunnable(this,
|
||||
database,
|
||||
intent.getParcelableExtra(ENTRY_KEY),
|
||||
newEntry,
|
||||
parent,
|
||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
@@ -362,8 +391,14 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||
) {
|
||||
val database = Database.getInstance()
|
||||
database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { oldEntry ->
|
||||
val newEntry: Entry = intent.getParcelableExtra(ENTRY_KEY)
|
||||
val entryId: NodeId<UUID>? = intent.getParcelableExtra(ENTRY_ID_KEY)
|
||||
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
|
||||
|
||||
if (entryId == null
|
||||
|| newEntry == null)
|
||||
return null
|
||||
|
||||
database.getEntryById(entryId)?.let { oldEntry ->
|
||||
UpdateEntryRunnable(this,
|
||||
database,
|
||||
oldEntry,
|
||||
@@ -383,7 +418,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||
) {
|
||||
val database = Database.getInstance()
|
||||
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { newParent ->
|
||||
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
|
||||
|
||||
database.getGroupById(parentId)?.let { newParent ->
|
||||
CopyNodesRunnable(this,
|
||||
database,
|
||||
getListNodesFromBundle(database, intent.extras!!),
|
||||
@@ -403,7 +440,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||
) {
|
||||
val database = Database.getInstance()
|
||||
database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { newParent ->
|
||||
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
|
||||
|
||||
database.getGroupById(parentId)?.let { newParent ->
|
||||
MoveNodesRunnable(this,
|
||||
database,
|
||||
getListNodesFromBundle(database, intent.extras!!),
|
||||
@@ -438,7 +477,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||
) {
|
||||
val database = Database.getInstance()
|
||||
database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { mainEntry ->
|
||||
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
|
||||
|
||||
database.getEntryById(entryId)?.let { mainEntry ->
|
||||
RestoreEntryHistoryDatabaseRunnable(this,
|
||||
database,
|
||||
mainEntry,
|
||||
@@ -456,7 +497,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||
) {
|
||||
val database = Database.getInstance()
|
||||
database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { mainEntry ->
|
||||
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
|
||||
|
||||
database.getEntryById(entryId)?.let { mainEntry ->
|
||||
DeleteEntryHistoryDatabaseRunnable(this,
|
||||
database,
|
||||
mainEntry,
|
||||
@@ -472,10 +515,18 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
||||
return if (intent.hasExtra(OLD_ELEMENT_KEY)
|
||||
&& intent.hasExtra(NEW_ELEMENT_KEY)
|
||||
&& intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||
|
||||
val oldElement: CompressionAlgorithm? = intent.getParcelableExtra(OLD_ELEMENT_KEY)
|
||||
val newElement: CompressionAlgorithm? = intent.getParcelableExtra(NEW_ELEMENT_KEY)
|
||||
|
||||
if (oldElement == null
|
||||
|| newElement == null)
|
||||
return null
|
||||
|
||||
return UpdateCompressionBinariesDatabaseRunnable(this,
|
||||
Database.getInstance(),
|
||||
intent.getParcelableExtra(OLD_ELEMENT_KEY),
|
||||
intent.getParcelableExtra(NEW_ELEMENT_KEY),
|
||||
oldElement,
|
||||
newElement,
|
||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||
).apply {
|
||||
mAfterSaveDatabase = { result ->
|
||||
|
||||
@@ -62,7 +62,9 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
||||
else -> {
|
||||
notificationManager?.cancel(notificationId)
|
||||
if (intent.hasExtra(ENTRY_INFO_KEY)) {
|
||||
newNotification(intent.getParcelableExtra(ENTRY_INFO_KEY))
|
||||
intent.getParcelableExtra<EntryInfo>(ENTRY_INFO_KEY)?.let {
|
||||
newNotification(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,31 +19,28 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.notifications
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.LockReceiver
|
||||
import com.kunzisoft.keepass.utils.registerLockReceiver
|
||||
import com.kunzisoft.keepass.utils.unregisterLockReceiver
|
||||
|
||||
abstract class LockNotificationService : NotificationService() {
|
||||
|
||||
private var lockBroadcastReceiver: BroadcastReceiver? = null
|
||||
private var mLockReceiver: LockReceiver? = null
|
||||
|
||||
protected open fun actionOnLock() {
|
||||
// Stop the service in all cases
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Register a lock receiver to stop notification service when lock on keyboard is performed
|
||||
lockBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
// Stop the service in all cases
|
||||
stopSelf()
|
||||
}
|
||||
mLockReceiver = LockReceiver {
|
||||
actionOnLock()
|
||||
}
|
||||
registerReceiver(lockBroadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(LOCK_ACTION)
|
||||
}
|
||||
)
|
||||
registerLockReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
protected fun stopTask(task: Thread?) {
|
||||
@@ -59,7 +56,7 @@ abstract class LockNotificationService : NotificationService() {
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
unregisterReceiver(lockBroadcastReceiver)
|
||||
unregisterLockReceiver(mLockReceiver)
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -281,8 +281,10 @@ object OtpEntryFields {
|
||||
// malformed
|
||||
return false
|
||||
}
|
||||
otpElement.period = matcher.group(1).toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
||||
otpElement.tokenType = OtpTokenType.getFromString(matcher.group(2))
|
||||
otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
|
||||
otpElement.tokenType = matcher.group(2)?.let {
|
||||
OtpTokenType.getFromString(it)
|
||||
} ?: OtpTokenType.RFC6238
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
return false
|
||||
|
||||
@@ -75,16 +75,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
activity?.let { activity ->
|
||||
allowCopyPassword()
|
||||
|
||||
findPreference<Preference>(getString(R.string.keyfile_key))?.setOnPreferenceChangeListener { _, newValue ->
|
||||
findPreference<Preference>(getString(R.string.remember_database_locations_key))?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (!(newValue as Boolean)) {
|
||||
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAllKeyFiles()
|
||||
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAll()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
findPreference<Preference>(getString(R.string.recentfile_key))?.setOnPreferenceChangeListener { _, newValue ->
|
||||
findPreference<Preference>(getString(R.string.remember_keyfile_locations_key))?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (!(newValue as Boolean)) {
|
||||
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAll()
|
||||
FileDatabaseHistoryAction.getInstance(activity.applicationContext).deleteAllKeyFiles()
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -95,8 +95,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
setPreferencesFromResource(R.xml.preferences_form_filling, rootKey)
|
||||
|
||||
activity?.let { activity ->
|
||||
val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key))
|
||||
val autofillManager = activity.getSystemService(AutofillManager::class.java)
|
||||
if (autofillManager != null && autofillManager.hasEnabledAutofillServices())
|
||||
autoFillEnablePreference?.isChecked = autofillManager.hasEnabledAutofillServices()
|
||||
@@ -143,13 +143,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
autoFillEnablePreference?.setOnPreferenceClickListener { preference ->
|
||||
(preference as SwitchPreference).isChecked = false
|
||||
val fragmentManager = fragmentManager!!
|
||||
UnavailableFeatureDialogFragment.getInstance(Build.VERSION_CODES.O)
|
||||
.show(fragmentManager, "unavailableFeatureDialog")
|
||||
false
|
||||
}
|
||||
findPreference<Preference>(getString(R.string.autofill_key))?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,13 +248,19 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric(
|
||||
activity,
|
||||
object : BiometricUnlockDatabaseHelper.BiometricUnlockErrorCallback {
|
||||
override fun onInvalidKeyException(e: Exception) {}
|
||||
|
||||
override fun onBiometricException(e: Exception) {
|
||||
fun showException(e: Exception) {
|
||||
Toast.makeText(context,
|
||||
getString(R.string.biometric_scanning_error, e.localizedMessage),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
|
||||
override fun onBiometricException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
CipherDatabaseAction.getInstance(context.applicationContext).deleteAll()
|
||||
@@ -282,6 +282,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
private fun onCreateAppearancePreferences(rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preferences_appearance, rootKey)
|
||||
|
||||
// To change list items appearance
|
||||
PreferencesUtil.APPEARANCE_CHANGED = true
|
||||
|
||||
activity?.let { activity ->
|
||||
findPreference<ListPreference>(getString(R.string.setting_style_key))?.setOnPreferenceChangeListener { _, newValue ->
|
||||
var styleEnabled = true
|
||||
@@ -342,8 +345,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
|
||||
activity?.let { activity ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key))
|
||||
if (autoFillEnablePreference != null) {
|
||||
findPreference<SwitchPreference?>(getString(R.string.settings_autofill_enable_key))?.let {
|
||||
autoFillEnablePreference ->
|
||||
val autofillManager = activity.getSystemService(AutofillManager::class.java)
|
||||
autoFillEnablePreference.isChecked = autofillManager != null
|
||||
&& autofillManager.hasEnabledAutofillServices()
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
package com.kunzisoft.keepass.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import android.net.Uri
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
@@ -28,16 +29,72 @@ import java.util.*
|
||||
|
||||
object PreferencesUtil {
|
||||
|
||||
fun rememberKeyFiles(context: Context): Boolean {
|
||||
var APPEARANCE_CHANGED = false
|
||||
|
||||
fun saveDefaultDatabasePath(context: Context, defaultDatabaseUri: Uri?) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.keyfile_key),
|
||||
context.resources.getBoolean(R.bool.keyfile_default))
|
||||
prefs?.edit()?.apply {
|
||||
defaultDatabaseUri?.let {
|
||||
putString(context.getString(R.string.default_database_path_key), it.toString())
|
||||
} ?: kotlin.run {
|
||||
remove(context.getString(R.string.default_database_path_key))
|
||||
}
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun getDefaultDatabasePath(context: Context): String? {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getString(context.getString(R.string.default_database_path_key), "")
|
||||
}
|
||||
|
||||
fun saveNodeSort(context: Context,
|
||||
sortNodeEnum: SortNodeEnum,
|
||||
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
prefs?.edit()?.apply {
|
||||
putString(context.getString(R.string.sort_node_key), sortNodeEnum.name)
|
||||
putBoolean(context.getString(R.string.sort_ascending_key), sortNodeParameters.ascending)
|
||||
putBoolean(context.getString(R.string.sort_group_before_key), sortNodeParameters.groupsBefore)
|
||||
putBoolean(context.getString(R.string.sort_recycle_bin_bottom_key), sortNodeParameters.recycleBinBottom)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun rememberDatabaseLocations(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.remember_database_locations_key),
|
||||
context.resources.getBoolean(R.bool.remember_database_locations_default))
|
||||
}
|
||||
|
||||
fun showRecentFiles(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.show_recent_files_key),
|
||||
context.resources.getBoolean(R.bool.show_recent_files_default))
|
||||
}
|
||||
|
||||
fun hideBrokenLocations(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.hide_broken_locations_key),
|
||||
context.resources.getBoolean(R.bool.hide_broken_locations_default))
|
||||
}
|
||||
|
||||
fun rememberKeyFileLocations(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.remember_keyfile_locations_key),
|
||||
context.resources.getBoolean(R.bool.remember_keyfile_locations_default))
|
||||
}
|
||||
|
||||
fun omitBackup(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.omitbackup_key),
|
||||
context.resources.getBoolean(R.bool.omitbackup_default))
|
||||
return prefs.getBoolean(context.getString(R.string.omit_backup_search_key),
|
||||
context.resources.getBoolean(R.bool.omit_backup_search_default))
|
||||
}
|
||||
|
||||
fun automaticallyFocusSearch(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.auto_focus_search_key),
|
||||
context.resources.getBoolean(R.bool.auto_focus_search_default))
|
||||
}
|
||||
|
||||
fun showUsernamesListEntries(context: Context): Boolean {
|
||||
@@ -123,6 +180,13 @@ object PreferencesUtil {
|
||||
}
|
||||
}
|
||||
|
||||
fun getClipboardTimeout(context: Context): Long {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getString(context.getString(R.string.clipboard_timeout_key),
|
||||
context.getString(R.string.clipboard_timeout_default))?.toLong()
|
||||
?: TimeoutHelper.DEFAULT_TIMEOUT
|
||||
}
|
||||
|
||||
fun isLockDatabaseWhenScreenShutOffEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.lock_database_screen_off_key),
|
||||
@@ -141,12 +205,6 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.enable_auto_save_database_default))
|
||||
}
|
||||
|
||||
fun isPersistentNotificationEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.persistent_notification_key),
|
||||
context.resources.getBoolean(R.bool.persistent_notification_default))
|
||||
}
|
||||
|
||||
fun isBiometricUnlockEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
|
||||
@@ -194,8 +252,8 @@ object PreferencesUtil {
|
||||
|
||||
fun isPasswordMask(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.maskpass_key),
|
||||
context.resources.getBoolean(R.bool.maskpass_default))
|
||||
return prefs.getBoolean(context.getString(R.string.hide_password_key),
|
||||
context.resources.getBoolean(R.bool.hide_password_default))
|
||||
}
|
||||
|
||||
fun fieldFontIsInVisibility(context: Context): Boolean {
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||
@@ -35,6 +36,7 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.view.showActionError
|
||||
|
||||
open class SettingsActivity
|
||||
: LockingActivity(),
|
||||
@@ -43,6 +45,7 @@ open class SettingsActivity
|
||||
|
||||
private var backupManager: BackupManager? = null
|
||||
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
companion object {
|
||||
@@ -74,6 +77,8 @@ open class SettingsActivity
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_toolbar)
|
||||
|
||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
toolbar?.setTitle(R.string.settings)
|
||||
setSupportActionBar(toolbar)
|
||||
@@ -92,6 +97,8 @@ open class SettingsActivity
|
||||
(supportFragmentManager
|
||||
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
|
||||
?.onProgressDialogThreadResult(actionTask, result)
|
||||
|
||||
coordinatorLayout?.showActionError(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ class HashedBlockInputStream(inputStream: InputStream) : InputStream() {
|
||||
}
|
||||
|
||||
val computedHash = messageDigest.digest(buffer)
|
||||
if (computedHash == null || computedHash.size != HASH_SIZE) {
|
||||
if (computedHash.size != HASH_SIZE) {
|
||||
throw IOException("Hash wrong size")
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import kotlin.math.min
|
||||
|
||||
class HashedBlockOutputStream : OutputStream {
|
||||
|
||||
@@ -61,11 +62,11 @@ class HashedBlockOutputStream : OutputStream {
|
||||
override fun close() {
|
||||
if (bufferPos != 0) {
|
||||
// Write remaining buffered amount
|
||||
WriteHashedBlock()
|
||||
writeHashedBlock()
|
||||
}
|
||||
|
||||
// Write terminating block
|
||||
WriteHashedBlock()
|
||||
writeHashedBlock()
|
||||
|
||||
flush()
|
||||
baseStream!!.close()
|
||||
@@ -82,12 +83,12 @@ class HashedBlockOutputStream : OutputStream {
|
||||
var counter = count
|
||||
while (counter > 0) {
|
||||
if (bufferPos == buffer!!.size) {
|
||||
WriteHashedBlock()
|
||||
writeHashedBlock()
|
||||
}
|
||||
|
||||
val copyLen = Math.min(buffer!!.size - bufferPos, counter)
|
||||
val copyLen = min(buffer!!.size - bufferPos, counter)
|
||||
|
||||
System.arraycopy(b, currentOffset, buffer, bufferPos, copyLen)
|
||||
System.arraycopy(b, currentOffset, buffer!!, bufferPos, copyLen)
|
||||
|
||||
currentOffset += copyLen
|
||||
bufferPos += copyLen
|
||||
@@ -97,21 +98,21 @@ class HashedBlockOutputStream : OutputStream {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun WriteHashedBlock() {
|
||||
private fun writeHashedBlock() {
|
||||
baseStream!!.writeUInt(bufferIndex)
|
||||
bufferIndex++
|
||||
|
||||
if (bufferPos > 0) {
|
||||
var md: MessageDigest? = null
|
||||
val messageDigest: MessageDigest
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256")
|
||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IOException("SHA-256 not implemented here.")
|
||||
}
|
||||
|
||||
val hash: ByteArray
|
||||
md!!.update(buffer, 0, bufferPos)
|
||||
hash = md.digest()
|
||||
messageDigest.update(buffer!!, 0, bufferPos)
|
||||
hash = messageDigest.digest()
|
||||
/*
|
||||
if ( bufferPos == buffer.length) {
|
||||
hash = md.digest(buffer);
|
||||
|
||||
@@ -25,7 +25,6 @@ import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.preference.PreferenceManager
|
||||
import android.text.SpannableString
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.util.Linkify
|
||||
@@ -33,6 +32,7 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.exception.ClipboardException
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
|
||||
class ClipboardHelper(private val context: Context) {
|
||||
@@ -47,7 +47,6 @@ class ClipboardHelper(private val context: Context) {
|
||||
return mClipboardManager
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun timeoutCopyToClipboard(text: String, toastString: String = "") {
|
||||
if (toastString.isNotEmpty())
|
||||
Toast.makeText(context, toastString, Toast.LENGTH_LONG).show()
|
||||
@@ -59,13 +58,9 @@ class ClipboardHelper(private val context: Context) {
|
||||
return
|
||||
}
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val sClipClear = prefs.getString(context.getString(R.string.clipboard_timeout_key),
|
||||
context.getString(R.string.clipboard_timeout_default))
|
||||
|
||||
val clipClearTime = (sClipClear ?: "300000").toLong()
|
||||
if (clipClearTime > 0) {
|
||||
mTimer.schedule(ClearClipboardTask(context, text), clipClearTime)
|
||||
val clipboardTimeout = PreferencesUtil.getClipboardTimeout(context)
|
||||
if (clipboardTimeout > 0) {
|
||||
mTimer.schedule(ClearClipboardTask(context, text), clipboardTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +85,7 @@ class ClipboardHelper(private val context: Context) {
|
||||
@Throws(ClipboardException::class)
|
||||
fun copyToClipboard(label: String, value: String) {
|
||||
try {
|
||||
getClipboardManager()?.primaryClip = ClipData.newPlainText(label, value)
|
||||
getClipboardManager()?.setPrimaryClip(ClipData.newPlainText(label, value))
|
||||
} catch (e: Exception) {
|
||||
throw ClipboardException(e)
|
||||
}
|
||||
@@ -98,12 +93,15 @@ class ClipboardHelper(private val context: Context) {
|
||||
}
|
||||
|
||||
@Throws(ClipboardException::class)
|
||||
@JvmOverloads
|
||||
fun cleanClipboard(label: String = "") {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
getClipboardManager()?.clearPrimaryClip()
|
||||
} else {
|
||||
copyToClipboard(label, "")
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
getClipboardManager()?.clearPrimaryClip()
|
||||
} else {
|
||||
copyToClipboard(label, "")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw ClipboardException(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,53 +46,63 @@ object TimeoutHelper {
|
||||
private set
|
||||
|
||||
private fun getLockPendingIntent(context: Context): PendingIntent {
|
||||
return PendingIntent.getBroadcast(context,
|
||||
return PendingIntent.getBroadcast(context.applicationContext,
|
||||
REQUEST_ID,
|
||||
Intent(LOCK_ACTION),
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the current time to check it later with checkTime
|
||||
* Start the lock timer by creating an alarm,
|
||||
* if the method is recalled with a previous lock timer pending, the previous one is deleted
|
||||
*/
|
||||
fun recordTime(context: Context) {
|
||||
// Record timeout time in case timeout service is killed
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
|
||||
private fun startLockTimer(context: Context) {
|
||||
if (Database.getInstance().loaded) {
|
||||
val timeout = PreferencesUtil.getAppTimeout(context)
|
||||
|
||||
// No timeout don't start timeout service
|
||||
if (timeout != NEVER) {
|
||||
val triggerTime = System.currentTimeMillis() + timeout
|
||||
val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
Log.d(TAG, "TimeoutHelper start")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
am.setExact(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
||||
} else {
|
||||
am.set(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
||||
// No timeout don't start timeout service
|
||||
(context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||
val triggerTime = System.currentTimeMillis() + timeout
|
||||
Log.d(TAG, "TimeoutHelper start")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
alarmManager.setExact(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
||||
} else {
|
||||
alarmManager.set(AlarmManager.RTC, triggerTime, getLockPendingIntent(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the lock timer currently pending, useful if lock was triggered by another way
|
||||
*/
|
||||
fun cancelLockTimer(context: Context) {
|
||||
(context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||
Log.d(TAG, "TimeoutHelper cancel")
|
||||
alarmManager.cancel(getLockPendingIntent(context))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the current time, to check it later with checkTime and start a new lock timer
|
||||
*/
|
||||
fun recordTime(context: Context) {
|
||||
// Record timeout time in case timeout service is killed
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
startLockTimer(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the time previously record with recordTime and do the [timeoutAction] if timeout
|
||||
* if temporarilyDisableTimeout() is called, the function as no effect until releaseTemporarilyDisableTimeoutAndCheckTime() is called
|
||||
* return 'false' if timeout, 'true' if in time
|
||||
* return 'false' and send broadcast lock action if timeout, 'true' if in time
|
||||
*/
|
||||
fun checkTime(context: Context, timeoutAction: (() -> Unit)? = null): Boolean {
|
||||
// No effect if temporarily disable
|
||||
if (temporarilyDisableTimeout)
|
||||
return true
|
||||
|
||||
// Cancel the lock PendingIntent
|
||||
if (Database.getInstance().loaded) {
|
||||
val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
Log.d(TAG, "TimeoutHelper cancel")
|
||||
am.cancel(getLockPendingIntent(context))
|
||||
}
|
||||
|
||||
// Check whether the timeout has expired
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
@@ -115,6 +125,7 @@ object TimeoutHelper {
|
||||
if (diff >= appTimeout) {
|
||||
// We have timed out
|
||||
timeoutAction?.invoke()
|
||||
context.sendBroadcast(Intent(LOCK_ACTION))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -142,27 +153,14 @@ object TimeoutHelper {
|
||||
/**
|
||||
* Temporarily disable timeout, checkTime() function always return true
|
||||
*/
|
||||
fun temporarilyDisableTimeout(context: Context) {
|
||||
fun temporarilyDisableTimeout() {
|
||||
temporarilyDisableTimeout = true
|
||||
|
||||
// Stop the opening notification
|
||||
DatabaseOpenNotificationService.stop(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the temporarily disable timeout and directly call checkTime()
|
||||
* Release the temporarily disable timeout
|
||||
*/
|
||||
fun releaseTemporarilyDisableTimeoutAndLockIfTimeout(context: Context): Boolean {
|
||||
fun releaseTemporarilyDisableTimeout() {
|
||||
temporarilyDisableTimeout = false
|
||||
val inTime = if (context is LockingActivity) {
|
||||
checkTimeAndLockIfTimeout(context)
|
||||
} else {
|
||||
checkTime(context)
|
||||
}
|
||||
if (inTime) {
|
||||
// Start the opening notification
|
||||
DatabaseOpenNotificationService.startIfAllowed(context)
|
||||
}
|
||||
return inTime
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,91 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.utils
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
|
||||
const val DATABASE_START_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_START_TASK_ACTION"
|
||||
const val DATABASE_STOP_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_STOP_TASK_ACTION"
|
||||
|
||||
const val LOCK_ACTION = "com.kunzisoft.keepass.LOCK"
|
||||
|
||||
const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY_MAGIKEYBOARD"
|
||||
|
||||
class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
|
||||
|
||||
private val screenOffHandler = Handler()
|
||||
private var screenOffRunnable: Runnable? = null
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
|
||||
screenOffRunnable?.let { runnable ->
|
||||
screenOffHandler.removeCallbacks(runnable)
|
||||
}
|
||||
// If allowed, lock and exit
|
||||
if (!TimeoutHelper.temporarilyDisableTimeout) {
|
||||
intent.action?.let {
|
||||
when (it) {
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) {
|
||||
screenOffRunnable = Runnable {
|
||||
lockAction.invoke()
|
||||
}
|
||||
// Launch the effective action after a small time
|
||||
screenOffHandler.postDelayed(screenOffRunnable!!,
|
||||
context.getString(R.string.timeout_screen_off).toLong())
|
||||
}
|
||||
}
|
||||
LOCK_ACTION,
|
||||
REMOVE_ENTRY_MAGIKEYBOARD_ACTION -> lockAction.invoke()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.registerLockReceiver(lockReceiver: LockReceiver?,
|
||||
registerRemoveEntryMagikeyboard: Boolean = false) {
|
||||
lockReceiver?.let {
|
||||
registerReceiver(it, IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
addAction(Intent.ACTION_SCREEN_ON)
|
||||
addAction(LOCK_ACTION)
|
||||
if (registerRemoveEntryMagikeyboard)
|
||||
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.unregisterLockReceiver(lockReceiver: LockReceiver?) {
|
||||
lockReceiver?.let {
|
||||
unregisterReceiver(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.closeDatabase() {
|
||||
// Stop the Magikeyboard service
|
||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||
MagikIME.removeEntry(this)
|
||||
|
||||
// Stop the notification service
|
||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||
|
||||
Log.i(Context::class.java.name, "Shutdown after inactivity or manual lock")
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?)?.apply {
|
||||
cancelAll()
|
||||
}
|
||||
// Clear data
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
}
|
||||
@@ -22,8 +22,6 @@ package com.kunzisoft.keepass.utils
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.text.format.Formatter
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
@@ -34,7 +32,7 @@ open class FileInfo : Serializable {
|
||||
var fileUri: Uri?
|
||||
var filePath: String? = null
|
||||
var fileName: String? = ""
|
||||
var lastModification = Date()
|
||||
var lastModification = Date(0L)
|
||||
var size: Long = 0L
|
||||
|
||||
constructor(context: Context, fileUri: Uri) {
|
||||
@@ -51,22 +49,11 @@ open class FileInfo : Serializable {
|
||||
|
||||
fun init() {
|
||||
this.filePath = fileUri?.path
|
||||
if (EXTERNAL_STORAGE_AUTHORITY == fileUri?.authority) {
|
||||
fileUri?.let { fileUri ->
|
||||
DocumentFile.fromSingleUri(context, fileUri)?.let { file ->
|
||||
size = file.length()
|
||||
fileName = file.name
|
||||
lastModification = Date(file.lastModified())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filePath?.let {
|
||||
File(it).let { file ->
|
||||
size = file.length()
|
||||
fileName = file.name
|
||||
lastModification = Date(file.lastModified())
|
||||
}
|
||||
}
|
||||
|
||||
UriUtil.getFileData(context, fileUri)?.let { file ->
|
||||
size = file.length()
|
||||
fileName = file.name
|
||||
lastModification = Date(file.lastModified())
|
||||
}
|
||||
|
||||
if (fileName == null || fileName!!.isEmpty()) {
|
||||
@@ -74,10 +61,18 @@ open class FileInfo : Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
fun found(): Boolean {
|
||||
fun lastModificationAccessible(): Boolean {
|
||||
return lastModification.after(Date(0L))
|
||||
}
|
||||
|
||||
fun sizeAccessible(): Boolean {
|
||||
return size != 0L
|
||||
}
|
||||
|
||||
fun dataAccessible(): Boolean {
|
||||
return UriUtil.isUriAccessible(context.contentResolver, fileUri)
|
||||
}
|
||||
|
||||
fun getModificationString(): String {
|
||||
return DateFormat.getDateTimeInstance()
|
||||
.format(lastModification)
|
||||
@@ -86,9 +81,4 @@ open class FileInfo : Serializable {
|
||||
fun getSizeString(): String {
|
||||
return Formatter.formatFileSize(context, size)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTERNAL_STORAGE_AUTHORITY = "com.android.externalstorage.documents"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,5 +100,5 @@ object ParcelableUtil {
|
||||
inline fun <reified T : Enum<T>> Parcel.readEnum() =
|
||||
readString()?.let { enumValueOf<T>(it) }
|
||||
|
||||
inline fun <T : Enum<T>> Parcel.writeEnum(value: T?) =
|
||||
fun <T : Enum<T>> Parcel.writeEnum(value: T?) =
|
||||
writeString(value?.name)
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.utils
|
||||
|
||||
import java.util.ArrayList
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
|
||||
object StringUtil {
|
||||
|
||||
@@ -85,5 +84,17 @@ object StringUtil {
|
||||
|
||||
return currentText
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun UUID.toKeePassRefString(): String {
|
||||
val tempString = toString().replace("-", "").toUpperCase(Locale.ENGLISH)
|
||||
return StringBuffer(reverseString2(tempString.substring(12, 16)))
|
||||
.append(reverseString2(tempString.substring(8, 12)))
|
||||
.append(reverseString2(tempString.substring(0, 8)))
|
||||
.append(reverseString2(tempString.substring(20, 32)))
|
||||
.append(reverseString2(tempString.substring(16, 20))).toString()
|
||||
}
|
||||
|
||||
private fun reverseString2(string: String): String {
|
||||
return string.chunked(2).reversed().joinToString("")
|
||||
}
|
||||
|
||||
@@ -24,29 +24,82 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.kunzisoft.keepass.R
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
|
||||
object UriUtil {
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun getUriInputStream(contentResolver: ContentResolver, uri: Uri?): InputStream? {
|
||||
if (uri == null)
|
||||
return null
|
||||
val scheme = uri.scheme
|
||||
return if (scheme == null || scheme.isEmpty() || scheme == "file") {
|
||||
FileInputStream(uri.path!!)
|
||||
} else if (scheme == "content") {
|
||||
contentResolver.openInputStream(uri)
|
||||
} else {
|
||||
null
|
||||
fun isUriAccessible(contentResolver: ContentResolver, fileUri: Uri?): Boolean {
|
||||
if (fileUri == null)
|
||||
return false
|
||||
return try {
|
||||
//https://developer.android.com/reference/android/content/res/AssetFileDescriptor
|
||||
contentResolver.openInputStream(fileUri)?.close()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(UriUtil.javaClass.name, "Unable to access uri $fileUri : ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun isUriWritable(fileUri: Uri?): Boolean {
|
||||
if (fileUri == null)
|
||||
return false
|
||||
// TODO Uri writeable detection
|
||||
return true
|
||||
}
|
||||
|
||||
fun getFileData(context: Context, fileUri: Uri?): DocumentFile? {
|
||||
if (fileUri == null)
|
||||
return null
|
||||
return when {
|
||||
isFileScheme(fileUri) -> {
|
||||
fileUri.path?.let {
|
||||
File(it).let { file ->
|
||||
return DocumentFile.fromFile(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
isContentScheme(fileUri) -> DocumentFile.fromSingleUri(context, fileUri)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun getUriInputStream(contentResolver: ContentResolver, fileUri: Uri?): InputStream? {
|
||||
if (fileUri == null)
|
||||
return null
|
||||
return when {
|
||||
isFileScheme(fileUri) -> fileUri.path?.let { FileInputStream(it) }
|
||||
isContentScheme(fileUri) -> contentResolver.openInputStream(fileUri)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isFileScheme(fileUri: Uri): Boolean {
|
||||
val scheme = fileUri.scheme
|
||||
if (scheme == null || scheme.isEmpty() || scheme.toLowerCase(Locale.ENGLISH) == "file") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isContentScheme(fileUri: Uri): Boolean {
|
||||
val scheme = fileUri.scheme
|
||||
if (scheme != null && scheme.toLowerCase(Locale.ENGLISH) == "content") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun parse(stringUri: String?): Uri? {
|
||||
return if (stringUri?.isNotEmpty() == true) {
|
||||
Uri.parse(stringUri)
|
||||
|
||||
@@ -69,8 +69,8 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
|
||||
private fun inflate(context: Context) {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
inflater.inflate(R.layout.view_button_add_node, this)
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_button_add_node, this)
|
||||
|
||||
addEntryEnable = true
|
||||
addGroupEnable = true
|
||||
@@ -132,7 +132,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
|
||||
fun showButton() {
|
||||
if (addButtonView?.visibility != VISIBLE)
|
||||
if (isEnable && addButtonView?.visibility != VISIBLE)
|
||||
addButtonView?.show(onAddButtonVisibilityChangedListener)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
||||
|
||||
init {
|
||||
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
inflater.inflate(R.layout.view_advanced_unlock, this)
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_advanced_unlock, this)
|
||||
|
||||
unlockContainerView = findViewById(R.id.fingerprint_container)
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsAdapter
|
||||
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
||||
@@ -40,8 +41,8 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.utils.toKeePassRefString
|
||||
import java.util.*
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
|
||||
|
||||
class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
@@ -91,6 +92,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
private val historyAdapter = EntryHistoryAdapter(context)
|
||||
|
||||
private val uuidView: TextView
|
||||
private val uuidReferenceView: TextView
|
||||
|
||||
val isUserNamePresent: Boolean
|
||||
get() = userNameContainerView.visibility == View.VISIBLE
|
||||
@@ -99,8 +101,8 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
get() = passwordContainerView.visibility == View.VISIBLE
|
||||
|
||||
init {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
inflater.inflate(R.layout.view_entry_contents, this)
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_entry_contents, this)
|
||||
|
||||
userNameContainerView = findViewById(R.id.entry_user_name_container)
|
||||
userNameView = findViewById(R.id.entry_user_name)
|
||||
@@ -146,6 +148,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
|
||||
uuidView = findViewById(R.id.entry_UUID)
|
||||
uuidReferenceView = findViewById(R.id.entry_UUID_reference)
|
||||
|
||||
val attrColorAccent = intArrayOf(R.attr.colorAccent)
|
||||
val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent)
|
||||
@@ -346,6 +349,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
|
||||
fun assignUUID(uuid: UUID) {
|
||||
uuidView.text = uuid.toString()
|
||||
uuidReferenceView.text = uuid.toKeePassRefString()
|
||||
}
|
||||
|
||||
/* -------------
|
||||
|
||||
@@ -43,8 +43,8 @@ open class EntryCustomField @JvmOverloads constructor(context: Context,
|
||||
|
||||
init {
|
||||
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
inflater.inflate(R.layout.item_entry_new_field, this)
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.item_entry_new_field, this)
|
||||
|
||||
labelView = findViewById(R.id.title)
|
||||
valueView = findViewById(R.id.value)
|
||||
|
||||
@@ -21,21 +21,21 @@ package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.*
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import org.joda.time.Duration
|
||||
import org.joda.time.Instant
|
||||
|
||||
class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@@ -52,16 +52,26 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
private val entryPasswordLayoutView: TextInputLayout
|
||||
private val entryPasswordView: EditText
|
||||
private val entryConfirmationPasswordView: EditText
|
||||
val generatePasswordView: View
|
||||
private val entryCommentView: EditText
|
||||
private val entryExpiresCheckBox: CompoundButton
|
||||
private val entryExpiresTextView: TextView
|
||||
private val entryNotesView: EditText
|
||||
private val entryExtraFieldsContainer: ViewGroup
|
||||
val addNewFieldButton: View
|
||||
|
||||
private var iconColor: Int = 0
|
||||
private var expiresInstant: DateInstant = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
|
||||
|
||||
var onDateClickListener: OnClickListener? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (entryExpiresCheckBox.isChecked)
|
||||
entryExpiresTextView.setOnClickListener(value)
|
||||
else
|
||||
entryExpiresTextView.setOnClickListener(null)
|
||||
}
|
||||
|
||||
init {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
inflater.inflate(R.layout.view_entry_edit_contents, this)
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_entry_edit_contents, this)
|
||||
|
||||
entryTitleLayoutView = findViewById(R.id.entry_edit_container_title)
|
||||
entryTitleView = findViewById(R.id.entry_edit_title)
|
||||
@@ -71,10 +81,14 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
entryPasswordLayoutView = findViewById(R.id.entry_edit_container_password)
|
||||
entryPasswordView = findViewById(R.id.entry_edit_password)
|
||||
entryConfirmationPasswordView = findViewById(R.id.entry_edit_confirmation_password)
|
||||
generatePasswordView = findViewById(R.id.entry_edit_generate_button)
|
||||
entryCommentView = findViewById(R.id.entry_edit_notes)
|
||||
entryExpiresCheckBox = findViewById(R.id.entry_edit_expires_checkbox)
|
||||
entryExpiresTextView = findViewById(R.id.entry_edit_expires_text)
|
||||
entryNotesView = findViewById(R.id.entry_edit_notes)
|
||||
entryExtraFieldsContainer = findViewById(R.id.entry_edit_advanced_container)
|
||||
addNewFieldButton = findViewById(R.id.entry_edit_add_new_field)
|
||||
|
||||
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
|
||||
assignExpiresDateText()
|
||||
}
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
@@ -141,32 +155,46 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnPasswordGeneratorClickListener(clickListener: () -> Unit) {
|
||||
generatePasswordView.setOnClickListener { clickListener.invoke() }
|
||||
private fun assignExpiresDateText() {
|
||||
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
||||
entryExpiresTextView.setOnClickListener(onDateClickListener)
|
||||
expiresInstant.getDateTimeString(resources)
|
||||
} else {
|
||||
entryExpiresTextView.setOnClickListener(null)
|
||||
resources.getString(R.string.never)
|
||||
}
|
||||
if (fontInVisibility)
|
||||
entryExpiresTextView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var expires: Boolean
|
||||
get() {
|
||||
return entryExpiresCheckBox.isChecked
|
||||
}
|
||||
set(value) {
|
||||
entryExpiresCheckBox.isChecked = value
|
||||
assignExpiresDateText()
|
||||
}
|
||||
|
||||
var expiresDate: DateInstant
|
||||
get() {
|
||||
return expiresInstant
|
||||
}
|
||||
set(value) {
|
||||
expiresInstant = value
|
||||
assignExpiresDateText()
|
||||
}
|
||||
|
||||
var notes: String
|
||||
get() {
|
||||
return entryCommentView.text.toString()
|
||||
return entryNotesView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryCommentView.setText(value)
|
||||
entryNotesView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryCommentView.applyFontVisibility()
|
||||
entryNotesView.applyFontVisibility()
|
||||
}
|
||||
|
||||
fun allowCustomField(allow: Boolean, action: () -> Unit) {
|
||||
addNewFieldButton.apply {
|
||||
if (allow) {
|
||||
visibility = View.VISIBLE
|
||||
setOnClickListener { action.invoke() }
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val customFields: MutableList<Field>
|
||||
get() {
|
||||
val customFieldsArray = ArrayList<Field>()
|
||||
@@ -228,14 +256,6 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
fun isValid(): Boolean {
|
||||
var isValid = true
|
||||
|
||||
// Require title
|
||||
if (entryTitleView.text.toString().isEmpty()) {
|
||||
entryTitleLayoutView.error = context.getString(R.string.error_title_required)
|
||||
isValid = false
|
||||
} else {
|
||||
entryTitleLayoutView.error = null
|
||||
}
|
||||
|
||||
// Validate password
|
||||
if (entryPasswordView.text.toString() != entryConfirmationPasswordView.text.toString()) {
|
||||
entryPasswordLayoutView.error = context.getString(R.string.error_pass_match)
|
||||
|
||||
@@ -54,8 +54,8 @@ class EntryEditCustomField @JvmOverloads constructor(context: Context,
|
||||
|
||||
init {
|
||||
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
inflater.inflate(R.layout.view_entry_new_field, this)
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_entry_new_field, this)
|
||||
|
||||
val deleteView = findViewById<View>(R.id.entry_new_field_delete)
|
||||
deleteView.setOnClickListener { deleteViewFromParent() }
|
||||
@@ -103,7 +103,7 @@ class EntryEditCustomField @JvmOverloads constructor(context: Context,
|
||||
parent.removeView(this)
|
||||
parent.invalidate()
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(javaClass.name, e.message)
|
||||
Log.e(javaClass.name, "Unable to delete view", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
|
||||
@@ -29,8 +29,11 @@ import android.view.View
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Replace font by monospace, must be called after seText()
|
||||
@@ -50,6 +53,11 @@ fun TextView.applyHiddenStyle(hide: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
fun TextView.setTextSize(unit: Int, defaultSize: Float, multiplier: Float) {
|
||||
if (multiplier > 0.0F && multiplier != 1.0F)
|
||||
setTextSize(unit, defaultSize * multiplier)
|
||||
}
|
||||
|
||||
fun TextView.strikeOut(strikeOut: Boolean) {
|
||||
paintFlags = if (strikeOut)
|
||||
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
||||
@@ -99,4 +107,14 @@ fun Toolbar.expand(animate: Boolean = true) {
|
||||
play(slideAnimator)
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun CoordinatorLayout.showActionError(result: ActionRunnable.Result) {
|
||||
if (!result.isSuccess) {
|
||||
result.exception?.errorId?.let { errorId ->
|
||||
Snackbar.make(this, errorId, Snackbar.LENGTH_LONG).asError().show()
|
||||
} ?: result.message?.let { message ->
|
||||
Snackbar.make(this, message, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,7 +384,7 @@ typedef struct _master_key {
|
||||
} master_key;
|
||||
|
||||
|
||||
void *generate_key_material(void *arg) {
|
||||
uint32_t generate_key_material(void *arg) {
|
||||
#if defined(KPD_PROFILE)
|
||||
struct timespec start, end;
|
||||
#endif
|
||||
@@ -435,7 +435,7 @@ void *generate_key_material(void *arg) {
|
||||
pthread_mutex_unlock(&mk->lock2);
|
||||
}
|
||||
|
||||
return (void *)flip;
|
||||
return flip;
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL Java_com_kunzisoft_keepass_crypto_finalkey_NativeFinalKey_nTransformMasterKey(JNIEnv *env, jobject this, jbyteArray seed, jbyteArray key, jlong rounds) {
|
||||
@@ -474,12 +474,12 @@ JNIEXPORT jbyteArray JNICALL Java_com_kunzisoft_keepass_crypto_finalkey_NativeFi
|
||||
(*env)->GetByteArrayRegion(env, key, 0, MASTER_KEY_SIZE, (jbyte *)mk.key1);
|
||||
|
||||
// step 2: encrypt the hash "rounds" (default: 6000) times
|
||||
iret = pthread_create( &t1, NULL, generate_key_material, (void*)&mk );
|
||||
iret = pthread_create( &t1, NULL, (void*)generate_key_material, (void*)&mk );
|
||||
if( iret != 0 ) {
|
||||
(*env)->ThrowNew(env, bad_arg, "TransformMasterKey: failed to launch thread 1"); // FIXME: get a better exception class for this...
|
||||
return NULL;
|
||||
}
|
||||
iret = pthread_create( &t2, NULL, generate_key_material, (void*)&mk );
|
||||
iret = pthread_create( &t2, NULL, (void*)generate_key_material, (void*)&mk );
|
||||
if( iret != 0 ) {
|
||||
(*env)->ThrowNew(env, bad_arg, "TransformMasterKey: failed to launch thread 2"); // FIXME: get a better exception class for this...
|
||||
return NULL;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:color="@color/white"
|
||||
tools:targetApi="lollipop">
|
||||
<item>
|
||||
<shape>
|
||||
<stroke android:color="?attr/colorAccent" android:width="1dp"/>
|
||||
<solid android:color="@color/transparent"/>
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<stroke android:color="@color/grey" android:width="1dp"/>
|
||||
<solid android:color="@color/transparent"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
9
app/src/main/res/drawable/ic_attach_file_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_attach_file_white_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M16.5,6v11.5c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4V5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5v10.5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V6H10v9.5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5V5c0,-2.21 -1.79,-4 -4,-4S7,2.79 7,5v12.5c0,3.04 2.46,5.5 5.5,5.5s5.5,-2.46 5.5,-5.5V6h-1.5z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M11,17c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1zM11,3v4h2L13,5.08c3.39,0.49 6,3.39 6,6.92 0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-1.68 0.59,-3.22 1.58,-4.42L12,13l1.41,-1.41 -6.8,-6.8v0.02C4.42,6.45 3,9.05 3,12c0,4.97 4.02,9 9,9 4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9h-1zM18,12c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1 0.45,1 1,1 1,-0.45 1,-1zM6,12c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_check_white_24dp.xml
Normal file
5
app/src/main/res/drawable/ic_check_white_24dp.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
</vector>
|
||||
118
app/src/main/res/drawable/ic_generate_password_white_24dp.xml
Normal file
118
app/src/main/res/drawable/ic_generate_password_white_24dp.xml
Normal file
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M9.77344,3.13281 C9.4515,3.1329,9.12601,3.22745,8.83789,3.42188 L1.73828,8.21289
|
||||
C1.25807,8.53693,1.00007,9.06311,1,9.59961
|
||||
C0.999958,9.92151,1.09268,10.247,1.28711,10.5352 L6.07813,17.6328
|
||||
C6.5966,18.4011,7.63207,18.6025,8.40039,18.084 L8.59375,17.9531
|
||||
C7.93444,17.5713,7.49368,16.9397,7.33398,16.2305
|
||||
C7.25701,16.1464,7.21481,16.0308,7.23828,15.9102 L7.27148,15.7422
|
||||
C7.271,15.7299,7.26758,15.7194,7.26758,15.707 L7.09961,15.8184
|
||||
C6.92859,15.9337,6.69736,15.8897,6.58203,15.7188
|
||||
C6.46665,15.5479,6.51062,15.3165,6.68164,15.2012 L6.86523,15.0801
|
||||
L6.65039,15.0371 C6.44804,14.9977,6.31618,14.8059,6.35547,14.6035
|
||||
C6.39476,14.4011,6.58863,14.2693,6.79102,14.3086 L7.00586,14.3516
|
||||
L6.88477,14.1699 C6.76939,13.9991,6.81335,13.7677,6.98438,13.6523
|
||||
C7.1554,13.537,7.38468,13.581,7.5,13.752 L7.62305,13.9316 L7.66406,13.7188
|
||||
C7.69855,13.5416,7.8519,13.4274,8.02539,13.4258 L11.1816,6.77344
|
||||
C11.4309,6.24809,11.8397,5.84958,12.3203,5.58984 L11.1602,3.87109
|
||||
C10.8361,3.3909,10.3119,3.13267,9.77539,3.13281 L9.77344,3.13281 Z
|
||||
M9.70703,5.15625 C9.90938,5.19561,10.0373,5.39136,9.99805,5.59375
|
||||
L9.95898,5.80859 L10.1387,5.68555
|
||||
C10.3097,5.57024,10.5409,5.61618,10.6563,5.78711
|
||||
C10.7716,5.95797,10.7257,6.18741,10.5547,6.30273 L10.375,6.42578
|
||||
L10.5879,6.46484 C10.7902,6.5042,10.922,6.70197,10.8828,6.9043
|
||||
C10.8435,7.1067,10.6496,7.23855,10.4473,7.19922 L10.2344,7.15625
|
||||
L10.3535,7.33594 C10.4689,7.5068,10.4249,7.73817,10.2539,7.85352
|
||||
C10.0829,7.9688,9.85163,7.92289,9.73633,7.75195 L9.61523,7.57031
|
||||
L9.57227,7.78516 C9.53298,7.98758,9.34104,8.11943,9.13867,8.08008
|
||||
C8.93633,8.04072,8.80451,7.84684,8.84375,7.64453 L8.88477,7.43164
|
||||
L8.70313,7.55273 C8.53228,7.66806,8.30282,7.62406,8.1875,7.45313
|
||||
C8.07212,7.28226,8.11609,7.05088,8.28711,6.93555 L8.46875,6.8125
|
||||
L8.25391,6.76953 C8.05154,6.73017,7.91971,6.53833,7.95898,6.33594
|
||||
C7.99827,6.13352,8.19412,6.00167,8.39648,6.04102 L8.61133,6.08398
|
||||
L8.48828,5.90234 C8.3729,5.73148,8.41882,5.50205,8.58984,5.38672
|
||||
C8.76069,5.27139,8.99014,5.31344,9.10547,5.48438 L9.22656,5.66602
|
||||
L9.26758,5.45313 C9.30687,5.25073,9.50467,5.11692,9.70703,5.15625 Z
|
||||
M13.6602,6.24414 C13.0088,6.21529,12.3742,6.57309,12.0762,7.20117
|
||||
L8.40625,14.9375 C8.00883,15.7749,8.36373,16.7686,9.20117,17.166
|
||||
L16.9375,20.8379 C17.7749,21.2353,18.7686,20.8804,19.166,20.043 L22.8379,12.3066
|
||||
C23.2353,11.4693,22.8804,10.4736,22.043,10.0762 L14.3066,6.40625
|
||||
C14.0973,6.30689,13.8773,6.25374,13.6602,6.24414 Z M4.62109,8.49023
|
||||
C4.66856,8.4801,4.71886,8.48028,4.76953,8.49023
|
||||
C4.97188,8.52959,5.10374,8.72142,5.06445,8.92383 L5.02148,9.13867
|
||||
L5.20313,9.01758 C5.37397,8.90227,5.60343,8.94625,5.71875,9.11719
|
||||
C5.83413,9.28805,5.78999,9.51944,5.61914,9.63477 L5.43945,9.75586
|
||||
L5.65039,9.79492 C5.85276,9.83428,5.9865,10.0321,5.94727,10.2344
|
||||
C5.90798,10.4368,5.71213,10.5686,5.50977,10.5293 L5.29688,10.4883
|
||||
L5.41797,10.668 C5.53335,10.8388,5.48938,11.0683,5.31836,11.1836
|
||||
C5.14751,11.2989,4.91806,11.253,4.80273,11.082 L4.67969,10.9023 L4.63672,11.1172
|
||||
C4.59743,11.3196,4.40355,11.4495,4.20117,11.4102
|
||||
C3.99883,11.3708,3.867,11.1769,3.90625,10.9746 L3.94922,10.7617 L3.76953,10.8828
|
||||
C3.59851,10.9981,3.36728,10.9541,3.25195,10.7832
|
||||
C3.13658,10.6123,3.18054,10.3829,3.35156,10.2676 L3.53516,10.1426
|
||||
L3.31836,10.1016 C3.11599,10.0622,2.98612,9.86842,3.02539,9.66602
|
||||
C3.06468,9.46362,3.25857,9.33371,3.46094,9.37305 L3.67578,9.41406
|
||||
L3.55078,9.23242 C3.4354,9.06156,3.48132,8.83214,3.65234,8.7168
|
||||
C3.82337,8.60149,4.05462,8.64547,4.16992,8.81641 L4.29102,8.99805
|
||||
L4.33203,8.7832 C4.36154,8.6314,4.47892,8.52021,4.62109,8.49023 Z
|
||||
M19.2363,10.7852 C19.3814,10.7935,19.5126,10.8876,19.5645,11.0332
|
||||
L19.6367,11.2363 L19.7305,11.041
|
||||
C19.8188,10.8547,20.0403,10.7749,20.2266,10.8633
|
||||
C20.4128,10.9517,20.4908,11.1712,20.4023,11.3574 L20.3086,11.5586
|
||||
L20.5156,11.4844 C20.7098,11.4152,20.9211,11.5148,20.9902,11.709
|
||||
C21.0594,11.9032,20.9597,12.1163,20.7656,12.1855 L20.5586,12.2578
|
||||
L20.7578,12.3516 C20.944,12.44,21.022,12.6614,20.9336,12.8477
|
||||
C20.8452,13.0339,20.6238,13.1118,20.4375,13.0234 L20.2402,12.9297
|
||||
L20.3125,13.1367 C20.3817,13.3309,20.282,13.5421,20.0879,13.6113
|
||||
C19.8937,13.6805,19.6825,13.5809,19.6133,13.3867 L19.5391,13.1777
|
||||
L19.4453,13.377 C19.357,13.5632,19.1374,13.6411,18.9512,13.5527 L18.9492,13.5527
|
||||
C18.763,13.4643,18.6832,13.2429,18.7715,13.0566 L18.8652,12.8613
|
||||
L18.6621,12.9336 C18.4679,13.0027,18.2547,12.9012,18.1855,12.707
|
||||
C18.1164,12.5128,18.218,12.3016,18.4121,12.2324 L18.6172,12.1582
|
||||
L18.4199,12.0664 C18.2337,11.978,18.1557,11.7566,18.2441,11.5703
|
||||
C18.3325,11.384,18.554,11.3042,18.7402,11.3926 L18.9355,11.4863 L18.8613,11.2832
|
||||
C18.7922,11.089,18.8938,10.8759,19.0879,10.8066
|
||||
C19.1364,10.7894,19.188,10.7824,19.2363,10.7852 Z M15.2695,12.1973
|
||||
C15.4146,12.2056,15.5478,12.2997,15.5996,12.4453 L15.6699,12.6484
|
||||
L15.7637,12.4531 C15.852,12.2669,16.0735,12.189,16.2598,12.2773
|
||||
C16.446,12.3657,16.524,12.5872,16.4355,12.7734 L16.3438,12.9707 L16.5488,12.8965
|
||||
C16.743,12.8273,16.9543,12.9289,17.0234,13.123
|
||||
C17.0926,13.3172,16.991,13.5304,16.7969,13.5996 L16.5938,13.6699
|
||||
L16.7891,13.7637 C16.9753,13.8521,17.0552,14.0735,16.9668,14.2598
|
||||
C16.8784,14.446,16.6569,14.5239,16.4707,14.4355 L16.2734,14.3438
|
||||
L16.3477,14.5488 C16.4168,14.743,16.3152,14.9542,16.1211,15.0234
|
||||
C15.9269,15.0926,15.7176,14.993,15.6484,14.7988 L15.5742,14.5918
|
||||
L15.4785,14.7891 C15.3902,14.9753,15.1687,15.0532,14.9824,14.9648
|
||||
C14.7962,14.8765,14.7182,14.657,14.8066,14.4707 L14.8984,14.2734
|
||||
L14.6953,14.3477 C14.5011,14.4168,14.2879,14.3152,14.2188,14.1211
|
||||
C14.1496,13.9269,14.2493,13.7157,14.4434,13.6465 L14.6504,13.5723
|
||||
L14.4531,13.4785 C14.2669,13.3901,14.1889,13.1687,14.2773,12.9824
|
||||
C14.3657,12.7962,14.5872,12.7183,14.7734,12.8066 L14.9688,12.8984
|
||||
L14.8945,12.6953 C14.8254,12.5011,14.9289,12.288,15.123,12.2188
|
||||
C15.1716,12.2015,15.2212,12.1945,15.2695,12.1973 Z M11.3008,13.6113
|
||||
C11.4458,13.6197,11.579,13.7138,11.6309,13.8594 L11.7051,14.0625
|
||||
L11.7969,13.8672 C11.8852,13.6809,12.1087,13.6011,12.2949,13.6895
|
||||
C12.4811,13.7778,12.5591,13.9993,12.4707,14.1855 L12.375,14.3848 L12.582,14.3105
|
||||
C12.7762,14.2414,12.9894,14.341,13.0586,14.5352
|
||||
C13.1277,14.7294,13.0242,14.9425,12.8301,15.0117 L12.627,15.084 L12.8242,15.1777
|
||||
C13.0104,15.2661,13.0885,15.4876,13,15.6738
|
||||
C12.9116,15.8601,12.6902,15.938,12.5039,15.8496 L12.3066,15.7559
|
||||
L12.3789,15.9629 C12.4481,16.1571,12.3484,16.3683,12.1543,16.4375
|
||||
C11.9601,16.5067,11.7489,16.4071,11.6797,16.2129 L11.6055,16.0039
|
||||
L11.5137,16.2031 C11.4253,16.3894,11.2019,16.4693,11.0156,16.3809
|
||||
L11.0156,16.3789 C10.8294,16.2905,10.7514,16.071,10.8398,15.8848
|
||||
L10.9336,15.6875 L10.7305,15.7598
|
||||
C10.5363,15.8289,10.3211,15.7273,10.252,15.5332
|
||||
C10.1828,15.339,10.2844,15.1278,10.4785,15.0586 L10.6855,14.9863
|
||||
L10.4863,14.8926 C10.3001,14.8042,10.2221,14.5828,10.3105,14.3965
|
||||
C10.3989,14.2102,10.6184,14.1304,10.8047,14.2188 L11.0039,14.3125
|
||||
L10.9297,14.1094 C10.8605,13.9152,10.9622,13.702,11.1563,13.6328
|
||||
C11.2048,13.6156,11.2524,13.6086,11.3008,13.6113 Z" />
|
||||
</vector>
|
||||
19
app/src/main/res/drawable/ic_new_field_white_24dp.xml
Normal file
19
app/src/main/res/drawable/ic_new_field_white_24dp.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<group>
|
||||
<path
|
||||
android:fillColor="#fffbfb"
|
||||
android:pathData="M 18 3 L 18 6 L 15 6 L 15 8 L 18 8 L 18 11 L 20 11 L 20 8 L 23 8 L 23 6 L 20 6 L
|
||||
20 3 L 18 3 z M 4 6 C 2.8920005 6 2.0000002 6.8920005 2 8 L 2 18 C 1.9999998
|
||||
19.108 2.8920005 20 4 20 L 18 20 C 19.108 20 19.999999 19.108 20 18 L 20 13 L 18
|
||||
13 L 18 16.533203 C 17.999999 17.345736 17.345736 18 16.533203 18 L 5.4667969 18
|
||||
C 4.6542635 18 3.9999998 17.345736 4 16.533203 L 4 9.4667969 C 4.0000002
|
||||
8.6542635 4.6542635 8 5.4667969 8 L 13 8 L 13 6 L 4 6 z M 5 10 L 5 12 L 17 12 L
|
||||
17 10 L 5 10 z M 5 14 L 5 16 L 17 16 L 17 14 L 5 14 z" />
|
||||
</group>
|
||||
</vector>
|
||||
74
app/src/main/res/drawable/ic_otp_white_24dp.xml
Normal file
74
app/src/main/res/drawable/ic_otp_white_24dp.xml
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M9.19922,3.13867 L9.19922,4.73828 L9.19922,6.33984 L10.8008,6.33984
|
||||
L10.8008,4.79297 C13.6944,5.15457,15.9841,7.44622,16.3457,10.3398
|
||||
L14.8008,10.3398 L14.8008,11.9395 L16.3438,11.9395
|
||||
C16.2972,12.3042,16.211,12.6543,16.1074,12.998 L17.7559,12.998
|
||||
C17.9,12.399,18,11.7819,18,11.1387 C18,6.72041,14.4183,3.13867,10,3.13867
|
||||
L9.19922,3.13867 Z M4.7207,5.17383 L4.59766,5.25
|
||||
C4.30768,5.51569,4.03687,5.80202,3.78906,6.10742
|
||||
C3.74408,6.16236,3.76404,6.13658,3.75391,6.14844
|
||||
C2.62072,7.56491,2.00133,9.32361,2,11.1387
|
||||
C2,11.4166,2.02145,11.6889,2.05078,11.959
|
||||
C2.06909,12.128,2.09815,12.2932,2.12695,12.459
|
||||
C2.15024,12.5993,2.17255,12.7392,2.20313,12.877
|
||||
C2.21201,12.9175,2.21732,12.9597,2.22656,13 L2.23633,13
|
||||
C2.42308,13.7827,2.72966,14.5165,3.12695,15.1914
|
||||
C3.30082,14.5824,3.66062,14.0532,4.14453,13.666
|
||||
C4.05066,13.4483,3.95825,13.2294,3.88867,13 L3.89063,13
|
||||
C3.88529,12.9838,3.88033,12.9674,3.875,12.9512
|
||||
C3.77847,12.6238,3.7054,12.2864,3.66211,11.9395 L5.19727,11.9395
|
||||
L5.20703,11.9395 L5.20703,10.3398 L5.19727,10.3398 L3.66406,10.3398
|
||||
C3.69055,10.1356,3.73432,9.93472,3.7793,9.73438
|
||||
C3.81343,9.59208,3.85116,9.45203,3.89258,9.3125
|
||||
C4.09265,8.63652,4.39089,7.99073,4.80859,7.41211 L9.10156,11.7051
|
||||
C9.41495,12.0185,9.91902,12.0185,10.2324,11.7051
|
||||
C10.5458,11.3917,10.5458,10.8876,10.2324,10.5742 L5.14258,5.48242
|
||||
C5.02985,5.36975,4.72451,5.175,4.72266,5.17383 L4.7207,5.17383 Z M6,14
|
||||
C4.892,14,4,14.892,4,16 L4,19 C4,20.108,4.892,21,6,21 L21,21
|
||||
C22.108,21,23,20.108,23,19 L23,16 C23,14.892,22.108,14,21,14 L6,14 Z M7.5,15
|
||||
C7.777,15,8,15.2787,8,15.625 L8,16.293 L8.47266,15.8203
|
||||
C8.71749,15.5755,9.07172,15.5366,9.26758,15.7324
|
||||
C9.46352,15.9283,9.42449,16.2825,9.17969,16.5273 L8.70703,17 L9.375,17
|
||||
C9.72124,17,10,17.223,10,17.5 C10,17.777,9.72124,18,9.375,18 L8.70703,18
|
||||
L9.17969,18.4727 C9.42449,18.7175,9.46367,19.0717,9.26758,19.2676
|
||||
C9.07172,19.4634,8.71749,19.4245,8.47266,19.1797 L8,18.707 L8,19.375
|
||||
C8,19.7212,7.777,20,7.5,20 C7.223,20,7,19.7212,7,19.375 L7,18.707
|
||||
L6.52734,19.1797 C6.28251,19.4245,5.92828,19.4634,5.73242,19.2676
|
||||
C5.53649,19.0717,5.57551,18.7175,5.82031,18.4727 L6.29297,18 L5.625,18
|
||||
C5.27876,18,5,17.777,5,17.5 C5,17.223,5.27876,17,5.625,17 L6.29297,17
|
||||
L5.82031,16.5273 C5.57551,16.2825,5.53633,15.9283,5.73242,15.7324
|
||||
C5.92828,15.5366,6.28251,15.5755,6.52734,15.8203 L7,16.293 L7,15.625
|
||||
C7,15.2787,7.223,15,7.5,15 Z M13.5,15 C13.777,15,14,15.2787,14,15.625 L14,16.293
|
||||
L14.4727,15.8203 C14.7175,15.5755,15.0717,15.5366,15.2676,15.7324
|
||||
C15.4635,15.9283,15.4245,16.2825,15.1797,16.5273 L14.707,17 L15.375,17
|
||||
C15.7212,17,16,17.223,16,17.5 C16,17.777,15.7212,18,15.375,18 L14.707,18
|
||||
L15.1797,18.4727 C15.4245,18.7175,15.4637,19.0717,15.2676,19.2676
|
||||
C15.0717,19.4634,14.7175,19.4245,14.4727,19.1797 L14,18.707 L14,19.375
|
||||
C14,19.7212,13.777,20,13.5,20 C13.223,20,13,19.7212,13,19.375 L13,18.707
|
||||
L12.5273,19.1797 C12.2825,19.4245,11.9283,19.4634,11.7324,19.2676
|
||||
C11.5365,19.0717,11.5755,18.7175,11.8203,18.4727 L12.293,18 L11.625,18
|
||||
C11.2788,18,11,17.777,11,17.5 C11,17.223,11.2788,17,11.625,17 L12.293,17
|
||||
L11.8203,16.5273 C11.5755,16.2825,11.5363,15.9283,11.7324,15.7324
|
||||
C11.9283,15.5366,12.2825,15.5755,12.5273,15.8203 L13,16.293 L13,15.625
|
||||
C13,15.2787,13.223,15,13.5,15 Z M19.5,15 C19.777,15,20,15.2787,20,15.625
|
||||
L20,16.293 L20.4727,15.8203 C20.7175,15.5755,21.0717,15.5366,21.2676,15.7324
|
||||
C21.4635,15.9283,21.4245,16.2825,21.1797,16.5273 L20.707,17 L21.375,17
|
||||
C21.7212,17,22,17.223,22,17.5 C22,17.777,21.7212,18,21.375,18 L20.707,18
|
||||
L21.1797,18.4727 C21.4245,18.7175,21.4637,19.0717,21.2676,19.2676
|
||||
C21.0717,19.4634,20.7175,19.4245,20.4727,19.1797 L20,18.707 L20,19.375
|
||||
C20,19.7212,19.777,20,19.5,20 C19.223,20,19,19.7212,19,19.375 L19,18.707
|
||||
L18.5273,19.1797 C18.2825,19.4245,17.9283,19.4634,17.7324,19.2676
|
||||
C17.5365,19.0717,17.5755,18.7175,17.8203,18.4727 L18.293,18 L17.625,18
|
||||
C17.2788,18,17,17.777,17,17.5 C17,17.223,17.2788,17,17.625,17 L18.293,17
|
||||
L17.8203,16.5273 C17.5755,16.2825,17.5363,15.9283,17.7324,15.7324
|
||||
C17.9283,15.5366,18.2825,15.5755,18.5273,15.8203 L19,16.293 L19,15.625
|
||||
C19,15.2787,19.223,15,19.5,15 Z" />
|
||||
</vector>
|
||||
@@ -1,19 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:width="24dp"
|
||||
android:height="24dp">
|
||||
<group
|
||||
android:translateY="-8">
|
||||
<group
|
||||
android:scaleX="1.777778"
|
||||
android:scaleY="1.777778"
|
||||
android:translateX="-205.4844"
|
||||
android:translateY="-31.99788">
|
||||
<path
|
||||
android:pathData="M125.00684 31.305444l0 -6.644528c0 -0.263013 -0.21159 -0.47461 -0.4746 -0.47461l-6.48633 0c-1.04808 0 -1.89843 0.850344 -1.89843 1.898435l0 6.328127c0 1.048093 0.85035 1.898437 1.89843 1.898437l6.48633 0c0.26301 0 0.4746 -0.2116 0.4746 -0.474611l0 -0.316409c0 -0.148311 -0.0692 -0.282785 -0.176 -0.369792 -0.083 -0.304543 -0.083 -1.172685 0 -1.477229 0.10684 -0.08504 0.176 -0.219501 0.176 -0.36782zm-6.32812 -4.469236c0 -0.06529 0.0534 -0.118652 0.11866 -0.118652l4.19238 0c0.0653 0 0.11866 0.05343 0.11866 0.118652l0 0.39551c0 0.06529 -0.0534 0.118653 -0.11866 0.118653l-4.19238 0c-0.0653 0 -0.11866 -0.05343 -0.11866 -0.118653l0 -0.39551zm0 1.265621c0 -0.06529 0.0534 -0.118653 0.11866 -0.118653l4.19238 0c0.0653 0 0.11866 0.05343 0.11866 0.118653l0 0.395509c0 0.06529 -0.0534 0.118653 -0.11866 0.118653l-4.19238 0c-0.0653 0 -0.11866 -0.05342 -0.11866 -0.118653l0 -0.395509zm5.0111 4.943847l-5.64391 0c-0.35003 0 -0.63282 -0.282781 -0.63282 -0.632808 0 -0.348045 0.28476 -0.632813 0.63282 -0.632813l5.64391 0c-0.0375 0.338162 -0.0375 0.927466 0 1.265621z"
|
||||
android:fillColor="#ffffff" />
|
||||
</group>
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M 12 1 A 11.000003 11.000001 0 0 0 1 12 A 11.000003 11.000001 0 0 0 4 19.529297
|
||||
L 4 18 L 4 16.119141 A 9.0000001 9.0000001 0 0 1 3 12 A 9.0000001 9.0000001 0 0
|
||||
1 4.9746094 6.3886719 L 17.617188 19.03125 A 9.0000001 9.0000001 0 0 1 12 21 A
|
||||
9.0000001 9.0000001 0 0 1 7.9121094 20 L 6 20 L 4.4609375 20 A 11.000003
|
||||
11.000001 0 0 0 12 23 A 11.000003 11.000001 0 0 0 23 12 A 11.000003 11.000001 0
|
||||
0 0 12 1 z M 12 3 A 9.0000001 9.0000001 0 0 1 21 12 A 9.0000001 9.0000001 0 0 1
|
||||
19.025391 17.611328 L 6.3828125 4.96875 A 9.0000001 9.0000001 0 0 1 12 3 z M
|
||||
16.566406 5.0078125 C 16.256202 5.0423192 15.932855 5.1941212 15.669922
|
||||
5.4570312 L 14.46875 6.6601562 L 17.339844 9.53125 L 18.542969 8.3300781 C
|
||||
18.895877 7.9770126 19.030492 7.5200648 18.966797 7.1269531 C 18.396436
|
||||
6.3122027 17.687797 5.6035638 16.873047 5.0332031 C 16.772829 5.0168634
|
||||
16.672741 4.995984 16.566406 5.0078125 z M 13.449219 7.6777344 L 11.978516
|
||||
9.1503906 L 14.849609 12.021484 L 16.320312 10.548828 L 13.449219 7.6777344 z M
|
||||
9.1484375 11.980469 L 5.65625 15.472656 L 5.015625 18.390625 C 4.9299361
|
||||
18.781059 5.2170219 19.069875 5.6074219 18.984375 L 8.5253906 18.345703 L
|
||||
12.019531 14.851562 L 9.1484375 11.980469 z" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,25 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:width="24dp"
|
||||
android:height="24dp">
|
||||
<group
|
||||
android:translateY="-8">
|
||||
<group
|
||||
android:scaleX="1.777778"
|
||||
android:scaleY="1.777778"
|
||||
android:translateX="-205.4844"
|
||||
android:translateY="-31.99788">
|
||||
<group
|
||||
android:scaleX="0.5625"
|
||||
android:scaleY="0.5625"
|
||||
android:translateX="115.585"
|
||||
android:translateY="22.49881">
|
||||
<path
|
||||
android:pathData="M4.375 3C2.5117466 3 1 4.5117271 1 6.375l0 11.25C1 19.488276 2.5117466 21 4.375 21l11.53125 0C16.373823 21 16.75 20.623825 16.75 20.15625l0 -0.5625c0 -0.263664 -0.122669 -0.503524 -0.3125 -0.658203 -0.147556 -0.54141 -0.147556 -2.08359 0 -2.625 0.189938 -0.151182 0.3125 -0.390619 0.3125 -0.654297l0 -0.669922 -2.378906 2.378906c-0.0089 0.500068 -0.0036 1.014773 0.03711 1.384766l-1.525391 0 -3.0507808 0.667969C9.333649 19.527124 8.7483479 19.391267 8.3632812 19.005859 8.2870146 18.929486 8.2287106 18.840044 8.171875 18.75L4.375 18.75C3.7527244 18.75 3.25 18.24727 3.25 17.625 3.25 17.006253 3.7562267 16.5 4.375 16.5l3.8027344 0L8.6503906 14.349609 12.125 10.875l-6.4140625 0C5.5948486 10.875 5.5 10.780031 5.5 10.664062l0 -0.7031245C5.5 9.8448664 5.5949375 9.75 5.7109375 9.75l7.4531245 0c0.02365 0 0.03921 0.018137 0.06055 0.025391L16.550781 6.4492188 16.75 6.25l0 -2.40625C16.75 3.3761713 16.373823 3 15.90625 3L4.375 3Zm16.015625 1.5898438c-0.30546 0.033979 -0.623906 0.1844704 -0.882813 0.4433593l-1.183593 1.1835938 2.828125 2.828125 1.183594 -1.1835938c0.517848 -0.5180889 0.601704 -1.2752558 0.1875 -1.6894531L21.195312 4.8457031C20.988219 4.6386018 20.696085 4.5558644 20.390625 4.5898438ZM17.320312 7.21875L9.6464844 14.894531 9.015625 17.767578c-0.08448 0.384462 0.1995577 0.670133 0.5839844 0.585938L12.472656 17.724609 20.148438 10.046875 17.320312 7.21875ZM5.7109375 7.5L13.164062 7.5C13.280151 7.5 13.375 7.594987 13.375 7.7109375l0 0.703125C13.375 8.5301336 13.28008 8.625 13.164062 8.625l-7.4531245 0C5.5948486 8.625 5.5 8.5300127 5.5 8.4140625l0 -0.703125C5.5 7.5948664 5.5949375 7.5 5.7109375 7.5Z"
|
||||
android:fillColor="#ffffff" />
|
||||
</group>
|
||||
</group>
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="1.01553178"
|
||||
android:pathData="M 16.566406,5.0078125 C 15.782726,5.1133592 15.352352,5.8416456
|
||||
14.802734,6.3261719 14.631179,6.503043 14.326338,6.6788074 14.664062,6.8554688 L
|
||||
17.339844,9.53125 C 17.821017,9.0226367 18.368011,8.5699819 18.791016,8.0117188
|
||||
19.125433,7.5187285 19.054409,6.8047721 18.564453,6.4453125 18.109555,6.0127792
|
||||
17.694742,5.5314578 17.210938,5.1328125 17.01828,5.0182169 16.787242,4.9827103
|
||||
16.566406,5.0078125 Z M 13.449219,7.6777344 C 10.851548,10.276042
|
||||
8.2539034,12.874349 5.65625,15.472656 5.4423833,16.493124 5.1885689,17.506656
|
||||
5,18.53125 c 0.019556,0.598212 0.681759,0.466668 1.0722656,0.351562
|
||||
0.8179378,-0.179027 1.6351872,-0.358083 2.453125,-0.537109 2.5983114,-2.598958
|
||||
5.1966104,-5.197918 7.7949214,-7.796875 z" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -33,7 +33,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintBottom_toTopOf="@+id/disclaimer">
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -96,53 +96,85 @@
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView android:id="@+id/about_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/about_description"/>
|
||||
|
||||
<TextView android:id="@+id/homepage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/about_homepage"
|
||||
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:id="@+id/activity_about_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="18dp"
|
||||
android:layout_marginStart="18dp"
|
||||
android:autoLink="web"
|
||||
android:text="@string/homepage"/>
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/about_description"/>
|
||||
|
||||
<TextView android:id="@+id/feedback"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/about_feedback"
|
||||
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
|
||||
<TextView android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="18dp"
|
||||
android:layout_marginStart="18dp"
|
||||
android:text="@string/issues"
|
||||
android:autoLink="web"/>
|
||||
<TextView
|
||||
android:id="@+id/activity_about_about_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/about"
|
||||
android:textStyle="bold"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Title"/>
|
||||
<TextView
|
||||
android:id="@+id/activity_about_licence_text"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
<TextView
|
||||
android:id="@+id/activity_about_contribution_text"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activity_about_contact_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/contact"
|
||||
android:textStyle="bold"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Title"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activity_about_homepage_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/homepage"
|
||||
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
|
||||
<TextView
|
||||
android:id="@+id/activity_about_homepage_link"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="web"
|
||||
android:text="@string/homepage_url"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activity_about_feedback_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/feedback"
|
||||
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
|
||||
<TextView
|
||||
android:id="@+id/activity_about_feedback_link"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/issues_url"
|
||||
android:autoLink="web"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activity_about_contribution_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/contribution"
|
||||
style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/>
|
||||
<TextView
|
||||
android:id="@+id/activity_about_contribution_link"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/contribution_url"
|
||||
android:autoLink="web"/>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/disclaimer"
|
||||
android:padding="20dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:text="@string/disclaimer_formal"
|
||||
style="@style/KeepassDXStyle.TextAppearance.TinyText"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -17,63 +17,100 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:importantForAutofill="noExcludeDescendants"
|
||||
tools:targetApi="o"
|
||||
android:id="@+id/entry_edit_coordinator_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:importantForAutofill="noExcludeDescendants"
|
||||
tools:targetApi="o" >
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<include
|
||||
android:id="@+id/toolbar"
|
||||
layout="@layout/toolbar_default"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/toolbar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:titleEnabled="false"
|
||||
app:toolbarId="@+id/toolbar"
|
||||
app:layout_scrollFlags="enterAlways|enterAlwaysCollapsed|scroll|exitUntilCollapsed|snap">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/entry_edit_scroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:fillViewport="true"
|
||||
android:scrollbars="none">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.kunzisoft.keepass.view.EntryEditContentsView
|
||||
android:id="@+id/entry_edit_contents"
|
||||
android:layout_width="0dp"
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="?attr/actionBarSize">
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:background="?attr/colorPrimaryDark"
|
||||
android:theme="?attr/toolbarAppearance"
|
||||
android:popupTheme="?attr/toolbarPopupAppearance">
|
||||
<androidx.appcompat.widget.ActionMenuView
|
||||
android:id="@+id/entry_edit_bottom_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="?attr/actionBarSize"/>
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
<View
|
||||
android:id="@+id/biometric_delimiter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/colorAccent"/>
|
||||
</FrameLayout>
|
||||
|
||||
</ScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="?attr/toolbarAppearance"
|
||||
android:popupTheme="?attr/toolbarPopupAppearance"
|
||||
app:layout_collapseMode="pin"
|
||||
tools:targetApi="lollipop" />
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/entry_edit_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbarStyle="insideOverlay"
|
||||
android:scrollbars="none"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.kunzisoft.keepass.view.EntryEditContentsView
|
||||
android:id="@+id/entry_edit_contents"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/entry_edit_save"
|
||||
android:layout_width="wrap_content"
|
||||
android:id="@+id/entry_edit_validate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:src="@drawable/ic_save_white_24dp"
|
||||
android:contentDescription="@string/content_description_entry_save"
|
||||
android:layout_margin="16dp"
|
||||
app:layout_anchorGravity="bottom|end"
|
||||
app:layout_anchor="@+id/entry_edit_scroll"
|
||||
android:src="@drawable/ic_check_white_24dp"
|
||||
android:contentDescription="@string/validate"
|
||||
app:useCompatPadding="true"
|
||||
style="@style/KeepassDXStyle.Fab"/>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -32,7 +32,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/pass_ok">
|
||||
app:layout_constraintBottom_toTopOf="@+id/activity_password_info_container">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
@@ -101,11 +101,10 @@
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scroll_container"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="fill_vertical"
|
||||
android:fillViewport="true">
|
||||
android:scrollbarStyle="insideOverlay"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -223,8 +222,36 @@
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/activity_password_info_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toTopOf="@+id/activity_password_open_button">
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/activity_password_info_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:minHeight="24dp"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingRight="24dp"
|
||||
style="@style/KeepassDXStyle.TextAppearance.TinyText"
|
||||
android:text="@string/warning_database_read_only"
|
||||
android:textColor="?attr/textColorInverse"
|
||||
android:background="?attr/colorAccent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/activity_password_info_delimiter"
|
||||
android:layout_gravity="bottom"/>
|
||||
<View
|
||||
android:id="@+id/activity_password_info_delimiter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/colorAccentLight"/>
|
||||
</LinearLayout>
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/pass_ok"
|
||||
android:id="@+id/activity_password_open_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
||||
@@ -17,18 +17,25 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/toolbar_coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include
|
||||
android:id="@+id/toolbar"
|
||||
layout="@layout/toolbar_default" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</LinearLayout>
|
||||
<include
|
||||
android:id="@+id/toolbar"
|
||||
layout="@layout/toolbar_default" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user