mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
377 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2e29ac4bd | ||
|
|
7df6309a68 | ||
|
|
a203ad9f64 | ||
|
|
44d6e09337 | ||
|
|
7c59ec019a | ||
|
|
b457f64ec8 | ||
|
|
e152e59a61 | ||
|
|
2c620ad69a | ||
|
|
c08b405fc2 | ||
|
|
585e39c591 | ||
|
|
60e857dba9 | ||
|
|
9a4aa2b08f | ||
|
|
2900b08b70 | ||
|
|
b0936563a2 | ||
|
|
3d327b245a | ||
|
|
9a14de3448 | ||
|
|
59f83545cf | ||
|
|
d9da1ef085 | ||
|
|
279a27f740 | ||
|
|
6ba822ee48 | ||
|
|
e4445949c8 | ||
|
|
8178ff583d | ||
|
|
0e0e19a802 | ||
|
|
1e2e7d841f | ||
|
|
263c2f00eb | ||
|
|
39c0b57652 | ||
|
|
677baf549c | ||
|
|
848478c28b | ||
|
|
98ad33a589 | ||
|
|
3f33733f40 | ||
|
|
cdc4ae4fb3 | ||
|
|
3edfa8a6ce | ||
|
|
6e99b667af | ||
|
|
23c8735568 | ||
|
|
f39065044f | ||
|
|
7d2ae47b0f | ||
|
|
7247de6908 | ||
|
|
4c2aef8504 | ||
|
|
ac8717cf1f | ||
|
|
7761928064 | ||
|
|
198047406b | ||
|
|
1d57309db9 | ||
|
|
07af9f36b2 | ||
|
|
d157fea9be | ||
|
|
7692caf622 | ||
|
|
dc65a8823f | ||
|
|
2210932fdf | ||
|
|
0cb1bf4b7f | ||
|
|
44d175dd40 | ||
|
|
097feb6437 | ||
|
|
250c7e5f20 | ||
|
|
83740f77bb | ||
|
|
8267e13d22 | ||
|
|
a7e9396f35 | ||
|
|
0f6c8601d2 | ||
|
|
66e68092d5 | ||
|
|
09616a594c | ||
|
|
7ae58ea7ea | ||
|
|
9f1f85a3c4 | ||
|
|
9992cdfce0 | ||
|
|
f5fa08ce94 | ||
|
|
67f70f7f85 | ||
|
|
840bd56a3d | ||
|
|
c1e2d31cfd | ||
|
|
c35322d44e | ||
|
|
9867e46c3f | ||
|
|
946a120038 | ||
|
|
2cd1a17aa2 | ||
|
|
6e29fe2932 | ||
|
|
e6c6bf6613 | ||
|
|
72c53169df | ||
|
|
b78cce7b4f | ||
|
|
c40499ec31 | ||
|
|
ee158e517e | ||
|
|
70aebcf9aa | ||
|
|
b15870d441 | ||
|
|
fe7074736a | ||
|
|
69c523ffad | ||
|
|
6aeefdf43d | ||
|
|
5f4c8be3d3 | ||
|
|
3092e4c557 | ||
|
|
ea119068da | ||
|
|
14371ecf94 | ||
|
|
45b0fcfe15 | ||
|
|
00aa5f5586 | ||
|
|
79fd53fd4c | ||
|
|
357ee3daf0 | ||
|
|
c2f7897f10 | ||
|
|
75455c0c48 | ||
|
|
a394bb9f8e | ||
|
|
2f5a846493 | ||
|
|
90376b361d | ||
|
|
229cf6bf5f | ||
|
|
bc46737353 | ||
|
|
1db2243a2e | ||
|
|
7cf836b3cb | ||
|
|
f7bbd295d6 | ||
|
|
62dbd95b48 | ||
|
|
df07e9c719 | ||
|
|
9aaf72726e | ||
|
|
5289927619 | ||
|
|
fa8c686f75 | ||
|
|
df5f28b7c4 | ||
|
|
280d8368fa | ||
|
|
80dbff1f21 | ||
|
|
7ee68a8481 | ||
|
|
ac8dd42c45 | ||
|
|
eed2148b2a | ||
|
|
dc5345b6d3 | ||
|
|
221af0b5bb | ||
|
|
a10ccc1eb0 | ||
|
|
b72d858480 | ||
|
|
60412cc90b | ||
|
|
512ac87dc9 | ||
|
|
d97020d1c5 | ||
|
|
f82cb617ba | ||
|
|
f79281a1a0 | ||
|
|
7be1dbb78b | ||
|
|
f875787799 | ||
|
|
f82c208556 | ||
|
|
f2150e3d85 | ||
|
|
ecc198e8a0 | ||
|
|
949bc58a80 | ||
|
|
7d79fff16f | ||
|
|
3aeb678292 | ||
|
|
160ac41bed | ||
|
|
9dfbcbe89c | ||
|
|
30da529348 | ||
|
|
dd8d114711 | ||
|
|
2191a4a848 | ||
|
|
8b9ea8d988 | ||
|
|
46dda8567d | ||
|
|
6953da4d9a | ||
|
|
e987d6647e | ||
|
|
359d85727e | ||
|
|
a994bf9dd8 | ||
|
|
14c4e095f6 | ||
|
|
59dce0e56f | ||
|
|
1f54a893a7 | ||
|
|
9489f1ee3d | ||
|
|
dc3d720e8d | ||
|
|
efe30b598b | ||
|
|
42515bfb2d | ||
|
|
acb3657d95 | ||
|
|
e7159c9d36 | ||
|
|
f3fdca368b | ||
|
|
4ea3e08a45 | ||
|
|
1eebc72b21 | ||
|
|
9a91be7e36 | ||
|
|
48476f9b88 | ||
|
|
68c991eb9b | ||
|
|
b51c77b01b | ||
|
|
57105db554 | ||
|
|
4bd3bdaddf | ||
|
|
9cf59b8d73 | ||
|
|
a793b0bb42 | ||
|
|
4d9e2e1471 | ||
|
|
1719887e55 | ||
|
|
2be00aca9d | ||
|
|
6fd05c5ad7 | ||
|
|
65e404374f | ||
|
|
0b78731bb3 | ||
|
|
65d318ed88 | ||
|
|
1bfcea55a9 | ||
|
|
6780eb004d | ||
|
|
0e12b2d021 | ||
|
|
1b356f87ec | ||
|
|
bf24d0bae1 | ||
|
|
97c831d4bb | ||
|
|
e3e48ffa6d | ||
|
|
b678416122 | ||
|
|
df722925fa | ||
|
|
4cbc0d9806 | ||
|
|
b0f3711b4e | ||
|
|
14ec6579b2 | ||
|
|
30bf039473 | ||
|
|
33bea317b0 | ||
|
|
c6814dc05e | ||
|
|
16808069ec | ||
|
|
e813974e29 | ||
|
|
7e0010b536 | ||
|
|
93171adcb3 | ||
|
|
9501cc76a4 | ||
|
|
5d7db046ac | ||
|
|
46c259bc3e | ||
|
|
3bacff91d3 | ||
|
|
bd79d483d2 | ||
|
|
16e31f4881 | ||
|
|
afa23c393d | ||
|
|
54d23cb781 | ||
|
|
c757e410e9 | ||
|
|
39dd25567d | ||
|
|
32cd998c2a | ||
|
|
691fc6335e | ||
|
|
b0025d1416 | ||
|
|
2467d8b0e7 | ||
|
|
28993c53e7 | ||
|
|
efdea870f0 | ||
|
|
b2995ec862 | ||
|
|
2bcc84dbb2 | ||
|
|
70cc98ce33 | ||
|
|
6e055f398d | ||
|
|
9f6234f032 | ||
|
|
6135544b72 | ||
|
|
5f32ec218b | ||
|
|
3b9a884db2 | ||
|
|
ada6b85868 | ||
|
|
2f8a4f447c | ||
|
|
9bd6499271 | ||
|
|
76fcc919ef | ||
|
|
a382297edf | ||
|
|
4987dfe4f6 | ||
|
|
d1a2e50b8d | ||
|
|
b9e44b6166 | ||
|
|
af601edc94 | ||
|
|
6640dcf9cd | ||
|
|
9b2734ed38 | ||
|
|
afcfce056f | ||
|
|
8f3af2f27b | ||
|
|
706aae47b3 | ||
|
|
d494295b21 | ||
|
|
c1600a253b | ||
|
|
a3bc29ad8f | ||
|
|
83225ed157 | ||
|
|
f13f6dc01f | ||
|
|
2d2489443a | ||
|
|
0e5b7fbfa2 | ||
|
|
d16a8068f7 | ||
|
|
c5ef11febc | ||
|
|
027f31447a | ||
|
|
41d1a4e5fb | ||
|
|
924e3191cb | ||
|
|
ebdc6b8fd9 | ||
|
|
5c05128cd7 | ||
|
|
5bf5685a12 | ||
|
|
f7391cb4c4 | ||
|
|
bad7bd8884 | ||
|
|
e0524a1656 | ||
|
|
f59af9baa3 | ||
|
|
b36890ca82 | ||
|
|
22703af08b | ||
|
|
c4108269b3 | ||
|
|
94c9d090cf | ||
|
|
28b9235a43 | ||
|
|
3aebae5e15 | ||
|
|
2631cb75d6 | ||
|
|
07b8b4156f | ||
|
|
7978967c1a | ||
|
|
3f24ff4de3 | ||
|
|
7253dd82a6 | ||
|
|
2b8eb3ae7e | ||
|
|
429eae71cd | ||
|
|
e5c552defb | ||
|
|
5c950a2e2c | ||
|
|
577ff78189 | ||
|
|
3f3cde05f7 | ||
|
|
b48f2c3276 | ||
|
|
8e818846f0 | ||
|
|
1554e37f8c | ||
|
|
affaabd011 | ||
|
|
f34a8f991c | ||
|
|
c2b14d610b | ||
|
|
02693d0cbb | ||
|
|
23155279ab | ||
|
|
2d23f7403d | ||
|
|
ae411c6fd5 | ||
|
|
ab8d6075a9 | ||
|
|
bc5ae29a67 | ||
|
|
1a8aabc30c | ||
|
|
8c51d7f713 | ||
|
|
8a1485e7ce | ||
|
|
614145431a | ||
|
|
db25f1999f | ||
|
|
4ed231b9bb | ||
|
|
25a5342c11 | ||
|
|
c7202e3ca9 | ||
|
|
89c2e94cea | ||
|
|
3dc46771b5 | ||
|
|
0eac4d4d7f | ||
|
|
a0ceb788db | ||
|
|
98fb36d03a | ||
|
|
a670006517 | ||
|
|
9cdbe67cd4 | ||
|
|
bbe8af452c | ||
|
|
f5edf28ce1 | ||
|
|
8fc30f590b | ||
|
|
e578f23ebe | ||
|
|
99c488fc9e | ||
|
|
f6a710660d | ||
|
|
a61744bb65 | ||
|
|
17c3078c24 | ||
|
|
5fa7731b56 | ||
|
|
c8e2be4d8c | ||
|
|
e3db613a07 | ||
|
|
0f7839027f | ||
|
|
31b322a108 | ||
|
|
b3c0494618 | ||
|
|
78ddb0533d | ||
|
|
da2158e7f2 | ||
|
|
d2a1efb6e7 | ||
|
|
98a880db2d | ||
|
|
cb82ef8703 | ||
|
|
d56246767b | ||
|
|
bb477984aa | ||
|
|
5a3e650e02 | ||
|
|
3c96dd2fac | ||
|
|
da051e3ff3 | ||
|
|
d15b6323c2 | ||
|
|
ec5f8fe4a4 | ||
|
|
71d84d76f8 | ||
|
|
d33ed52ec2 | ||
|
|
3a970544bb | ||
|
|
792ac3a2e8 | ||
|
|
60bbc27401 | ||
|
|
cf7cbcb6e6 | ||
|
|
c126a8eba9 | ||
|
|
66a60d0357 | ||
|
|
1acdadd027 | ||
|
|
200be9dadd | ||
|
|
a73e2872a4 | ||
|
|
ce6f7729c5 | ||
|
|
c285411371 | ||
|
|
46394c600e | ||
|
|
bea9cb3248 | ||
|
|
1087dcd714 | ||
|
|
0b63029b7e | ||
|
|
9679d24414 | ||
|
|
7947fd53e5 | ||
|
|
8dedf75565 | ||
|
|
b5b7c12b49 | ||
|
|
51f4e3cc3a | ||
|
|
24b0315d2e | ||
|
|
be446220eb | ||
|
|
a3ef2d332e | ||
|
|
ba6fe576e3 | ||
|
|
abcef38102 | ||
|
|
d5780b2f30 | ||
|
|
f7e498a0a2 | ||
|
|
51ac7ca2de | ||
|
|
c94535f6b5 | ||
|
|
07457ae368 | ||
|
|
4767fff08c | ||
|
|
f0c3498ecc | ||
|
|
1eca52d0fe | ||
|
|
7fbac9ad2f | ||
|
|
6fb80ed50b | ||
|
|
47f63ac81b | ||
|
|
42cc0b28ba | ||
|
|
993806f781 | ||
|
|
8a5af33aaa | ||
|
|
a974e36e9e | ||
|
|
17bcb2b39e | ||
|
|
cddf02d0c1 | ||
|
|
75ff7ece37 | ||
|
|
ec2b407a20 | ||
|
|
1dc9d78e54 | ||
|
|
5742a75c9d | ||
|
|
b5e9ad6d7e | ||
|
|
6393025219 | ||
|
|
9ab3e289bc | ||
|
|
6454474886 | ||
|
|
c5720a7a03 | ||
|
|
41b15adc6d | ||
|
|
05b962e718 | ||
|
|
1f01ca7b85 | ||
|
|
5d3b4fa5ec | ||
|
|
754d2b2dd3 | ||
|
|
0461206a61 | ||
|
|
663f9e3962 | ||
|
|
34ee948c8e | ||
|
|
1bb9c2e4fe | ||
|
|
8ab18ce5cc | ||
|
|
71be16826e | ||
|
|
926c09d9df | ||
|
|
66c065ae7f | ||
|
|
083ed7775c | ||
|
|
5185452495 |
23
CHANGELOG
23
CHANGELOG
@@ -1,3 +1,26 @@
|
|||||||
|
KeePassDX(2.8.4)
|
||||||
|
* Fix incomplete attachment deletion #684
|
||||||
|
* Fix opening database v1 without backup folder #692
|
||||||
|
* Fix ANR during first entry education #685
|
||||||
|
* Entry edition as fragment and manual views to fix focus #686
|
||||||
|
* Fix opening database with corrupted attachment #691
|
||||||
|
* Manage empty keyfile #679
|
||||||
|
|
||||||
|
KeePassDX(2.8.3)
|
||||||
|
* Upload attachments
|
||||||
|
* Visibility button for each hidden field
|
||||||
|
* Fix read header file
|
||||||
|
* Fix deletion in KDB database
|
||||||
|
* Fix minor issues
|
||||||
|
|
||||||
|
KeePassDX(2.8.2)
|
||||||
|
* Fix themes / new UI
|
||||||
|
* Fix multiples notifications
|
||||||
|
* Fix entry in Magikeyboard memory
|
||||||
|
* Fix biometric view visibility
|
||||||
|
* Fix fields order
|
||||||
|
* Upgrade code with ViewModel and LiveData
|
||||||
|
|
||||||
KeePassDX(2.8.1)
|
KeePassDX(2.8.1)
|
||||||
* Capture exceptions in coroutines
|
* Capture exceptions in coroutines
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 14
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode = 37
|
versionCode = 40
|
||||||
versionName = "2.8.1"
|
versionName = "2.8.4"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -50,7 +50,7 @@ android {
|
|||||||
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "false"
|
buildConfigField "boolean", "CLOSED_STORE", "false"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
}
|
}
|
||||||
pro {
|
pro {
|
||||||
@@ -69,7 +69,7 @@ android {
|
|||||||
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
buildConfigField "boolean", "FULL_VERSION", "false"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{}"
|
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
||||||
}
|
}
|
||||||
@@ -95,16 +95,15 @@ def room_version = "2.2.5"
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
implementation 'androidx.preference:preference:1.1.1'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.biometric:biometric:1.0.1'
|
implementation 'androidx.biometric:biometric:1.0.1'
|
||||||
implementation 'androidx.core:core-ktx:1.2.0'
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
|
implementation "androidx.core:core-ktx:1.3.1"
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
|
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||||
// TODO #538 implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
|
|
||||||
// To upgrade with style
|
// To upgrade with style
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'com.google.android.material:material:1.0.0'
|
||||||
// Database
|
// Database
|
||||||
|
|||||||
@@ -124,8 +124,7 @@
|
|||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
||||||
android:configChanges="keyboardHidden"
|
android:windowSoftInputMode="adjustPan|stateAlwaysHidden" />
|
||||||
android:windowSoftInputMode="adjustResize" />
|
|
||||||
<!-- About and Settings -->
|
<!-- About and Settings -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||||
@@ -161,10 +160,6 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false" />
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
|||||||
@@ -39,14 +39,15 @@ import com.google.android.material.appbar.CollapsingToolbarLayout
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryAttachment
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
@@ -86,7 +87,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
private var mShowPassword: Boolean = false
|
private var mShowPassword: Boolean = false
|
||||||
|
|
||||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
private var mAttachmentsToDownload: HashMap<Int, EntryAttachment> = HashMap()
|
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||||
|
|
||||||
private var clipboardHelper: ClipboardHelper? = null
|
private var clipboardHelper: ClipboardHelper? = null
|
||||||
private var mFirstLaunchOfActivity: Boolean = false
|
private var mFirstLaunchOfActivity: Boolean = false
|
||||||
@@ -140,7 +141,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
// Init attachment service binder manager
|
// Init attachment service binder manager
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
mProgressDialogThread?.onActionFinish = { actionTask, result ->
|
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
||||||
@@ -212,8 +213,8 @@ class EntryActivity : LockingActivity() {
|
|||||||
mAttachmentFileBinderManager?.apply {
|
mAttachmentFileBinderManager?.apply {
|
||||||
registerProgressTask()
|
registerProgressTask()
|
||||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||||
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
entryContentsView?.updateAttachmentDownloadProgress(attachment)
|
entryContentsView?.putAttachment(entryAttachmentState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,14 +241,13 @@ class EntryActivity : LockingActivity() {
|
|||||||
toolbar?.title = entryTitle
|
toolbar?.title = entryTitle
|
||||||
|
|
||||||
// Assign basic fields
|
// Assign basic fields
|
||||||
entryContentsView?.assignUserName(entry.username)
|
entryContentsView?.assignUserName(entry.username) {
|
||||||
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
|
|
||||||
database.startManageEntry(entry)
|
database.startManageEntry(entry)
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
||||||
getString(R.string.copy_field,
|
getString(R.string.copy_field,
|
||||||
getString(R.string.entry_user_name)))
|
getString(R.string.entry_user_name)))
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
})
|
}
|
||||||
|
|
||||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||||
@@ -274,23 +274,25 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
|
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
||||||
if (allowCopyPasswordAndProtectedFields) {
|
View.OnClickListener {
|
||||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
|
||||||
database.startManageEntry(entry)
|
database.startManageEntry(entry)
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
||||||
getString(R.string.copy_field,
|
getString(R.string.copy_field,
|
||||||
getString(R.string.entry_password)))
|
getString(R.string.entry_password)))
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
})
|
}
|
||||||
} else {
|
} else {
|
||||||
// If dialog not already shown
|
// If dialog not already shown
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||||
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
|
showWarningClipboardDialogOnClickListener
|
||||||
} else {
|
} else {
|
||||||
entryContentsView?.assignPasswordCopyListener(null)
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
entryContentsView?.assignPassword(entry.password,
|
||||||
|
allowCopyPasswordAndProtectedFields,
|
||||||
|
onPasswordCopyClickListener)
|
||||||
|
|
||||||
//Assign OTP field
|
//Assign OTP field
|
||||||
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
||||||
@@ -304,24 +306,22 @@ class EntryActivity : LockingActivity() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
entryContentsView?.assignURL(entry.url)
|
entryContentsView?.assignURL(entry.url)
|
||||||
entryContentsView?.assignComment(entry.notes)
|
entryContentsView?.assignNotes(entry.notes)
|
||||||
|
|
||||||
// Assign custom fields
|
// Assign custom fields
|
||||||
if (entry.allowCustomFields()) {
|
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||||
entryContentsView?.clearExtraFields()
|
entryContentsView?.clearExtraFields()
|
||||||
|
entry.getExtraFields().forEach { field ->
|
||||||
for (element in entry.customFields.entries) {
|
val label = field.name
|
||||||
val label = element.key
|
val value = field.protectedValue
|
||||||
val value = element.value
|
|
||||||
|
|
||||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||||
if (allowCopyProtectedField) {
|
if (allowCopyProtectedField) {
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
clipboardHelper?.timeoutCopyToClipboard(
|
||||||
value.toString(),
|
value.toString(),
|
||||||
getString(R.string.copy_field, label)
|
getString(R.string.copy_field, label)
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
} else {
|
} else {
|
||||||
// If dialog not already shown
|
// If dialog not already shown
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||||
@@ -332,28 +332,16 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||||
|
|
||||||
// Manage attachments
|
// Manage attachments
|
||||||
val attachments = entry.getAttachments()
|
mDatabase?.binaryPool?.let { binaryPool ->
|
||||||
val showAttachmentsView = attachments.isNotEmpty()
|
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||||
entryContentsView?.showAttachments(showAttachmentsView)
|
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||||
if (showAttachmentsView) {
|
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||||
entryContentsView?.assignAttachments(attachments)
|
|
||||||
entryContentsView?.onAttachmentClick { attachmentItem, _ ->
|
|
||||||
when (attachmentItem.downloadState) {
|
|
||||||
AttachmentState.NULL, AttachmentState.ERROR, AttachmentState.COMPLETE -> {
|
|
||||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
|
||||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// TODO Stop download
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entryContentsView?.refreshAttachments()
|
|
||||||
|
|
||||||
// Assign dates
|
// Assign dates
|
||||||
entryContentsView?.assignCreationDate(entry.creationTime)
|
entryContentsView?.assignCreationDate(entry.creationTime)
|
||||||
@@ -373,16 +361,9 @@ class EntryActivity : LockingActivity() {
|
|||||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||||
taColorAccent.recycle()
|
taColorAccent.recycle()
|
||||||
}
|
}
|
||||||
val entryHistory = entry.getHistory()
|
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
|
||||||
val showHistoryView = entryHistory.isNotEmpty()
|
launch(this, historyItem, mReadOnly, position)
|
||||||
entryContentsView?.showHistory(showHistoryView)
|
|
||||||
if (showHistoryView) {
|
|
||||||
entryContentsView?.assignHistory(entryHistory)
|
|
||||||
entryContentsView?.onHistoryClick { historyItem, position ->
|
|
||||||
launch(this, historyItem, mReadOnly, position)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
entryContentsView?.refreshHistory()
|
|
||||||
|
|
||||||
// Assign special data
|
// Assign special data
|
||||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||||
@@ -411,16 +392,6 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
|
|
||||||
if (mShowPassword) {
|
|
||||||
togglePassword?.setTitle(R.string.menu_hide_password)
|
|
||||||
togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp)
|
|
||||||
} else {
|
|
||||||
togglePassword?.setTitle(R.string.menu_showpass)
|
|
||||||
togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
@@ -436,15 +407,6 @@ class EntryActivity : LockingActivity() {
|
|||||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
val togglePassword = menu.findItem(R.id.menu_toggle_pass)
|
|
||||||
entryContentsView?.let {
|
|
||||||
if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) {
|
|
||||||
changeShowPasswordIcon(togglePassword)
|
|
||||||
} else {
|
|
||||||
togglePassword?.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
||||||
gotoUrl?.apply {
|
gotoUrl?.apply {
|
||||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
||||||
@@ -467,28 +429,31 @@ class EntryActivity : LockingActivity() {
|
|||||||
|
|
||||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||||
menu: Menu) {
|
menu: Menu) {
|
||||||
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
|
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
||||||
|
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||||
findViewById(R.id.entry_user_name_action_image),
|
entryFieldCopyView,
|
||||||
{
|
{
|
||||||
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
|
val appNameString = getString(R.string.app_name)
|
||||||
getString(R.string.copy_field,
|
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||||
getString(R.string.entry_user_name)))
|
getString(R.string.copy_field, appNameString))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryActivityEducation, menu)
|
performedNextEducation(entryActivityEducation, menu)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!entryCopyEducationPerformed) {
|
if (!entryCopyEducationPerformed) {
|
||||||
|
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||||
// entryEditEducationPerformed
|
// entryEditEducationPerformed
|
||||||
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||||
toolbar!!.findViewById(R.id.menu_edit),
|
menuEditView,
|
||||||
{
|
{
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryActivityEducation, menu)
|
performedNextEducation(entryActivityEducation, menu)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,12 +463,6 @@ class EntryActivity : LockingActivity() {
|
|||||||
MenuUtil.onContributionItemSelected(this)
|
MenuUtil.onContributionItemSelected(this)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_toggle_pass -> {
|
|
||||||
mShowPassword = !mShowPassword
|
|
||||||
changeShowPasswordIcon(item)
|
|
||||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.menu_edit -> {
|
R.id.menu_edit -> {
|
||||||
mEntry?.let {
|
mEntry?.let {
|
||||||
EntryEditActivity.launch(this@EntryActivity, it)
|
EntryEditActivity.launch(this@EntryActivity, it)
|
||||||
@@ -523,7 +482,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
R.id.menu_restore_entry_history -> {
|
R.id.menu_restore_entry_history -> {
|
||||||
mEntryLastVersion?.let { mainEntry ->
|
mEntryLastVersion?.let { mainEntry ->
|
||||||
mProgressDialogThread?.startDatabaseRestoreEntryHistory(
|
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
|
||||||
mainEntry,
|
mainEntry,
|
||||||
mEntryHistoryPosition,
|
mEntryHistoryPosition,
|
||||||
!mReadOnly && mAutoSaveEnable)
|
!mReadOnly && mAutoSaveEnable)
|
||||||
@@ -531,14 +490,14 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
R.id.menu_delete_entry_history -> {
|
R.id.menu_delete_entry_history -> {
|
||||||
mEntryLastVersion?.let { mainEntry ->
|
mEntryLastVersion?.let { mainEntry ->
|
||||||
mProgressDialogThread?.startDatabaseDeleteEntryHistory(
|
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
|
||||||
mainEntry,
|
mainEntry,
|
||||||
mEntryHistoryPosition,
|
mEntryHistoryPosition,
|
||||||
!mReadOnly && mAutoSaveEnable)
|
!mReadOnly && mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||||
}
|
}
|
||||||
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import android.app.Activity
|
|||||||
import android.app.DatePickerDialog
|
import android.app.DatePickerDialog
|
||||||
import android.app.TimePickerDialog
|
import android.app.TimePickerDialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -31,59 +33,74 @@ import android.view.View
|
|||||||
import android.widget.DatePicker
|
import android.widget.DatePicker
|
||||||
import android.widget.TimePicker
|
import android.widget.TimePicker
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.ActionMenuView
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.NestedScrollView
|
import androidx.core.widget.NestedScrollView
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.*
|
import com.kunzisoft.keepass.activities.dialogs.*
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.*
|
||||||
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.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.*
|
||||||
|
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.view.EntryEditContentsView
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.view.showActionError
|
import com.kunzisoft.keepass.view.showActionError
|
||||||
|
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class EntryEditActivity : LockingActivity(),
|
class EntryEditActivity : LockingActivity(),
|
||||||
IconPickerDialogFragment.IconPickerListener,
|
IconPickerDialogFragment.IconPickerListener,
|
||||||
|
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
||||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||||
SetOTPDialogFragment.CreateOtpListener,
|
SetOTPDialogFragment.CreateOtpListener,
|
||||||
DatePickerDialog.OnDateSetListener,
|
DatePickerDialog.OnDateSetListener,
|
||||||
TimePickerDialog.OnTimeSetListener {
|
TimePickerDialog.OnTimeSetListener,
|
||||||
|
FileTooBigDialogFragment.ActionChooseListener,
|
||||||
|
ReplaceFileDialogFragment.ActionChooseListener {
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
// Refs of an entry and group in database, are not modifiable
|
// Refs of an entry and group in database, are not modifiable
|
||||||
private var mEntry: Entry? = null
|
private var mEntry: Entry? = null
|
||||||
private var mParent: Group? = null
|
private var mParent: Group? = null
|
||||||
// New or copy of mEntry in the database to be modifiable
|
|
||||||
private var mNewEntry: Entry? = null
|
|
||||||
private var mIsNew: Boolean = false
|
private var mIsNew: Boolean = false
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var scrollView: NestedScrollView? = null
|
private var scrollView: NestedScrollView? = null
|
||||||
private var entryEditContentsView: EntryEditContentsView? = null
|
private var entryEditFragment: EntryEditFragment? = null
|
||||||
private var entryEditAddToolBar: ActionMenuView? = null
|
private var entryEditAddToolBar: Toolbar? = null
|
||||||
private var validateButton: View? = null
|
private var validateButton: View? = null
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
|
|
||||||
|
// To manage attachments
|
||||||
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
|
private var mAllowMultipleAttachments: Boolean = false
|
||||||
|
private var mTempAttachments = ArrayList<Attachment>()
|
||||||
|
|
||||||
// Education
|
// Education
|
||||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||||
|
|
||||||
@@ -102,19 +119,6 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
scrollView = findViewById(R.id.entry_edit_scroll)
|
scrollView = findViewById(R.id.entry_edit_scroll)
|
||||||
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
lockView?.setOnClickListener {
|
lockView?.setOnClickListener {
|
||||||
lockAndExit()
|
lockAndExit()
|
||||||
@@ -129,6 +133,8 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
// Likely the app has been killed exit the activity
|
// Likely the app has been killed exit the activity
|
||||||
mDatabase = Database.getInstance()
|
mDatabase = Database.getInstance()
|
||||||
|
|
||||||
|
var tempEntryInfo: EntryInfo? = null
|
||||||
|
|
||||||
// Entry is retrieve, it's an entry to update
|
// Entry is retrieve, it's an entry to update
|
||||||
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let {
|
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let {
|
||||||
mIsNew = false
|
mIsNew = false
|
||||||
@@ -144,89 +150,103 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
entry.parent = mParent
|
entry.parent = mParent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tempEntryInfo = mEntry?.getEntryInfo(mDatabase, true)
|
||||||
// Create the new entry from the current one
|
|
||||||
if (savedInstanceState == null
|
|
||||||
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
|
||||||
mEntry?.let { entry ->
|
|
||||||
// Create a copy to modify
|
|
||||||
mNewEntry = Entry(entry).also { newEntry ->
|
|
||||||
// WARNING Remove the parent to keep memory with parcelable
|
|
||||||
newEntry.removeParent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent is retrieve, it's a new entry to create
|
// Parent is retrieve, it's a new entry to create
|
||||||
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let {
|
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let {
|
||||||
mIsNew = true
|
mIsNew = true
|
||||||
// Create an empty new entry
|
|
||||||
if (savedInstanceState == null
|
|
||||||
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
|
||||||
mNewEntry = mDatabase?.createEntry()
|
|
||||||
}
|
|
||||||
mParent = mDatabase?.getGroupById(it)
|
mParent = mDatabase?.getGroupById(it)
|
||||||
// Add the default icon from parent if not a folder
|
// Add the default icon from parent if not a folder
|
||||||
val parentIcon = mParent?.icon
|
val parentIcon = mParent?.icon
|
||||||
|
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
|
||||||
|
// Set default icon
|
||||||
if (parentIcon != null
|
if (parentIcon != null
|
||||||
&& parentIcon.iconId != IconImage.UNKNOWN_ID
|
&& parentIcon.iconId != IconImage.UNKNOWN_ID
|
||||||
&& parentIcon.iconId != IconImageStandard.FOLDER) {
|
&& parentIcon.iconId != IconImageStandard.FOLDER) {
|
||||||
temporarilySaveAndShowSelectedIcon(parentIcon)
|
tempEntryInfo?.icon = parentIcon
|
||||||
} else {
|
}
|
||||||
mDatabase?.drawFactory?.let { iconFactory ->
|
// Set default username
|
||||||
entryEditContentsView?.setDefaultIcon(iconFactory)
|
tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build fragment to manage entry modification
|
||||||
|
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
|
||||||
|
if (entryEditFragment == null) {
|
||||||
|
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo)
|
||||||
|
}
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
|
||||||
|
.commit()
|
||||||
|
entryEditFragment?.apply {
|
||||||
|
drawFactory = mDatabase?.drawFactory
|
||||||
|
setOnDateClickListener = View.OnClickListener {
|
||||||
|
expiryTime.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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setOnPasswordGeneratorClickListener = View.OnClickListener {
|
||||||
|
openPasswordGenerator()
|
||||||
|
}
|
||||||
|
// Add listener to the icon
|
||||||
|
setOnIconViewClickListener = View.OnClickListener {
|
||||||
|
IconPickerDialogFragment.launch(this@EntryEditActivity)
|
||||||
|
}
|
||||||
|
setOnRemoveAttachment = { attachment ->
|
||||||
|
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
|
||||||
|
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
|
||||||
|
}
|
||||||
|
setOnEditCustomField = { field ->
|
||||||
|
editCustomField(field)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the new entry after an orientation change
|
// Retrieve temp attachments in case of deletion
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState?.containsKey(TEMP_ATTACHMENTS) == true) {
|
||||||
&& savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
|
||||||
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the activity if entry or parent can't be retrieve
|
|
||||||
if (mNewEntry == null || mParent == null) {
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
populateViewsWithEntry(mNewEntry!!)
|
|
||||||
|
|
||||||
// Assign title
|
// Assign title
|
||||||
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
|
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
|
||||||
|
|
||||||
// Add listener to the icon
|
|
||||||
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
|
|
||||||
|
|
||||||
// Bottom Bar
|
// Bottom Bar
|
||||||
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
||||||
entryEditAddToolBar?.apply {
|
entryEditAddToolBar?.apply {
|
||||||
menuInflater.inflate(R.menu.entry_edit, menu)
|
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||||
|
|
||||||
menu.findItem(R.id.menu_add_field).apply {
|
menu.findItem(R.id.menu_add_field).apply {
|
||||||
val allowCustomField = mNewEntry?.allowCustomFields() == true
|
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
|
||||||
isEnabled = allowCustomField
|
isEnabled = allowCustomField
|
||||||
isVisible = allowCustomField
|
isVisible = allowCustomField
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attachment not compatible below KitKat
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
menu.findItem(R.id.menu_add_attachment).isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
menu.findItem(R.id.menu_add_otp).apply {
|
menu.findItem(R.id.menu_add_otp).apply {
|
||||||
val allowOTP = mDatabase?.allowOTP == true
|
val allowOTP = mDatabase?.allowOTP == true
|
||||||
isEnabled = allowOTP
|
isEnabled = allowOTP
|
||||||
isVisible = allowOTP
|
// OTP not compatible below KitKat
|
||||||
|
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnMenuItemClickListener { item ->
|
setOnMenuItemClickListener { item ->
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_generate_password -> {
|
|
||||||
openPasswordGenerator()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.menu_add_field -> {
|
R.id.menu_add_field -> {
|
||||||
addNewCustomField()
|
addNewCustomField()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.menu_add_attachment -> {
|
||||||
|
addNewAttachment(item)
|
||||||
|
true
|
||||||
|
}
|
||||||
R.id.menu_add_otp -> {
|
R.id.menu_add_otp -> {
|
||||||
setupOTP()
|
setupOTP()
|
||||||
true
|
true
|
||||||
@@ -236,6 +256,10 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To retrieve attachment
|
||||||
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
// Save button
|
// Save button
|
||||||
validateButton = findViewById(R.id.entry_edit_validate)
|
validateButton = findViewById(R.id.entry_edit_validate)
|
||||||
validateButton?.setOnClickListener { saveEntry() }
|
validateButton?.setOnClickListener { saveEntry() }
|
||||||
@@ -244,12 +268,26 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||||
|
|
||||||
// Create progress dialog
|
// Create progress dialog
|
||||||
mProgressDialogThread?.onActionFinish = { actionTask, result ->
|
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
||||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
if (result.isSuccess)
|
try {
|
||||||
finish()
|
if (result.isSuccess) {
|
||||||
|
var newNodes: List<Node> = ArrayList()
|
||||||
|
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newNodes.size == 1) {
|
||||||
|
mEntry = newNodes[0] as Entry?
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve entry after database action", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
coordinatorLayout?.showActionError(result)
|
coordinatorLayout?.showActionError(result)
|
||||||
@@ -264,67 +302,58 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
} else {
|
} else {
|
||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun populateViewsWithEntry(newEntry: Entry) {
|
// Padding if lock button visible
|
||||||
// Don't start the field reference manager, we want to see the raw ref
|
entryEditAddToolBar?.updateLockPaddingLeft()
|
||||||
mDatabase?.stopManageEntry(newEntry)
|
|
||||||
|
|
||||||
// Set info in temp parameters
|
mAllowMultipleAttachments = mDatabase?.allowMultipleAttachments == true
|
||||||
temporarilySaveAndShowSelectedIcon(newEntry.icon)
|
mAttachmentFileBinderManager?.apply {
|
||||||
|
registerProgressTask()
|
||||||
// Set info in view
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||||
entryEditContentsView?.apply {
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
title = newEntry.title
|
when (entryAttachmentState.downloadState) {
|
||||||
username = if (mIsNew && newEntry.username.isEmpty())
|
AttachmentState.START -> {
|
||||||
mDatabase?.defaultUsername ?: ""
|
entryEditFragment?.apply {
|
||||||
else
|
// When only one attachment is allowed
|
||||||
newEntry.username
|
if (!mAllowMultipleAttachments) {
|
||||||
url = newEntry.url
|
clearAttachments()
|
||||||
password = newEntry.password
|
}
|
||||||
expires = newEntry.expires
|
putAttachment(entryAttachmentState)
|
||||||
if (expires)
|
// Scroll to the attachment position
|
||||||
expiresDate = newEntry.expiryTime
|
getAttachmentViewPosition(entryAttachmentState) {
|
||||||
notes = newEntry.notes
|
scrollView?.smoothScrollTo(0, it.toInt())
|
||||||
for (entry in newEntry.customFields.entries) {
|
}
|
||||||
post {
|
}
|
||||||
putCustomField(entry.key, entry.value)
|
}
|
||||||
|
AttachmentState.IN_PROGRESS -> {
|
||||||
|
entryEditFragment?.putAttachment(entryAttachmentState)
|
||||||
|
}
|
||||||
|
AttachmentState.COMPLETE -> {
|
||||||
|
entryEditFragment?.apply {
|
||||||
|
putAttachment(entryAttachmentState)
|
||||||
|
// Scroll to the attachment position
|
||||||
|
getAttachmentViewPosition(entryAttachmentState) {
|
||||||
|
scrollView?.smoothScrollTo(0, it.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AttachmentState.ERROR -> {
|
||||||
|
entryEditFragment?.removeAttachment(entryAttachmentState)
|
||||||
|
coordinatorLayout?.let {
|
||||||
|
Snackbar.make(it, R.string.error_file_not_create, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateEntryWithViews(newEntry: Entry) {
|
override fun onPause() {
|
||||||
|
mAttachmentFileBinderManager?.unregisterProgressTask()
|
||||||
|
|
||||||
mDatabase?.startManageEntry(newEntry)
|
super.onPause()
|
||||||
|
|
||||||
newEntry.apply {
|
|
||||||
// Build info from view
|
|
||||||
entryEditContentsView?.let { entryView ->
|
|
||||||
removeAllFields()
|
|
||||||
title = entryView.title
|
|
||||||
username = entryView.username
|
|
||||||
url = entryView.url
|
|
||||||
password = entryView.password
|
|
||||||
expires = entryView.expires
|
|
||||||
if (entryView.expires) {
|
|
||||||
expiryTime = entryView.expiresDate
|
|
||||||
}
|
|
||||||
notes = entryView. notes
|
|
||||||
entryView.customFields.forEach { customField ->
|
|
||||||
putExtraField(customField.name, customField.protectedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabase?.stopManageEntry(newEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun temporarilySaveAndShowSelectedIcon(icon: IconImage) {
|
|
||||||
mNewEntry?.icon = icon
|
|
||||||
mDatabase?.drawFactory?.let { iconDrawFactory ->
|
|
||||||
entryEditContentsView?.setIcon(iconDrawFactory, icon)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -335,16 +364,98 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new customized field view and scroll to bottom
|
* Add a new customized field
|
||||||
*/
|
*/
|
||||||
private fun addNewCustomField() {
|
private fun addNewCustomField() {
|
||||||
entryEditContentsView?.addEmptyCustomField()
|
EntryCustomFieldDialogFragment.getInstance().show(supportFragmentManager, "customFieldDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun editCustomField(field: Field) {
|
||||||
|
EntryCustomFieldDialogFragment.getInstance(field).show(supportFragmentManager, "customFieldDialog")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewCustomFieldApproved(newField: Field) {
|
||||||
|
entryEditFragment?.apply {
|
||||||
|
putExtraField(newField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
|
||||||
|
entryEditFragment?.replaceExtraField(oldField, newField)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteCustomFieldApproved(oldField: Field) {
|
||||||
|
entryEditFragment?.removeExtraField(oldField)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new attachment
|
||||||
|
*/
|
||||||
|
private fun addNewAttachment(item: MenuItem) {
|
||||||
|
mSelectFileHelper?.selectFileOnClickViewListener?.onMenuItemClick(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
|
||||||
|
if (attachmentToUploadUri != null && fileName != null) {
|
||||||
|
buildNewAttachment(attachmentToUploadUri, fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
||||||
|
startUploadAttachment(attachmentToUploadUri, attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
||||||
|
if (attachmentToUploadUri != null && attachment != null) {
|
||||||
|
// Start uploading in service
|
||||||
|
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
|
||||||
|
// Add in temp list
|
||||||
|
mTempAttachments.add(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
|
||||||
|
val compression = mDatabase?.compressionForNewEntry() ?: false
|
||||||
|
mDatabase?.buildNewBinary(applicationContext.filesDir, false, compression)?.let { binaryAttachment ->
|
||||||
|
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||||
|
// Ask to replace the current attachment
|
||||||
|
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
|
||||||
|
entryEditFragment?.containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD)) == true) {
|
||||||
|
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
|
||||||
|
.show(supportFragmentManager, "replacementFileFragment")
|
||||||
|
} else {
|
||||||
|
startUploadAttachment(attachmentToUploadUri, entryAttachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
|
uri?.let { attachmentToUploadUri ->
|
||||||
|
// TODO Async to get the name
|
||||||
|
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||||
|
documentFile.name?.let { fileName ->
|
||||||
|
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
||||||
|
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
|
||||||
|
.show(supportFragmentManager, "fileTooBigFragment")
|
||||||
|
} else {
|
||||||
|
buildNewAttachment(attachmentToUploadUri, fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up OTP (HOTP or TOTP) and add it as extra field
|
||||||
|
*/
|
||||||
private fun setupOTP() {
|
private fun setupOTP() {
|
||||||
// Retrieve the current otpElement if exists
|
// Retrieve the current otpElement if exists
|
||||||
// and open the dialog to set up the OTP
|
// and open the dialog to set up the OTP
|
||||||
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
|
SetOTPDialogFragment.build(entryEditFragment?.getEntryInfo()?.otpModel)
|
||||||
.show(supportFragmentManager, "addOTPDialog")
|
.show(supportFragmentManager, "addOTPDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,24 +463,35 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
* Saves the new entry or update an existing entry in the database
|
* Saves the new entry or update an existing entry in the database
|
||||||
*/
|
*/
|
||||||
private fun saveEntry() {
|
private fun saveEntry() {
|
||||||
|
// Get the temp entry
|
||||||
|
entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
|
||||||
|
|
||||||
// Launch a validation and show the error if present
|
if (mIsNew) {
|
||||||
if (entryEditContentsView?.isValid() == true) {
|
// Create new one
|
||||||
// Clone the entry
|
mDatabase?.createEntry()
|
||||||
mNewEntry?.let { newEntry ->
|
} else {
|
||||||
|
// Create a clone
|
||||||
|
Entry(mEntry!!)
|
||||||
|
}?.let { newEntry ->
|
||||||
|
|
||||||
// WARNING Add the parent previously deleted
|
newEntry.setEntryInfo(mDatabase, newEntryInfo)
|
||||||
newEntry.parent = mEntry?.parent
|
|
||||||
// Build info
|
// Build info
|
||||||
newEntry.lastAccessTime = DateInstant()
|
newEntry.lastAccessTime = DateInstant()
|
||||||
newEntry.lastModificationTime = DateInstant()
|
newEntry.lastModificationTime = DateInstant()
|
||||||
|
|
||||||
populateEntryWithViews(newEntry)
|
// Delete temp attachment if not used
|
||||||
|
mTempAttachments.forEach {
|
||||||
|
mDatabase?.binaryPool?.let { binaryPool ->
|
||||||
|
if (!newEntry.getAttachments(binaryPool).contains(it)) {
|
||||||
|
mDatabase?.removeAttachmentIfNotUsed(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Open a progress dialog and save entry
|
// Open a progress dialog and save entry
|
||||||
if (mIsNew) {
|
if (mIsNew) {
|
||||||
mParent?.let { parent ->
|
mParent?.let { parent ->
|
||||||
mProgressDialogThread?.startDatabaseCreateEntry(
|
mProgressDatabaseTaskProvider?.startDatabaseCreateEntry(
|
||||||
newEntry,
|
newEntry,
|
||||||
parent,
|
parent,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
@@ -377,7 +499,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mEntry?.let { oldEntry ->
|
mEntry?.let { oldEntry ->
|
||||||
mProgressDialogThread?.startDatabaseUpdateEntry(
|
mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry(
|
||||||
oldEntry,
|
oldEntry,
|
||||||
newEntry,
|
newEntry,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
@@ -397,30 +519,23 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||||
|
|
||||||
entryEditActivityEducation?.let {
|
|
||||||
Handler().post { performedNextEducation(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
|
||||||
val passwordGeneratorView: View? = entryEditAddToolBar?.findViewById(R.id.menu_generate_password)
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
val generatePasswordEducationPerformed = passwordGeneratorView != null
|
entryEditActivityEducation?.let {
|
||||||
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
Handler().post { performedNextEducation(it) }
|
||||||
passwordGeneratorView,
|
}
|
||||||
{
|
return super.onPrepareOptionsMenu(menu)
|
||||||
openPasswordGenerator()
|
}
|
||||||
},
|
|
||||||
{
|
fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||||
performedNextEducation(entryEditActivityEducation)
|
if (entryEditFragment?.generatePasswordEducationPerformed(entryEditActivityEducation) != true) {
|
||||||
}
|
|
||||||
)
|
|
||||||
if (!generatePasswordEducationPerformed) {
|
|
||||||
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
|
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
|
||||||
val addNewFieldEducationPerformed = mNewEntry != null
|
val addNewFieldEducationPerformed = mDatabase?.allowEntryCustomFields() == true
|
||||||
&& mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
|
&& addNewFieldView != null
|
||||||
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
|
&& addNewFieldView.isVisible
|
||||||
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||||
addNewFieldView,
|
addNewFieldView,
|
||||||
{
|
{
|
||||||
@@ -431,13 +546,29 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!addNewFieldEducationPerformed) {
|
if (!addNewFieldEducationPerformed) {
|
||||||
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
|
||||||
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
|
val addAttachmentEducationPerformed = attachmentView != null
|
||||||
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
&& attachmentView.isVisible
|
||||||
setupOtpView,
|
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||||
|
attachmentView,
|
||||||
{
|
{
|
||||||
setupOTP()
|
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(attachmentView)
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(entryEditActivityEducation)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!addAttachmentEducationPerformed) {
|
||||||
|
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
||||||
|
setupOtpView != null
|
||||||
|
&& setupOtpView.isVisible
|
||||||
|
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||||
|
setupOtpView,
|
||||||
|
{
|
||||||
|
setupOTP()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,7 +576,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||||
}
|
}
|
||||||
R.id.menu_contribute -> {
|
R.id.menu_contribute -> {
|
||||||
MenuUtil.onContributionItemSelected(this)
|
MenuUtil.onContributionItemSelected(this)
|
||||||
@@ -460,16 +591,28 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOtpCreated(otpElement: OtpElement) {
|
override fun onOtpCreated(otpElement: OtpElement) {
|
||||||
|
var titleOTP: String? = null
|
||||||
|
var usernameOTP: String? = null
|
||||||
|
// Build a temp entry to get title and username (by ref)
|
||||||
|
entryEditFragment?.getEntryInfo()?.let { entryInfo ->
|
||||||
|
val entryTemp = mDatabase?.createEntry()
|
||||||
|
entryTemp?.setEntryInfo(mDatabase, entryInfo)
|
||||||
|
mDatabase?.startManageEntry(entryTemp)
|
||||||
|
titleOTP = entryTemp?.title
|
||||||
|
usernameOTP = entryTemp?.username
|
||||||
|
mDatabase?.stopManageEntry(mEntry)
|
||||||
|
}
|
||||||
// Update the otp field with otpauth:// url
|
// Update the otp field with otpauth:// url
|
||||||
val otpField = OtpEntryFields.buildOtpField(otpElement,
|
val otpField = OtpEntryFields.buildOtpField(otpElement, titleOTP, usernameOTP)
|
||||||
mEntry?.title, mEntry?.username)
|
mEntry?.putExtraField(Field(otpField.name, otpField.protectedValue))
|
||||||
entryEditContentsView?.putCustomField(otpField.name, otpField.protectedValue)
|
entryEditFragment?.apply {
|
||||||
mEntry?.putExtraField(otpField.name, otpField.protectedValue)
|
putExtraField(otpField)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun iconPicked(bundle: Bundle) {
|
override fun iconPicked(bundle: Bundle) {
|
||||||
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
|
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
|
||||||
temporarilySaveAndShowSelectedIcon(icon)
|
entryEditFragment?.icon = icon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,9 +620,9 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
// To fix android 4.4 issue
|
// To fix android 4.4 issue
|
||||||
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
||||||
if (datePicker?.isShown == true) {
|
if (datePicker?.isShown == true) {
|
||||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
|
||||||
// Save the date
|
// Save the date
|
||||||
entryEditContentsView?.expiresDate =
|
entryEditFragment?.expiryTime =
|
||||||
DateInstant(DateTime(expiresDate)
|
DateInstant(DateTime(expiresDate)
|
||||||
.withYear(year)
|
.withYear(year)
|
||||||
.withMonthOfYear(month + 1)
|
.withMonthOfYear(month + 1)
|
||||||
@@ -496,9 +639,9 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
|
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
|
||||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
|
||||||
// Save the date
|
// Save the date
|
||||||
entryEditContentsView?.expiresDate =
|
entryEditFragment?.expiryTime =
|
||||||
DateInstant(DateTime(expiresDate)
|
DateInstant(DateTime(expiresDate)
|
||||||
.withHourOfDay(hours)
|
.withHourOfDay(hours)
|
||||||
.withMinuteOfHour(minutes)
|
.withMinuteOfHour(minutes)
|
||||||
@@ -507,17 +650,15 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
mNewEntry?.let {
|
|
||||||
populateEntryWithViews(it)
|
outState.putParcelableArrayList(TEMP_ATTACHMENTS, mTempAttachments)
|
||||||
outState.putParcelable(KEY_NEW_ENTRY, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun acceptPassword(bundle: Bundle) {
|
override fun acceptPassword(bundle: Bundle) {
|
||||||
bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let {
|
bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let {
|
||||||
entryEditContentsView?.password = it
|
entryEditFragment?.password = it
|
||||||
}
|
}
|
||||||
|
|
||||||
entryEditActivityEducation?.let {
|
entryEditActivityEducation?.let {
|
||||||
@@ -541,10 +682,10 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
override fun finish() {
|
override fun finish() {
|
||||||
// Assign entry callback as a result in all case
|
// Assign entry callback as a result in all case
|
||||||
try {
|
try {
|
||||||
mNewEntry?.let {
|
mEntry?.let { entry ->
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
val intentEntry = Intent()
|
val intentEntry = Intent()
|
||||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mNewEntry)
|
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry)
|
||||||
intentEntry.putExtras(bundle)
|
intentEntry.putExtras(bundle)
|
||||||
if (mIsNew) {
|
if (mIsNew) {
|
||||||
setResult(ADD_ENTRY_RESULT_CODE, intentEntry)
|
setResult(ADD_ENTRY_RESULT_CODE, intentEntry)
|
||||||
@@ -568,7 +709,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
const val KEY_PARENT = "parent"
|
const val KEY_PARENT = "parent"
|
||||||
|
|
||||||
// SaveInstanceState
|
// SaveInstanceState
|
||||||
const val KEY_NEW_ENTRY = "new_entry"
|
const val TEMP_ATTACHMENTS = "TEMP_ATTACHMENTS"
|
||||||
|
|
||||||
// Keys for callback
|
// Keys for callback
|
||||||
const val ADD_ENTRY_RESULT_CODE = 31
|
const val ADD_ENTRY_RESULT_CODE = 31
|
||||||
@@ -576,6 +717,8 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
|
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
|
||||||
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
||||||
|
|
||||||
|
const val ENTRY_EDIT_FRAGMENT_TAG = "ENTRY_EDIT_FRAGMENT_TAG"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to update an existing entry
|
* Launch EntryEditActivity to update an existing entry
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,535 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
|
import com.kunzisoft.keepass.model.*
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
|
import com.kunzisoft.keepass.view.collapse
|
||||||
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
|
||||||
|
class EntryEditFragment: StylishFragment() {
|
||||||
|
|
||||||
|
private lateinit var entryTitleLayoutView: TextInputLayout
|
||||||
|
private lateinit var entryTitleView: EditText
|
||||||
|
private lateinit var entryIconView: ImageView
|
||||||
|
private lateinit var entryUserNameView: EditText
|
||||||
|
private lateinit var entryUrlView: EditText
|
||||||
|
private lateinit var entryPasswordLayoutView: TextInputLayout
|
||||||
|
private lateinit var entryPasswordView: EditText
|
||||||
|
private lateinit var entryPasswordGeneratorView: View
|
||||||
|
private lateinit var entryExpiresCheckBox: CompoundButton
|
||||||
|
private lateinit var entryExpiresTextView: TextView
|
||||||
|
private lateinit var entryNotesView: EditText
|
||||||
|
private lateinit var extraFieldsContainerView: View
|
||||||
|
private lateinit var extraFieldsListView: ViewGroup
|
||||||
|
private lateinit var attachmentsContainerView: View
|
||||||
|
private lateinit var attachmentsListView: RecyclerView
|
||||||
|
|
||||||
|
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
|
||||||
|
|
||||||
|
private var fontInVisibility: Boolean = false
|
||||||
|
private var iconColor: Int = 0
|
||||||
|
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
|
||||||
|
|
||||||
|
var drawFactory: IconDrawableFactory? = null
|
||||||
|
var setOnDateClickListener: View.OnClickListener? = null
|
||||||
|
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
||||||
|
var setOnIconViewClickListener: View.OnClickListener? = null
|
||||||
|
var setOnEditCustomField: ((Field) -> Unit)? = null
|
||||||
|
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
|
||||||
|
|
||||||
|
// Elements to modify the current entry
|
||||||
|
private var mEntryInfo = EntryInfo()
|
||||||
|
private var mLastFocusedEditField: FocusedEditField? = null
|
||||||
|
private var mExtraViewToRequestFocus: EditText? = null
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
val rootView = inflater.cloneInContext(contextThemed)
|
||||||
|
.inflate(R.layout.fragment_entry_edit_contents, container, false)
|
||||||
|
|
||||||
|
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
|
||||||
|
|
||||||
|
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
|
||||||
|
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
||||||
|
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
||||||
|
entryIconView.setOnClickListener {
|
||||||
|
setOnIconViewClickListener?.onClick(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
|
||||||
|
entryUrlView = rootView.findViewById(R.id.entry_edit_url)
|
||||||
|
entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password)
|
||||||
|
entryPasswordView = rootView.findViewById(R.id.entry_edit_password)
|
||||||
|
entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button)
|
||||||
|
entryPasswordGeneratorView.setOnClickListener {
|
||||||
|
setOnPasswordGeneratorClickListener?.onClick(it)
|
||||||
|
}
|
||||||
|
entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox)
|
||||||
|
entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text)
|
||||||
|
entryExpiresTextView.setOnClickListener {
|
||||||
|
if (entryExpiresCheckBox.isChecked)
|
||||||
|
setOnDateClickListener?.onClick(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
|
||||||
|
|
||||||
|
extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container)
|
||||||
|
extraFieldsListView = rootView.findViewById(R.id.extra_fields_list)
|
||||||
|
|
||||||
|
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
||||||
|
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
||||||
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
||||||
|
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
||||||
|
if (previousSize > 0 && newSize == 0) {
|
||||||
|
attachmentsContainerView.collapse(true)
|
||||||
|
} else if (previousSize == 0 && newSize == 1) {
|
||||||
|
attachmentsContainerView.expand(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attachmentsListView.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
adapter = attachmentsAdapter
|
||||||
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
|
||||||
|
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
|
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
||||||
|
taIconColor?.recycle()
|
||||||
|
|
||||||
|
// Retrieve the new entry after an orientation change
|
||||||
|
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
|
||||||
|
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||||
|
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
|
||||||
|
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
|
||||||
|
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
|
||||||
|
}
|
||||||
|
|
||||||
|
populateViewsWithEntry()
|
||||||
|
|
||||||
|
return rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
super.onDetach()
|
||||||
|
|
||||||
|
drawFactory = null
|
||||||
|
setOnDateClickListener = null
|
||||||
|
setOnPasswordGeneratorClickListener = null
|
||||||
|
setOnIconViewClickListener = null
|
||||||
|
setOnRemoveAttachment = null
|
||||||
|
setOnEditCustomField = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryInfo(): EntryInfo? {
|
||||||
|
populateEntryWithViews()
|
||||||
|
return mEntryInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
|
||||||
|
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||||
|
entryPasswordGeneratorView,
|
||||||
|
{
|
||||||
|
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
|
||||||
|
} catch (ignore: Exception) {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateViewsWithEntry() {
|
||||||
|
// Set info in view
|
||||||
|
icon = mEntryInfo.icon
|
||||||
|
title = mEntryInfo.title
|
||||||
|
username = mEntryInfo.username
|
||||||
|
url = mEntryInfo.url
|
||||||
|
password = mEntryInfo.password
|
||||||
|
expires = mEntryInfo.expires
|
||||||
|
expiryTime = mEntryInfo.expiryTime
|
||||||
|
notes = mEntryInfo.notes
|
||||||
|
assignExtraFields(mEntryInfo.customFields) { fields ->
|
||||||
|
setOnEditCustomField?.invoke(fields)
|
||||||
|
}
|
||||||
|
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
|
||||||
|
setOnRemoveAttachment?.invoke(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateEntryWithViews() {
|
||||||
|
// Icon already populate
|
||||||
|
mEntryInfo.title = title
|
||||||
|
mEntryInfo.username = username
|
||||||
|
mEntryInfo.url = url
|
||||||
|
mEntryInfo.password = password
|
||||||
|
mEntryInfo.expires = expires
|
||||||
|
mEntryInfo.expiryTime = expiryTime
|
||||||
|
mEntryInfo.notes = notes
|
||||||
|
mEntryInfo.customFields = getExtraFields()
|
||||||
|
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
|
||||||
|
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
|
||||||
|
}?.otpModel
|
||||||
|
mEntryInfo.attachments = getAttachments()
|
||||||
|
}
|
||||||
|
|
||||||
|
var title: String
|
||||||
|
get() {
|
||||||
|
return entryTitleView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryTitleView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryTitleView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var icon: IconImage
|
||||||
|
get() {
|
||||||
|
return mEntryInfo.icon
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
mEntryInfo.icon = value
|
||||||
|
drawFactory?.let { drawFactory ->
|
||||||
|
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var username: String
|
||||||
|
get() {
|
||||||
|
return entryUserNameView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryUserNameView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryUserNameView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var url: String
|
||||||
|
get() {
|
||||||
|
return entryUrlView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryUrlView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryUrlView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var password: String
|
||||||
|
get() {
|
||||||
|
return entryPasswordView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryPasswordView.setText(value)
|
||||||
|
if (fontInVisibility) {
|
||||||
|
entryPasswordView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignExpiresDateText() {
|
||||||
|
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
||||||
|
entryExpiresTextView.setOnClickListener(setOnDateClickListener)
|
||||||
|
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) {
|
||||||
|
if (!value) {
|
||||||
|
expiresInstant = DateInstant.IN_ONE_MONTH
|
||||||
|
}
|
||||||
|
entryExpiresCheckBox.isChecked = value
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiryTime: DateInstant
|
||||||
|
get() {
|
||||||
|
return if (expires)
|
||||||
|
expiresInstant
|
||||||
|
else
|
||||||
|
DateInstant.NEVER_EXPIRE
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
if (expires)
|
||||||
|
expiresInstant = value
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
|
var notes: String
|
||||||
|
get() {
|
||||||
|
return entryNotesView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryNotesView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryNotesView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Extra Fields
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
private var mExtraFieldsList: MutableList<Field> = ArrayList()
|
||||||
|
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
|
||||||
|
|
||||||
|
private fun buildViewFromField(extraField: Field): View? {
|
||||||
|
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
|
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false)
|
||||||
|
itemView?.id = View.NO_ID
|
||||||
|
|
||||||
|
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
||||||
|
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected
|
||||||
|
extraFieldValueContainer?.hint = extraField.name
|
||||||
|
extraFieldValueContainer?.id = View.NO_ID
|
||||||
|
|
||||||
|
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
|
||||||
|
extraFieldValue?.apply {
|
||||||
|
if (extraField.protectedValue.isProtected) {
|
||||||
|
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
}
|
||||||
|
setText(extraField.protectedValue.toString())
|
||||||
|
if (fontInVisibility)
|
||||||
|
applyFontVisibility()
|
||||||
|
}
|
||||||
|
extraFieldValue?.id = View.NO_ID
|
||||||
|
extraFieldValue?.tag = "FIELD_VALUE_TAG"
|
||||||
|
if (mLastFocusedEditField?.field == extraField) {
|
||||||
|
mExtraViewToRequestFocus = extraFieldValue
|
||||||
|
}
|
||||||
|
|
||||||
|
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit)
|
||||||
|
extraFieldEditButton?.setOnClickListener {
|
||||||
|
mOnEditButtonClickListener?.invoke(extraField)
|
||||||
|
}
|
||||||
|
extraFieldEditButton?.id = View.NO_ID
|
||||||
|
|
||||||
|
return itemView
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExtraFields(): List<Field> {
|
||||||
|
mLastFocusedEditField = null
|
||||||
|
for (index in 0 until extraFieldsListView.childCount) {
|
||||||
|
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
|
||||||
|
.findViewWithTag("FIELD_VALUE_TAG")
|
||||||
|
val extraField = mExtraFieldsList[index]
|
||||||
|
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
|
||||||
|
if (extraFieldValue.isFocused) {
|
||||||
|
mLastFocusedEditField = FocusedEditField().apply {
|
||||||
|
field = extraField
|
||||||
|
cursorSelectionStart = extraFieldValue.selectionStart
|
||||||
|
cursorSelectionEnd = extraFieldValue.selectionEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mExtraFieldsList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all children and add new views for each field
|
||||||
|
*/
|
||||||
|
fun assignExtraFields(fields: List<Field>,
|
||||||
|
onEditButtonClickListener: ((item: Field)->Unit)?) {
|
||||||
|
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
// Reinit focused field
|
||||||
|
mExtraFieldsList.clear()
|
||||||
|
mExtraFieldsList.addAll(fields)
|
||||||
|
extraFieldsListView.removeAllViews()
|
||||||
|
fields.forEach {
|
||||||
|
extraFieldsListView.addView(buildViewFromField(it))
|
||||||
|
}
|
||||||
|
// Request last focus
|
||||||
|
mLastFocusedEditField?.let { focusField ->
|
||||||
|
mExtraViewToRequestFocus?.apply {
|
||||||
|
requestFocus()
|
||||||
|
setSelection(focusField.cursorSelectionStart,
|
||||||
|
focusField.cursorSelectionEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mLastFocusedEditField = null
|
||||||
|
mOnEditButtonClickListener = onEditButtonClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an extra field or create a new one if doesn't exists
|
||||||
|
*/
|
||||||
|
fun putExtraField(extraField: Field) {
|
||||||
|
extraFieldsContainerView.visibility = View.VISIBLE
|
||||||
|
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
|
||||||
|
oldField?.let {
|
||||||
|
val index = mExtraFieldsList.indexOf(oldField)
|
||||||
|
mExtraFieldsList.removeAt(index)
|
||||||
|
mExtraFieldsList.add(index, extraField)
|
||||||
|
extraFieldsListView.removeViewAt(index)
|
||||||
|
val newView = buildViewFromField(extraField)
|
||||||
|
extraFieldsListView.addView(newView, index)
|
||||||
|
newView?.requestFocus()
|
||||||
|
} ?: kotlin.run {
|
||||||
|
mExtraFieldsList.add(extraField)
|
||||||
|
val newView = buildViewFromField(extraField)
|
||||||
|
extraFieldsListView.addView(newView)
|
||||||
|
newView?.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
|
||||||
|
extraFieldsContainerView.visibility = View.VISIBLE
|
||||||
|
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||||
|
mExtraFieldsList.removeAt(index)
|
||||||
|
mExtraFieldsList.add(index, newExtraField)
|
||||||
|
extraFieldsListView.removeViewAt(index)
|
||||||
|
extraFieldsListView.addView(buildViewFromField(newExtraField), index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeExtraField(oldExtraField: Field) {
|
||||||
|
val previousSize = mExtraFieldsList.size
|
||||||
|
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||||
|
extraFieldsListView.getChildAt(index)?.let {
|
||||||
|
it.collapse(true) {
|
||||||
|
mExtraFieldsList.removeAt(index)
|
||||||
|
extraFieldsListView.removeViewAt(index)
|
||||||
|
val newSize = mExtraFieldsList.size
|
||||||
|
|
||||||
|
if (previousSize > 0 && newSize == 0) {
|
||||||
|
extraFieldsContainerView.collapse(true)
|
||||||
|
} else if (previousSize == 0 && newSize == 1) {
|
||||||
|
extraFieldsContainerView.expand(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Attachments
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun getAttachments(): List<Attachment> {
|
||||||
|
return attachmentsAdapter.itemsList.map { it.attachment }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assignAttachments(attachments: List<Attachment>,
|
||||||
|
streamDirection: StreamDirection,
|
||||||
|
onDeleteItem: (attachment: Attachment)->Unit) {
|
||||||
|
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||||
|
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
||||||
|
onDeleteItem.invoke(item.attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsAttachment(): Boolean {
|
||||||
|
return !attachmentsAdapter.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||||
|
return attachmentsAdapter.contains(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAttachment(attachment: EntryAttachmentState) {
|
||||||
|
attachmentsContainerView.visibility = View.VISIBLE
|
||||||
|
attachmentsAdapter.putItem(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachment(attachment: EntryAttachmentState) {
|
||||||
|
attachmentsAdapter.removeItem(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAttachments() {
|
||||||
|
attachmentsAdapter.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
|
||||||
|
attachmentsListView.postDelayed({
|
||||||
|
position.invoke(attachmentsContainerView.y
|
||||||
|
+ attachmentsListView.y
|
||||||
|
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
|
||||||
|
?: 0F)
|
||||||
|
)
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
populateEntryWithViews()
|
||||||
|
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
||||||
|
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
||||||
|
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
||||||
|
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
||||||
|
|
||||||
|
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
||||||
|
return EntryEditFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -32,9 +32,11 @@ import android.util.Log
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
@@ -43,19 +45,22 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
import kotlinx.android.synthetic.main.activity_file_selection.*
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
@@ -64,10 +69,11 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var fileManagerExplanationButton: View? = null
|
|
||||||
private var createDatabaseButtonView: View? = null
|
private var createDatabaseButtonView: View? = null
|
||||||
private var openDatabaseButtonView: View? = null
|
private var openDatabaseButtonView: View? = null
|
||||||
|
|
||||||
|
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
|
||||||
|
|
||||||
// Adapter to manage database history list
|
// Adapter to manage database history list
|
||||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||||
|
|
||||||
@@ -75,9 +81,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
private var mProgressDialogThread: ProgressDialogThread? = null
|
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -91,20 +97,15 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
toolbar.title = ""
|
toolbar.title = ""
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
fileManagerExplanationButton = findViewById(R.id.file_manager_explanation_button)
|
|
||||||
fileManagerExplanationButton?.setOnClickListener {
|
|
||||||
UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create database button
|
// Create database button
|
||||||
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
||||||
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||||
|
|
||||||
// Open database button
|
// Open database button
|
||||||
mOpenFileHelper = OpenFileHelper(this)
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||||
openDatabaseButtonView?.apply {
|
openDatabaseButtonView?.apply {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||||
setOnClickListener(it)
|
setOnClickListener(it)
|
||||||
setOnLongClickListener(it)
|
setOnLongClickListener(it)
|
||||||
}
|
}
|
||||||
@@ -117,26 +118,25 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
// Construct adapter with listeners
|
// Construct adapter with listeners
|
||||||
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
|
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
|
||||||
|
mAdapterDatabaseHistory?.setOnDefaultDatabaseListener { databaseFile ->
|
||||||
|
databaseFilesViewModel.setDefaultDatabase(databaseFile)
|
||||||
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.databaseUri)?.let { databaseFileUri ->
|
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||||
launchPasswordActivity(
|
launchPasswordActivity(
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
|
fileDatabaseHistoryEntityToOpen.keyFileUri
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||||
// Remove from app database
|
// Remove from app database
|
||||||
mFileDatabaseHistoryAction?.deleteFileDatabaseHistory(fileDatabaseHistoryToDelete) { fileHistoryDeleted ->
|
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
||||||
// Remove from adapter
|
|
||||||
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
|
|
||||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
|
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
|
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
|
||||||
mFileDatabaseHistoryAction?.addOrUpdateFileDatabaseHistory(fileDatabaseHistoryWithNewAlias)
|
// Update in app database
|
||||||
|
databaseFilesViewModel.updateDatabaseFile(fileDatabaseHistoryWithNewAlias)
|
||||||
}
|
}
|
||||||
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
||||||
|
|
||||||
@@ -159,12 +159,47 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Observe list of databases
|
||||||
|
databaseFilesViewModel.databaseFilesLoaded.observe(this, Observer { databaseFiles ->
|
||||||
|
when (databaseFiles.databaseFileAction) {
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
||||||
|
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
||||||
|
}
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
||||||
|
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
||||||
|
}
|
||||||
|
GroupActivity.launch(this@FileDatabaseSelectActivity)
|
||||||
|
}
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
||||||
|
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
||||||
|
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
databaseFilesViewModel.consumeAction()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Observe default database
|
||||||
|
databaseFilesViewModel.defaultDatabase.observe(this, Observer {
|
||||||
|
// Retrieve settings for default database
|
||||||
|
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||||
|
})
|
||||||
|
|
||||||
// Attach the dialog thread to this activity
|
// Attach the dialog thread to this activity
|
||||||
mProgressDialogThread = ProgressDialogThread(this).apply {
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||||
onActionFinish = { actionTask, _ ->
|
onActionFinish = { actionTask, result ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_TASK -> {
|
ACTION_DATABASE_CREATE_TASK -> {
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity)
|
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||||
|
val keyFileUri = result.data?.getParcelable<Uri?>(KEY_FILE_URI_KEY)
|
||||||
|
databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,34 +321,20 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
} else {
|
} else {
|
||||||
// Construct adapter with listeners
|
// Construct adapter with listeners
|
||||||
if (PreferencesUtil.showRecentFiles(this)) {
|
if (PreferencesUtil.showRecentFiles(this)) {
|
||||||
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
databaseFilesViewModel.loadListOfDatabases()
|
||||||
databaseFileHistoryList?.let { historyList ->
|
|
||||||
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(this@FileDatabaseSelectActivity)
|
|
||||||
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(
|
|
||||||
// Show only uri accessible
|
|
||||||
historyList.filter {
|
|
||||||
if (hideBrokenLocations) {
|
|
||||||
FileDatabaseInfo(this@FileDatabaseSelectActivity,
|
|
||||||
it.databaseUri).exists
|
|
||||||
} else
|
|
||||||
true
|
|
||||||
})
|
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register progress task
|
// Register progress task
|
||||||
mProgressDialogThread?.registerProgressTask()
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
// Unregister progress task
|
// Unregister progress task
|
||||||
mProgressDialogThread?.unregisterProgressTask()
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
@@ -334,7 +355,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
|
|
||||||
// Create the new database
|
// Create the new database
|
||||||
mProgressDialogThread?.startDatabaseCreate(
|
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
masterPasswordChecked,
|
masterPasswordChecked,
|
||||||
masterPassword,
|
masterPassword,
|
||||||
@@ -362,8 +383,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
) { uri ->
|
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
launchPasswordActivityWithPath(uri)
|
launchPasswordActivityWithPath(uri)
|
||||||
}
|
}
|
||||||
@@ -419,7 +439,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
openDatabaseButtonView!!,
|
openDatabaseButtonView!!,
|
||||||
{tapTargetView ->
|
{tapTargetView ->
|
||||||
tapTargetView?.let {
|
tapTargetView?.let {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
@@ -428,6 +448,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
||||||
|
}
|
||||||
|
|
||||||
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,10 +76,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.view.AddNodeButtonView
|
import com.kunzisoft.keepass.view.*
|
||||||
import com.kunzisoft.keepass.view.ToolbarAction
|
|
||||||
import com.kunzisoft.keepass.view.asError
|
|
||||||
import com.kunzisoft.keepass.view.showActionError
|
|
||||||
|
|
||||||
class GroupActivity : LockingActivity(),
|
class GroupActivity : LockingActivity(),
|
||||||
GroupEditDialogFragment.EditGroupListener,
|
GroupEditDialogFragment.EditGroupListener,
|
||||||
@@ -108,6 +105,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
private var mCurrentGroupIsASearch: Boolean = false
|
private var mCurrentGroupIsASearch: Boolean = false
|
||||||
private var mRequestStartupSearch = true
|
private var mRequestStartupSearch = true
|
||||||
|
|
||||||
|
private var actionNodeMode: ActionMode? = null
|
||||||
|
|
||||||
// To manage history in selection mode
|
// To manage history in selection mode
|
||||||
private var mSelectionModeCountBackStack = 0
|
private var mSelectionModeCountBackStack = 0
|
||||||
|
|
||||||
@@ -123,9 +122,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (isFinishing) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mDatabase = Database.getInstance()
|
mDatabase = Database.getInstance()
|
||||||
|
|
||||||
// Construct main view
|
// Construct main view
|
||||||
@@ -222,7 +218,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
|
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
|
||||||
|
|
||||||
// Init dialog thread
|
// Init dialog thread
|
||||||
mProgressDialogThread?.onActionFinish = { actionTask, result ->
|
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||||
|
|
||||||
var oldNodes: List<Node> = ArrayList()
|
var oldNodes: List<Node> = ArrayList()
|
||||||
result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle ->
|
result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle ->
|
||||||
@@ -481,7 +477,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
enableAddGroup(addGroupEnabled)
|
enableAddGroup(addGroupEnabled)
|
||||||
enableAddEntry(addEntryEnabled)
|
enableAddEntry(addEntryEnabled)
|
||||||
|
|
||||||
showButton()
|
if (actionNodeMode == null)
|
||||||
|
showButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,7 +508,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrolled(dy: Int) {
|
override fun onScrolled(dy: Int) {
|
||||||
addNodeButtonView?.hideButtonOnScrollListener(dy)
|
if (actionNodeMode == null)
|
||||||
|
addNodeButtonView?.hideOrShowButtonOnScrollListener(dy)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNodeClick(node: Node) {
|
override fun onNodeClick(node: Node) {
|
||||||
@@ -554,18 +552,28 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var actionNodeMode: ActionMode? = null
|
|
||||||
|
|
||||||
private fun finishNodeAction() {
|
private fun finishNodeAction() {
|
||||||
actionNodeMode?.finish()
|
actionNodeMode?.finish()
|
||||||
actionNodeMode = null
|
|
||||||
addNodeButtonView?.showButton()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNodeSelected(nodes: List<Node>): Boolean {
|
override fun onNodeSelected(nodes: List<Node>): Boolean {
|
||||||
if (nodes.isNotEmpty()) {
|
if (nodes.isNotEmpty()) {
|
||||||
if (actionNodeMode == null || toolbarAction?.getSupportActionModeCallback() == null) {
|
if (actionNodeMode == null || toolbarAction?.getSupportActionModeCallback() == null) {
|
||||||
mListNodesFragment?.actionNodesCallback(nodes, this)?.let {
|
mListNodesFragment?.actionNodesCallback(nodes, this, object: ActionMode.Callback {
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||||
|
actionNodeMode = null
|
||||||
|
addNodeButtonView?.showButton()
|
||||||
|
}
|
||||||
|
})?.let {
|
||||||
actionNodeMode = toolbarAction?.startSupportActionMode(it)
|
actionNodeMode = toolbarAction?.startSupportActionMode(it)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -622,7 +630,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
|
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
|
||||||
// Copy
|
// Copy
|
||||||
mCurrentGroup?.let { newParent ->
|
mCurrentGroup?.let { newParent ->
|
||||||
mProgressDialogThread?.startDatabaseCopyNodes(
|
mProgressDatabaseTaskProvider?.startDatabaseCopyNodes(
|
||||||
nodes,
|
nodes,
|
||||||
newParent,
|
newParent,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
@@ -632,7 +640,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
|
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
|
||||||
// Move
|
// Move
|
||||||
mCurrentGroup?.let { newParent ->
|
mCurrentGroup?.let { newParent ->
|
||||||
mProgressDialogThread?.startDatabaseMoveNodes(
|
mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
|
||||||
nodes,
|
nodes,
|
||||||
newParent,
|
newParent,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
@@ -666,7 +674,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
&& database.isRecycleBinEnabled
|
&& database.isRecycleBinEnabled
|
||||||
&& database.recycleBin != mCurrentGroup) {
|
&& database.recycleBin != mCurrentGroup) {
|
||||||
|
|
||||||
mProgressDialogThread?.startDatabaseDeleteNodes(
|
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
|
||||||
nodes,
|
nodes,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
)
|
)
|
||||||
@@ -681,7 +689,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun permanentlyDeleteNodes(nodes: List<Node>) {
|
override fun permanentlyDeleteNodes(nodes: List<Node>) {
|
||||||
mProgressDialogThread?.startDatabaseDeleteNodes(
|
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
|
||||||
nodes,
|
nodes,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
)
|
)
|
||||||
@@ -700,6 +708,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
assignGroupViewElements()
|
assignGroupViewElements()
|
||||||
// Refresh suggestions to change preferences
|
// Refresh suggestions to change preferences
|
||||||
mSearchSuggestionAdapter?.reInit(this)
|
mSearchSuggestionAdapter?.reInit(this)
|
||||||
|
// Padding if lock button visible
|
||||||
|
toolbarAction?.updateLockPaddingLeft()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -840,7 +850,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
//onSearchRequested();
|
//onSearchRequested();
|
||||||
return true
|
return true
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_empty_recycle_bin -> {
|
R.id.menu_empty_recycle_bin -> {
|
||||||
@@ -874,7 +884,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
// Not really needed here because added in runnable but safe
|
// Not really needed here because added in runnable but safe
|
||||||
newGroup.parent = currentGroup
|
newGroup.parent = currentGroup
|
||||||
|
|
||||||
mProgressDialogThread?.startDatabaseCreateGroup(
|
mProgressDatabaseTaskProvider?.startDatabaseCreateGroup(
|
||||||
newGroup,
|
newGroup,
|
||||||
currentGroup,
|
currentGroup,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
@@ -896,7 +906,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If group updated save it in the database
|
// If group updated save it in the database
|
||||||
mProgressDialogThread?.startDatabaseUpdateGroup(
|
mProgressDatabaseTaskProvider?.startDatabaseUpdateGroup(
|
||||||
oldGroupToUpdate,
|
oldGroupToUpdate,
|
||||||
updateGroup,
|
updateGroup,
|
||||||
!mReadOnly && mAutoSaveEnable
|
!mReadOnly && mAutoSaveEnable
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
nodeClickListener = null
|
||||||
|
onScrollListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -266,14 +272,15 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun actionNodesCallback(nodes: List<Node>,
|
fun actionNodesCallback(nodes: List<Node>,
|
||||||
menuListener: NodesActionMenuListener?) : ActionMode.Callback {
|
menuListener: NodesActionMenuListener?,
|
||||||
|
actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
|
||||||
|
|
||||||
return object : ActionMode.Callback {
|
return object : ActionMode.Callback {
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
return true
|
return actionModeCallback.onCreateActionMode(mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
@@ -318,7 +325,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
// Add the number of items selected in title
|
// Add the number of items selected in title
|
||||||
mode?.title = nodes.size.toString()
|
mode?.title = nodes.size.toString()
|
||||||
|
|
||||||
return true
|
return actionModeCallback.onPrepareActionMode(mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||||
@@ -348,7 +355,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
returnValue
|
returnValue
|
||||||
}
|
}
|
||||||
else -> false
|
else -> actionModeCallback.onActionItemClicked(mode, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +365,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
mAdapter?.unselectActionNodes()
|
mAdapter?.unselectActionNodes()
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
|
actionModeCallback.onDestroyActionMode(mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.activities
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.app.backup.BackupManager
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -34,24 +33,25 @@ import android.util.Log
|
|||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
@@ -60,14 +60,17 @@ import com.kunzisoft.keepass.model.SearchInfo
|
|||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_password.*
|
import kotlinx.android.synthetic.main.activity_password.*
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
@@ -81,16 +84,17 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var checkboxPasswordView: CompoundButton? = null
|
private var checkboxPasswordView: CompoundButton? = null
|
||||||
private var checkboxKeyFileView: CompoundButton? = null
|
private var checkboxKeyFileView: CompoundButton? = null
|
||||||
private var checkboxDefaultDatabaseView: CompoundButton? = null
|
|
||||||
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
||||||
private var infoContainerView: ViewGroup? = null
|
private var infoContainerView: ViewGroup? = null
|
||||||
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
||||||
|
|
||||||
|
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
private var mDatabaseKeyFileUri: Uri? = null
|
private var mDatabaseKeyFileUri: Uri? = null
|
||||||
|
|
||||||
private var mRememberKeyFile: Boolean = false
|
private var mRememberKeyFile: Boolean = false
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
private var mPermissionAsked = false
|
private var mPermissionAsked = false
|
||||||
private var readOnly: Boolean = false
|
private var readOnly: Boolean = false
|
||||||
@@ -105,7 +109,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mProgressDialogThread: ProgressDialogThread? = null
|
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
|
||||||
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
|
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
|
||||||
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
||||||
@@ -127,16 +131,16 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||||
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
|
||||||
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
|
|
||||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||||
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.apply {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||||
setOnClickListener(it)
|
setOnClickListener(it)
|
||||||
setOnLongClickListener(it)
|
setOnLongClickListener(it)
|
||||||
}
|
}
|
||||||
@@ -163,12 +167,34 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||||
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
|
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
|
||||||
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
||||||
}
|
}
|
||||||
|
|
||||||
mProgressDialogThread = ProgressDialogThread(this).apply {
|
// Observe database file change
|
||||||
|
databaseFileViewModel.databaseFileLoaded.observe(this, Observer { databaseFile ->
|
||||||
|
// Force read only if the file does not exists
|
||||||
|
mForceReadOnly = databaseFile?.let {
|
||||||
|
!it.databaseFileExists
|
||||||
|
} ?: true
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
|
// Post init uri with KeyFile only if needed
|
||||||
|
val keyFileUri =
|
||||||
|
if (mRememberKeyFile
|
||||||
|
&& (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
||||||
|
databaseFile?.keyFileUri
|
||||||
|
} else {
|
||||||
|
mDatabaseKeyFileUri
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define title
|
||||||
|
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
||||||
|
|
||||||
|
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
||||||
|
})
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||||
onActionFinish = { actionTask, result ->
|
onActionFinish = { actionTask, result ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
@@ -205,7 +231,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
result.data?.let { resultData ->
|
result.data?.let { resultData ->
|
||||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||||
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
|
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
|
||||||
keyFileUri = resultData.getParcelable(KEY_FILE_KEY)
|
keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY)
|
||||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||||
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||||
}
|
}
|
||||||
@@ -351,7 +377,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
mProgressDialogThread?.registerProgressTask()
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
|
||||||
// Back to previous keyboard is setting activated
|
// Back to previous keyboard is setting activated
|
||||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
|
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
|
||||||
@@ -364,72 +390,23 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
else
|
else
|
||||||
mAllowAutoOpenBiometricPrompt
|
mAllowAutoOpenBiometricPrompt
|
||||||
|
|
||||||
initUriFromIntent()
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
|
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
checkPermission()
|
checkPermission()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initUriFromIntent() {
|
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||||
/*
|
|
||||||
// "canXrite" doesn't work with Google Drive, don't really know why?
|
|
||||||
mForceReadOnly = mDatabaseFileUri?.let {
|
|
||||||
!FileDatabaseInfo(this, it).canWrite
|
|
||||||
} ?: false
|
|
||||||
*/
|
|
||||||
mForceReadOnly = mDatabaseFileUri?.let {
|
|
||||||
!FileDatabaseInfo(this, it).exists
|
|
||||||
} ?: true
|
|
||||||
|
|
||||||
// Post init uri with KeyFile if needed
|
|
||||||
if (mRememberKeyFile && (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
|
||||||
// Retrieve KeyFile in a thread
|
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
|
||||||
FileDatabaseHistoryAction.getInstance(applicationContext)
|
|
||||||
.getKeyFileUriByDatabaseUri(databaseUri) {
|
|
||||||
onPostInitUri(databaseUri, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onPostInitUri(mDatabaseFileUri, mDatabaseKeyFileUri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
|
||||||
// Define title
|
|
||||||
databaseFileUri?.let {
|
|
||||||
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
|
|
||||||
filenameView?.text = title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define Key File text
|
// Define Key File text
|
||||||
if (mRememberKeyFile) {
|
if (mRememberKeyFile) {
|
||||||
populateKeyFileTextView(keyFileUri)
|
populateKeyFileTextView(keyFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define listeners for default database checkbox and validate button
|
// Define listener for validate button
|
||||||
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
var newDefaultFileUri: Uri? = null
|
|
||||||
if (isChecked) {
|
|
||||||
newDefaultFileUri = databaseFileUri ?: newDefaultFileUri
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferencesUtil.saveDefaultDatabasePath(this, newDefaultFileUri)
|
|
||||||
|
|
||||||
val backupManager = BackupManager(this@PasswordActivity)
|
|
||||||
backupManager.dataChanged()
|
|
||||||
}
|
|
||||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
||||||
|
|
||||||
// Retrieve settings for default database
|
|
||||||
val defaultFilename = PreferencesUtil.getDefaultDatabasePath(this)
|
|
||||||
if (databaseFileUri != null
|
|
||||||
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
|
|
||||||
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
|
|
||||||
checkboxDefaultDatabaseView?.isChecked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// If Activity is launch with a password and want to open directly
|
// If Activity is launch with a password and want to open directly
|
||||||
val intent = intent
|
val intent = intent
|
||||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||||
@@ -446,7 +423,6 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
var biometricInitialize = false
|
var biometricInitialize = false
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
||||||
|
|
||||||
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
||||||
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
@@ -478,6 +454,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
biometricInitialize = true
|
biometricInitialize = true
|
||||||
} else {
|
} else {
|
||||||
advancedUnlockedManager?.destroy()
|
advancedUnlockedManager?.destroy()
|
||||||
|
advancedUnlockInfoView?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!biometricInitialize) {
|
if (!biometricInitialize) {
|
||||||
@@ -533,7 +510,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
mProgressDialogThread?.unregisterProgressTask()
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
advancedUnlockedManager?.destroy()
|
advancedUnlockedManager?.destroy()
|
||||||
@@ -608,7 +585,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||||
fixDuplicateUUID: Boolean) {
|
fixDuplicateUUID: Boolean) {
|
||||||
mProgressDialogThread?.startDatabaseLoad(
|
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
password,
|
password,
|
||||||
keyFile,
|
keyFile,
|
||||||
@@ -772,7 +749,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var keyFileResult = false
|
var keyFileResult = false
|
||||||
mOpenFileHelper?.let {
|
mSelectFileHelper?.let {
|
||||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||||
) { uri ->
|
) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
|
|||||||
@@ -25,16 +25,19 @@ import android.content.DialogInterface
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
|
||||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||||
@@ -56,7 +59,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private var mListener: AssignPasswordDialogListener? = null
|
private var mListener: AssignPasswordDialogListener? = null
|
||||||
|
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
|
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||||
|
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||||
|
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
||||||
|
|
||||||
private val passwordTextWatcher = object : TextWatcher {
|
private val passwordTextWatcher = object : TextWatcher {
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
@@ -85,6 +92,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
mEmptyPasswordConfirmationDialog?.dismiss()
|
||||||
|
mEmptyPasswordConfirmationDialog = null
|
||||||
|
mNoKeyConfirmationDialog?.dismiss()
|
||||||
|
mNoKeyConfirmationDialog = null
|
||||||
|
mEmptyKeyFileConfirmationDialog?.dismiss()
|
||||||
|
mEmptyKeyFileConfirmationDialog = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
|
|
||||||
@@ -99,11 +117,15 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
||||||
builder.setView(rootView)
|
builder.setView(rootView)
|
||||||
.setTitle(R.string.assign_master_key)
|
|
||||||
// Add action buttons
|
// Add action buttons
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
|
val credentialsInfo: ImageView? = rootView?.findViewById(R.id.credentials_information)
|
||||||
|
credentialsInfo?.setOnClickListener {
|
||||||
|
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
||||||
|
}
|
||||||
|
|
||||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
||||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||||
@@ -113,10 +135,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
mOpenFileHelper = OpenFileHelper(this)
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.apply {
|
||||||
setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||||
setOnLongClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
@@ -129,7 +151,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
mMasterPassword = ""
|
mMasterPassword = ""
|
||||||
mKeyFile = null
|
mKeyFile = null
|
||||||
|
|
||||||
var error = verifyPassword() || verifyFile()
|
var error = verifyPassword() || verifyKeyFile()
|
||||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||||
error = true
|
error = true
|
||||||
if (allowNoMasterKey)
|
if (allowNoMasterKey)
|
||||||
@@ -199,7 +221,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyFile(): Boolean {
|
private fun verifyKeyFile(): Boolean {
|
||||||
var error = false
|
var error = false
|
||||||
if (keyFileCheckBox != null
|
if (keyFileCheckBox != null
|
||||||
&& keyFileCheckBox!!.isChecked) {
|
&& keyFileCheckBox!!.isChecked) {
|
||||||
@@ -219,7 +241,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(it)
|
val builder = AlertDialog.Builder(it)
|
||||||
builder.setMessage(R.string.warning_empty_password)
|
builder.setMessage(R.string.warning_empty_password)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
if (!verifyFile()) {
|
if (!verifyKeyFile()) {
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||||
@@ -227,7 +249,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
builder.create().show()
|
mEmptyPasswordConfirmationDialog = builder.create()
|
||||||
|
mEmptyPasswordConfirmationDialog?.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,18 +265,44 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
this@AssignMasterKeyDialogFragment.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
builder.create().show()
|
mNoKeyConfirmationDialog = builder.create()
|
||||||
|
mNoKeyConfirmationDialog?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEmptyKeyFileConfirmationDialog() {
|
||||||
|
activity?.let {
|
||||||
|
val builder = AlertDialog.Builder(it)
|
||||||
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
|
append(getString(R.string.warning_empty_keyfile))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_empty_keyfile_explanation))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
})
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
keyFileCheckBox?.isChecked = false
|
||||||
|
keyFileSelectionView?.uri = null
|
||||||
|
}
|
||||||
|
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||||
|
mEmptyKeyFileConfirmationDialog?.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
) { uri ->
|
|
||||||
uri?.let { pathUri ->
|
uri?.let { pathUri ->
|
||||||
keyFileCheckBox?.isChecked = true
|
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||||
keyFileSelectionView?.uri = pathUri
|
keyFileSelectionView?.error = null
|
||||||
|
keyFileCheckBox?.isChecked = true
|
||||||
|
keyFileSelectionView?.uri = pathUri
|
||||||
|
if (lengthFile <= 0L) {
|
||||||
|
showEmptyKeyFileConfirmationDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ class DatePickerFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
// Create a new instance of DatePickerDialog and return it
|
// Create a new instance of DatePickerDialog and return it
|
||||||
return context?.let {
|
return context?.let {
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ class DeleteNodesDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
|
import com.kunzisoft.keepass.model.Field
|
||||||
|
|
||||||
|
|
||||||
|
class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||||
|
|
||||||
|
private var oldField: Field? = null
|
||||||
|
|
||||||
|
private var entryCustomFieldListener: EntryCustomFieldListener? = null
|
||||||
|
|
||||||
|
private var customFieldLabelContainer: TextInputLayout? = null
|
||||||
|
private var customFieldLabel: TextView? = null
|
||||||
|
private var customFieldDeleteButton: ImageView? = null
|
||||||
|
private var customFieldProtectionButton: CompoundButton? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
entryCustomFieldListener = context as EntryCustomFieldListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
// The activity doesn't implement the interface, throw exception
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + EntryCustomFieldListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
entryCustomFieldListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_entry_new_field, null)
|
||||||
|
customFieldLabelContainer = root?.findViewById(R.id.entry_custom_field_label_container)
|
||||||
|
customFieldLabel = root?.findViewById(R.id.entry_custom_field_label)
|
||||||
|
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
|
||||||
|
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
|
||||||
|
|
||||||
|
oldField = arguments?.getParcelable(KEY_FIELD)
|
||||||
|
oldField?.let { oldCustomField ->
|
||||||
|
customFieldLabel?.text = oldCustomField.name
|
||||||
|
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
|
||||||
|
|
||||||
|
customFieldDeleteButton?.visibility = View.VISIBLE
|
||||||
|
customFieldDeleteButton?.setOnClickListener {
|
||||||
|
entryCustomFieldListener?.onDeleteCustomFieldApproved(oldCustomField)
|
||||||
|
(dialog as AlertDialog?)?.dismiss()
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
customFieldDeleteButton?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setView(root)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
val dialogCreated = builder.create()
|
||||||
|
|
||||||
|
customFieldLabel?.requestFocus()
|
||||||
|
customFieldLabel?.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||||
|
customFieldLabel?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
|
approveIfValid()
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogCreated.window?.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE)
|
||||||
|
return dialogCreated
|
||||||
|
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// To prevent auto dismiss
|
||||||
|
val d = dialog as AlertDialog?
|
||||||
|
if (d != null) {
|
||||||
|
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||||
|
positiveButton.setOnClickListener {
|
||||||
|
approveIfValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun approveIfValid() {
|
||||||
|
if (isValid()) {
|
||||||
|
oldField?.let {
|
||||||
|
// New property with old value
|
||||||
|
entryCustomFieldListener?.onEditCustomFieldApproved(it,
|
||||||
|
Field(customFieldLabel?.text?.toString() ?: "",
|
||||||
|
ProtectedString(customFieldProtectionButton?.isChecked == true,
|
||||||
|
it.protectedValue.stringValue))
|
||||||
|
)
|
||||||
|
} ?: run {
|
||||||
|
entryCustomFieldListener?.onNewCustomFieldApproved(
|
||||||
|
Field(customFieldLabel?.text?.toString() ?: "",
|
||||||
|
ProtectedString(customFieldProtectionButton?.isChecked == true))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
(dialog as AlertDialog?)?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValid(): Boolean {
|
||||||
|
return if (customFieldLabel?.text?.toString()?.isNotEmpty() != true) {
|
||||||
|
setError(R.string.error_string_key)
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
setError(null)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setError(@StringRes errorId: Int?) {
|
||||||
|
customFieldLabelContainer?.error = if (errorId == null) null else {
|
||||||
|
requireContext().getString(errorId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntryCustomFieldListener {
|
||||||
|
fun onNewCustomFieldApproved(newField: Field)
|
||||||
|
fun onEditCustomFieldApproved(oldField: Field, newField: Field)
|
||||||
|
fun onDeleteCustomFieldApproved(oldField: Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val KEY_FIELD = "KEY_FIELD"
|
||||||
|
|
||||||
|
fun getInstance(): EntryCustomFieldDialogFragment {
|
||||||
|
return EntryCustomFieldDialogFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInstance(field: Field): EntryCustomFieldDialogFragment {
|
||||||
|
return EntryCustomFieldDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_FIELD, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Dialog to confirm big file to upload
|
||||||
|
*/
|
||||||
|
class FileTooBigDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mActionChooseListener: ActionChooseListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
// Verify that the host activity implements the callback interface
|
||||||
|
try {
|
||||||
|
mActionChooseListener = context as ActionChooseListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + ActionChooseListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mActionChooseListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
// Use the Builder class for convenient dialog construction
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
|
append(getString(R.string.warning_file_too_big))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
})
|
||||||
|
builder.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||||
|
mActionChooseListener?.onValidateUploadFileTooBig(
|
||||||
|
arguments?.getParcelable(KEY_FILE_URI),
|
||||||
|
arguments?.getString(KEY_FILE_NAME))
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.no) { _, _ ->
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
// Create the AlertDialog object and return it
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionChooseListener {
|
||||||
|
fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_WARNING_BINARY_FILE = 5242880
|
||||||
|
|
||||||
|
private const val KEY_FILE_URI = "KEY_FILE_URI"
|
||||||
|
private const val KEY_FILE_NAME = "KEY_FILE_NAME"
|
||||||
|
|
||||||
|
fun build(attachmentToUploadUri: Uri,
|
||||||
|
fileName: String): FileTooBigDialogFragment {
|
||||||
|
val fragment = FileTooBigDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
|
||||||
|
putString(KEY_FILE_NAME, fileName)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,6 +64,11 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|||||||
@@ -73,7 +73,11 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
throw ClassCastException(context.toString()
|
throw ClassCastException(context.toString()
|
||||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
editGroupListener = null
|
||||||
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.widget.ImageViewCompat
|
import androidx.core.widget.ImageViewCompat
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.icons.IconPack
|
import com.kunzisoft.keepass.icons.IconPack
|
||||||
@@ -56,6 +57,11 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
iconPickerListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
@@ -132,7 +138,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
return bundle.getParcelable(KEY_ICON_STANDARD)
|
return bundle.getParcelable(KEY_ICON_STANDARD)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launch(activity: AppCompatActivity) {
|
fun launch(activity: FragmentActivity) {
|
||||||
// Create an instance of the dialog fragment and show it
|
// Create an instance of the dialog fragment and show it
|
||||||
val dialog = IconPickerDialogFragment()
|
val dialog = IconPickerDialogFragment()
|
||||||
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
||||||
|
|||||||
@@ -21,20 +21,51 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.DialogInterface
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
|
private var mListener: Listener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
mListener = context as Listener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + Listener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
|
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
||||||
|
val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false
|
||||||
|
val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY)
|
||||||
|
val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false
|
||||||
|
val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY)
|
||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
||||||
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mListener?.onPasswordEncodingValidateListener(
|
||||||
|
databaseUri,
|
||||||
|
masterPasswordChecked,
|
||||||
|
masterPassword,
|
||||||
|
keyFileChecked,
|
||||||
|
keyFile
|
||||||
|
)
|
||||||
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
@@ -42,5 +73,36 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||||
|
masterPasswordChecked: Boolean,
|
||||||
|
masterPassword: String?,
|
||||||
|
keyFileChecked: Boolean,
|
||||||
|
keyFile: Uri?)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||||
|
private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
|
||||||
|
private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
|
||||||
|
private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
|
||||||
|
private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
|
||||||
|
|
||||||
|
fun getInstance(databaseUri: Uri,
|
||||||
|
masterPasswordChecked: Boolean,
|
||||||
|
masterPassword: String?,
|
||||||
|
keyFileChecked: Boolean,
|
||||||
|
keyFile: Uri?): SortDialogFragment {
|
||||||
|
val fragment = SortDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||||
|
putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||||
|
putString(MASTER_PASSWORD_KEY, masterPassword)
|
||||||
|
putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
|
||||||
|
putParcelable(KEY_FILE_URI_KEY, keyFile)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Dialog to confirm big file to upload
|
||||||
|
*/
|
||||||
|
class ReplaceFileDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mActionChooseListener: ActionChooseListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
// Verify that the host activity implements the callback interface
|
||||||
|
try {
|
||||||
|
mActionChooseListener = context as ActionChooseListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + ActionChooseListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mActionChooseListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
// Use the Builder class for convenient dialog construction
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
|
append(getString(R.string.warning_replace_file))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
})
|
||||||
|
builder.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||||
|
mActionChooseListener?.onValidateReplaceFile(
|
||||||
|
arguments?.getParcelable(KEY_FILE_URI),
|
||||||
|
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.no) { _, _ ->
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
// Create the AlertDialog object and return it
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionChooseListener {
|
||||||
|
fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_FILE_URI = "KEY_FILE_URI"
|
||||||
|
private const val KEY_ENTRY_ATTACHMENT = "KEY_ENTRY_ATTACHMENT"
|
||||||
|
|
||||||
|
fun build(attachmentToUploadUri: Uri,
|
||||||
|
attachment: Attachment): ReplaceFileDialogFragment {
|
||||||
|
val fragment = ReplaceFileDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
|
||||||
|
putParcelable(KEY_ENTRY_ATTACHMENT, attachment)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import android.text.TextWatcher
|
|||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
@@ -106,6 +107,11 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mCreateOTPElementListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
@@ -152,6 +158,28 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
otpCounterTextView?.setOnTouchListener(mOnTouchListener)
|
otpCounterTextView?.setOnTouchListener(mOnTouchListener)
|
||||||
otpDigitsTextView?.setOnTouchListener(mOnTouchListener)
|
otpDigitsTextView?.setOnTouchListener(mOnTouchListener)
|
||||||
|
|
||||||
|
// To manage focus
|
||||||
|
otpPeriodTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||||
|
otpDigitsTextView?.requestFocus()
|
||||||
|
true
|
||||||
|
} else
|
||||||
|
false
|
||||||
|
}
|
||||||
|
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||||
|
otpDigitsTextView?.requestFocus()
|
||||||
|
true
|
||||||
|
} else
|
||||||
|
false
|
||||||
|
}
|
||||||
|
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||||
|
root?.requestFocus(View.FOCUS_DOWN)
|
||||||
|
true
|
||||||
|
} else
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
// HOTP / TOTP Type selection
|
// HOTP / TOTP Type selection
|
||||||
val otpTypeArray = OtpType.values()
|
val otpTypeArray = OtpType.values()
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ class SortDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ class TimePickerFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
// Create a new instance of DatePickerDialog and return it
|
// Create a new instance of DatePickerDialog and return it
|
||||||
return context?.let {
|
return context?.let {
|
||||||
|
|||||||
@@ -28,19 +28,20 @@ import android.content.pm.PackageManager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class OpenFileHelper {
|
class SelectFileHelper {
|
||||||
|
|
||||||
private var activity: Activity? = null
|
private var activity: Activity? = null
|
||||||
private var fragment: Fragment? = null
|
private var fragment: Fragment? = null
|
||||||
|
|
||||||
val openFileOnClickViewListener: OpenFileOnClickViewListener
|
val selectFileOnClickViewListener: SelectFileOnClickViewListener
|
||||||
get() = OpenFileOnClickViewListener()
|
get() = SelectFileOnClickViewListener()
|
||||||
|
|
||||||
constructor(context: Activity) {
|
constructor(context: Activity) {
|
||||||
this.activity = context
|
this.activity = context
|
||||||
@@ -52,7 +53,10 @@ class OpenFileHelper {
|
|||||||
this.fragment = context
|
this.fragment = context
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class OpenFileOnClickViewListener : View.OnClickListener, View.OnLongClickListener {
|
inner class SelectFileOnClickViewListener :
|
||||||
|
View.OnClickListener,
|
||||||
|
View.OnLongClickListener,
|
||||||
|
MenuItem.OnMenuItemClickListener {
|
||||||
|
|
||||||
private fun onAbstractClick(longClick: Boolean = false) {
|
private fun onAbstractClick(longClick: Boolean = false) {
|
||||||
try {
|
try {
|
||||||
@@ -85,6 +89,11 @@ class OpenFileHelper {
|
|||||||
onAbstractClick(true)
|
onAbstractClick(true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||||
|
onAbstractClick()
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
@@ -27,7 +27,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
@@ -51,7 +51,7 @@ abstract class LockingActivity : SpecialModeActivity() {
|
|||||||
private var mReadOnlyToSave: Boolean = false
|
private var mReadOnlyToSave: Boolean = false
|
||||||
protected var mAutoSaveEnable: Boolean = true
|
protected var mAutoSaveEnable: Boolean = true
|
||||||
|
|
||||||
var mProgressDialogThread: ProgressDialogThread? = null
|
var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -80,7 +80,7 @@ abstract class LockingActivity : SpecialModeActivity() {
|
|||||||
|
|
||||||
mExitLock = false
|
mExitLock = false
|
||||||
|
|
||||||
mProgressDialogThread = ProgressDialogThread(this)
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
@@ -96,7 +96,7 @@ abstract class LockingActivity : SpecialModeActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
mProgressDialogThread?.registerProgressTask()
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
|
||||||
// To refresh when back to normal workflow from selection workflow
|
// To refresh when back to normal workflow from selection workflow
|
||||||
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
|
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
|
||||||
@@ -131,7 +131,7 @@ abstract class LockingActivity : SpecialModeActivity() {
|
|||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE = false
|
LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||||
|
|
||||||
mProgressDialogThread?.unregisterProgressTask()
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
|
|||||||
@@ -46,12 +46,19 @@ abstract class StylishFragment : Fragment() {
|
|||||||
// To fix status bar color
|
// To fix status bar color
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
val window = requireActivity().window
|
val window = requireActivity().window
|
||||||
|
|
||||||
val attrColorPrimaryDark = intArrayOf(android.R.attr.colorPrimaryDark)
|
|
||||||
val taColorPrimaryDark = contextThemed?.theme?.obtainStyledAttributes(attrColorPrimaryDark)
|
|
||||||
val defaultColor = Color.BLACK
|
val defaultColor = Color.BLACK
|
||||||
window.statusBarColor = taColorPrimaryDark?.getColor(0, defaultColor) ?: defaultColor
|
|
||||||
taColorPrimaryDark?.recycle()
|
try {
|
||||||
|
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
|
||||||
|
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||||
|
taStatusBarColor?.recycle()
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
|
||||||
|
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||||
|
taNavigationBarColor?.recycle()
|
||||||
|
} catch (e: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onCreateView(inflater, container, savedInstanceState)
|
return super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.view.collapse
|
||||||
|
|
||||||
|
abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val context: Context)
|
||||||
|
: RecyclerView.Adapter<T>() {
|
||||||
|
|
||||||
|
protected val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
var itemsList: MutableList<Item> = ArrayList()
|
||||||
|
private set
|
||||||
|
|
||||||
|
var onDeleteButtonClickListener: ((item: Item)->Unit)? = null
|
||||||
|
private var mItemToRemove: Item? = null
|
||||||
|
|
||||||
|
var onListSizeChangedListener: ((previousSize: Int, newSize: Int)->Unit)? = null
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return itemsList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun assignItems(items: List<Item>) {
|
||||||
|
val previousSize = itemsList.size
|
||||||
|
itemsList.apply {
|
||||||
|
clear()
|
||||||
|
addAll(items)
|
||||||
|
}
|
||||||
|
notifyDataSetChanged()
|
||||||
|
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun isEmpty(): Boolean {
|
||||||
|
return itemsList.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun contains(item: Item): Boolean {
|
||||||
|
return itemsList.contains(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun indexOf(item: Item): Int {
|
||||||
|
return itemsList.indexOf(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun putItem(item: Item) {
|
||||||
|
val previousSize = itemsList.size
|
||||||
|
if (itemsList.contains(item)) {
|
||||||
|
val index = itemsList.indexOf(item)
|
||||||
|
itemsList.removeAt(index)
|
||||||
|
itemsList.add(index, item)
|
||||||
|
notifyItemChanged(index)
|
||||||
|
} else {
|
||||||
|
itemsList.add(item)
|
||||||
|
notifyItemInserted(itemsList.indexOf(item))
|
||||||
|
}
|
||||||
|
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only replace [oldItem] by [newItem] if [oldItem] exists
|
||||||
|
*/
|
||||||
|
open fun replaceItem(oldItem: Item, newItem: Item) {
|
||||||
|
if (itemsList.contains(oldItem)) {
|
||||||
|
val index = itemsList.indexOf(oldItem)
|
||||||
|
itemsList.removeAt(index)
|
||||||
|
itemsList.add(index, newItem)
|
||||||
|
notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only remove [item] if doesn't exists
|
||||||
|
*/
|
||||||
|
open fun removeItem(item: Item) {
|
||||||
|
if (itemsList.contains(item)) {
|
||||||
|
mItemToRemove = item
|
||||||
|
notifyItemChanged(itemsList.indexOf(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun performDeletion(holder: T, item: Item): Boolean {
|
||||||
|
val effectivelyDeletionPerformed = mItemToRemove == item
|
||||||
|
if (effectivelyDeletionPerformed) {
|
||||||
|
holder.itemView.collapse(true) {
|
||||||
|
deleteItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return effectivelyDeletionPerformed
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
|
||||||
|
deleteButton.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
if (performDeletion(holder, item)) {
|
||||||
|
setOnClickListener(null)
|
||||||
|
} else {
|
||||||
|
setOnClickListener {
|
||||||
|
onDeleteButtonClickListener?.invoke(item)
|
||||||
|
mItemToRemove = item
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteItem(item: Item) {
|
||||||
|
val previousSize = itemsList.size
|
||||||
|
val position = itemsList.indexOf(item)
|
||||||
|
if (position >= 0) {
|
||||||
|
itemsList.removeAt(position)
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
mItemToRemove = null
|
||||||
|
for (i in 0 until itemsList.size) {
|
||||||
|
notifyItemChanged(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
itemsList.clear()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.adapters
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.text.format.Formatter
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
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
|
|
||||||
|
|
||||||
class EntryAttachmentsAdapter(val context: Context) : RecyclerView.Adapter<EntryAttachmentsAdapter.EntryBinariesViewHolder>() {
|
|
||||||
|
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
|
|
||||||
val entryAttachment = entryAttachmentsList[position]
|
|
||||||
|
|
||||||
holder.binaryFileTitle.text = entryAttachment.name
|
|
||||||
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
|
||||||
entryAttachment.binaryAttachment.length())
|
|
||||||
holder.binaryFileCompression.apply {
|
|
||||||
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|
|
||||||
|| entryAttachment.binaryAttachment.isCompressed == true) {
|
|
||||||
text = CompressionAlgorithm.GZip.getName(context.resources)
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
text = ""
|
|
||||||
visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
holder.binaryFileProgress.apply {
|
|
||||||
visibility = when (entryAttachment.downloadState) {
|
|
||||||
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
|
|
||||||
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
|
|
||||||
}
|
|
||||||
progress = entryAttachment.downloadProgression
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.itemView.setOnClickListener {
|
|
||||||
onItemClickListener?.invoke(entryAttachment, position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return entryAttachmentsList.size
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateProgress(entryAttachment: EntryAttachment) {
|
|
||||||
val indexEntryAttachment = entryAttachmentsList.indexOfLast { current -> current.name == entryAttachment.name }
|
|
||||||
if (indexEntryAttachment != -1) {
|
|
||||||
entryAttachmentsList[indexEntryAttachment] = entryAttachment
|
|
||||||
notifyItemChanged(indexEntryAttachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clear() {
|
|
||||||
entryAttachmentsList.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
|
|
||||||
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
|
||||||
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
|
||||||
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
|
||||||
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.TypedArray
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
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.CompressionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
|
||||||
|
|
||||||
|
class EntryAttachmentsItemsAdapter(context: Context)
|
||||||
|
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
||||||
|
|
||||||
|
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
||||||
|
|
||||||
|
private var mTitleColor: Int
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Get the primary text color of the theme
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
context.theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true)
|
||||||
|
val typedArray: TypedArray = context.obtainStyledAttributes(typedValue.data, intArrayOf(
|
||||||
|
android.R.attr.textColor))
|
||||||
|
mTitleColor = typedArray.getColor(0, -1)
|
||||||
|
typedArray.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
|
||||||
|
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
|
||||||
|
val entryAttachmentState = itemsList[position]
|
||||||
|
|
||||||
|
holder.itemView.visibility = View.VISIBLE
|
||||||
|
holder.binaryFileBroken.apply {
|
||||||
|
setColorFilter(Color.RED)
|
||||||
|
visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
|
||||||
|
if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
||||||
|
holder.binaryFileTitle.setTextColor(Color.RED)
|
||||||
|
} else {
|
||||||
|
holder.binaryFileTitle.setTextColor(mTitleColor)
|
||||||
|
}
|
||||||
|
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
||||||
|
entryAttachmentState.attachment.binaryAttachment.length())
|
||||||
|
holder.binaryFileCompression.apply {
|
||||||
|
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
|
||||||
|
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
text = ""
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (entryAttachmentState.streamDirection) {
|
||||||
|
StreamDirection.UPLOAD -> {
|
||||||
|
holder.binaryFileProgressIcon.isActivated = true
|
||||||
|
when (entryAttachmentState.downloadState) {
|
||||||
|
AttachmentState.START,
|
||||||
|
AttachmentState.IN_PROGRESS -> {
|
||||||
|
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||||
|
holder.binaryFileProgress.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
progress = entryAttachmentState.downloadProgression
|
||||||
|
}
|
||||||
|
holder.binaryFileDeleteButton.apply {
|
||||||
|
visibility = View.GONE
|
||||||
|
setOnClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AttachmentState.NULL,
|
||||||
|
AttachmentState.ERROR,
|
||||||
|
AttachmentState.COMPLETE -> {
|
||||||
|
holder.binaryFileProgressContainer.visibility = View.GONE
|
||||||
|
holder.binaryFileProgress.visibility = View.GONE
|
||||||
|
holder.binaryFileDeleteButton.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
onBindDeleteButton(holder, this, entryAttachmentState, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.itemView.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
StreamDirection.DOWNLOAD -> {
|
||||||
|
holder.binaryFileProgressIcon.isActivated = false
|
||||||
|
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||||
|
holder.binaryFileDeleteButton.visibility = View.GONE
|
||||||
|
holder.binaryFileProgress.apply {
|
||||||
|
visibility = when (entryAttachmentState.downloadState) {
|
||||||
|
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
|
||||||
|
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||||
|
}
|
||||||
|
progress = entryAttachmentState.downloadProgression
|
||||||
|
}
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
onItemClickListener?.invoke(entryAttachmentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken)
|
||||||
|
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
||||||
|
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
||||||
|
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
||||||
|
var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
|
||||||
|
var binaryFileProgressIcon: ImageView = itemView.findViewById(R.id.item_attachment_icon)
|
||||||
|
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
|
||||||
|
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,31 +22,33 @@ package com.kunzisoft.keepass.adapters
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.*
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.*
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.ViewSwitcher
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryEntity
|
import com.kunzisoft.keepass.model.DatabaseFile
|
||||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
import com.kunzisoft.keepass.view.collapse
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
|
||||||
class FileDatabaseHistoryAdapter(private val context: Context)
|
class FileDatabaseHistoryAdapter(context: Context)
|
||||||
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
|
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
|
||||||
|
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
private var fileItemOpenListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
private var defaultDatabaseListener: ((DatabaseFile?) -> Unit)? = null
|
||||||
private var fileSelectClearListener: ((FileDatabaseHistoryEntity)->Boolean)? = null
|
private var fileItemOpenListener: ((DatabaseFile)->Unit)? = null
|
||||||
private var saveAliasListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null
|
||||||
|
private var saveAliasListener: ((DatabaseFile)->Unit)? = null
|
||||||
|
|
||||||
private val listDatabaseFiles = ArrayList<FileDatabaseHistoryEntity>()
|
private val listDatabaseFiles = ArrayList<DatabaseFile>()
|
||||||
|
|
||||||
private var mExpandedPosition = -1
|
private var mDefaultDatabaseFile: DatabaseFile? = null
|
||||||
private var mPreviousExpandedPosition = -1
|
private var mExpandedDatabaseFile: DatabaseFile? = null
|
||||||
|
private var mPreviousExpandedDatabaseFile: DatabaseFile? = null
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val defaultColor: Int
|
private val defaultColor: Int
|
||||||
@@ -63,43 +65,49 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
|
||||||
val view = inflater.inflate(R.layout.item_file_row, parent, false)
|
val view = inflater.inflate(R.layout.item_file_info, parent, false)
|
||||||
return FileDatabaseHistoryViewHolder(view)
|
return FileDatabaseHistoryViewHolder(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
||||||
// Get info from position
|
// Get info from position
|
||||||
val fileHistoryEntity = listDatabaseFiles[position]
|
val databaseFile = listDatabaseFiles[position]
|
||||||
val fileDatabaseInfo = FileDatabaseInfo(context, fileHistoryEntity.databaseUri)
|
|
||||||
|
|
||||||
// Click item to open file
|
// Click item to open file
|
||||||
if (fileItemOpenListener != null)
|
holder.fileContainer.setOnClickListener {
|
||||||
holder.fileContainer.setOnClickListener {
|
fileItemOpenListener?.invoke(databaseFile)
|
||||||
fileItemOpenListener?.invoke(fileHistoryEntity)
|
}
|
||||||
|
|
||||||
|
// Default database
|
||||||
|
holder.defaultFileButton.apply {
|
||||||
|
this.isChecked = mDefaultDatabaseFile == databaseFile
|
||||||
|
setOnClickListener {
|
||||||
|
defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// File alias
|
// File alias
|
||||||
holder.fileAlias.text = fileDatabaseInfo.retrieveDatabaseAlias(fileHistoryEntity.databaseAlias)
|
holder.fileAlias.text = databaseFile.databaseAlias
|
||||||
|
|
||||||
// File path
|
// File path
|
||||||
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
|
holder.filePath.text = databaseFile.databaseDecodedPath
|
||||||
|
|
||||||
if (fileDatabaseInfo.exists) {
|
if (databaseFile.databaseFileExists) {
|
||||||
holder.fileInformation.clearColorFilter()
|
holder.fileInformationButton.clearColorFilter()
|
||||||
} else {
|
} else {
|
||||||
holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
|
holder.fileInformationButton.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modification
|
// Modification
|
||||||
fileDatabaseInfo.getModificationString()?.let {
|
databaseFile.databaseLastModified?.let {
|
||||||
holder.fileModification.text = it
|
holder.fileModification.text = it
|
||||||
holder.fileModification.visibility = View.VISIBLE
|
holder.fileModificationContainer.visibility = View.VISIBLE
|
||||||
} ?: run {
|
} ?: run {
|
||||||
holder.fileModification.visibility = View.GONE
|
holder.fileModificationContainer.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size
|
// Size
|
||||||
fileDatabaseInfo.getSizeString()?.let {
|
databaseFile.databaseSize?.let {
|
||||||
holder.fileSize.text = it
|
holder.fileSize.text = it
|
||||||
holder.fileSize.visibility = View.VISIBLE
|
holder.fileSize.visibility = View.VISIBLE
|
||||||
} ?: run {
|
} ?: run {
|
||||||
@@ -107,15 +115,24 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Click on information
|
// Click on information
|
||||||
val isExpanded = position == mExpandedPosition
|
val isExpanded = databaseFile == mExpandedDatabaseFile
|
||||||
//This line hides or shows the layout in question
|
// Hides or shows info
|
||||||
holder.fileExpandContainer.visibility = if (isExpanded) View.VISIBLE else View.GONE
|
holder.fileExpandContainer.apply {
|
||||||
|
if (isExpanded) {
|
||||||
|
if (visibility != View.VISIBLE) {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
collapse(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save alias modification
|
// Save alias modification
|
||||||
holder.fileAliasCloseButton.setOnClickListener {
|
holder.fileAliasCloseButton.setOnClickListener {
|
||||||
// Change the alias
|
// Change the alias
|
||||||
fileHistoryEntity.databaseAlias = holder.fileAliasEdit.text.toString()
|
databaseFile.databaseAlias = holder.fileAliasEdit.text.toString()
|
||||||
saveAliasListener?.invoke(fileHistoryEntity)
|
saveAliasListener?.invoke(databaseFile)
|
||||||
|
|
||||||
// Finish save mode
|
// Finish save mode
|
||||||
holder.fileMainSwitcher.showPrevious()
|
holder.fileMainSwitcher.showPrevious()
|
||||||
@@ -130,20 +147,22 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
holder.fileDeleteButton.setOnClickListener {
|
holder.fileDeleteButton.setOnClickListener {
|
||||||
fileSelectClearListener?.invoke(fileHistoryEntity)
|
fileSelectClearListener?.invoke(databaseFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
mPreviousExpandedPosition = position
|
mPreviousExpandedDatabaseFile = databaseFile
|
||||||
}
|
}
|
||||||
|
holder.fileInformationButton.apply {
|
||||||
holder.fileInformation.setOnClickListener {
|
animate().rotation(if (isExpanded) 180F else 0F).start()
|
||||||
mExpandedPosition = if (isExpanded) -1 else position
|
setOnClickListener {
|
||||||
|
mExpandedDatabaseFile = if (isExpanded) null else databaseFile
|
||||||
// Notify change
|
// Notify change
|
||||||
if (mPreviousExpandedPosition < itemCount)
|
val previousExpandedPosition = listDatabaseFiles.indexOf(mPreviousExpandedDatabaseFile)
|
||||||
notifyItemChanged(mPreviousExpandedPosition)
|
notifyItemChanged(previousExpandedPosition)
|
||||||
notifyItemChanged(position)
|
val expandedPosition = listDatabaseFiles.indexOf(mExpandedDatabaseFile)
|
||||||
|
notifyItemChanged(expandedPosition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh View / Close alias modification if not contains fileAlias
|
// Refresh View / Close alias modification if not contains fileAlias
|
||||||
@@ -160,33 +179,68 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
listDatabaseFiles.clear()
|
listDatabaseFiles.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
|
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
|
||||||
listDatabaseFiles.clear()
|
listDatabaseFiles.add(0, fileDatabaseHistoryToAdd)
|
||||||
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
notifyItemInserted(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: FileDatabaseHistoryEntity) {
|
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
|
||||||
listDatabaseFiles.remove(fileDatabaseHistoryToDelete)
|
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToUpdate)
|
||||||
|
if (listDatabaseFiles.remove(fileDatabaseHistoryToUpdate)) {
|
||||||
|
listDatabaseFiles.add(index, fileDatabaseHistoryToUpdate)
|
||||||
|
notifyItemChanged(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnFileDatabaseHistoryOpenListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) {
|
||||||
|
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToDelete)
|
||||||
|
if (listDatabaseFiles.remove(fileDatabaseHistoryToDelete)) {
|
||||||
|
notifyItemRemoved(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) {
|
||||||
|
if (listDatabaseFiles.isEmpty()) {
|
||||||
|
listFileDatabaseHistoryToAdd.forEach {
|
||||||
|
listDatabaseFiles.add(it)
|
||||||
|
notifyItemInserted(listDatabaseFiles.size)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
listDatabaseFiles.clear()
|
||||||
|
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultDatabase(databaseUri: Uri?) {
|
||||||
|
val defaultDatabaseFile = listDatabaseFiles.firstOrNull { it.databaseUri == databaseUri }
|
||||||
|
mDefaultDatabaseFile = defaultDatabaseFile
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) {
|
||||||
|
this.defaultDatabaseListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnFileDatabaseHistoryOpenListener(listener : ((DatabaseFile)->Unit)?) {
|
||||||
this.fileItemOpenListener = listener
|
this.fileItemOpenListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnFileDatabaseHistoryDeleteListener(listener : ((FileDatabaseHistoryEntity)->Boolean)?) {
|
fun setOnFileDatabaseHistoryDeleteListener(listener : ((DatabaseFile)->Boolean)?) {
|
||||||
this.fileSelectClearListener = listener
|
this.fileSelectClearListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnSaveAliasListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
fun setOnSaveAliasListener(listener : ((DatabaseFile)->Unit)?) {
|
||||||
this.saveAliasListener = listener
|
this.saveAliasListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
|
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
|
||||||
|
|
||||||
|
var defaultFileButton: CompoundButton = itemView.findViewById(R.id.default_file_button)
|
||||||
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
|
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
|
||||||
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
|
var fileInformationButton: ImageView = itemView.findViewById(R.id.file_information_button)
|
||||||
|
|
||||||
var fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
|
var fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
|
||||||
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
|
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
|
||||||
@@ -196,6 +250,7 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
|
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
|
||||||
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
||||||
var filePath: TextView = itemView.findViewById(R.id.file_path)
|
var filePath: TextView = itemView.findViewById(R.id.file_path)
|
||||||
|
var fileModificationContainer: ViewGroup = itemView.findViewById(R.id.file_modification_container)
|
||||||
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
|
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
|
||||||
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,6 +338,9 @@ class NodeAdapter (private val context: Context)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.attachmentIcon?.visibility =
|
||||||
|
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
mDatabase.stopManageEntry(entry)
|
mDatabase.stopManageEntry(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,6 +394,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||||
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
||||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||||
|
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
|
|
||||||
fun getCipherDatabase(databaseUri: Uri,
|
fun getCipherDatabase(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
@@ -51,7 +51,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
|
|
||||||
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
|
|
||||||
fun deleteByDatabaseUri(databaseUri: Uri,
|
fun deleteByDatabaseUri(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
@@ -81,7 +81,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.deleteAll()
|
cipherDatabaseDao.deleteAll()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,31 +21,44 @@ package com.kunzisoft.keepass.app.database
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import com.kunzisoft.keepass.model.DatabaseFile
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
|
||||||
|
|
||||||
class FileDatabaseHistoryAction(applicationContext: Context) {
|
class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||||
|
|
||||||
private val databaseFileHistoryDao =
|
private val databaseFileHistoryDao =
|
||||||
AppDatabase
|
AppDatabase
|
||||||
.getDatabase(applicationContext)
|
.getDatabase(applicationContext)
|
||||||
.fileDatabaseHistoryDao()
|
.fileDatabaseHistoryDao()
|
||||||
|
|
||||||
fun getFileDatabaseHistory(databaseUri: Uri,
|
fun getDatabaseFile(databaseUri: Uri,
|
||||||
fileHistoryResultListener: (fileDatabaseHistoryResult: FileDatabaseHistoryEntity?) -> Unit) {
|
databaseFileResult: (DatabaseFile?) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
val fileDatabaseHistoryEntity = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
|
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, databaseUri)
|
||||||
|
DatabaseFile(
|
||||||
|
databaseUri,
|
||||||
|
UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri),
|
||||||
|
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
|
||||||
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
|
||||||
|
fileDatabaseInfo.exists,
|
||||||
|
fileDatabaseInfo.getModificationString(),
|
||||||
|
fileDatabaseInfo.getSizeString()
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fileHistoryResultListener.invoke(it)
|
databaseFileResult.invoke(it)
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
|
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
|
||||||
keyFileUriResultListener: (Uri?) -> Unit) {
|
keyFileUriResultListener: (Uri?) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
@@ -59,61 +72,120 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllFileDatabaseHistories(fileHistoryResultListener: (fileDatabaseHistoryResult: List<FileDatabaseHistoryEntity>?) -> Unit) {
|
fun getDatabaseFileList(databaseFileListResult: (List<DatabaseFile>) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.getAll()
|
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(applicationContext)
|
||||||
|
// Show only uri accessible
|
||||||
|
val databaseFileListLoaded = ArrayList<DatabaseFile>()
|
||||||
|
databaseFileHistoryDao.getAll().forEach { fileDatabaseHistoryEntity ->
|
||||||
|
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, fileDatabaseHistoryEntity.databaseUri)
|
||||||
|
if (hideBrokenLocations && fileDatabaseInfo.exists
|
||||||
|
|| !hideBrokenLocations) {
|
||||||
|
databaseFileListLoaded.add(
|
||||||
|
DatabaseFile(
|
||||||
|
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
|
||||||
|
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
|
||||||
|
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
|
||||||
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||||
|
fileDatabaseInfo.exists,
|
||||||
|
fileDatabaseInfo.getModificationString(),
|
||||||
|
fileDatabaseInfo.getSizeString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
databaseFileListLoaded
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fileHistoryResultListener.invoke(it)
|
databaseFileList ->
|
||||||
}
|
databaseFileList?.let {
|
||||||
).execute()
|
databaseFileListResult.invoke(it)
|
||||||
}
|
|
||||||
|
|
||||||
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null) {
|
|
||||||
addOrUpdateFileDatabaseHistory(FileDatabaseHistoryEntity(
|
|
||||||
databaseUri.toString(),
|
|
||||||
"",
|
|
||||||
keyFileUri?.toString(),
|
|
||||||
System.currentTimeMillis()
|
|
||||||
), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addOrUpdateFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity, unmodifiedAlias: Boolean = false) {
|
|
||||||
ActionDatabaseAsyncTask(
|
|
||||||
{
|
|
||||||
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(fileDatabaseHistory.databaseUri)
|
|
||||||
|
|
||||||
if (unmodifiedAlias) {
|
|
||||||
fileDatabaseHistory.databaseAlias = fileDatabaseHistoryRetrieve?.databaseAlias ?: ""
|
|
||||||
}
|
|
||||||
// Update values if history element not yet in the database
|
|
||||||
if (fileDatabaseHistoryRetrieve == null) {
|
|
||||||
databaseFileHistoryDao.add(fileDatabaseHistory)
|
|
||||||
} else {
|
|
||||||
databaseFileHistoryDao.update(fileDatabaseHistory)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity,
|
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
|
||||||
fileHistoryDeletedResult: (FileDatabaseHistoryEntity?) -> Unit) {
|
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||||
ActionDatabaseAsyncTask(
|
addOrUpdateDatabaseFile(DatabaseFile(
|
||||||
|
databaseUri,
|
||||||
|
keyFileUri
|
||||||
|
), databaseFileAddedOrUpdatedResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addOrUpdateDatabaseFile(databaseFileToAddOrUpdate: DatabaseFile,
|
||||||
|
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||||
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.delete(fileDatabaseHistory)
|
databaseFileToAddOrUpdate.databaseUri?.let { databaseUri ->
|
||||||
|
// Try to get info in database first
|
||||||
|
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
|
|
||||||
|
// Complete alias if not exists
|
||||||
|
val fileDatabaseHistory = FileDatabaseHistoryEntity(
|
||||||
|
databaseUri.toString(),
|
||||||
|
databaseFileToAddOrUpdate.databaseAlias
|
||||||
|
?: fileDatabaseHistoryRetrieve?.databaseAlias
|
||||||
|
?: "",
|
||||||
|
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
||||||
|
System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update values if history element not yet in the database
|
||||||
|
if (fileDatabaseHistoryRetrieve == null) {
|
||||||
|
databaseFileHistoryDao.add(fileDatabaseHistory)
|
||||||
|
} else {
|
||||||
|
databaseFileHistoryDao.update(fileDatabaseHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
|
||||||
|
fileDatabaseHistory.databaseUri)
|
||||||
|
DatabaseFile(
|
||||||
|
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||||
|
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||||
|
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||||
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||||
|
fileDatabaseInfo.exists,
|
||||||
|
fileDatabaseInfo.getModificationString(),
|
||||||
|
fileDatabaseInfo.getSizeString()
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
if (it != null && it > 0)
|
databaseFileAddedOrUpdatedResult?.invoke(it)
|
||||||
fileHistoryDeletedResult.invoke(fileDatabaseHistory)
|
}
|
||||||
else
|
).execute()
|
||||||
fileHistoryDeletedResult.invoke(null)
|
}
|
||||||
|
|
||||||
|
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile,
|
||||||
|
databaseFileDeletedResult: (DatabaseFile?) -> Unit) {
|
||||||
|
IOActionTask(
|
||||||
|
{
|
||||||
|
databaseFileToDelete.databaseUri?.let { databaseUri ->
|
||||||
|
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())?.let { fileDatabaseHistory ->
|
||||||
|
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
|
||||||
|
if (returnValue > 0) {
|
||||||
|
DatabaseFile(
|
||||||
|
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||||
|
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||||
|
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||||
|
databaseFileToDelete.databaseAlias
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
databaseFileDeletedResult.invoke(it)
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||||
}
|
}
|
||||||
@@ -121,7 +193,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAllKeyFiles() {
|
fun deleteAllKeyFiles() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteAllKeyFiles()
|
databaseFileHistoryDao.deleteAllKeyFiles()
|
||||||
}
|
}
|
||||||
@@ -129,7 +201,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteAll()
|
databaseFileHistoryDao.deleteAll()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,21 +19,27 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.app.database
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
import android.os.AsyncTask
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private class to invoke each method in a separate thread
|
* Class to invoke action in a separate IO thread
|
||||||
*/
|
*/
|
||||||
class ActionDatabaseAsyncTask<T>(
|
class IOActionTask<T>(
|
||||||
private val action: () -> T ,
|
private val action: () -> T ,
|
||||||
private val afterActionDatabaseListener: ((T?) -> Unit)? = null
|
private val afterActionDatabaseListener: ((T?) -> Unit)? = null) {
|
||||||
) : AsyncTask<Void, Void, T>() {
|
|
||||||
|
|
||||||
override fun doInBackground(vararg args: Void?): T? {
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
return action.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostExecute(result: T?) {
|
fun execute() {
|
||||||
afterActionDatabaseListener?.invoke(result)
|
mainScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val asyncResult: Deferred<T?> = async {
|
||||||
|
action.invoke()
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
afterActionDatabaseListener?.invoke(asyncResult.await())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@ import android.provider.Settings
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
@@ -334,7 +335,9 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showFingerPrintViews(show: Boolean) {
|
private fun showFingerPrintViews(show: Boolean) {
|
||||||
context.runOnUiThread { advancedUnlockInfoView?.hide = !show }
|
context.runOnUiThread {
|
||||||
|
advancedUnlockInfoView?.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ open class AssignPasswordInDatabaseRunnable (
|
|||||||
: SaveDatabaseRunnable(context, database, true) {
|
: SaveDatabaseRunnable(context, database, true) {
|
||||||
|
|
||||||
private var mMasterPassword: String? = null
|
private var mMasterPassword: String? = null
|
||||||
protected var mKeyFile: Uri? = null
|
protected var mKeyFileUri: Uri? = null
|
||||||
|
|
||||||
private var mBackupKey: ByteArray? = null
|
private var mBackupKey: ByteArray? = null
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ open class AssignPasswordInDatabaseRunnable (
|
|||||||
if (withMasterPassword)
|
if (withMasterPassword)
|
||||||
this.mMasterPassword = masterPassword
|
this.mMasterPassword = masterPassword
|
||||||
if (withKeyFile)
|
if (withKeyFile)
|
||||||
this.mKeyFile = keyFile
|
this.mKeyFileUri = keyFile
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
@@ -55,7 +55,7 @@ open class AssignPasswordInDatabaseRunnable (
|
|||||||
mBackupKey = ByteArray(database.masterKey.size)
|
mBackupKey = ByteArray(database.masterKey.size)
|
||||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
||||||
|
|
||||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFile)
|
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFileUri)
|
||||||
database.retrieveMasterKey(mMasterPassword, uriInputStream)
|
database.retrieveMasterKey(mMasterPassword, uriInputStream)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
erase(mBackupKey)
|
erase(mBackupKey)
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ class CreateDatabaseRunnable(context: Context,
|
|||||||
withMasterPassword: Boolean,
|
withMasterPassword: Boolean,
|
||||||
masterPassword: String?,
|
masterPassword: String?,
|
||||||
withKeyFile: Boolean,
|
withKeyFile: Boolean,
|
||||||
keyFile: Uri?)
|
keyFile: Uri?,
|
||||||
|
private val createDatabaseResult: ((Result) -> Unit)?)
|
||||||
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
|
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
@@ -42,29 +43,36 @@ class CreateDatabaseRunnable(context: Context,
|
|||||||
// Create new database record
|
// Create new database record
|
||||||
mDatabase.apply {
|
mDatabase.apply {
|
||||||
createData(mDatabaseUri, databaseName, rootName)
|
createData(mDatabaseUri, databaseName, rootName)
|
||||||
// Set Database state
|
|
||||||
loaded = true
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
mDatabase.closeAndClear()
|
mDatabase.closeAndClear(context.applicationContext.filesDir)
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onStartRun()
|
super.onStartRun()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinishRun() {
|
override fun onActionRun() {
|
||||||
super.onFinishRun()
|
super.onActionRun()
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
// Add database to recent files
|
// Add database to recent files
|
||||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||||
.addOrUpdateDatabaseUri(mDatabaseUri,
|
.addOrUpdateDatabaseUri(mDatabaseUri,
|
||||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFile else null)
|
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFileUri else null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register the current time to init the lock timer
|
||||||
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
} else {
|
} else {
|
||||||
Log.e("CreateDatabaseRunnable", "Unable to create the database")
|
Log.e("CreateDatabaseRunnable", "Unable to create the database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFinishRun() {
|
||||||
|
super.onFinishRun()
|
||||||
|
|
||||||
|
createDatabaseResult?.invoke(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,14 +40,12 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
private val mCipherEntity: CipherDatabaseEntity?,
|
private val mCipherEntity: CipherDatabaseEntity?,
|
||||||
private val mFixDuplicateUUID: Boolean,
|
private val mFixDuplicateUUID: Boolean,
|
||||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
private val mDuplicateUuidAction: ((Result) -> Unit)?)
|
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||||
: ActionRunnable() {
|
: ActionRunnable() {
|
||||||
|
|
||||||
private val cacheDirectory = context.applicationContext.filesDir
|
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
// Clear before we load
|
// Clear before we load
|
||||||
mDatabase.closeAndClear(cacheDirectory)
|
mDatabase.closeAndClear(context.applicationContext.filesDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
@@ -55,20 +53,17 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
mDatabase.loadData(mUri, mPass, mKey,
|
mDatabase.loadData(mUri, mPass, mKey,
|
||||||
mReadonly,
|
mReadonly,
|
||||||
context.contentResolver,
|
context.contentResolver,
|
||||||
cacheDirectory,
|
context.applicationContext.filesDir,
|
||||||
mFixDuplicateUUID,
|
mFixDuplicateUUID,
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
}
|
}
|
||||||
catch (e: DuplicateUuidDatabaseException) {
|
catch (e: DuplicateUuidDatabaseException) {
|
||||||
mDuplicateUuidAction?.invoke(result)
|
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
catch (e: LoadDatabaseException) {
|
catch (e: LoadDatabaseException) {
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinishRun() {
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
// Save keyFile in app database
|
// Save keyFile in app database
|
||||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||||
@@ -86,7 +81,11 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
// Register the current time to init the lock timer
|
// Register the current time to init the lock timer
|
||||||
PreferencesUtil.saveCurrentTime(context)
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
} else {
|
} else {
|
||||||
mDatabase.closeAndClear(cacheDirectory)
|
mDatabase.closeAndClear(context.applicationContext.filesDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFinishRun() {
|
||||||
|
mLoadDatabaseResult?.invoke(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import android.content.*
|
|||||||
import android.content.Context.BIND_ABOVE_CLIENT
|
import android.content.Context.BIND_ABOVE_CLIENT
|
||||||
import android.content.Context.BIND_NOT_FOREGROUND
|
import android.content.Context.BIND_NOT_FOREGROUND
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
@@ -46,6 +45,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
|||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
|
||||||
@@ -71,12 +71,12 @@ import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class ProgressDialogThread(private val activity: FragmentActivity) {
|
class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||||
|
|
||||||
var onActionFinish: ((actionTask: String,
|
var onActionFinish: ((actionTask: String,
|
||||||
result: ActionRunnable.Result) -> Unit)? = null
|
result: ActionRunnable.Result) -> Unit)? = null
|
||||||
|
|
||||||
private var intentDatabaseTask = Intent(activity, DatabaseTaskNotificationService::class.java)
|
private var intentDatabaseTask = Intent(activity.applicationContext, DatabaseTaskNotificationService::class.java)
|
||||||
|
|
||||||
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
||||||
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
||||||
@@ -218,12 +218,8 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
activity.stopService(intentDatabaseTask)
|
activity.stopService(intentDatabaseTask)
|
||||||
if (bundle != null)
|
if (bundle != null)
|
||||||
intentDatabaseTask.putExtras(bundle)
|
intentDatabaseTask.putExtras(bundle)
|
||||||
intentDatabaseTask.action = actionTask
|
intentDatabaseTask.action = actionTask
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
activity.startService(intentDatabaseTask)
|
||||||
activity.startForegroundService(intentDatabaseTask)
|
|
||||||
} else {
|
|
||||||
activity.startService(intentDatabaseTask)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -242,7 +238,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
||||||
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
||||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
|
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_CREATE_TASK)
|
, ACTION_DATABASE_CREATE_TASK)
|
||||||
}
|
}
|
||||||
@@ -256,7 +252,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
||||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
|
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
|
||||||
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
||||||
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
|
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
@@ -275,7 +271,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
||||||
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
||||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
|
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
|
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
|
||||||
}
|
}
|
||||||
@@ -467,6 +463,13 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
||||||
|
start(Bundle().apply {
|
||||||
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
|
}
|
||||||
|
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||||
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
||||||
newMaxHistoryItems: Int,
|
newMaxHistoryItems: Int,
|
||||||
save: Boolean) {
|
save: Boolean) {
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.action
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
|
||||||
|
class RemoveUnlinkedDataDatabaseRunnable (
|
||||||
|
context: Context,
|
||||||
|
database: Database,
|
||||||
|
saveDatabase: Boolean)
|
||||||
|
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||||
|
|
||||||
|
override fun onActionRun() {
|
||||||
|
try {
|
||||||
|
database.removeUnlinkedAttachments()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setError(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onActionRun()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ class DeleteEntryHistoryDatabaseRunnable (
|
|||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
try {
|
try {
|
||||||
mainEntry.removeEntryFromHistory(entryHistoryPosition)
|
database.removeEntryHistory(mainEntry, entryHistoryPosition)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ class DeleteNodesRunnable(context: Context,
|
|||||||
} else {
|
} else {
|
||||||
database.deleteEntry(currentNode)
|
database.deleteEntry(currentNode)
|
||||||
}
|
}
|
||||||
|
// Remove the oldest attachments
|
||||||
|
currentNode.getAttachments(database.binaryPool).forEach {
|
||||||
|
database.removeAttachmentIfNotUsed(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.database.action.node
|
package com.kunzisoft.keepass.database.action.node
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
@@ -40,16 +41,34 @@ class UpdateEntryRunnable constructor(
|
|||||||
// WARNING : Re attribute parent removed in entry edit activity to save memory
|
// WARNING : Re attribute parent removed in entry edit activity to save memory
|
||||||
mNewEntry.addParentFrom(mOldEntry)
|
mNewEntry.addParentFrom(mOldEntry)
|
||||||
|
|
||||||
|
// Build oldest attachments
|
||||||
|
val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool, true)
|
||||||
|
val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool, true)
|
||||||
|
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
|
||||||
|
// Not use equals because only check name
|
||||||
|
newEntryAttachments.forEach { newAttachment ->
|
||||||
|
oldEntryAttachments.forEach { oldAttachment ->
|
||||||
|
if (oldAttachment.name == newAttachment.name
|
||||||
|
&& oldAttachment.binaryAttachment == newAttachment.binaryAttachment)
|
||||||
|
attachmentsToRemove.remove(oldAttachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update entry with new values
|
// Update entry with new values
|
||||||
mOldEntry.updateWith(mNewEntry)
|
mOldEntry.updateWith(mNewEntry)
|
||||||
mNewEntry.touch(modified = true, touchParents = true)
|
mNewEntry.touch(modified = true, touchParents = true)
|
||||||
|
|
||||||
// Create an entry history (an entry history don't have history)
|
// Create an entry history (an entry history don't have history)
|
||||||
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
|
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
|
||||||
database.removeOldestEntryHistory(mOldEntry)
|
database.removeOldestEntryHistory(mOldEntry, database.binaryPool)
|
||||||
|
|
||||||
// Only change data in index
|
// Only change data in index
|
||||||
database.updateEntry(mOldEntry)
|
database.updateEntry(mOldEntry)
|
||||||
|
|
||||||
|
// Remove oldest attachments
|
||||||
|
attachmentsToRemove.forEach {
|
||||||
|
database.removeAttachmentIfNotUsed(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(): ActionNodesValues {
|
override fun nodeFinish(): ActionNodesValues {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.element
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||||
|
|
||||||
|
data class Attachment(var name: String,
|
||||||
|
var binaryAttachment: BinaryAttachment) : Parcelable {
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this(
|
||||||
|
parcel.readString() ?: "",
|
||||||
|
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment()
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(name)
|
||||||
|
parcel.writeParcelable(binaryAttachment, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$name at $binaryAttachment"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is Attachment) return false
|
||||||
|
|
||||||
|
if (name != other.name) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return name.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<Attachment> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): Attachment {
|
||||||
|
return Attachment(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<Attachment?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,9 +25,7 @@ import android.net.Uri
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.*
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
@@ -52,6 +50,7 @@ import com.kunzisoft.keepass.utils.SingletonHolder
|
|||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
@@ -70,6 +69,13 @@ class Database {
|
|||||||
val drawFactory = IconDrawableFactory()
|
val drawFactory = IconDrawableFactory()
|
||||||
|
|
||||||
var loaded = false
|
var loaded = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
loadTimestamp = if (field) System.currentTimeMillis() else null
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadTimestamp: Long? = null
|
||||||
|
private set
|
||||||
|
|
||||||
val iconFactory: IconImageFactory
|
val iconFactory: IconImageFactory
|
||||||
get() {
|
get() {
|
||||||
@@ -150,6 +156,17 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun compressionForNewEntry(): Boolean {
|
||||||
|
if (mDatabaseKDB != null)
|
||||||
|
return false
|
||||||
|
// Default compression not necessary if stored in header
|
||||||
|
mDatabaseKDBX?.let {
|
||||||
|
return it.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||||
|
&& it.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||||
newCompression: CompressionAlgorithm) {
|
newCompression: CompressionAlgorithm) {
|
||||||
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
||||||
@@ -261,14 +278,14 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if RecycleBin is available or not for this version of database
|
* Determine if a configurable RecycleBin is available or not for this version of database
|
||||||
* @return true if RecycleBin available
|
* @return true if a configurable RecycleBin available
|
||||||
*/
|
*/
|
||||||
val allowRecycleBin: Boolean
|
val allowConfigurableRecycleBin: Boolean
|
||||||
get() = mDatabaseKDBX != null
|
get() = mDatabaseKDBX != null
|
||||||
|
|
||||||
var isRecycleBinEnabled: Boolean
|
var isRecycleBinEnabled: Boolean
|
||||||
// TODO #394 isRecycleBinEnabled mDatabaseKDB
|
// Backup is always enabled in KDB database
|
||||||
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
|
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
|
||||||
set(value) {
|
set(value) {
|
||||||
mDatabaseKDBX?.isRecycleBinEnabled = value
|
mDatabaseKDBX?.isRecycleBinEnabled = value
|
||||||
@@ -286,12 +303,12 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun ensureRecycleBinExists(resources: Resources) {
|
fun ensureRecycleBinExists(resources: Resources) {
|
||||||
mDatabaseKDB?.ensureRecycleBinExists()
|
mDatabaseKDB?.ensureBackupExists()
|
||||||
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeRecycleBin() {
|
fun removeRecycleBin() {
|
||||||
// TODO #394 delete backup mDatabaseKDB?.removeRecycleBin()
|
// Don't allow remove backup in KDB
|
||||||
mDatabaseKDBX?.removeRecycleBin()
|
mDatabaseKDBX?.removeRecycleBin()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,6 +325,8 @@ class Database {
|
|||||||
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
||||||
setDatabaseKDBX(DatabaseKDBX(databaseName, rootName))
|
setDatabaseKDBX(DatabaseKDBX(databaseName, rootName))
|
||||||
this.fileUri = databaseUri
|
this.fileUri = databaseUri
|
||||||
|
// Set Database state
|
||||||
|
this.loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
@@ -419,6 +438,38 @@ class Database {
|
|||||||
}, omitBackup, max)
|
}, omitBackup, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val binaryPool: BinaryPool
|
||||||
|
get() {
|
||||||
|
return mDatabaseKDBX?.binaryPool ?: BinaryPool()
|
||||||
|
}
|
||||||
|
|
||||||
|
val allowMultipleAttachments: Boolean
|
||||||
|
get() {
|
||||||
|
if (mDatabaseKDB != null)
|
||||||
|
return false
|
||||||
|
if (mDatabaseKDBX != null)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildNewBinary(cacheDirectory: File,
|
||||||
|
enableProtection: Boolean = false,
|
||||||
|
compressed: Boolean = false): BinaryAttachment? {
|
||||||
|
return mDatabaseKDB?.buildNewBinary(cacheDirectory)
|
||||||
|
?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, enableProtection, compressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
||||||
|
// No need in KDB database because unique attachment by entry
|
||||||
|
// Don't clear to fix upload multiple times
|
||||||
|
mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryAttachment, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUnlinkedAttachments() {
|
||||||
|
// No check in database KDB because unique attachment by entry
|
||||||
|
mDatabaseKDBX?.removeUnlinkedAttachments(true)
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
fun saveData(contentResolver: ContentResolver) {
|
fun saveData(contentResolver: ContentResolver) {
|
||||||
try {
|
try {
|
||||||
@@ -464,7 +515,7 @@ class Database {
|
|||||||
} else {
|
} else {
|
||||||
var outputStream: OutputStream? = null
|
var outputStream: OutputStream? = null
|
||||||
try {
|
try {
|
||||||
outputStream = contentResolver.openOutputStream(uri)
|
outputStream = contentResolver.openOutputStream(uri, "rwt")
|
||||||
outputStream?.let { definedOutputStream ->
|
outputStream?.let { definedOutputStream ->
|
||||||
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
||||||
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
|
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
|
||||||
@@ -709,7 +760,7 @@ class Database {
|
|||||||
fun canRecycle(entry: Entry): Boolean {
|
fun canRecycle(entry: Entry): Boolean {
|
||||||
var canRecycle: Boolean? = null
|
var canRecycle: Boolean? = null
|
||||||
entry.entryKDB?.let {
|
entry.entryKDB?.let {
|
||||||
canRecycle = mDatabaseKDB?.canRecycle()
|
canRecycle = mDatabaseKDB?.canRecycle(it)
|
||||||
}
|
}
|
||||||
entry.entryKDBX?.let {
|
entry.entryKDBX?.let {
|
||||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||||
@@ -720,7 +771,7 @@ class Database {
|
|||||||
fun canRecycle(group: Group): Boolean {
|
fun canRecycle(group: Group): Boolean {
|
||||||
var canRecycle: Boolean? = null
|
var canRecycle: Boolean? = null
|
||||||
group.groupKDB?.let {
|
group.groupKDB?.let {
|
||||||
canRecycle = mDatabaseKDB?.canRecycle()
|
canRecycle = mDatabaseKDB?.canRecycle(it)
|
||||||
}
|
}
|
||||||
group.groupKDBX?.let {
|
group.groupKDBX?.let {
|
||||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||||
@@ -772,18 +823,25 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startManageEntry(entry: Entry) {
|
fun startManageEntry(entry: Entry?) {
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
entry.startToManageFieldReferences(it)
|
entry?.startToManageFieldReferences(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopManageEntry(entry: Entry) {
|
fun stopManageEntry(entry: Entry?) {
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
entry.stopToManageFieldReferences()
|
entry?.stopToManageFieldReferences()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if database allows custom field
|
||||||
|
*/
|
||||||
|
fun allowEntryCustomFields(): Boolean {
|
||||||
|
return mDatabaseKDBX != null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove oldest history for each entry if more than max items or max memory
|
* Remove oldest history for each entry if more than max items or max memory
|
||||||
*/
|
*/
|
||||||
@@ -791,7 +849,7 @@ class Database {
|
|||||||
rootGroup?.doForEachChildAndForIt(
|
rootGroup?.doForEachChildAndForIt(
|
||||||
object : NodeHandler<Entry>() {
|
object : NodeHandler<Entry>() {
|
||||||
override fun operate(node: Entry): Boolean {
|
override fun operate(node: Entry): Boolean {
|
||||||
removeOldestEntryHistory(node)
|
removeOldestEntryHistory(node, binaryPool)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -799,34 +857,19 @@ class Database {
|
|||||||
override fun operate(node: Group): Boolean {
|
override fun operate(node: Group): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
fun removeEachEntryHistory() {
|
|
||||||
rootGroup?.doForEachChildAndForIt(
|
|
||||||
object : NodeHandler<Entry>() {
|
|
||||||
override fun operate(node: Entry): Boolean {
|
|
||||||
node.removeAllHistory()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
object : NodeHandler<Group>() {
|
|
||||||
override fun operate(node: Group): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove oldest history if more than max items or max memory
|
* Remove oldest history if more than max items or max memory
|
||||||
*/
|
*/
|
||||||
fun removeOldestEntryHistory(entry: Entry) {
|
fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) {
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
|
|
||||||
val maxItems = historyMaxItems
|
val maxItems = historyMaxItems
|
||||||
if (maxItems >= 0) {
|
if (maxItems >= 0) {
|
||||||
while (entry.getHistory().size > maxItems) {
|
while (entry.getHistory().size > maxItems) {
|
||||||
entry.removeOldestEntryFromHistory()
|
removeOldestEntryHistory(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,11 +878,10 @@ class Database {
|
|||||||
while (true) {
|
while (true) {
|
||||||
var historySize: Long = 0
|
var historySize: Long = 0
|
||||||
for (entryHistory in entry.getHistory()) {
|
for (entryHistory in entry.getHistory()) {
|
||||||
historySize += entryHistory.getSize()
|
historySize += entryHistory.getSize(binaryPool)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (historySize > maxSize) {
|
if (historySize > maxSize) {
|
||||||
entry.removeOldestEntryFromHistory()
|
removeOldestEntryHistory(entry)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -848,6 +890,22 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removeOldestEntryHistory(entry: Entry) {
|
||||||
|
entry.removeOldestEntryFromHistory()?.let {
|
||||||
|
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
||||||
|
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
|
||||||
|
entry.removeEntryFromHistory(entryHistoryPosition)?.let {
|
||||||
|
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
||||||
|
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object : SingletonHolder<Database>(::Database) {
|
companion object : SingletonHolder<Database>(::Database) {
|
||||||
|
|
||||||
private val TAG = Database::class.java.name
|
private val TAG = Database::class.java.name
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import android.content.res.Resources
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
|
import org.joda.time.Duration
|
||||||
|
import org.joda.time.Instant
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -95,6 +97,7 @@ class DateInstant : Parcelable {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val NEVER_EXPIRE = neverExpire
|
val NEVER_EXPIRE = neverExpire
|
||||||
|
val IN_ONE_MONTH = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
|
||||||
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
|
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
|
||||||
|
|
||||||
private val neverExpire: DateInstant
|
private val neverExpire: DateInstant
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.element
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryPool
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
@@ -32,9 +33,6 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
|
||||||
import com.kunzisoft.keepass.model.EntryAttachment
|
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.Field
|
import com.kunzisoft.keepass.model.Field
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
@@ -285,66 +283,90 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve custom fields to show, key is the label, value is the value of field (protected or not)
|
* Retrieve extra fields to show, key is the label, value is the value of field (protected or not)
|
||||||
* @return Map of label/value
|
* @return Map of label/value
|
||||||
*/
|
*/
|
||||||
val customFields: HashMap<String, ProtectedString>
|
fun getExtraFields(): List<Field> {
|
||||||
get() = entryKDBX?.customFields ?: HashMap()
|
val extraFields = ArrayList<Field>()
|
||||||
|
entryKDBX?.let {
|
||||||
/**
|
for (field in it.customFields) {
|
||||||
* To redefine if version of entry allow custom field,
|
extraFields.add(Field(field.key, field.value))
|
||||||
* @return true if entry allows custom field
|
}
|
||||||
*/
|
}
|
||||||
fun allowCustomFields(): Boolean {
|
return extraFields
|
||||||
return entryKDBX?.allowCustomFields() ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAllFields() {
|
|
||||||
entryKDBX?.removeAllFields()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update or add an extra field to the list (standard or custom)
|
* Update or add an extra field to the list (standard or custom)
|
||||||
* @param label Label of field, must be unique
|
|
||||||
* @param value Value of field
|
|
||||||
*/
|
*/
|
||||||
fun putExtraField(label: String, value: ProtectedString) {
|
fun putExtraField(field: Field) {
|
||||||
entryKDBX?.putExtraField(label, value)
|
entryKDBX?.putExtraField(field.name, field.protectedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOtpElement(): OtpElement? {
|
private fun addExtraFields(fields: List<Field>) {
|
||||||
return OtpEntryFields.parseFields { key ->
|
fields.forEach {
|
||||||
customFields[key]?.toString()
|
putExtraField(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startToManageFieldReferences(db: DatabaseKDBX) {
|
private fun removeAllFields() {
|
||||||
entryKDBX?.startToManageFieldReferences(db)
|
entryKDBX?.removeAllFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOtpElement(): OtpElement? {
|
||||||
|
entryKDBX?.let {
|
||||||
|
return OtpEntryFields.parseFields { key ->
|
||||||
|
it.customFields[key]?.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
||||||
|
entryKDBX?.startToManageFieldReferences(database)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopToManageFieldReferences() {
|
fun stopToManageFieldReferences() {
|
||||||
entryKDBX?.stopToManageFieldReferences()
|
entryKDBX?.stopToManageFieldReferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAttachments(): ArrayList<EntryAttachment> {
|
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
|
||||||
val attachments = ArrayList<EntryAttachment>()
|
val attachments = ArrayList<Attachment>()
|
||||||
|
entryKDB?.getAttachment()?.let {
|
||||||
val binaryDescriptionKDB = entryKDB?.binaryDescription ?: ""
|
attachments.add(it)
|
||||||
val binaryKDB = entryKDB?.binaryData
|
|
||||||
if (binaryKDB != null) {
|
|
||||||
attachments.add(EntryAttachment(binaryDescriptionKDB, binaryKDB))
|
|
||||||
}
|
}
|
||||||
|
entryKDBX?.getAttachments(binaryPool, inHistory)?.let {
|
||||||
val actionEach = object : (Map.Entry<String, BinaryAttachment>)->Unit {
|
attachments.addAll(it)
|
||||||
override fun invoke(mapEntry: Map.Entry<String, BinaryAttachment>) {
|
|
||||||
attachments.add(EntryAttachment(mapEntry.key, mapEntry.value))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
entryKDBX?.binaries?.forEach(actionEach)
|
|
||||||
|
|
||||||
return attachments
|
return attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun containsAttachment(): Boolean {
|
||||||
|
return entryKDB?.containsAttachment() == true
|
||||||
|
|| entryKDBX?.containsAttachment() == true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addAttachments(binaryPool: BinaryPool, attachments: List<Attachment>) {
|
||||||
|
attachments.forEach {
|
||||||
|
putAttachment(it, binaryPool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeAttachment(attachment: Attachment) {
|
||||||
|
entryKDB?.removeAttachment(attachment)
|
||||||
|
entryKDBX?.removeAttachment(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeAllAttachments() {
|
||||||
|
entryKDB?.removeAttachment()
|
||||||
|
entryKDBX?.removeAttachments()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
|
||||||
|
entryKDB?.putAttachment(attachment)
|
||||||
|
entryKDBX?.putAttachment(attachment, binaryPool)
|
||||||
|
}
|
||||||
|
|
||||||
fun getHistory(): ArrayList<Entry> {
|
fun getHistory(): ArrayList<Entry> {
|
||||||
val history = ArrayList<Entry>()
|
val history = ArrayList<Entry>()
|
||||||
val entryKDBXHistory = entryKDBX?.history ?: ArrayList()
|
val entryKDBXHistory = entryKDBX?.history ?: ArrayList()
|
||||||
@@ -360,20 +382,22 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeEntryFromHistory(position: Int) {
|
fun removeEntryFromHistory(position: Int): Entry? {
|
||||||
entryKDBX?.removeEntryFromHistory(position)
|
entryKDBX?.removeEntryFromHistory(position)?.let {
|
||||||
|
return Entry(it)
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAllHistory() {
|
fun removeOldestEntryFromHistory(): Entry? {
|
||||||
entryKDBX?.removeAllHistory()
|
entryKDBX?.removeOldestEntryFromHistory()?.let {
|
||||||
|
return Entry(it)
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeOldestEntryFromHistory() {
|
fun getSize(binaryPool: BinaryPool): Long {
|
||||||
entryKDBX?.removeOldestEntryFromHistory()
|
return entryKDBX?.getSize(binaryPool) ?: 0L
|
||||||
}
|
|
||||||
|
|
||||||
fun getSize(): Long {
|
|
||||||
return entryKDBX?.size ?: 0L
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsCustomData(): Boolean {
|
fun containsCustomData(): Boolean {
|
||||||
@@ -396,26 +420,54 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
database?.stopManageEntry(this)
|
database?.stopManageEntry(this)
|
||||||
else
|
else
|
||||||
database?.startManageEntry(this)
|
database?.startManageEntry(this)
|
||||||
|
|
||||||
entryInfo.id = nodeId.toString()
|
entryInfo.id = nodeId.toString()
|
||||||
entryInfo.title = title
|
entryInfo.title = title
|
||||||
entryInfo.icon = icon
|
entryInfo.icon = icon
|
||||||
entryInfo.username = username
|
entryInfo.username = username
|
||||||
entryInfo.password = password
|
entryInfo.password = password
|
||||||
|
entryInfo.expires = expires
|
||||||
|
entryInfo.expiryTime = expiryTime
|
||||||
entryInfo.url = url
|
entryInfo.url = url
|
||||||
entryInfo.notes = notes
|
entryInfo.notes = notes
|
||||||
for (entry in customFields.entries) {
|
entryInfo.customFields = getExtraFields()
|
||||||
entryInfo.customFields.add(
|
|
||||||
Field(entry.key, entry.value))
|
|
||||||
}
|
|
||||||
// Add otpElement to generate token
|
// Add otpElement to generate token
|
||||||
entryInfo.otpModel = getOtpElement()?.otpModel
|
entryInfo.otpModel = getOtpElement()?.otpModel
|
||||||
// Replace parameter fields by generated OTP fields
|
if (!raw) {
|
||||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
// Replace parameter fields by generated OTP fields
|
||||||
|
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||||
|
}
|
||||||
|
database?.binaryPool?.let { binaryPool ->
|
||||||
|
entryInfo.attachments = getAttachments(binaryPool)
|
||||||
|
}
|
||||||
|
|
||||||
if (!raw)
|
if (!raw)
|
||||||
database?.stopManageEntry(this)
|
database?.stopManageEntry(this)
|
||||||
return entryInfo
|
return entryInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setEntryInfo(database: Database?, newEntryInfo: EntryInfo) {
|
||||||
|
database?.startManageEntry(this)
|
||||||
|
|
||||||
|
removeAllFields()
|
||||||
|
removeAllAttachments()
|
||||||
|
// NodeId stay as is
|
||||||
|
title = newEntryInfo.title
|
||||||
|
icon = newEntryInfo.icon
|
||||||
|
username = newEntryInfo.username
|
||||||
|
password = newEntryInfo.password
|
||||||
|
expires = newEntryInfo.expires
|
||||||
|
expiryTime = newEntryInfo.expiryTime
|
||||||
|
url = newEntryInfo.url
|
||||||
|
notes = newEntryInfo.notes
|
||||||
|
addExtraFields(newEntryInfo.customFields)
|
||||||
|
database?.binaryPool?.let { binaryPool ->
|
||||||
|
addAttachments(binaryPool, newEntryInfo.attachments)
|
||||||
|
}
|
||||||
|
|
||||||
|
database?.stopManageEntry(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|||||||
@@ -17,10 +17,8 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.security
|
package com.kunzisoft.keepass.database.element.database
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.stream.readBytes
|
import com.kunzisoft.keepass.stream.readBytes
|
||||||
@@ -30,10 +28,11 @@ import java.util.zip.GZIPOutputStream
|
|||||||
|
|
||||||
class BinaryAttachment : Parcelable {
|
class BinaryAttachment : Parcelable {
|
||||||
|
|
||||||
var isCompressed: Boolean? = null
|
var isCompressed: Boolean = false
|
||||||
private set
|
private set
|
||||||
var isProtected: Boolean = false
|
var isProtected: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
var isCorrupted: Boolean = false
|
||||||
private var dataFile: File? = null
|
private var dataFile: File? = null
|
||||||
|
|
||||||
fun length(): Long {
|
fun length(): Long {
|
||||||
@@ -45,13 +44,9 @@ class BinaryAttachment : Parcelable {
|
|||||||
/**
|
/**
|
||||||
* Empty protected binary
|
* Empty protected binary
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor()
|
||||||
this.isCompressed = null
|
|
||||||
this.isProtected = false
|
|
||||||
this.dataFile = null
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean? = null) {
|
constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean = false) {
|
||||||
this.isCompressed = compressed
|
this.isCompressed = compressed
|
||||||
this.isProtected = enableProtection
|
this.isProtected = enableProtection
|
||||||
this.dataFile = dataFile
|
this.dataFile = dataFile
|
||||||
@@ -59,8 +54,9 @@ class BinaryAttachment : Parcelable {
|
|||||||
|
|
||||||
private constructor(parcel: Parcel) {
|
private constructor(parcel: Parcel) {
|
||||||
val compressedByte = parcel.readByte().toInt()
|
val compressedByte = parcel.readByte().toInt()
|
||||||
isCompressed = if (compressedByte == 2) null else compressedByte != 0
|
isCompressed = compressedByte != 0
|
||||||
isProtected = parcel.readByte().toInt() != 0
|
isProtected = parcel.readByte().toInt() != 0
|
||||||
|
isCorrupted = parcel.readByte().toInt() != 0
|
||||||
parcel.readString()?.let {
|
parcel.readString()?.let {
|
||||||
dataFile = File(it)
|
dataFile = File(it)
|
||||||
}
|
}
|
||||||
@@ -74,32 +70,51 @@ class BinaryAttachment : Parcelable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getUnGzipInputDataStream(): InputStream {
|
||||||
|
return if (isCompressed)
|
||||||
|
GZIPInputStream(getInputDataStream())
|
||||||
|
else
|
||||||
|
getInputDataStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getOutputDataStream(): OutputStream {
|
||||||
|
return when {
|
||||||
|
dataFile != null -> FileOutputStream(dataFile!!)
|
||||||
|
else -> throw IOException("Unable to write in an unknown file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getGzipOutputDataStream(): OutputStream {
|
||||||
|
return if (isCompressed) {
|
||||||
|
GZIPOutputStream(getOutputDataStream())
|
||||||
|
} else {
|
||||||
|
getOutputDataStream()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||||
dataFile?.let { concreteDataFile ->
|
dataFile?.let { concreteDataFile ->
|
||||||
// To compress, create a new binary with file
|
// To compress, create a new binary with file
|
||||||
if (isCompressed != true) {
|
if (!isCompressed) {
|
||||||
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||||
var outputStream: GZIPOutputStream? = null
|
GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream ->
|
||||||
var inputStream: InputStream? = null
|
getInputDataStream().use { inputStream ->
|
||||||
try {
|
inputStream.readBytes(bufferSize) { buffer ->
|
||||||
outputStream = GZIPOutputStream(FileOutputStream(fileBinaryCompress))
|
outputStream.write(buffer)
|
||||||
inputStream = getInputDataStream()
|
|
||||||
inputStream.readBytes(bufferSize) { buffer ->
|
|
||||||
outputStream.write(buffer)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
inputStream?.close()
|
|
||||||
outputStream?.close()
|
|
||||||
|
|
||||||
// Remove unGzip file
|
|
||||||
if (concreteDataFile.delete()) {
|
|
||||||
if (fileBinaryCompress.renameTo(concreteDataFile)) {
|
|
||||||
// Harmonize with database compression
|
|
||||||
isCompressed = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Remove unGzip file
|
||||||
|
if (concreteDataFile.delete()) {
|
||||||
|
if (fileBinaryCompress.renameTo(concreteDataFile)) {
|
||||||
|
// Harmonize with database compression
|
||||||
|
isCompressed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,52 +122,20 @@ class BinaryAttachment : Parcelable {
|
|||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||||
dataFile?.let { concreteDataFile ->
|
dataFile?.let { concreteDataFile ->
|
||||||
if (isCompressed != false) {
|
if (isCompressed) {
|
||||||
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||||
var outputStream: FileOutputStream? = null
|
FileOutputStream(fileBinaryDecompress).use { outputStream ->
|
||||||
var inputStream: GZIPInputStream? = null
|
getUnGzipInputDataStream().use { inputStream ->
|
||||||
try {
|
inputStream.readBytes(bufferSize) { buffer ->
|
||||||
outputStream = FileOutputStream(fileBinaryDecompress)
|
outputStream.write(buffer)
|
||||||
inputStream = GZIPInputStream(getInputDataStream())
|
|
||||||
inputStream.readBytes(bufferSize) { buffer ->
|
|
||||||
outputStream.write(buffer)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
inputStream?.close()
|
|
||||||
outputStream?.close()
|
|
||||||
|
|
||||||
// Remove gzip file
|
|
||||||
if (concreteDataFile.delete()) {
|
|
||||||
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
|
|
||||||
// Harmonize with database compression
|
|
||||||
isCompressed = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// Remove gzip file
|
||||||
}
|
if (concreteDataFile.delete()) {
|
||||||
}
|
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
|
||||||
|
// Harmonize with database compression
|
||||||
fun download(createdFileUri: Uri,
|
isCompressed = false
|
||||||
contentResolver: ContentResolver,
|
|
||||||
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
|
||||||
update: ((percent: Int)->Unit)? = null) {
|
|
||||||
|
|
||||||
var dataDownloaded = 0
|
|
||||||
contentResolver.openOutputStream(createdFileUri).use { outputStream ->
|
|
||||||
outputStream?.let { fileOutputStream ->
|
|
||||||
if (isCompressed == true) {
|
|
||||||
GZIPInputStream(getInputDataStream())
|
|
||||||
} else {
|
|
||||||
getInputDataStream()
|
|
||||||
}.use { inputStream ->
|
|
||||||
inputStream.readBytes(bufferSize) { buffer ->
|
|
||||||
fileOutputStream.write(buffer)
|
|
||||||
dataDownloaded += buffer.size
|
|
||||||
try {
|
|
||||||
val percentDownload = (100 * dataDownloaded / length()).toInt()
|
|
||||||
update?.invoke(percentDownload)
|
|
||||||
} catch (e: Exception) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,25 +162,32 @@ class BinaryAttachment : Parcelable {
|
|||||||
|
|
||||||
return isCompressed == other.isCompressed
|
return isCompressed == other.isCompressed
|
||||||
&& isProtected == other.isProtected
|
&& isProtected == other.isProtected
|
||||||
|
&& isCorrupted == other.isCorrupted
|
||||||
&& sameData
|
&& sameData
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
|
|
||||||
var result = 0
|
var result = 0
|
||||||
result = 31 * result + if (isCompressed == null) 2 else if (isCompressed!!) 1 else 0
|
result = 31 * result + if (isCompressed) 1 else 0
|
||||||
result = 31 * result + if (isProtected) 1 else 0
|
result = 31 * result + if (isProtected) 1 else 0
|
||||||
|
result = 31 * result + if (isCorrupted) 1 else 0
|
||||||
result = 31 * result + dataFile!!.hashCode()
|
result = 31 * result + dataFile!!.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return dataFile.toString()
|
||||||
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
dest.writeByte((if (isCompressed == null) 2 else if (isCompressed!!) 1 else 0).toByte())
|
dest.writeByte((if (isCompressed) 1 else 0).toByte())
|
||||||
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
||||||
|
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
|
||||||
dest.writeString(dataFile?.absolutePath)
|
dest.writeString(dataFile?.absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,52 +19,126 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.database
|
||||||
|
|
||||||
import android.util.SparseArray
|
|
||||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class BinaryPool {
|
class BinaryPool {
|
||||||
private val pool = SparseArray<BinaryAttachment>()
|
private val pool = LinkedHashMap<Int, BinaryAttachment>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To get a binary by the pool key (ref attribute in entry)
|
||||||
|
*/
|
||||||
operator fun get(key: Int): BinaryAttachment? {
|
operator fun get(key: Int): BinaryAttachment? {
|
||||||
return pool[key]
|
return pool[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun put(key: Int, value: BinaryAttachment) {
|
/**
|
||||||
pool.put(key, value)
|
* To linked a binary with a pool key, if the pool key doesn't exists, create an unused one
|
||||||
|
*/
|
||||||
|
fun put(key: Int?, value: BinaryAttachment) {
|
||||||
|
if (key == null)
|
||||||
|
put(value)
|
||||||
|
else
|
||||||
|
pool[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doForEachBinary(action: (key: Int, binary: BinaryAttachment) -> Unit) {
|
/**
|
||||||
for (i in 0 until pool.size()) {
|
* To put a [binaryAttachment] in the pool,
|
||||||
action.invoke(i, pool.get(pool.keyAt(i)))
|
* if already exists, replace the current one,
|
||||||
|
* else add it with a new key
|
||||||
|
*/
|
||||||
|
fun put(binaryAttachment: BinaryAttachment): Int {
|
||||||
|
var key = findKey(binaryAttachment)
|
||||||
|
if (key == null) {
|
||||||
|
key = findUnusedKey()
|
||||||
}
|
}
|
||||||
|
pool[key] = binaryAttachment
|
||||||
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a binary from the pool, the file is not deleted
|
||||||
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun clear() {
|
fun remove(binaryAttachment: BinaryAttachment) {
|
||||||
doForEachBinary { _, binary ->
|
findKey(binaryAttachment)?.let {
|
||||||
binary.clear()
|
pool.remove(it)
|
||||||
}
|
}
|
||||||
pool.clear()
|
// Don't clear attachment here because a file can be used in many BinaryAttachment
|
||||||
}
|
}
|
||||||
|
|
||||||
fun add(fileBinary: BinaryAttachment) {
|
/**
|
||||||
if (findKey(fileBinary) == null) {
|
* Utility method to find an unused key in the pool
|
||||||
pool.put(findUnusedKey(), fileBinary)
|
*/
|
||||||
}
|
private fun findUnusedKey(): Int {
|
||||||
}
|
var unusedKey = 0
|
||||||
|
while (pool[unusedKey] != null)
|
||||||
fun findUnusedKey(): Int {
|
|
||||||
var unusedKey = pool.size()
|
|
||||||
while (get(unusedKey) != null)
|
|
||||||
unusedKey++
|
unusedKey++
|
||||||
return unusedKey
|
return unusedKey
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findKey(pb: BinaryAttachment): Int? {
|
/**
|
||||||
for (i in 0 until pool.size()) {
|
* Return key of [binaryAttachmentToRetrieve] or null if not found
|
||||||
if (pool.get(pool.keyAt(i)) == pb) return i
|
*/
|
||||||
|
private fun findKey(binaryAttachmentToRetrieve: BinaryAttachment): Int? {
|
||||||
|
val contains = pool.containsValue(binaryAttachmentToRetrieve)
|
||||||
|
return if (!contains)
|
||||||
|
null
|
||||||
|
else {
|
||||||
|
for ((key, binary) in pool) {
|
||||||
|
if (binary == binaryAttachmentToRetrieve) {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to order binaries and solve index problem in database v4
|
||||||
|
*/
|
||||||
|
private fun orderedBinaries(): List<KeyBinary> {
|
||||||
|
val keyBinaryList = ArrayList<KeyBinary>()
|
||||||
|
for ((key, binary) in pool) {
|
||||||
|
keyBinaryList.add(KeyBinary(key, binary))
|
||||||
|
}
|
||||||
|
return keyBinaryList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To register a binary with a ref corresponding to an ordered index
|
||||||
|
*/
|
||||||
|
fun getBinaryIndexFromKey(key: Int): Int? {
|
||||||
|
val index = orderedBinaries().indexOfFirst { it.key == key }
|
||||||
|
return if (index < 0)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Different from doForEach, provide an ordered index to each binary
|
||||||
|
*/
|
||||||
|
fun doForEachOrderedBinary(action: (index: Int, keyBinary: KeyBinary) -> Unit) {
|
||||||
|
orderedBinaries().forEachIndexed(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To do an action on each binary in the pool
|
||||||
|
*/
|
||||||
|
fun doForEachBinary(action: (binary: BinaryAttachment) -> Unit) {
|
||||||
|
pool.values.forEach { action.invoke(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun clear() {
|
||||||
|
doForEachBinary {
|
||||||
|
it.clear()
|
||||||
|
}
|
||||||
|
pool.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility data class to order binaries
|
||||||
|
*/
|
||||||
|
data class KeyBinary(val key: Int, val binary: BinaryAttachment)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
|||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.DigestOutputStream
|
import java.security.DigestOutputStream
|
||||||
@@ -38,7 +40,7 @@ import kotlin.collections.ArrayList
|
|||||||
|
|
||||||
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||||
|
|
||||||
var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
|
private var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
|
||||||
|
|
||||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||||
|
|
||||||
@@ -57,7 +59,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
|
|
||||||
// Retrieve backup group in index
|
// Retrieve backup group in index
|
||||||
val backupGroup: GroupKDB?
|
val backupGroup: GroupKDB?
|
||||||
get() = if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID) null else getGroupById(backupGroupId)
|
get() {
|
||||||
|
return if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
getGroupById(backupGroupId)
|
||||||
|
}
|
||||||
|
|
||||||
override val kdfEngine: KdfEngine?
|
override val kdfEngine: KdfEngine?
|
||||||
get() = kdfListV3[0]
|
get() = kdfListV3[0]
|
||||||
@@ -177,6 +184,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
override fun isInRecycleBin(group: GroupKDB): Boolean {
|
override fun isInRecycleBin(group: GroupKDB): Boolean {
|
||||||
var currentGroup: GroupKDB? = group
|
var currentGroup: GroupKDB? = group
|
||||||
|
|
||||||
|
if (backupGroup == null)
|
||||||
|
return false
|
||||||
|
|
||||||
if (currentGroup == backupGroup)
|
if (currentGroup == backupGroup)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
@@ -192,10 +202,10 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that the recycle bin tree exists, if enabled and create it
|
* Ensure that the backup tree exists if enabled, and create it
|
||||||
* if it doesn't exist
|
* if it doesn't exist
|
||||||
*/
|
*/
|
||||||
fun ensureRecycleBinExists() {
|
fun ensureBackupExists() {
|
||||||
rootGroups.forEach { currentGroup ->
|
rootGroups.forEach { currentGroup ->
|
||||||
if (currentGroup.level == 0
|
if (currentGroup.level == 0
|
||||||
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
|
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
|
||||||
@@ -219,21 +229,25 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
* @param node Node to remove
|
* @param node Node to remove
|
||||||
* @return true if node can be recycle, false elsewhere
|
* @return true if node can be recycle, false elsewhere
|
||||||
*/
|
*/
|
||||||
// TODO #394 Backup KDB
|
fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
|
||||||
// fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
|
if (backupGroup == null)
|
||||||
fun canRecycle(): Boolean {
|
ensureBackupExists()
|
||||||
|
if (node == backupGroup)
|
||||||
|
return false
|
||||||
|
backupGroup?.let {
|
||||||
|
if (node.isContainedIn(it))
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle(group: GroupKDB) {
|
fun recycle(group: GroupKDB) {
|
||||||
ensureRecycleBinExists()
|
|
||||||
removeGroupFrom(group, group.parent)
|
removeGroupFrom(group, group.parent)
|
||||||
addGroupTo(group, backupGroup)
|
addGroupTo(group, backupGroup)
|
||||||
group.afterAssignNewParent()
|
group.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle(entry: EntryKDB) {
|
fun recycle(entry: EntryKDB) {
|
||||||
ensureRecycleBinExists()
|
|
||||||
removeEntryFrom(entry, entry.parent)
|
removeEntryFrom(entry, entry.parent)
|
||||||
addEntryTo(entry, backupGroup)
|
addEntryTo(entry, backupGroup)
|
||||||
entry.afterAssignNewParent()
|
entry.afterAssignNewParent()
|
||||||
@@ -249,6 +263,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
addEntryTo(entry, origParent)
|
addEntryTo(entry, origParent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildNewBinary(cacheDirectory: File): BinaryAttachment {
|
||||||
|
// Generate an unique new file with timestamp
|
||||||
|
val fileInCache = File(cacheDirectory, System.currentTimeMillis().toString())
|
||||||
|
return BinaryAttachment(fileInCache)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TYPE = DatabaseKDB::class.java
|
val TYPE = DatabaseKDB::class.java
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
|||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
||||||
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||||
@@ -46,6 +48,7 @@ import com.kunzisoft.keepass.utils.UnsignedInt
|
|||||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
import org.w3c.dom.Text
|
import org.w3c.dom.Text
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
@@ -173,33 +176,51 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
|
|
||||||
fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
|
fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||||
newCompression: CompressionAlgorithm) {
|
newCompression: CompressionAlgorithm) {
|
||||||
binaryPool.doForEachBinary { key, binary ->
|
when (oldCompression) {
|
||||||
|
CompressionAlgorithm.None -> {
|
||||||
try {
|
when (newCompression) {
|
||||||
when (oldCompression) {
|
CompressionAlgorithm.None -> {}
|
||||||
CompressionAlgorithm.None -> {
|
|
||||||
when (newCompression) {
|
|
||||||
CompressionAlgorithm.None -> {
|
|
||||||
}
|
|
||||||
CompressionAlgorithm.GZip -> {
|
|
||||||
// To compress, create a new binary with file
|
|
||||||
binary.compress(BUFFER_SIZE_BYTES)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CompressionAlgorithm.GZip -> {
|
CompressionAlgorithm.GZip -> {
|
||||||
when (newCompression) {
|
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
|
||||||
CompressionAlgorithm.None -> {
|
if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
|
||||||
// To decompress, create a new binary with file
|
compressAllBinaries()
|
||||||
binary.decompress(BUFFER_SIZE_BYTES)
|
|
||||||
}
|
|
||||||
CompressionAlgorithm.GZip -> {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
CompressionAlgorithm.GZip -> {
|
||||||
|
// In databaseV4 the header is zipped during the save, so not necessary here
|
||||||
|
if (kdbxVersion.toKotlinLong() >= FILE_VERSION_32_4.toKotlinLong()) {
|
||||||
|
decompressAllBinaries()
|
||||||
|
} else {
|
||||||
|
when (newCompression) {
|
||||||
|
CompressionAlgorithm.None -> {
|
||||||
|
decompressAllBinaries()
|
||||||
|
}
|
||||||
|
CompressionAlgorithm.GZip -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compressAllBinaries() {
|
||||||
|
binaryPool.doForEachBinary { binary ->
|
||||||
|
try {
|
||||||
|
// To compress, create a new binary with file
|
||||||
|
binary.compress(BUFFER_SIZE_BYTES)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to change compression for $key")
|
Log.e(TAG, "Unable to compress $binary", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decompressAllBinaries() {
|
||||||
|
binaryPool.doForEachBinary { binary ->
|
||||||
|
try {
|
||||||
|
binary.decompress(BUFFER_SIZE_BYTES)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to decompress $binary", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -536,6 +557,59 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return publicCustomData.size() > 0
|
return publicCustomData.size() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildNewBinary(cacheDirectory: File,
|
||||||
|
protection: Boolean,
|
||||||
|
compression: Boolean,
|
||||||
|
binaryPoolId: Int? = null): BinaryAttachment {
|
||||||
|
// New file with current time
|
||||||
|
val fileInCache = File(cacheDirectory, System.currentTimeMillis().toString())
|
||||||
|
val binaryAttachment = BinaryAttachment(fileInCache, protection, compression)
|
||||||
|
// add attachment to pool
|
||||||
|
binaryPool.put(binaryPoolId, binaryAttachment)
|
||||||
|
return binaryAttachment
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUnlinkedAttachment(binary: BinaryAttachment, clear: Boolean) {
|
||||||
|
val listBinaries = ArrayList<BinaryAttachment>()
|
||||||
|
listBinaries.add(binary)
|
||||||
|
removeUnlinkedAttachments(listBinaries, clear)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUnlinkedAttachments(clear: Boolean) {
|
||||||
|
removeUnlinkedAttachments(emptyList(), clear)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeUnlinkedAttachments(binaries: List<BinaryAttachment>, clear: Boolean) {
|
||||||
|
// Build binaries to remove with all binaries known
|
||||||
|
val binariesToRemove = ArrayList<BinaryAttachment>()
|
||||||
|
if (binaries.isEmpty()) {
|
||||||
|
binaryPool.doForEachBinary { binary ->
|
||||||
|
binariesToRemove.add(binary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binariesToRemove.addAll(binaries)
|
||||||
|
}
|
||||||
|
// Remove binaries from the list
|
||||||
|
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
|
||||||
|
override fun operate(node: EntryKDBX): Boolean {
|
||||||
|
node.getAttachments(binaryPool, true).forEach {
|
||||||
|
binariesToRemove.remove(it.binaryAttachment)
|
||||||
|
}
|
||||||
|
return binariesToRemove.isNotEmpty()
|
||||||
|
}
|
||||||
|
}, null)
|
||||||
|
// Effective removing
|
||||||
|
binariesToRemove.forEach {
|
||||||
|
try {
|
||||||
|
binaryPool.remove(it)
|
||||||
|
if (clear)
|
||||||
|
it.clear()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Unable to clean binaries", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||||
if (password == null)
|
if (password == null)
|
||||||
return true
|
return true
|
||||||
@@ -564,7 +638,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
private const val KeyElementName = "Key"
|
private const val KeyElementName = "Key"
|
||||||
private const val KeyDataElementName = "Data"
|
private const val KeyDataElementName = "Data"
|
||||||
|
|
||||||
const val BASE_64_FLAG = Base64.NO_WRAP
|
const val BASE_64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
||||||
|
|
||||||
const val BUFFER_SIZE_BYTES = 3 * 128
|
const val BUFFER_SIZE_BYTES = 3 * 128
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
|||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.KeyFileEmptyDatabaseException
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
@@ -136,7 +135,6 @@ abstract class DatabaseVersioned<
|
|||||||
}
|
}
|
||||||
|
|
||||||
when (keyData.size.toLong()) {
|
when (keyData.size.toLong()) {
|
||||||
0L -> throw KeyFileEmptyDatabaseException()
|
|
||||||
32L -> return keyData
|
32L -> return keyData
|
||||||
64L -> try {
|
64L -> try {
|
||||||
return hexStringToByteArray(String(keyData))
|
return hexStringToByteArray(String(keyData))
|
||||||
|
|||||||
@@ -25,14 +25,12 @@ import android.os.Parcelable
|
|||||||
import com.kunzisoft.keepass.utils.ParcelableUtil
|
import com.kunzisoft.keepass.utils.ParcelableUtil
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
|
||||||
import java.util.HashMap
|
|
||||||
|
|
||||||
class AutoType : Parcelable {
|
class AutoType : Parcelable {
|
||||||
|
|
||||||
var enabled = true
|
var enabled = true
|
||||||
var obfuscationOptions = OBF_OPT_NONE
|
var obfuscationOptions = OBF_OPT_NONE
|
||||||
var defaultSequence = ""
|
var defaultSequence = ""
|
||||||
private var windowSeqPairs = HashMap<String, String>()
|
private var windowSeqPairs = LinkedHashMap<String, String>()
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Structure containing information about one entry.
|
* Structure containing information about one entry.
|
||||||
@@ -135,6 +137,29 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
override val type: Type
|
override val type: Type
|
||||||
get() = Type.ENTRY
|
get() = Type.ENTRY
|
||||||
|
|
||||||
|
fun getAttachment(): Attachment? {
|
||||||
|
val binary = binaryData
|
||||||
|
return if (binary != null)
|
||||||
|
Attachment(binaryDescription, binary)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsAttachment(): Boolean {
|
||||||
|
return binaryData != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAttachment(attachment: Attachment) {
|
||||||
|
this.binaryDescription = attachment.name
|
||||||
|
this.binaryData = attachment.binaryAttachment
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachment(attachment: Attachment? = null) {
|
||||||
|
if (attachment == null || this.binaryDescription == attachment.name) {
|
||||||
|
this.binaryDescription = ""
|
||||||
|
this.binaryData = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
/** Size of byte buffer needed to hold this struct. */
|
/** Size of byte buffer needed to hold this struct. */
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ package com.kunzisoft.keepass.database.element.entry
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryPool
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
@@ -31,11 +33,12 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.utils.ParcelableUtil
|
import com.kunzisoft.keepass.utils.ParcelableUtil
|
||||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
import kotlin.collections.LinkedHashMap
|
||||||
|
|
||||||
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
||||||
|
|
||||||
@@ -58,9 +61,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
super.icon = value
|
super.icon = value
|
||||||
}
|
}
|
||||||
var iconCustom = IconImageCustom.UNKNOWN_ICON
|
var iconCustom = IconImageCustom.UNKNOWN_ICON
|
||||||
private var customData = HashMap<String, String>()
|
private var customData = LinkedHashMap<String, String>()
|
||||||
var fields = HashMap<String, ProtectedString>()
|
// TODO Private
|
||||||
var binaries = HashMap<String, BinaryAttachment>()
|
var fields = LinkedHashMap<String, ProtectedString>()
|
||||||
|
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
|
||||||
var foregroundColor = ""
|
var foregroundColor = ""
|
||||||
var backgroundColor = ""
|
var backgroundColor = ""
|
||||||
var overrideURL = ""
|
var overrideURL = ""
|
||||||
@@ -69,36 +73,32 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
var additional = ""
|
var additional = ""
|
||||||
var tags = ""
|
var tags = ""
|
||||||
|
|
||||||
val size: Long
|
fun getSize(binaryPool: BinaryPool): Long {
|
||||||
get() {
|
var size = FIXED_LENGTH_SIZE
|
||||||
var size = FIXED_LENGTH_SIZE
|
|
||||||
|
|
||||||
for (entry in fields.entries) {
|
for (entry in fields.entries) {
|
||||||
size += entry.key.length.toLong()
|
size += entry.key.length.toLong()
|
||||||
size += entry.value.length().toLong()
|
size += entry.value.length().toLong()
|
||||||
}
|
|
||||||
|
|
||||||
for ((key, value) in binaries) {
|
|
||||||
size += key.length.toLong()
|
|
||||||
size += value.length()
|
|
||||||
}
|
|
||||||
|
|
||||||
size += autoType.defaultSequence.length.toLong()
|
|
||||||
for ((key, value) in autoType.entrySet()) {
|
|
||||||
size += key.length.toLong()
|
|
||||||
size += value.length.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (entry in history) {
|
|
||||||
size += entry.size
|
|
||||||
}
|
|
||||||
|
|
||||||
size += overrideURL.length.toLong()
|
|
||||||
size += tags.length.toLong()
|
|
||||||
|
|
||||||
return size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size += getAttachmentsSize(binaryPool)
|
||||||
|
|
||||||
|
size += autoType.defaultSequence.length.toLong()
|
||||||
|
for ((key, value) in autoType.entrySet()) {
|
||||||
|
size += key.length.toLong()
|
||||||
|
size += value.length.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entry in history) {
|
||||||
|
size += entry.getSize(binaryPool)
|
||||||
|
}
|
||||||
|
|
||||||
|
size += overrideURL.length.toLong()
|
||||||
|
size += tags.length.toLong()
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
override var expires: Boolean = false
|
override var expires: Boolean = false
|
||||||
|
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
@@ -109,7 +109,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
||||||
customData = ParcelableUtil.readStringParcelableMap(parcel)
|
customData = ParcelableUtil.readStringParcelableMap(parcel)
|
||||||
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
|
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
|
||||||
binaries = ParcelableUtil.readStringParcelableMap(parcel, BinaryAttachment::class.java)
|
binaries = ParcelableUtil.readStringIntMap(parcel)
|
||||||
foregroundColor = parcel.readString() ?: foregroundColor
|
foregroundColor = parcel.readString() ?: foregroundColor
|
||||||
backgroundColor = parcel.readString() ?: backgroundColor
|
backgroundColor = parcel.readString() ?: backgroundColor
|
||||||
overrideURL = parcel.readString() ?: overrideURL
|
overrideURL = parcel.readString() ?: overrideURL
|
||||||
@@ -127,7 +127,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
dest.writeParcelable(locationChanged, flags)
|
dest.writeParcelable(locationChanged, flags)
|
||||||
ParcelableUtil.writeStringParcelableMap(dest, customData)
|
ParcelableUtil.writeStringParcelableMap(dest, customData)
|
||||||
ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
|
ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
|
||||||
ParcelableUtil.writeStringParcelableMap(dest, flags, binaries)
|
ParcelableUtil.writeStringIntMap(dest, binaries)
|
||||||
dest.writeString(foregroundColor)
|
dest.writeString(foregroundColor)
|
||||||
dest.writeString(backgroundColor)
|
dest.writeString(backgroundColor)
|
||||||
dest.writeString(overrideURL)
|
dest.writeString(overrideURL)
|
||||||
@@ -166,8 +166,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
tags = source.tags
|
tags = source.tags
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startToManageFieldReferences(db: DatabaseKDBX) {
|
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
||||||
this.mDatabase = db
|
this.mDatabase = database
|
||||||
this.mDecodeRef = true
|
this.mDecodeRef = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,23 +260,17 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
|| key == STR_NOTES)
|
|| key == STR_NOTES)
|
||||||
}
|
}
|
||||||
|
|
||||||
var customFields = HashMap<String, ProtectedString>()
|
var customFields = LinkedHashMap<String, ProtectedString>()
|
||||||
get() {
|
get() {
|
||||||
field.clear()
|
field.clear()
|
||||||
for (entry in fields.entries) {
|
for ((key, value) in fields) {
|
||||||
val key = entry.key
|
if (!isStandardField(key)) {
|
||||||
val value = entry.value
|
|
||||||
if (!isStandardField(entry.key)) {
|
|
||||||
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key))
|
field[key] = ProtectedString(value.isProtected, decodeRefKey(mDecodeRef, key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
|
|
||||||
fun allowCustomFields(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAllFields() {
|
fun removeAllFields() {
|
||||||
fields.clear()
|
fields.clear()
|
||||||
}
|
}
|
||||||
@@ -285,12 +279,47 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
fields[label] = value
|
fields[label] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putProtectedBinary(key: String, value: BinaryAttachment) {
|
/**
|
||||||
binaries[key] = value
|
* It's a list because history labels can be defined multiple times
|
||||||
|
*/
|
||||||
|
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
|
||||||
|
val entryAttachmentList = ArrayList<Attachment>()
|
||||||
|
for ((label, poolId) in binaries) {
|
||||||
|
binaryPool[poolId]?.let { binary ->
|
||||||
|
entryAttachmentList.add(Attachment(label, binary))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inHistory) {
|
||||||
|
history.forEach {
|
||||||
|
entryAttachmentList.addAll(it.getAttachments(binaryPool, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entryAttachmentList
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sizeOfHistory(): Int {
|
fun containsAttachment(): Boolean {
|
||||||
return history.size
|
return binaries.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
|
||||||
|
binaries[attachment.name] = binaryPool.put(attachment.binaryAttachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachment(attachment: Attachment) {
|
||||||
|
binaries.remove(attachment.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachments() {
|
||||||
|
binaries.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAttachmentsSize(binaryPool: BinaryPool): Long {
|
||||||
|
var size = 0L
|
||||||
|
for ((label, poolId) in binaries) {
|
||||||
|
size += label.length.toLong()
|
||||||
|
size += binaryPool[poolId]?.length() ?: 0
|
||||||
|
}
|
||||||
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putCustomData(key: String, value: String) {
|
override fun putCustomData(key: String, value: String) {
|
||||||
@@ -305,15 +334,11 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
history.add(entry)
|
history.add(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeEntryFromHistory(position: Int) {
|
fun removeEntryFromHistory(position: Int): EntryKDBX? {
|
||||||
history.removeAt(position)
|
return history.removeAt(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAllHistory() {
|
fun removeOldestEntryFromHistory(): EntryKDBX? {
|
||||||
history.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeOldestEntryFromHistory() {
|
|
||||||
var min: Date? = null
|
var min: Date? = null
|
||||||
var index = -1
|
var index = -1
|
||||||
|
|
||||||
@@ -326,9 +351,9 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index != -1) {
|
return if (index != -1) {
|
||||||
history.removeAt(index)
|
history.removeAt(index)
|
||||||
}
|
} else null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun touch(modified: Boolean, touchParents: Boolean) {
|
override fun touch(modified: Boolean, touchParents: Boolean) {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class ProtectedString : Parcelable {
|
|||||||
|
|
||||||
var isProtected: Boolean = false
|
var isProtected: Boolean = false
|
||||||
private set
|
private set
|
||||||
private var stringValue: String = ""
|
var stringValue: String = ""
|
||||||
|
|
||||||
constructor(toCopy: ProtectedString) {
|
constructor(toCopy: ProtectedString) {
|
||||||
this.isProtected = toCopy.isProtected
|
this.isProtected = toCopy.isProtected
|
||||||
|
|||||||
@@ -116,13 +116,6 @@ class InvalidCredentialsDatabaseException : LoadDatabaseException {
|
|||||||
constructor(exception: Throwable) : super(exception)
|
constructor(exception: Throwable) : super(exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
class KeyFileEmptyDatabaseException : LoadDatabaseException {
|
|
||||||
@StringRes
|
|
||||||
override var errorId: Int = R.string.keyfile_is_empty
|
|
||||||
constructor() : super()
|
|
||||||
constructor(exception: Throwable) : super(exception)
|
|
||||||
}
|
|
||||||
|
|
||||||
class NoMemoryDatabaseException: LoadDatabaseException {
|
class NoMemoryDatabaseException: LoadDatabaseException {
|
||||||
@StringRes
|
@StringRes
|
||||||
override var errorId: Int = R.string.error_out_of_memory
|
override var errorId: Int = R.string.error_out_of_memory
|
||||||
|
|||||||
@@ -192,10 +192,11 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fieldID == PwDbHeaderV4Fields.EndOfHeader)
|
||||||
|
return true
|
||||||
|
|
||||||
if (fieldData != null)
|
if (fieldData != null)
|
||||||
when (fieldID) {
|
when (fieldID) {
|
||||||
PwDbHeaderV4Fields.EndOfHeader -> return true
|
|
||||||
|
|
||||||
PwDbHeaderV4Fields.CipherID -> setCipher(fieldData)
|
PwDbHeaderV4Fields.CipherID -> setCipher(fieldData)
|
||||||
|
|
||||||
PwDbHeaderV4Fields.CompressionFlags -> setCompressionFlags(fieldData)
|
PwDbHeaderV4Fields.CompressionFlags -> setCompressionFlags(fieldData)
|
||||||
|
|||||||
@@ -27,14 +27,12 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
|||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.exception.*
|
import com.kunzisoft.keepass.database.exception.*
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.stream.*
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import org.joda.time.Instant
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.security.*
|
import java.security.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -282,11 +280,9 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
0x000E -> {
|
0x000E -> {
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
if (fieldSize > 0) {
|
if (fieldSize > 0) {
|
||||||
// Generate an unique new file with timestamp
|
val binaryAttachment = mDatabaseToOpen.buildNewBinary(cacheDirectory)
|
||||||
val binaryFile = File(cacheDirectory,
|
entry.binaryData = binaryAttachment
|
||||||
Instant.now().millis.toString())
|
BufferedOutputStream(binaryAttachment.getOutputDataStream()).use { outputStream ->
|
||||||
entry.binaryData = BinaryAttachment(binaryFile)
|
|
||||||
BufferedOutputStream(FileOutputStream(binaryFile)).use { outputStream ->
|
|
||||||
cipherInputStream.readBytes(fieldSize,
|
cipherInputStream.readBytes(fieldSize,
|
||||||
DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
|
DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
|
||||||
outputStream.write(buffer)
|
outputStream.write(buffer)
|
||||||
|
|||||||
@@ -20,12 +20,14 @@
|
|||||||
package com.kunzisoft.keepass.database.file.input
|
package com.kunzisoft.keepass.database.file.input
|
||||||
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||||
import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
||||||
@@ -35,7 +37,7 @@ import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
|||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.database.exception.*
|
import com.kunzisoft.keepass.database.exception.*
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||||
@@ -49,12 +51,14 @@ import org.bouncycastle.crypto.StreamCipher
|
|||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
import org.xmlpull.v1.XmlPullParserException
|
import org.xmlpull.v1.XmlPullParserException
|
||||||
import org.xmlpull.v1.XmlPullParserFactory
|
import org.xmlpull.v1.XmlPullParserFactory
|
||||||
import java.io.*
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.UnsupportedEncodingException
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import java.util.zip.GZIPOutputStream
|
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.CipherInputStream
|
import javax.crypto.CipherInputStream
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -68,9 +72,6 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
|
|
||||||
private var hashOfHeader: ByteArray? = null
|
private var hashOfHeader: ByteArray? = null
|
||||||
|
|
||||||
private val unusedCacheFileName: String
|
|
||||||
get() = mDatabase.binaryPool.findUnusedKey().toString()
|
|
||||||
|
|
||||||
private var readNextNode = true
|
private var readNextNode = true
|
||||||
private val ctxGroups = Stack<GroupKDBX>()
|
private val ctxGroups = Stack<GroupKDBX>()
|
||||||
private var ctxGroup: GroupKDBX? = null
|
private var ctxGroup: GroupKDBX? = null
|
||||||
@@ -233,8 +234,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
|
|
||||||
var data = ByteArray(0)
|
var data = ByteArray(0)
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
|
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) {
|
||||||
|
// TODO OOM here
|
||||||
data = dataInputStream.readBytes(size)
|
data = dataInputStream.readBytes(size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = true
|
var result = true
|
||||||
@@ -249,18 +252,16 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
header.innerRandomStreamKey = data
|
header.innerRandomStreamKey = data
|
||||||
}
|
}
|
||||||
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary -> {
|
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary -> {
|
||||||
val flag = dataInputStream.readBytes(1)[0].toInt() != 0
|
|
||||||
val protectedFlag = flag && DatabaseHeaderKDBX.KdbxBinaryFlags.Protected.toInt() != DatabaseHeaderKDBX.KdbxBinaryFlags.None.toInt()
|
|
||||||
val byteLength = size - 1
|
|
||||||
// Read in a file
|
// Read in a file
|
||||||
val file = File(cacheDirectory, unusedCacheFileName)
|
val protectedFlag = dataInputStream.readBytes(1)[0].toInt() != 0
|
||||||
FileOutputStream(file).use { outputStream ->
|
val byteLength = size - 1
|
||||||
|
// No compression at this level
|
||||||
|
val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, protectedFlag, false)
|
||||||
|
protectedBinary.getOutputDataStream().use { outputStream ->
|
||||||
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer ->
|
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer ->
|
||||||
outputStream.write(buffer)
|
outputStream.write(buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val protectedBinary = BinaryAttachment(file, protectedFlag)
|
|
||||||
mDatabase.binaryPool.add(protectedBinary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,14 +444,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
}
|
}
|
||||||
|
|
||||||
KdbContext.Binaries -> if (name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
KdbContext.Binaries -> if (name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
||||||
val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)
|
readBinary(xpp)
|
||||||
if (key != null) {
|
|
||||||
val pbData = readBinary(xpp)
|
|
||||||
val id = Integer.parseInt(key)
|
|
||||||
mDatabase.binaryPool.put(id, pbData!!)
|
|
||||||
} else {
|
|
||||||
readUnknown(xpp)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
readUnknown(xpp)
|
readUnknown(xpp)
|
||||||
}
|
}
|
||||||
@@ -749,8 +743,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
if (entryInHistory) {
|
if (entryInHistory) {
|
||||||
ctxEntry = ctxHistoryBase
|
ctxEntry = ctxHistoryBase
|
||||||
return KdbContext.EntryHistory
|
return KdbContext.EntryHistory
|
||||||
}
|
} else if (ctxEntry != null) {
|
||||||
else if (ctxEntry != null) {
|
|
||||||
// Add entry to the index only when close the XML element
|
// Add entry to the index only when close the XML element
|
||||||
mDatabase.addEntryIndex(ctxEntry!!)
|
mDatabase.addEntryIndex(ctxEntry!!)
|
||||||
}
|
}
|
||||||
@@ -766,8 +759,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
|
|
||||||
return KdbContext.Entry
|
return KdbContext.Entry
|
||||||
} else if (ctx == KdbContext.EntryBinary && name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
} else if (ctx == KdbContext.EntryBinary && name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
||||||
if (ctxBinaryName != null && ctxBinaryValue != null)
|
if (ctxBinaryName != null && ctxBinaryValue != null) {
|
||||||
ctxEntry?.putProtectedBinary(ctxBinaryName!!, ctxBinaryValue!!)
|
ctxEntry?.putAttachment(Attachment(ctxBinaryName!!, ctxBinaryValue!!), mDatabase.binaryPool)
|
||||||
|
}
|
||||||
ctxBinaryName = null
|
ctxBinaryName = null
|
||||||
ctxBinaryValue = null
|
ctxBinaryValue = null
|
||||||
|
|
||||||
@@ -885,9 +879,14 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
if (encoded.isEmpty()) {
|
if (encoded.isEmpty()) {
|
||||||
return DatabaseVersioned.UUID_ZERO
|
return DatabaseVersioned.UUID_ZERO
|
||||||
}
|
}
|
||||||
val buf = Base64.decode(encoded, BASE_64_FLAG)
|
|
||||||
|
|
||||||
return bytes16ToUuid(buf)
|
return try {
|
||||||
|
val buf = Base64.decode(encoded, BASE_64_FLAG)
|
||||||
|
bytes16ToUuid(buf)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to read base 64 UUID, create a random one", e)
|
||||||
|
UUID.randomUUID()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, XmlPullParserException::class)
|
@Throws(IOException::class, XmlPullParserException::class)
|
||||||
@@ -947,50 +946,63 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
|
|
||||||
// Reference Id to a binary already present in binary pool
|
// Reference Id to a binary already present in binary pool
|
||||||
val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
|
val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
|
||||||
if (ref != null) {
|
// New id to a binary
|
||||||
xpp.next() // Consume end tag
|
val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)
|
||||||
|
|
||||||
val id = Integer.parseInt(ref)
|
return when {
|
||||||
return mDatabase.binaryPool[id]
|
ref != null -> {
|
||||||
}
|
xpp.next() // Consume end tag
|
||||||
|
val id = Integer.parseInt(ref)
|
||||||
// New binary to retrieve
|
// A ref is not necessarily an index in Database V3.1
|
||||||
else {
|
mDatabase.binaryPool[id]
|
||||||
var compressed = false
|
|
||||||
var protected = false
|
|
||||||
|
|
||||||
if (xpp.attributeCount > 0) {
|
|
||||||
val compress = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrCompressed)
|
|
||||||
if (compress != null) {
|
|
||||||
compressed = compress.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
|
|
||||||
if (protect != null) {
|
|
||||||
protected = protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
key != null -> {
|
||||||
val base64 = readString(xpp)
|
createBinary(key.toIntOrNull(), xpp)
|
||||||
if (base64.isEmpty())
|
}
|
||||||
return BinaryAttachment()
|
else -> {
|
||||||
val data = Base64.decode(base64, BASE_64_FLAG)
|
// New binary to retrieve
|
||||||
|
createBinary(null, xpp)
|
||||||
val file = File(cacheDirectory, unusedCacheFileName)
|
|
||||||
return FileOutputStream(file).use { outputStream ->
|
|
||||||
// Force compression in this specific case
|
|
||||||
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|
|
||||||
&& !compressed) {
|
|
||||||
GZIPOutputStream(outputStream).write(data)
|
|
||||||
BinaryAttachment(file, protected, true)
|
|
||||||
} else {
|
|
||||||
outputStream.write(data)
|
|
||||||
BinaryAttachment(file, protected, compressed)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class, XmlPullParserException::class)
|
||||||
|
private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryAttachment? {
|
||||||
|
var compressed = false
|
||||||
|
var protected = false
|
||||||
|
|
||||||
|
if (xpp.attributeCount > 0) {
|
||||||
|
val compress = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrCompressed)
|
||||||
|
if (compress != null) {
|
||||||
|
compressed = compress.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
|
||||||
|
if (protect != null) {
|
||||||
|
protected = protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val base64 = readString(xpp)
|
||||||
|
if (base64.isEmpty())
|
||||||
|
return null
|
||||||
|
|
||||||
|
// Build the new binary and compress
|
||||||
|
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, protected, compressed, binaryId)
|
||||||
|
try {
|
||||||
|
binaryAttachment.getOutputDataStream().use { outputStream ->
|
||||||
|
outputStream.write(Base64.decode(base64, BASE_64_FLAG))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to read base 64 attachment", e)
|
||||||
|
binaryAttachment.isCorrupted = true
|
||||||
|
binaryAttachment.getOutputDataStream().use { outputStream ->
|
||||||
|
outputStream.write(base64.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return binaryAttachment
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, XmlPullParserException::class)
|
@Throws(IOException::class, XmlPullParserException::class)
|
||||||
private fun readString(xpp: XmlPullParser): String {
|
private fun readString(xpp: XmlPullParser): String {
|
||||||
val buf = readProtectedBase64String(xpp)
|
val buf = readProtectedBase64String(xpp)
|
||||||
@@ -1045,6 +1057,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = DatabaseInputKDBX::class.java.name
|
||||||
|
|
||||||
private val DEFAULT_HISTORY_DAYS = UnsignedInt(365)
|
private val DEFAULT_HISTORY_DAYS = UnsignedInt(365)
|
||||||
|
|
||||||
@Throws(XmlPullParserException::class)
|
@Throws(XmlPullParserException::class)
|
||||||
|
|||||||
@@ -47,18 +47,25 @@ class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
|
|||||||
dataOutputStream.writeInt(streamKeySize)
|
dataOutputStream.writeInt(streamKeySize)
|
||||||
dataOutputStream.write(header.innerRandomStreamKey)
|
dataOutputStream.write(header.innerRandomStreamKey)
|
||||||
|
|
||||||
database.binaryPool.doForEachBinary { _, protectedBinary ->
|
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
|
||||||
|
val protectedBinary = keyBinary.binary
|
||||||
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
||||||
if (protectedBinary.isProtected) {
|
if (protectedBinary.isProtected) {
|
||||||
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force decompression to add binary in header
|
||||||
|
protectedBinary.decompress()
|
||||||
|
|
||||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary.toInt())
|
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary.toInt())
|
||||||
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1) // TODO verify
|
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1)
|
||||||
dataOutputStream.write(flag.toInt())
|
dataOutputStream.write(flag.toInt())
|
||||||
|
|
||||||
protectedBinary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
// if was compressed in cache, uncompress it
|
||||||
dataOutputStream.write(buffer)
|
protectedBinary.getInputDataStream().use { inputStream ->
|
||||||
|
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||||
|
dataOutputStream.write(buffer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
|||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
@@ -55,7 +55,6 @@ import java.io.OutputStream
|
|||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.zip.GZIPInputStream
|
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.CipherOutputStream
|
import javax.crypto.CipherOutputStream
|
||||||
@@ -422,7 +421,6 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
private fun writeBinary(binary : BinaryAttachment) {
|
private fun writeBinary(binary : BinaryAttachment) {
|
||||||
val binaryLength = binary.length()
|
val binaryLength = binary.length()
|
||||||
if (binaryLength > 0) {
|
if (binaryLength > 0) {
|
||||||
|
|
||||||
if (binary.isProtected) {
|
if (binary.isProtected) {
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
||||||
|
|
||||||
@@ -433,21 +431,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
xml.text(charArray, 0, charArray.size)
|
xml.text(charArray, 0, charArray.size)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Force binary compression from database (compression was harmonized during import)
|
if (binary.isCompressed) {
|
||||||
if (mDatabaseKDBX.compressionAlgorithm === CompressionAlgorithm.GZip) {
|
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force decompression in this specific case
|
|
||||||
val binaryInputStream = if (mDatabaseKDBX.compressionAlgorithm == CompressionAlgorithm.None
|
|
||||||
&& binary.isCompressed == true) {
|
|
||||||
GZIPInputStream(binary.getInputDataStream())
|
|
||||||
} else {
|
|
||||||
binary.getInputDataStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the XML
|
// Write the XML
|
||||||
binaryInputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||||
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
|
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
|
||||||
xml.text(charArray, 0, charArray.size)
|
xml.text(charArray, 0, charArray.size)
|
||||||
}
|
}
|
||||||
@@ -459,10 +447,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
private fun writeMetaBinaries() {
|
private fun writeMetaBinaries() {
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
||||||
|
|
||||||
mDatabaseKDBX.binaryPool.doForEachBinary { key, binary ->
|
// Use indexes because necessarily in DatabaseV4 (binary header ref is the order)
|
||||||
|
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary ->
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrId, key.toString())
|
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
|
||||||
writeBinary(binary)
|
writeBinary(keyBinary.binary)
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,23 +548,22 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
private fun writeEntryBinaries(binaries: Map<String, BinaryAttachment>) {
|
private fun writeEntryBinaries(binaries: LinkedHashMap<String, Int>) {
|
||||||
for ((key, binary) in binaries) {
|
for ((label, poolId) in binaries) {
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
// Retrieve the right index with the poolId, don't use ref because of header in DatabaseV4
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemKey)
|
mDatabaseKDBX.binaryPool.getBinaryIndexFromKey(poolId)?.toString()?.let { indexString ->
|
||||||
xml.text(safeXmlString(key))
|
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemKey)
|
xml.startTag(null, DatabaseKDBXXML.ElemKey)
|
||||||
|
xml.text(safeXmlString(label))
|
||||||
|
xml.endTag(null, DatabaseKDBXXML.ElemKey)
|
||||||
|
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemValue)
|
xml.startTag(null, DatabaseKDBXXML.ElemValue)
|
||||||
val ref = mDatabaseKDBX.binaryPool.findKey(binary)
|
// Use only pool data in Meta to save binaries
|
||||||
if (ref != null) {
|
xml.attribute(null, DatabaseKDBXXML.AttrRef, indexString)
|
||||||
xml.attribute(null, DatabaseKDBXXML.AttrRef, ref.toString())
|
xml.endTag(null, DatabaseKDBXXML.ElemValue)
|
||||||
} else {
|
|
||||||
writeBinary(binary)
|
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||||
}
|
}
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemValue)
|
|
||||||
|
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,9 +95,9 @@ open class Education(val activity: Activity) {
|
|||||||
R.string.education_entry_edit_key,
|
R.string.education_entry_edit_key,
|
||||||
R.string.education_password_generator_key,
|
R.string.education_password_generator_key,
|
||||||
R.string.education_entry_new_field_key,
|
R.string.education_entry_new_field_key,
|
||||||
|
R.string.education_add_attachment_key,
|
||||||
R.string.education_setup_OTP_key)
|
R.string.education_setup_OTP_key)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get preferences bundle for education
|
* Get preferences bundle for education
|
||||||
*/
|
*/
|
||||||
@@ -272,6 +272,18 @@ open class Education(val activity: Activity) {
|
|||||||
context.resources.getBoolean(R.bool.education_entry_new_field_default))
|
context.resources.getBoolean(R.bool.education_entry_new_field_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the explanatory view of the new attachment button in an entry has already been displayed.
|
||||||
|
*
|
||||||
|
* @param context The context to open the SharedPreferences
|
||||||
|
* @return boolean value of education_add_attachment_key key
|
||||||
|
*/
|
||||||
|
fun isEducationAddAttachmentPerformed(context: Context): Boolean {
|
||||||
|
val prefs = getEducationSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.education_add_attachment_key),
|
||||||
|
context.resources.getBoolean(R.bool.education_add_attachment_default))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether the explanatory view to setup OTP has already been displayed.
|
* Determines whether the explanatory view to setup OTP has already been displayed.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ import com.kunzisoft.keepass.R
|
|||||||
class EntryEditActivityEducation(activity: Activity)
|
class EntryEditActivityEducation(activity: Activity)
|
||||||
: Education(activity) {
|
: Education(activity) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and display learning views
|
||||||
|
* Displays the explanation for the password generator
|
||||||
|
*/
|
||||||
fun checkAndPerformedGeneratePasswordEducation(educationView: View,
|
fun checkAndPerformedGeneratePasswordEducation(educationView: View,
|
||||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
||||||
@@ -56,7 +60,7 @@ class EntryEditActivityEducation(activity: Activity)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check and display learning views
|
* Check and display learning views
|
||||||
* Displays the explanation for the icon selection, the password generator and for a new field
|
* Displays the explanation to create a new field
|
||||||
*/
|
*/
|
||||||
fun checkAndPerformedEntryNewFieldEducation(educationView: View,
|
fun checkAndPerformedEntryNewFieldEducation(educationView: View,
|
||||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||||
@@ -83,6 +87,35 @@ class EntryEditActivityEducation(activity: Activity)
|
|||||||
R.string.education_entry_new_field_key)
|
R.string.education_entry_new_field_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and display learning views
|
||||||
|
* Displays the explanation for to upload attachment
|
||||||
|
*/
|
||||||
|
fun checkAndPerformedAttachmentEducation(educationView: View,
|
||||||
|
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||||
|
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
||||||
|
return checkAndPerformedEducation(!isEducationAddAttachmentPerformed(activity),
|
||||||
|
TapTarget.forView(educationView,
|
||||||
|
activity.getString(R.string.education_add_attachment_title),
|
||||||
|
activity.getString(R.string.education_add_attachment_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_add_attachment_key)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check and display learning views
|
* Check and display learning views
|
||||||
* Displays the explanation to setup OTP
|
* Displays the explanation to setup OTP
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.content.res.Resources
|
|||||||
import android.util.SparseIntArray
|
import android.util.SparseIntArray
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class who construct dynamically database icons contains in a separate library
|
* Class who construct dynamically database icons contains in a separate library
|
||||||
@@ -35,17 +36,13 @@ import java.text.DecimalFormat
|
|||||||
*
|
*
|
||||||
* See *icon-pack-classic* module as sample
|
* See *icon-pack-classic* module as sample
|
||||||
*
|
*
|
||||||
*
|
|
||||||
*/
|
|
||||||
class IconPack
|
|
||||||
/**
|
|
||||||
* Construct dynamically the icon pack provide by the string resource id
|
* Construct dynamically the icon pack provide by the string resource id
|
||||||
*
|
*
|
||||||
* @param packageName Context of the app to retrieve the resources
|
* @param packageName Context of the app to retrieve the resources
|
||||||
* @param packageName Context of the app to retrieve the resources
|
* @param packageName Context of the app to retrieve the resources
|
||||||
* @param resourceId String Id of the pack (ex : com.kunzisoft.keepass.icon.classic.R.string.resource_id)
|
* @param resourceId String Id of the pack (ex : com.kunzisoft.keepass.icon.classic.R.string.resource_id)
|
||||||
*/
|
*/
|
||||||
internal constructor(packageName: String, resources: Resources, resourceId: Int) {
|
class IconPack(packageName: String, resources: Resources, resourceId: Int) {
|
||||||
|
|
||||||
private val icons: SparseIntArray = SparseIntArray()
|
private val icons: SparseIntArray = SparseIntArray()
|
||||||
/**
|
/**
|
||||||
@@ -84,7 +81,7 @@ internal constructor(packageName: String, resources: Resources, resourceId: Int)
|
|||||||
while (num <= NB_ICONS) {
|
while (num <= NB_ICONS) {
|
||||||
// To construct the id with name_ic_XX_32dp (ex : classic_ic_08_32dp )
|
// To construct the id with name_ic_XX_32dp (ex : classic_ic_08_32dp )
|
||||||
val resId = resources.getIdentifier(
|
val resId = resources.getIdentifier(
|
||||||
id + "_" + DecimalFormat("00").format(num.toLong()) + "_32dp",
|
id + "_" + String.format(Locale.ENGLISH, "%02d", num) + "_32dp",
|
||||||
"drawable",
|
"drawable",
|
||||||
packageName)
|
packageName)
|
||||||
icons.put(num, resId)
|
icons.put(num, resId)
|
||||||
|
|||||||
@@ -108,8 +108,17 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
val closeView = popupFieldsView.findViewById<View>(R.id.keyboard_popup_close)
|
val closeView = popupFieldsView.findViewById<View>(R.id.keyboard_popup_close)
|
||||||
closeView.setOnClickListener { popupCustomKeys?.dismiss() }
|
closeView.setOnClickListener { popupCustomKeys?.dismiss() }
|
||||||
|
|
||||||
if (!Database.getInstance().loaded)
|
// Remove entry info if the database is not loaded
|
||||||
|
// or if entry info timestamp is before database loaded timestamp
|
||||||
|
val database = Database.getInstance()
|
||||||
|
val databaseTime = database.loadTimestamp
|
||||||
|
val entryTime = entryInfoTimestamp
|
||||||
|
if (!database.loaded
|
||||||
|
|| databaseTime == null
|
||||||
|
|| entryTime == null
|
||||||
|
|| entryTime < databaseTime) {
|
||||||
removeEntryInfo()
|
removeEntryInfo()
|
||||||
|
}
|
||||||
assignKeyboardView()
|
assignKeyboardView()
|
||||||
keyboardView?.setOnKeyboardActionListener(this)
|
keyboardView?.setOnKeyboardActionListener(this)
|
||||||
keyboardView?.isPreviewEnabled = false
|
keyboardView?.isPreviewEnabled = false
|
||||||
@@ -321,10 +330,13 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
private const val KEY_URL = 520
|
private const val KEY_URL = 520
|
||||||
private const val KEY_FIELDS = 530
|
private const val KEY_FIELDS = 530
|
||||||
|
|
||||||
|
// TODO Retrieve entry info from id and service when database is open
|
||||||
private var entryInfoKey: EntryInfo? = null
|
private var entryInfoKey: EntryInfo? = null
|
||||||
|
private var entryInfoTimestamp: Long? = null
|
||||||
|
|
||||||
private fun removeEntryInfo() {
|
private fun removeEntryInfo() {
|
||||||
entryInfoKey = null
|
entryInfoKey = null
|
||||||
|
entryInfoTimestamp = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeEntry(context: Context) {
|
fun removeEntry(context: Context) {
|
||||||
@@ -334,6 +346,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
|||||||
fun addEntryAndLaunchNotificationIfAllowed(context: Context, entry: EntryInfo, toast: Boolean = false) {
|
fun addEntryAndLaunchNotificationIfAllowed(context: Context, entry: EntryInfo, toast: Boolean = false) {
|
||||||
// Add a new entry
|
// Add a new entry
|
||||||
entryInfoKey = entry
|
entryInfoKey = entry
|
||||||
|
entryInfoTimestamp = System.currentTimeMillis()
|
||||||
// Launch notification if allowed
|
// Launch notification if allowed
|
||||||
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
|
KeyboardEntryNotificationService.launchNotificationIfAllowed(context, entry, toast)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
data class DatabaseFile(var databaseUri: Uri? = null,
|
||||||
|
var keyFileUri: Uri? = null,
|
||||||
|
var databaseDecodedPath: String? = null,
|
||||||
|
var databaseAlias: String? = null,
|
||||||
|
var databaseFileExists: Boolean = false,
|
||||||
|
var databaseLastModified: String? = null,
|
||||||
|
var databaseSize: String? = null) {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is DatabaseFile) return false
|
||||||
|
|
||||||
|
if (databaseUri == null || other.databaseUri == null) return false
|
||||||
|
if (databaseUri != other.databaseUri) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return databaseUri?.hashCode() ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,24 +21,25 @@ package com.kunzisoft.keepass.model
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||||
import com.kunzisoft.keepass.utils.readEnum
|
import com.kunzisoft.keepass.utils.readEnum
|
||||||
import com.kunzisoft.keepass.utils.writeEnum
|
import com.kunzisoft.keepass.utils.writeEnum
|
||||||
|
|
||||||
data class EntryAttachment(var name: String,
|
data class EntryAttachmentState(var attachment: Attachment,
|
||||||
var binaryAttachment: BinaryAttachment,
|
var streamDirection: StreamDirection,
|
||||||
var downloadState: AttachmentState = AttachmentState.NULL,
|
var downloadState: AttachmentState = AttachmentState.NULL,
|
||||||
var downloadProgression: Int = 0) : Parcelable {
|
var downloadProgression: Int = 0) : Parcelable {
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
parcel.readString() ?: "",
|
parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryAttachment()),
|
||||||
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment(),
|
parcel.readEnum<StreamDirection>() ?: StreamDirection.DOWNLOAD,
|
||||||
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
|
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
|
||||||
parcel.readInt())
|
parcel.readInt())
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeString(name)
|
parcel.writeParcelable(attachment, flags)
|
||||||
parcel.writeParcelable(binaryAttachment, flags)
|
parcel.writeEnum(streamDirection)
|
||||||
parcel.writeEnum(downloadState)
|
parcel.writeEnum(downloadState)
|
||||||
parcel.writeInt(downloadProgression)
|
parcel.writeInt(downloadProgression)
|
||||||
}
|
}
|
||||||
@@ -47,12 +48,25 @@ data class EntryAttachment(var name: String,
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<EntryAttachment> {
|
override fun equals(other: Any?): Boolean {
|
||||||
override fun createFromParcel(parcel: Parcel): EntryAttachment {
|
if (this === other) return true
|
||||||
return EntryAttachment(parcel)
|
if (other !is EntryAttachmentState) return false
|
||||||
|
|
||||||
|
if (attachment != other.attachment) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return attachment.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<EntryAttachmentState> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): EntryAttachmentState {
|
||||||
|
return EntryAttachmentState(parcel)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<EntryAttachment?> {
|
override fun newArray(size: Int): Array<EntryAttachmentState?> {
|
||||||
return arrayOfNulls(size)
|
return arrayOfNulls(size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,10 @@ package com.kunzisoft.keepass.model
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -30,12 +33,15 @@ class EntryInfo : Parcelable {
|
|||||||
|
|
||||||
var id: String = ""
|
var id: String = ""
|
||||||
var title: String = ""
|
var title: String = ""
|
||||||
var icon: IconImage? = null
|
var icon: IconImage = IconImageStandard()
|
||||||
var username: String = ""
|
var username: String = ""
|
||||||
var password: String = ""
|
var password: String = ""
|
||||||
|
var expires: Boolean = false
|
||||||
|
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
|
||||||
var url: String = ""
|
var url: String = ""
|
||||||
var notes: String = ""
|
var notes: String = ""
|
||||||
var customFields: MutableList<Field> = ArrayList()
|
var customFields: List<Field> = ArrayList()
|
||||||
|
var attachments: List<Attachment> = ArrayList()
|
||||||
var otpModel: OtpModel? = null
|
var otpModel: OtpModel? = null
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
@@ -43,12 +49,15 @@ class EntryInfo : Parcelable {
|
|||||||
private constructor(parcel: Parcel) {
|
private constructor(parcel: Parcel) {
|
||||||
id = parcel.readString() ?: id
|
id = parcel.readString() ?: id
|
||||||
title = parcel.readString() ?: title
|
title = parcel.readString() ?: title
|
||||||
icon = parcel.readParcelable(IconImage::class.java.classLoader)
|
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
|
||||||
username = parcel.readString() ?: username
|
username = parcel.readString() ?: username
|
||||||
password = parcel.readString() ?: password
|
password = parcel.readString() ?: password
|
||||||
|
expires = parcel.readInt() != 0
|
||||||
|
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
|
||||||
url = parcel.readString() ?: url
|
url = parcel.readString() ?: url
|
||||||
notes = parcel.readString() ?: notes
|
notes = parcel.readString() ?: notes
|
||||||
parcel.readList(customFields as List<Field>, Field::class.java.classLoader)
|
parcel.readList(customFields, Field::class.java.classLoader)
|
||||||
|
parcel.readList(attachments, Attachment::class.java.classLoader)
|
||||||
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
|
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,9 +71,12 @@ class EntryInfo : Parcelable {
|
|||||||
parcel.writeParcelable(icon, flags)
|
parcel.writeParcelable(icon, flags)
|
||||||
parcel.writeString(username)
|
parcel.writeString(username)
|
||||||
parcel.writeString(password)
|
parcel.writeString(password)
|
||||||
|
parcel.writeInt(if (expires) 1 else 0)
|
||||||
|
parcel.writeParcelable(expiryTime, flags)
|
||||||
parcel.writeString(url)
|
parcel.writeString(url)
|
||||||
parcel.writeString(notes)
|
parcel.writeString(notes)
|
||||||
parcel.writeArray(customFields.toTypedArray())
|
parcel.writeArray(customFields.toTypedArray())
|
||||||
|
parcel.writeArray(attachments.toTypedArray())
|
||||||
parcel.writeParcelable(otpModel, flags)
|
parcel.writeParcelable(otpModel, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,9 +49,7 @@ class Field : Parcelable {
|
|||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (other !is Field) return false
|
||||||
|
|
||||||
other as Field
|
|
||||||
|
|
||||||
if (name != other.name) return false
|
if (name != other.name) return false
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
class FocusedEditField : Parcelable {
|
||||||
|
|
||||||
|
var field: Field? = null
|
||||||
|
var cursorSelectionStart: Int = -1
|
||||||
|
var cursorSelectionEnd: Int = -1
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) {
|
||||||
|
this.field = parcel.readParcelable(Field::class.java.classLoader)
|
||||||
|
this.cursorSelectionStart = parcel.readInt()
|
||||||
|
this.cursorSelectionEnd = parcel.readInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
this.field = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeParcelable(field, flags)
|
||||||
|
parcel.writeInt(cursorSelectionStart)
|
||||||
|
parcel.writeInt(cursorSelectionEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is FocusedEditField) return false
|
||||||
|
|
||||||
|
if (field != other.field) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return field?.hashCode() ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<FocusedEditField> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): FocusedEditField {
|
||||||
|
return FocusedEditField(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<FocusedEditField?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
enum class StreamDirection {
|
||||||
|
UPLOAD, DOWNLOAD
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.notifications
|
package com.kunzisoft.keepass.notifications
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
@@ -27,53 +28,55 @@ import android.os.IBinder
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryAttachment
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileAsyncTask
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
import com.kunzisoft.keepass.stream.readBytes
|
||||||
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.io.BufferedInputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.HashMap
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
|
|
||||||
class AttachmentFileNotificationService: LockNotificationService() {
|
class AttachmentFileNotificationService: LockNotificationService() {
|
||||||
|
|
||||||
override val notificationId: Int = 10000
|
override val notificationId: Int = 10000
|
||||||
|
private val attachmentNotificationList = CopyOnWriteArrayList<AttachmentNotification>()
|
||||||
|
|
||||||
private var mActionTaskBinder = ActionTaskBinder()
|
private var mActionTaskBinder = ActionTaskBinder()
|
||||||
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
|
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
|
||||||
|
|
||||||
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
inner class ActionTaskBinder: Binder() {
|
inner class ActionTaskBinder: Binder() {
|
||||||
|
|
||||||
fun getService(): AttachmentFileNotificationService = this@AttachmentFileNotificationService
|
fun getService(): AttachmentFileNotificationService = this@AttachmentFileNotificationService
|
||||||
|
|
||||||
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
|
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
|
||||||
mActionTaskListeners.add(actionTaskListener)
|
mActionTaskListeners.add(actionTaskListener)
|
||||||
|
|
||||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
|
||||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
|
||||||
entry.value.attachmentTask?.onUpdate = { uri, attachment, notificationIdAttach ->
|
|
||||||
newNotification(uri, attachment, notificationIdAttach)
|
|
||||||
mActionTaskListeners.forEach { actionListener ->
|
|
||||||
actionListener.onAttachmentProgress(entry.key, attachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
|
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
|
||||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
|
||||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
|
||||||
entry.value.attachmentTask?.onUpdate = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
mActionTaskListeners.remove(actionTaskListener)
|
mActionTaskListeners.remove(actionTaskListener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val attachmentFileActionListener = object: AttachmentFileAction.AttachmentFileActionListener {
|
||||||
|
override fun onUpdate(attachmentNotification: AttachmentNotification) {
|
||||||
|
newNotification(attachmentNotification)
|
||||||
|
mActionTaskListeners.forEach { actionListener ->
|
||||||
|
actionListener.onAttachmentAction(attachmentNotification.uri,
|
||||||
|
attachmentNotification.entryAttachmentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface ActionTaskListener {
|
interface ActionTaskListener {
|
||||||
fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment)
|
fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
@@ -83,46 +86,36 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
|
|
||||||
val downloadFileUri: Uri? = if (intent?.hasExtra(DOWNLOAD_FILE_URI_KEY) == true) {
|
val downloadFileUri: Uri? = if (intent?.hasExtra(FILE_URI_KEY) == true) {
|
||||||
intent.getParcelableExtra(DOWNLOAD_FILE_URI_KEY)
|
intent.getParcelableExtra(FILE_URI_KEY)
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
when(intent?.action) {
|
when(intent?.action) {
|
||||||
|
ACTION_ATTACHMENT_FILE_START_UPLOAD -> {
|
||||||
|
actionUploadOrDownload(downloadFileUri,
|
||||||
|
intent,
|
||||||
|
StreamDirection.UPLOAD)
|
||||||
|
}
|
||||||
ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> {
|
ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> {
|
||||||
if (downloadFileUri != null
|
actionUploadOrDownload(downloadFileUri,
|
||||||
&& intent.hasExtra(ATTACHMENT_KEY)) {
|
intent,
|
||||||
|
StreamDirection.DOWNLOAD)
|
||||||
val nextNotificationId = (downloadFileUris.values.maxBy { it.notificationId }
|
}
|
||||||
?.notificationId ?: notificationId) + 1
|
ACTION_ATTACHMENT_REMOVE -> {
|
||||||
|
intent.getParcelableExtra<Attachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
|
||||||
try {
|
attachmentNotificationList.firstOrNull { it.entryAttachmentState.attachment == entryAttachment }?.let { elementToRemove ->
|
||||||
intent.getParcelableExtra<EntryAttachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
|
attachmentNotificationList.remove(elementToRemove)
|
||||||
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()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to download $downloadFileUri", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (downloadFileUri != null) {
|
if (downloadFileUri != null) {
|
||||||
downloadFileUris[downloadFileUri]?.notificationId?.let {
|
attachmentNotificationList.firstOrNull { it.uri == downloadFileUri }?.let { elementToRemove ->
|
||||||
notificationManager?.cancel(it)
|
notificationManager?.cancel(elementToRemove.notificationId)
|
||||||
downloadFileUris.remove(downloadFileUri)
|
attachmentNotificationList.remove(elementToRemove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (downloadFileUris.isEmpty()) {
|
if (attachmentNotificationList.isEmpty()) {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,25 +124,35 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
return START_REDELIVER_INTENT
|
return START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
fun checkCurrentAttachmentProgress() {
|
fun checkCurrentAttachmentProgress() {
|
||||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
attachmentNotificationList.forEach { attachmentNotification ->
|
||||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
mActionTaskListeners.forEach { actionListener ->
|
||||||
mActionTaskListeners.forEach { actionListener ->
|
actionListener.onAttachmentAction(
|
||||||
actionListener.onAttachmentProgress(entry.key, entry.value.entryAttachment)
|
attachmentNotification.uri,
|
||||||
}
|
attachmentNotification.entryAttachmentState
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newNotification(downloadFileUri: Uri,
|
@Synchronized
|
||||||
entryAttachment: EntryAttachment,
|
fun removeAttachmentAction(entryAttachment: EntryAttachmentState) {
|
||||||
notificationIdAttachment: Int) {
|
attachmentNotificationList.firstOrNull {
|
||||||
|
it.entryAttachmentState == entryAttachment
|
||||||
|
}?.let {
|
||||||
|
attachmentNotificationList.remove(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newNotification(attachmentNotification: AttachmentNotification) {
|
||||||
|
|
||||||
val pendingContentIntent = PendingIntent.getActivity(this,
|
val pendingContentIntent = PendingIntent.getActivity(this,
|
||||||
0,
|
0,
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
action = Intent.ACTION_VIEW
|
action = Intent.ACTION_VIEW
|
||||||
setDataAndType(downloadFileUri, contentResolver.getType(downloadFileUri))
|
setDataAndType(attachmentNotification.uri,
|
||||||
|
contentResolver.getType(attachmentNotification.uri))
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||||
|
|
||||||
@@ -157,54 +160,84 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
0,
|
0,
|
||||||
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
||||||
// No action to delete the service
|
// No action to delete the service
|
||||||
putExtra(DOWNLOAD_FILE_URI_KEY, downloadFileUri)
|
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
||||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||||
|
|
||||||
val fileName = DocumentFile.fromSingleUri(this, downloadFileUri)?.name ?: ""
|
val fileName = DocumentFile.fromSingleUri(this, attachmentNotification.uri)?.name ?: ""
|
||||||
|
|
||||||
val builder = buildNewNotification().apply {
|
val builder = buildNewNotification().apply {
|
||||||
setSmallIcon(R.drawable.ic_file_download_white_24dp)
|
when (attachmentNotification.entryAttachmentState.streamDirection) {
|
||||||
setContentTitle(getString(R.string.download_attachment, fileName))
|
StreamDirection.UPLOAD -> {
|
||||||
|
setSmallIcon(R.drawable.ic_file_upload_white_24dp)
|
||||||
|
setContentTitle(getString(R.string.upload_attachment, fileName))
|
||||||
|
}
|
||||||
|
StreamDirection.DOWNLOAD -> {
|
||||||
|
setSmallIcon(R.drawable.ic_file_download_white_24dp)
|
||||||
|
setContentTitle(getString(R.string.download_attachment, fileName))
|
||||||
|
}
|
||||||
|
}
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
when (entryAttachment.downloadState) {
|
when (attachmentNotification.entryAttachmentState.downloadState) {
|
||||||
AttachmentState.NULL, AttachmentState.START -> {
|
AttachmentState.NULL, AttachmentState.START -> {
|
||||||
setContentText(getString(R.string.download_initialization))
|
setContentText(getString(R.string.download_initialization))
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
}
|
}
|
||||||
AttachmentState.IN_PROGRESS -> {
|
AttachmentState.IN_PROGRESS -> {
|
||||||
if (entryAttachment.downloadProgression > 100) {
|
if (attachmentNotification.entryAttachmentState.downloadProgression > 100) {
|
||||||
setContentText(getString(R.string.download_finalization))
|
setContentText(getString(R.string.download_finalization))
|
||||||
} else {
|
} else {
|
||||||
setProgress(100, entryAttachment.downloadProgression, false)
|
setProgress(100,
|
||||||
setContentText(getString(R.string.download_progression, entryAttachment.downloadProgression))
|
attachmentNotification.entryAttachmentState.downloadProgression,
|
||||||
|
false)
|
||||||
|
setContentText(getString(R.string.download_progression,
|
||||||
|
attachmentNotification.entryAttachmentState.downloadProgression))
|
||||||
}
|
}
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
}
|
}
|
||||||
AttachmentState.COMPLETE, AttachmentState.ERROR -> {
|
AttachmentState.COMPLETE -> {
|
||||||
setContentText(getString(R.string.download_complete))
|
setContentText(getString(R.string.download_complete))
|
||||||
setContentIntent(pendingContentIntent)
|
when (attachmentNotification.entryAttachmentState.streamDirection) {
|
||||||
|
StreamDirection.UPLOAD -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
StreamDirection.DOWNLOAD -> {
|
||||||
|
setContentIntent(pendingContentIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
setDeleteIntent(pendingDeleteIntent)
|
setDeleteIntent(pendingDeleteIntent)
|
||||||
setOngoing(false)
|
setOngoing(false)
|
||||||
}
|
}
|
||||||
|
AttachmentState.ERROR -> {
|
||||||
|
setContentText(getString(R.string.error_file_not_create))
|
||||||
|
setOngoing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (attachmentNotification.entryAttachmentState.downloadState) {
|
||||||
|
AttachmentState.ERROR,
|
||||||
|
AttachmentState.COMPLETE -> {
|
||||||
|
stopForeground(false)
|
||||||
|
notificationManager?.notify(attachmentNotification.notificationId, builder.build())
|
||||||
|
} else -> {
|
||||||
|
startForeground(attachmentNotification.notificationId, builder.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notificationManager?.notify(notificationIdAttachment, builder.build())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
|
attachmentNotificationList.forEach { attachmentNotification ->
|
||||||
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
|
attachmentNotification.attachmentFileAction?.listener = null
|
||||||
entry.value.attachmentTask?.onUpdate = null
|
notificationManager?.cancel(attachmentNotification.notificationId)
|
||||||
notificationManager?.cancel(entry.value.notificationId)
|
}
|
||||||
}
|
attachmentNotificationList.clear()
|
||||||
})
|
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AttachmentNotification(var notificationId: Int,
|
private data class AttachmentNotification(var uri: Uri,
|
||||||
var entryAttachment: EntryAttachment,
|
var notificationId: Int,
|
||||||
var attachmentTask: AttachmentFileAsyncTask? = null) {
|
var entryAttachmentState: EntryAttachmentState,
|
||||||
|
var attachmentFileAction: AttachmentFileAction? = null) {
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
@@ -221,15 +254,188 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun actionUploadOrDownload(downloadFileUri: Uri?,
|
||||||
|
intent: Intent,
|
||||||
|
streamDirection: StreamDirection) {
|
||||||
|
if (downloadFileUri != null
|
||||||
|
&& intent.hasExtra(ATTACHMENT_KEY)) {
|
||||||
|
try {
|
||||||
|
intent.getParcelableExtra<Attachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
|
||||||
|
|
||||||
|
val nextNotificationId = (attachmentNotificationList.maxByOrNull { it.notificationId }
|
||||||
|
?.notificationId ?: notificationId) + 1
|
||||||
|
val entryAttachmentState = EntryAttachmentState(entryAttachment, streamDirection)
|
||||||
|
val attachmentNotification = AttachmentNotification(downloadFileUri, nextNotificationId, entryAttachmentState)
|
||||||
|
|
||||||
|
// Add action to the list on start
|
||||||
|
attachmentNotificationList.add(attachmentNotification)
|
||||||
|
|
||||||
|
mainScope.launch {
|
||||||
|
AttachmentFileAction(attachmentNotification,
|
||||||
|
contentResolver).apply {
|
||||||
|
listener = attachmentFileActionListener
|
||||||
|
}.executeAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to upload/download $downloadFileUri", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AttachmentFileAction(
|
||||||
|
private val attachmentNotification: AttachmentNotification,
|
||||||
|
private val contentResolver: ContentResolver) {
|
||||||
|
|
||||||
|
private val updateMinFrequency = 1000
|
||||||
|
private var previousSaveTime = System.currentTimeMillis()
|
||||||
|
var listener: AttachmentFileActionListener? = null
|
||||||
|
|
||||||
|
interface AttachmentFileActionListener {
|
||||||
|
fun onUpdate(attachmentNotification: AttachmentNotification)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun executeAction() {
|
||||||
|
|
||||||
|
// on pre execute
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
TimeoutHelper.temporarilyDisableTimeout()
|
||||||
|
|
||||||
|
attachmentNotification.attachmentFileAction = this@AttachmentFileAction
|
||||||
|
attachmentNotification.entryAttachmentState.apply {
|
||||||
|
downloadState = AttachmentState.START
|
||||||
|
downloadProgression = 0
|
||||||
|
}
|
||||||
|
listener?.onUpdate(attachmentNotification)
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
// on Progress with thread
|
||||||
|
val asyncResult: Deferred<Boolean> = async {
|
||||||
|
var progressResult = true
|
||||||
|
try {
|
||||||
|
attachmentNotification.entryAttachmentState.apply {
|
||||||
|
downloadState = AttachmentState.IN_PROGRESS
|
||||||
|
|
||||||
|
when (streamDirection) {
|
||||||
|
StreamDirection.UPLOAD -> {
|
||||||
|
uploadToDatabase(
|
||||||
|
attachmentNotification.uri,
|
||||||
|
attachment.binaryAttachment,
|
||||||
|
contentResolver, 1024) { percent ->
|
||||||
|
publishProgress(percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StreamDirection.DOWNLOAD -> {
|
||||||
|
downloadFromDatabase(
|
||||||
|
attachmentNotification.uri,
|
||||||
|
attachment.binaryAttachment,
|
||||||
|
contentResolver, 1024) { percent ->
|
||||||
|
publishProgress(percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to upload or download file", e)
|
||||||
|
progressResult = false
|
||||||
|
}
|
||||||
|
progressResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// on post execute
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val result = asyncResult.await()
|
||||||
|
attachmentNotification.attachmentFileAction = null
|
||||||
|
attachmentNotification.entryAttachmentState.apply {
|
||||||
|
downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR
|
||||||
|
downloadProgression = 100
|
||||||
|
}
|
||||||
|
listener?.onUpdate(attachmentNotification)
|
||||||
|
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadFromDatabase(attachmentToUploadUri: Uri,
|
||||||
|
binaryAttachment: BinaryAttachment,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
||||||
|
update: ((percent: Int)->Unit)? = null) {
|
||||||
|
var dataDownloaded = 0L
|
||||||
|
val fileSize = binaryAttachment.length()
|
||||||
|
UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream ->
|
||||||
|
binaryAttachment.getUnGzipInputDataStream().use { inputStream ->
|
||||||
|
inputStream.readBytes(bufferSize) { buffer ->
|
||||||
|
outputStream.write(buffer)
|
||||||
|
dataDownloaded += buffer.size
|
||||||
|
try {
|
||||||
|
val percentDownload = (100 * dataDownloaded / fileSize).toInt()
|
||||||
|
update?.invoke(percentDownload)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uploadToDatabase(attachmentFromDownloadUri: Uri,
|
||||||
|
binaryAttachment: BinaryAttachment,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
||||||
|
update: ((percent: Int)->Unit)? = null) {
|
||||||
|
var dataUploaded = 0L
|
||||||
|
val fileSize = contentResolver.openFileDescriptor(attachmentFromDownloadUri, "r")?.statSize ?: 0
|
||||||
|
UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.let { inputStream ->
|
||||||
|
binaryAttachment.getGzipOutputDataStream().use { outputStream ->
|
||||||
|
BufferedInputStream(inputStream).use { attachmentBufferedInputStream ->
|
||||||
|
attachmentBufferedInputStream.readBytes(bufferSize) { buffer ->
|
||||||
|
outputStream.write(buffer)
|
||||||
|
dataUploaded += buffer.size
|
||||||
|
try {
|
||||||
|
val percentDownload = (100 * dataUploaded / fileSize).toInt()
|
||||||
|
update?.invoke(percentDownload)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun publishProgress(percent: Int) {
|
||||||
|
// Publish progress
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (previousSaveTime + updateMinFrequency < currentTime) {
|
||||||
|
attachmentNotification.entryAttachmentState.apply {
|
||||||
|
downloadState = AttachmentState.IN_PROGRESS
|
||||||
|
downloadProgression = percent
|
||||||
|
}
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
listener?.onUpdate(attachmentNotification)
|
||||||
|
Log.d(TAG, "Download file ${attachmentNotification.uri} : $percent%")
|
||||||
|
}
|
||||||
|
previousSaveTime = currentTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = AttachmentFileAction::class.java.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = AttachmentFileNotificationService::javaClass.name
|
private val TAG = AttachmentFileNotificationService::javaClass.name
|
||||||
|
|
||||||
|
const val ACTION_ATTACHMENT_FILE_START_UPLOAD = "ACTION_ATTACHMENT_FILE_START_UPLOAD"
|
||||||
const val ACTION_ATTACHMENT_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD"
|
const val ACTION_ATTACHMENT_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD"
|
||||||
|
const val ACTION_ATTACHMENT_REMOVE = "ACTION_ATTACHMENT_REMOVE"
|
||||||
|
|
||||||
const val DOWNLOAD_FILE_URI_KEY = "DOWNLOAD_FILE_URI_KEY"
|
const val FILE_URI_KEY = "FILE_URI_KEY"
|
||||||
const val ATTACHMENT_KEY = "ATTACHMENT_KEY"
|
const val ATTACHMENT_KEY = "ATTACHMENT_KEY"
|
||||||
|
|
||||||
private val downloadFileUris = HashMap<Uri, AttachmentNotification>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.notifications
|
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
|
||||||
import com.kunzisoft.keepass.utils.closeDatabase
|
|
||||||
|
|
||||||
class DatabaseOpenNotificationService: LockNotificationService() {
|
|
||||||
|
|
||||||
override val notificationId: Int = 340
|
|
||||||
|
|
||||||
private fun stopNotificationAndSendLock() {
|
|
||||||
// Send lock action
|
|
||||||
sendBroadcast(Intent(LOCK_ACTION))
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkIntent(intent: Intent?) {
|
|
||||||
val notificationBuilder = buildNewNotification().apply {
|
|
||||||
setSmallIcon(R.drawable.notification_ic_database_open)
|
|
||||||
setContentTitle(getString(R.string.database_opened))
|
|
||||||
setAutoCancel(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
when(intent?.action) {
|
|
||||||
ACTION_CLOSE_DATABASE -> {
|
|
||||||
startForeground(notificationId, notificationBuilder.build())
|
|
||||||
stopNotificationAndSendLock()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val databaseIntent = Intent(this, GroupActivity::class.java)
|
|
||||||
var pendingDatabaseFlag = 0
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
pendingDatabaseFlag = PendingIntent.FLAG_IMMUTABLE
|
|
||||||
}
|
|
||||||
val pendingDatabaseIntent = PendingIntent.getActivity(this, 0, databaseIntent, pendingDatabaseFlag)
|
|
||||||
val deleteIntent = Intent(this, DatabaseOpenNotificationService::class.java).apply {
|
|
||||||
action = ACTION_CLOSE_DATABASE
|
|
||||||
}
|
|
||||||
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
|
|
||||||
val database = Database.getInstance()
|
|
||||||
if (database.loaded) {
|
|
||||||
startForeground(notificationId, notificationBuilder.apply {
|
|
||||||
setContentText(database.name + " (" + database.version + ")")
|
|
||||||
setContentIntent(pendingDatabaseIntent)
|
|
||||||
// Unfortunately swipe is disabled in lollipop+
|
|
||||||
setDeleteIntent(pendingDeleteIntent)
|
|
||||||
addAction(R.drawable.ic_lock_white_24dp, getString(R.string.lock),
|
|
||||||
pendingDeleteIntent)
|
|
||||||
}.build())
|
|
||||||
} else {
|
|
||||||
startForeground(notificationId, notificationBuilder.build())
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
|
||||||
checkIntent(intent)
|
|
||||||
return super.onBind(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
super.onStartCommand(intent, flags, startId)
|
|
||||||
checkIntent(intent)
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ACTION_CLOSE_DATABASE = "ACTION_CLOSE_DATABASE"
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop(context: Context) {
|
|
||||||
// Stop the opening notification
|
|
||||||
context.stopService(Intent(context, DatabaseOpenNotificationService::class.java))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -19,12 +19,15 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.notifications
|
package com.kunzisoft.keepass.notifications
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.database.action.*
|
import com.kunzisoft.keepass.database.action.*
|
||||||
import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable
|
import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable
|
||||||
@@ -42,22 +45,28 @@ import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
|||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.closeDatabase
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdater {
|
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
|
||||||
|
|
||||||
override val notificationId: Int = 575
|
override val notificationId: Int = 575
|
||||||
|
|
||||||
|
private lateinit var mDatabase: Database
|
||||||
|
|
||||||
private val mainScope = CoroutineScope(Dispatchers.Main)
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
private var mActionTaskBinder = ActionTaskBinder()
|
private var mActionTaskBinder = ActionTaskBinder()
|
||||||
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
|
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
|
||||||
private var mAllowFinishAction = AtomicBoolean()
|
private var mAllowFinishAction = AtomicBoolean()
|
||||||
|
private var mActionRunning = false
|
||||||
|
|
||||||
private var mTitleId: Int? = null
|
private var mIconId: Int = R.drawable.notification_ic_database_load
|
||||||
|
private var mTitleId: Int = R.string.database_opened
|
||||||
private var mMessageId: Int? = null
|
private var mMessageId: Int? = null
|
||||||
private var mWarningId: Int? = null
|
private var mWarningId: Int? = null
|
||||||
|
|
||||||
@@ -66,8 +75,8 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
fun getService(): DatabaseTaskNotificationService = this@DatabaseTaskNotificationService
|
fun getService(): DatabaseTaskNotificationService = this@DatabaseTaskNotificationService
|
||||||
|
|
||||||
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
|
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
|
||||||
mActionTaskListeners.add(actionTaskListener)
|
|
||||||
mAllowFinishAction.set(true)
|
mAllowFinishAction.set(true)
|
||||||
|
mActionTaskListeners.add(actionTaskListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
|
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
|
||||||
@@ -84,66 +93,41 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
fun onStopAction(actionTask: String, result: ActionRunnable.Result)
|
fun onStopAction(actionTask: String, result: ActionRunnable.Result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force to call [ActionTaskListener.onStartAction] if the action is still running
|
||||||
|
*/
|
||||||
fun checkAction() {
|
fun checkAction() {
|
||||||
mActionTaskListeners.forEach { actionTaskListener ->
|
if (mActionRunning) {
|
||||||
actionTaskListener.onStartAction(mTitleId, mMessageId, mWarningId)
|
mActionTaskListeners.forEach { actionTaskListener ->
|
||||||
}
|
actionTaskListener.onStartAction(mTitleId, mMessageId, mWarningId)
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildNotification(intent: Intent?) {
|
|
||||||
var saveAction = true
|
|
||||||
if (intent != null && intent.hasExtra(SAVE_DATABASE_KEY)) {
|
|
||||||
saveAction = intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
val intentAction = intent?.action
|
|
||||||
val titleId: Int = when (intentAction) {
|
|
||||||
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
|
|
||||||
ACTION_DATABASE_LOAD_TASK -> R.string.loading_database
|
|
||||||
else -> {
|
|
||||||
if (saveAction)
|
|
||||||
R.string.saving_database
|
|
||||||
else
|
|
||||||
R.string.command_execution
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val messageId: Int? = when (intentAction) {
|
|
||||||
ACTION_DATABASE_LOAD_TASK -> null
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
val warningId: Int? =
|
|
||||||
if (!saveAction
|
|
||||||
|| intentAction == ACTION_DATABASE_LOAD_TASK)
|
|
||||||
null
|
|
||||||
else
|
|
||||||
R.string.do_not_kill_app
|
|
||||||
|
|
||||||
// Assign elements for updates
|
|
||||||
mTitleId = titleId
|
|
||||||
mMessageId = messageId
|
|
||||||
mWarningId = warningId
|
|
||||||
// Create the notification
|
|
||||||
startForeground(notificationId, buildNewNotification()
|
|
||||||
.setSmallIcon(R.drawable.notification_ic_database_load)
|
|
||||||
.setContentTitle(getString(intent?.getIntExtra(DATABASE_TASK_TITLE_KEY, titleId) ?: titleId))
|
|
||||||
.setAutoCancel(false)
|
|
||||||
.setContentIntent(null).build())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
buildNotification(intent)
|
super.onBind(intent)
|
||||||
return mActionTaskBinder
|
return mActionTaskBinder
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
|
|
||||||
buildNotification(intent)
|
mDatabase = Database.getInstance()
|
||||||
|
|
||||||
if (intent == null) return START_REDELIVER_INTENT
|
// Create the notification
|
||||||
|
buildMessage(intent)
|
||||||
|
|
||||||
val intentAction = intent.action
|
val intentAction = intent?.action
|
||||||
val actionRunnable: ActionRunnable? = when (intentAction) {
|
|
||||||
|
if (intentAction == null && !mDatabase.loaded) {
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
if (intentAction == ACTION_DATABASE_CLOSE) {
|
||||||
|
// Send lock action
|
||||||
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}
|
||||||
|
|
||||||
|
val actionRunnable: ActionRunnable? = when (intentAction) {
|
||||||
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
|
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
|
||||||
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent)
|
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent)
|
||||||
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent)
|
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent)
|
||||||
@@ -157,6 +141,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY -> buildDatabaseRestoreEntryHistoryActionTask(intent)
|
ACTION_DATABASE_RESTORE_ENTRY_HISTORY -> buildDatabaseRestoreEntryHistoryActionTask(intent)
|
||||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> buildDatabaseDeleteEntryHistoryActionTask(intent)
|
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> buildDatabaseDeleteEntryHistoryActionTask(intent)
|
||||||
ACTION_DATABASE_UPDATE_COMPRESSION_TASK -> buildDatabaseUpdateCompressionActionTask(intent)
|
ACTION_DATABASE_UPDATE_COMPRESSION_TASK -> buildDatabaseUpdateCompressionActionTask(intent)
|
||||||
|
ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK -> buildDatabaseRemoveUnlinkedDataActionTask(intent)
|
||||||
ACTION_DATABASE_UPDATE_NAME_TASK,
|
ACTION_DATABASE_UPDATE_NAME_TASK,
|
||||||
ACTION_DATABASE_UPDATE_DESCRIPTION_TASK,
|
ACTION_DATABASE_UPDATE_DESCRIPTION_TASK,
|
||||||
ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK,
|
ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK,
|
||||||
@@ -172,12 +157,13 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
actionRunnable?.let { actionRunnableNotNull ->
|
// Build and launch the action
|
||||||
|
if (actionRunnable != null) {
|
||||||
// Build and launch the action
|
|
||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
executeAction(this@DatabaseTaskNotificationService,
|
executeAction(this@DatabaseTaskNotificationService,
|
||||||
{
|
{
|
||||||
|
mActionRunning = true
|
||||||
|
|
||||||
sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply {
|
sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply {
|
||||||
putExtra(DATABASE_TASK_TITLE_KEY, mTitleId)
|
putExtra(DATABASE_TASK_TITLE_KEY, mTitleId)
|
||||||
putExtra(DATABASE_TASK_MESSAGE_KEY, mMessageId)
|
putExtra(DATABASE_TASK_MESSAGE_KEY, mMessageId)
|
||||||
@@ -190,22 +176,160 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
actionRunnableNotNull
|
actionRunnable
|
||||||
},
|
},
|
||||||
{ result ->
|
{ result ->
|
||||||
mActionTaskListeners.forEach { actionTaskListener ->
|
try {
|
||||||
actionTaskListener.onStopAction(intentAction!!, result)
|
mActionTaskListeners.forEach { actionTaskListener ->
|
||||||
|
actionTaskListener.onStopAction(intentAction!!, result)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
removeIntentData(intent)
|
||||||
|
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
||||||
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
|
||||||
|
if (!mDatabase.loaded) {
|
||||||
|
stopSelf()
|
||||||
|
} else {
|
||||||
|
// Restart the service to open lock notification
|
||||||
|
startService(Intent(applicationContext,
|
||||||
|
DatabaseTaskNotificationService::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendBroadcast(Intent(DATABASE_STOP_TASK_ACTION))
|
sendBroadcast(Intent(DATABASE_STOP_TASK_ACTION))
|
||||||
|
|
||||||
stopSelf()
|
mActionRunning = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return START_REDELIVER_INTENT
|
return when (intentAction) {
|
||||||
|
ACTION_DATABASE_LOAD_TASK, null -> {
|
||||||
|
START_STICKY
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Relaunch action if failed
|
||||||
|
START_REDELIVER_INTENT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildMessage(intent: Intent?) {
|
||||||
|
// Assign elements for updates
|
||||||
|
val intentAction = intent?.action
|
||||||
|
|
||||||
|
var saveAction = false
|
||||||
|
if (intent != null && intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||||
|
saveAction = intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
mIconId = if (intentAction == null)
|
||||||
|
R.drawable.notification_ic_database_open
|
||||||
|
else
|
||||||
|
R.drawable.notification_ic_database_load
|
||||||
|
|
||||||
|
mTitleId = when {
|
||||||
|
saveAction -> {
|
||||||
|
R.string.saving_database
|
||||||
|
}
|
||||||
|
intentAction == null -> {
|
||||||
|
R.string.database_opened
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
when (intentAction) {
|
||||||
|
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> R.string.loading_database
|
||||||
|
ACTION_DATABASE_SAVE -> R.string.saving_database
|
||||||
|
else -> {
|
||||||
|
R.string.command_execution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mMessageId = when (intentAction) {
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> null
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
mWarningId =
|
||||||
|
if (!saveAction
|
||||||
|
|| intentAction == ACTION_DATABASE_LOAD_TASK)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
R.string.do_not_kill_app
|
||||||
|
|
||||||
|
val notificationBuilder = buildNewNotification().apply {
|
||||||
|
setSmallIcon(mIconId)
|
||||||
|
intent?.let {
|
||||||
|
setContentTitle(getString(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mTitleId)))
|
||||||
|
}
|
||||||
|
setAutoCancel(false)
|
||||||
|
setContentIntent(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intentAction == null) {
|
||||||
|
// Database is normally open
|
||||||
|
if (mDatabase.loaded) {
|
||||||
|
// Build Intents for notification action
|
||||||
|
var pendingDatabaseFlag = 0
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
pendingDatabaseFlag = PendingIntent.FLAG_IMMUTABLE
|
||||||
|
}
|
||||||
|
val pendingDatabaseIntent = PendingIntent.getActivity(this,
|
||||||
|
0,
|
||||||
|
Intent(this, GroupActivity::class.java),
|
||||||
|
pendingDatabaseFlag)
|
||||||
|
val deleteIntent = Intent(this, DatabaseTaskNotificationService::class.java).apply {
|
||||||
|
action = ACTION_DATABASE_CLOSE
|
||||||
|
}
|
||||||
|
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
// Add actions in notifications
|
||||||
|
notificationBuilder.apply {
|
||||||
|
setContentText(mDatabase.name + " (" + mDatabase.version + ")")
|
||||||
|
setContentIntent(pendingDatabaseIntent)
|
||||||
|
// Unfortunately swipe is disabled in lollipop+
|
||||||
|
setDeleteIntent(pendingDeleteIntent)
|
||||||
|
addAction(R.drawable.ic_lock_white_24dp, getString(R.string.lock),
|
||||||
|
pendingDeleteIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the notification
|
||||||
|
startForeground(notificationId, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeIntentData(intent: Intent?) {
|
||||||
|
intent?.action = null
|
||||||
|
|
||||||
|
intent?.removeExtra(DATABASE_TASK_TITLE_KEY)
|
||||||
|
intent?.removeExtra(DATABASE_TASK_MESSAGE_KEY)
|
||||||
|
intent?.removeExtra(DATABASE_TASK_WARNING_KEY)
|
||||||
|
|
||||||
|
intent?.removeExtra(DATABASE_URI_KEY)
|
||||||
|
intent?.removeExtra(MASTER_PASSWORD_CHECKED_KEY)
|
||||||
|
intent?.removeExtra(MASTER_PASSWORD_KEY)
|
||||||
|
intent?.removeExtra(KEY_FILE_CHECKED_KEY)
|
||||||
|
intent?.removeExtra(KEY_FILE_URI_KEY)
|
||||||
|
intent?.removeExtra(READ_ONLY_KEY)
|
||||||
|
intent?.removeExtra(CIPHER_ENTITY_KEY)
|
||||||
|
intent?.removeExtra(FIX_DUPLICATE_UUID_KEY)
|
||||||
|
intent?.removeExtra(GROUP_KEY)
|
||||||
|
intent?.removeExtra(ENTRY_KEY)
|
||||||
|
intent?.removeExtra(GROUP_ID_KEY)
|
||||||
|
intent?.removeExtra(ENTRY_ID_KEY)
|
||||||
|
intent?.removeExtra(GROUPS_ID_KEY)
|
||||||
|
intent?.removeExtra(ENTRIES_ID_KEY)
|
||||||
|
intent?.removeExtra(PARENT_ID_KEY)
|
||||||
|
intent?.removeExtra(ENTRY_HISTORY_POSITION_KEY)
|
||||||
|
intent?.removeExtra(SAVE_DATABASE_KEY)
|
||||||
|
intent?.removeExtra(OLD_NODES_KEY)
|
||||||
|
intent?.removeExtra(NEW_NODES_KEY)
|
||||||
|
intent?.removeExtra(OLD_ELEMENT_KEY)
|
||||||
|
intent?.removeExtra(NEW_ELEMENT_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -235,15 +359,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
try {
|
onPostExecute.invoke(asyncResult.await())
|
||||||
onPostExecute.invoke(asyncResult.await())
|
|
||||||
} finally {
|
|
||||||
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
|
||||||
// Start the opening notification
|
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
|
|
||||||
DatabaseOpenNotificationService.start(this@DatabaseTaskNotificationService)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,22 +372,32 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun actionOnLock() {
|
||||||
|
if (!TimeoutHelper.temporarilyDisableTimeout) {
|
||||||
|
closeDatabase()
|
||||||
|
// Remove the lock timer (no more needed if it exists)
|
||||||
|
TimeoutHelper.cancelLockTimer(this)
|
||||||
|
// Service is stopped after receive the broadcast
|
||||||
|
super.actionOnLock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? {
|
private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? {
|
||||||
|
|
||||||
if (intent.hasExtra(DATABASE_URI_KEY)
|
if (intent.hasExtra(DATABASE_URI_KEY)
|
||||||
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
|
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
|
||||||
&& intent.hasExtra(MASTER_PASSWORD_KEY)
|
&& intent.hasExtra(MASTER_PASSWORD_KEY)
|
||||||
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
|
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
|
||||||
&& intent.hasExtra(KEY_FILE_KEY)
|
&& intent.hasExtra(KEY_FILE_URI_KEY)
|
||||||
) {
|
) {
|
||||||
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
|
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
|
||||||
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
|
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY)
|
||||||
|
|
||||||
if (databaseUri == null)
|
if (databaseUri == null)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
return CreateDatabaseRunnable(this,
|
return CreateDatabaseRunnable(this,
|
||||||
Database.getInstance(),
|
mDatabase,
|
||||||
databaseUri,
|
databaseUri,
|
||||||
getString(R.string.database_default_name),
|
getString(R.string.database_default_name),
|
||||||
getString(R.string.database),
|
getString(R.string.database),
|
||||||
@@ -279,7 +405,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
intent.getStringExtra(MASTER_PASSWORD_KEY),
|
intent.getStringExtra(MASTER_PASSWORD_KEY),
|
||||||
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
|
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
|
||||||
keyFileUri
|
keyFileUri
|
||||||
)
|
) { result ->
|
||||||
|
result.data = Bundle().apply {
|
||||||
|
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||||
|
putParcelable(KEY_FILE_URI_KEY, keyFileUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -289,15 +420,14 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|
|
||||||
if (intent.hasExtra(DATABASE_URI_KEY)
|
if (intent.hasExtra(DATABASE_URI_KEY)
|
||||||
&& intent.hasExtra(MASTER_PASSWORD_KEY)
|
&& intent.hasExtra(MASTER_PASSWORD_KEY)
|
||||||
&& intent.hasExtra(KEY_FILE_KEY)
|
&& intent.hasExtra(KEY_FILE_URI_KEY)
|
||||||
&& intent.hasExtra(READ_ONLY_KEY)
|
&& intent.hasExtra(READ_ONLY_KEY)
|
||||||
&& intent.hasExtra(CIPHER_ENTITY_KEY)
|
&& intent.hasExtra(CIPHER_ENTITY_KEY)
|
||||||
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
|
&& 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 masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY)
|
||||||
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
|
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY)
|
||||||
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
|
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
|
||||||
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
|
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
|
||||||
|
|
||||||
@@ -306,7 +436,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|
|
||||||
return LoadDatabaseRunnable(
|
return LoadDatabaseRunnable(
|
||||||
this,
|
this,
|
||||||
database,
|
mDatabase,
|
||||||
databaseUri,
|
databaseUri,
|
||||||
masterPassword,
|
masterPassword,
|
||||||
keyFileUri,
|
keyFileUri,
|
||||||
@@ -319,7 +449,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
result.data = Bundle().apply {
|
result.data = Bundle().apply {
|
||||||
putParcelable(DATABASE_URI_KEY, databaseUri)
|
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||||
putString(MASTER_PASSWORD_KEY, masterPassword)
|
putString(MASTER_PASSWORD_KEY, masterPassword)
|
||||||
putParcelable(KEY_FILE_KEY, keyFileUri)
|
putParcelable(KEY_FILE_URI_KEY, keyFileUri)
|
||||||
putBoolean(READ_ONLY_KEY, readOnly)
|
putBoolean(READ_ONLY_KEY, readOnly)
|
||||||
putParcelable(CIPHER_ENTITY_KEY, cipherEntity)
|
putParcelable(CIPHER_ENTITY_KEY, cipherEntity)
|
||||||
}
|
}
|
||||||
@@ -334,16 +464,16 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
|
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
|
||||||
&& intent.hasExtra(MASTER_PASSWORD_KEY)
|
&& intent.hasExtra(MASTER_PASSWORD_KEY)
|
||||||
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
|
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
|
||||||
&& intent.hasExtra(KEY_FILE_KEY)
|
&& intent.hasExtra(KEY_FILE_URI_KEY)
|
||||||
) {
|
) {
|
||||||
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
|
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
|
||||||
AssignPasswordInDatabaseRunnable(this,
|
AssignPasswordInDatabaseRunnable(this,
|
||||||
Database.getInstance(),
|
mDatabase,
|
||||||
databaseUri,
|
databaseUri,
|
||||||
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
|
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
|
||||||
intent.getStringExtra(MASTER_PASSWORD_KEY),
|
intent.getStringExtra(MASTER_PASSWORD_KEY),
|
||||||
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
|
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
|
||||||
intent.getParcelableExtra(KEY_FILE_KEY)
|
intent.getParcelableExtra(KEY_FILE_URI_KEY)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -365,7 +495,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(PARENT_ID_KEY)
|
&& intent.hasExtra(PARENT_ID_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
|
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
|
||||||
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
|
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
|
||||||
|
|
||||||
@@ -373,9 +502,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|| newGroup == null)
|
|| newGroup == null)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
database.getGroupById(parentId)?.let { parent ->
|
mDatabase.getGroupById(parentId)?.let { parent ->
|
||||||
AddGroupRunnable(this,
|
AddGroupRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
newGroup,
|
newGroup,
|
||||||
parent,
|
parent,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
@@ -391,7 +520,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(GROUP_KEY)
|
&& intent.hasExtra(GROUP_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val groupId: NodeId<*>? = intent.getParcelableExtra(GROUP_ID_KEY)
|
val groupId: NodeId<*>? = intent.getParcelableExtra(GROUP_ID_KEY)
|
||||||
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
|
val newGroup: Group? = intent.getParcelableExtra(GROUP_KEY)
|
||||||
|
|
||||||
@@ -399,9 +527,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|| newGroup == null)
|
|| newGroup == null)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
database.getGroupById(groupId)?.let { oldGroup ->
|
mDatabase.getGroupById(groupId)?.let { oldGroup ->
|
||||||
UpdateGroupRunnable(this,
|
UpdateGroupRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
oldGroup,
|
oldGroup,
|
||||||
newGroup,
|
newGroup,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
@@ -417,7 +545,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(PARENT_ID_KEY)
|
&& intent.hasExtra(PARENT_ID_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
|
val parentId: NodeId<*>? = intent.getParcelableExtra(PARENT_ID_KEY)
|
||||||
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
|
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
|
||||||
|
|
||||||
@@ -425,9 +552,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|| newEntry == null)
|
|| newEntry == null)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
database.getGroupById(parentId)?.let { parent ->
|
mDatabase.getGroupById(parentId)?.let { parent ->
|
||||||
AddEntryRunnable(this,
|
AddEntryRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
newEntry,
|
newEntry,
|
||||||
parent,
|
parent,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
@@ -443,7 +570,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(ENTRY_KEY)
|
&& intent.hasExtra(ENTRY_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val entryId: NodeId<UUID>? = intent.getParcelableExtra(ENTRY_ID_KEY)
|
val entryId: NodeId<UUID>? = intent.getParcelableExtra(ENTRY_ID_KEY)
|
||||||
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
|
val newEntry: Entry? = intent.getParcelableExtra(ENTRY_KEY)
|
||||||
|
|
||||||
@@ -451,9 +577,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|| newEntry == null)
|
|| newEntry == null)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
database.getEntryById(entryId)?.let { oldEntry ->
|
mDatabase.getEntryById(entryId)?.let { oldEntry ->
|
||||||
UpdateEntryRunnable(this,
|
UpdateEntryRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
oldEntry,
|
oldEntry,
|
||||||
newEntry,
|
newEntry,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
@@ -470,13 +596,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(PARENT_ID_KEY)
|
&& intent.hasExtra(PARENT_ID_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
|
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
|
||||||
|
|
||||||
database.getGroupById(parentId)?.let { newParent ->
|
mDatabase.getGroupById(parentId)?.let { newParent ->
|
||||||
CopyNodesRunnable(this,
|
CopyNodesRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
getListNodesFromBundle(database, intent.extras!!),
|
getListNodesFromBundle(mDatabase, intent.extras!!),
|
||||||
newParent,
|
newParent,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
AfterActionNodesRunnable())
|
AfterActionNodesRunnable())
|
||||||
@@ -492,13 +617,12 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(PARENT_ID_KEY)
|
&& intent.hasExtra(PARENT_ID_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
|
val parentId: NodeId<*> = intent.getParcelableExtra(PARENT_ID_KEY) ?: return null
|
||||||
|
|
||||||
database.getGroupById(parentId)?.let { newParent ->
|
mDatabase.getGroupById(parentId)?.let { newParent ->
|
||||||
MoveNodesRunnable(this,
|
MoveNodesRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
getListNodesFromBundle(database, intent.extras!!),
|
getListNodesFromBundle(mDatabase, intent.extras!!),
|
||||||
newParent,
|
newParent,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
AfterActionNodesRunnable())
|
AfterActionNodesRunnable())
|
||||||
@@ -513,10 +637,9 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(ENTRIES_ID_KEY)
|
&& intent.hasExtra(ENTRIES_ID_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
DeleteNodesRunnable(this,
|
DeleteNodesRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
getListNodesFromBundle(database, intent.extras!!),
|
getListNodesFromBundle(mDatabase, intent.extras!!),
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||||
AfterActionNodesRunnable())
|
AfterActionNodesRunnable())
|
||||||
} else {
|
} else {
|
||||||
@@ -529,12 +652,11 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(ENTRY_HISTORY_POSITION_KEY)
|
&& intent.hasExtra(ENTRY_HISTORY_POSITION_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
|
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
|
||||||
|
|
||||||
database.getEntryById(entryId)?.let { mainEntry ->
|
mDatabase.getEntryById(entryId)?.let { mainEntry ->
|
||||||
RestoreEntryHistoryDatabaseRunnable(this,
|
RestoreEntryHistoryDatabaseRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
mainEntry,
|
mainEntry,
|
||||||
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
||||||
@@ -549,12 +671,11 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
&& intent.hasExtra(ENTRY_HISTORY_POSITION_KEY)
|
&& intent.hasExtra(ENTRY_HISTORY_POSITION_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val database = Database.getInstance()
|
|
||||||
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
|
val entryId: NodeId<UUID> = intent.getParcelableExtra(ENTRY_ID_KEY) ?: return null
|
||||||
|
|
||||||
database.getEntryById(entryId)?.let { mainEntry ->
|
mDatabase.getEntryById(entryId)?.let { mainEntry ->
|
||||||
DeleteEntryHistoryDatabaseRunnable(this,
|
DeleteEntryHistoryDatabaseRunnable(this,
|
||||||
database,
|
mDatabase,
|
||||||
mainEntry,
|
mainEntry,
|
||||||
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
||||||
@@ -577,7 +698,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
return UpdateCompressionBinariesDatabaseRunnable(this,
|
return UpdateCompressionBinariesDatabaseRunnable(this,
|
||||||
Database.getInstance(),
|
mDatabase,
|
||||||
oldElement,
|
oldElement,
|
||||||
newElement,
|
newElement,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
@@ -591,10 +712,26 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildDatabaseRemoveUnlinkedDataActionTask(intent: Intent): ActionRunnable? {
|
||||||
|
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||||
|
|
||||||
|
return RemoveUnlinkedDataDatabaseRunnable(this,
|
||||||
|
mDatabase,
|
||||||
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
|
).apply {
|
||||||
|
mAfterSaveDatabase = { result ->
|
||||||
|
result.data = intent.extras
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildDatabaseUpdateElementActionTask(intent: Intent): ActionRunnable? {
|
private fun buildDatabaseUpdateElementActionTask(intent: Intent): ActionRunnable? {
|
||||||
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||||
return SaveDatabaseRunnable(this,
|
return SaveDatabaseRunnable(this,
|
||||||
Database.getInstance(),
|
mDatabase,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
).apply {
|
).apply {
|
||||||
mAfterSaveDatabase = { result ->
|
mAfterSaveDatabase = { result ->
|
||||||
@@ -612,7 +749,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
private fun buildDatabaseSave(intent: Intent): ActionRunnable? {
|
private fun buildDatabaseSave(intent: Intent): ActionRunnable? {
|
||||||
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||||
SaveDatabaseRunnable(this,
|
SaveDatabaseRunnable(this,
|
||||||
Database.getInstance(),
|
mDatabase,
|
||||||
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -623,10 +760,6 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
|
|
||||||
private val TAG = DatabaseTaskNotificationService::class.java.name
|
private val TAG = DatabaseTaskNotificationService::class.java.name
|
||||||
|
|
||||||
const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
|
|
||||||
const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"
|
|
||||||
const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY"
|
|
||||||
|
|
||||||
const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
|
const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
|
||||||
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
|
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
|
||||||
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"
|
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"
|
||||||
@@ -644,6 +777,7 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
const val ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK = "ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK"
|
const val ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK = "ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK"
|
||||||
const val ACTION_DATABASE_UPDATE_COLOR_TASK = "ACTION_DATABASE_UPDATE_COLOR_TASK"
|
const val ACTION_DATABASE_UPDATE_COLOR_TASK = "ACTION_DATABASE_UPDATE_COLOR_TASK"
|
||||||
const val ACTION_DATABASE_UPDATE_COMPRESSION_TASK = "ACTION_DATABASE_UPDATE_COMPRESSION_TASK"
|
const val ACTION_DATABASE_UPDATE_COMPRESSION_TASK = "ACTION_DATABASE_UPDATE_COMPRESSION_TASK"
|
||||||
|
const val ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK = "ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK"
|
||||||
const val ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK = "ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK"
|
const val ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK = "ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK"
|
||||||
const val ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK = "ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK"
|
const val ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK = "ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK"
|
||||||
const val ACTION_DATABASE_UPDATE_ENCRYPTION_TASK = "ACTION_DATABASE_UPDATE_ENCRYPTION_TASK"
|
const val ACTION_DATABASE_UPDATE_ENCRYPTION_TASK = "ACTION_DATABASE_UPDATE_ENCRYPTION_TASK"
|
||||||
@@ -652,12 +786,17 @@ class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdat
|
|||||||
const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK"
|
const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK"
|
||||||
const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK"
|
const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK"
|
||||||
const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE"
|
const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE"
|
||||||
|
const val ACTION_DATABASE_CLOSE = "ACTION_DATABASE_CLOSE"
|
||||||
|
|
||||||
|
const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
|
||||||
|
const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"
|
||||||
|
const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY"
|
||||||
|
|
||||||
const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||||
const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
|
const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
|
||||||
const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
|
const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
|
||||||
const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
|
const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
|
||||||
const val KEY_FILE_KEY = "KEY_FILE_KEY"
|
const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
|
||||||
const val READ_ONLY_KEY = "READ_ONLY_KEY"
|
const val READ_ONLY_KEY = "READ_ONLY_KEY"
|
||||||
const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY"
|
const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY"
|
||||||
const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY"
|
const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.notifications
|
package com.kunzisoft.keepass.notifications
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.LockReceiver
|
import com.kunzisoft.keepass.utils.LockReceiver
|
||||||
import com.kunzisoft.keepass.utils.registerLockReceiver
|
import com.kunzisoft.keepass.utils.registerLockReceiver
|
||||||
import com.kunzisoft.keepass.utils.unregisterLockReceiver
|
import com.kunzisoft.keepass.utils.unregisterLockReceiver
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ object OtpEntryFields {
|
|||||||
* Build new generated fields in a new list from [fieldsToParse] in parameter,
|
* Build new generated fields in a new list from [fieldsToParse] in parameter,
|
||||||
* Remove parameters fields use to generate auto fields
|
* Remove parameters fields use to generate auto fields
|
||||||
*/
|
*/
|
||||||
fun generateAutoFields(fieldsToParse: MutableList<Field>): MutableList<Field> {
|
fun generateAutoFields(fieldsToParse: List<Field>): MutableList<Field> {
|
||||||
val newCustomFields: MutableList<Field> = ArrayList(fieldsToParse)
|
val newCustomFields: MutableList<Field> = ArrayList(fieldsToParse)
|
||||||
// Remove parameter fields
|
// Remove parameter fields
|
||||||
val otpField = Field(OTP_FIELD)
|
val otpField = Field(OTP_FIELD)
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mCallback = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
findPreference<Preference>(getString(R.string.database_version_key))
|
findPreference<Preference>(getString(R.string.database_version_key))
|
||||||
?.summary = mDatabase.version
|
?.summary = mDatabase.version
|
||||||
|
|
||||||
val dbCompressionPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_compression_key))
|
val dbCompressionPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_data_key))
|
||||||
|
|
||||||
// Database compression
|
// Database compression
|
||||||
dbDataCompressionPref = findPreference(getString(R.string.database_data_compression_key))
|
dbDataCompressionPref = findPreference(getString(R.string.database_data_compression_key))
|
||||||
@@ -150,7 +150,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
recycleBinGroupPref = findPreference(getString(R.string.recycle_bin_group_key))
|
recycleBinGroupPref = findPreference(getString(R.string.recycle_bin_group_key))
|
||||||
|
|
||||||
// Recycle bin
|
// Recycle bin
|
||||||
if (mDatabase.allowRecycleBin) {
|
if (mDatabase.allowConfigurableRecycleBin) {
|
||||||
val recycleBinEnablePref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_enable_key))
|
val recycleBinEnablePref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_enable_key))
|
||||||
recycleBinEnablePref?.apply {
|
recycleBinEnablePref?.apply {
|
||||||
isChecked = mDatabase.isRecycleBinEnabled
|
isChecked = mDatabase.isRecycleBinEnabled
|
||||||
@@ -166,7 +166,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
refreshRecycleBinGroup()
|
refreshRecycleBinGroup()
|
||||||
// Save the database if not in readonly mode
|
// Save the database if not in readonly mode
|
||||||
(context as SettingsActivity?)?.
|
(context as SettingsActivity?)?.
|
||||||
mProgressDialogThread?.startDatabaseSave(mDatabaseAutoSaveEnabled)
|
mProgressDatabaseTaskProvider?.startDatabaseSave(mDatabaseAutoSaveEnabled)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -482,6 +482,9 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
getString(R.string.database_data_compression_key) -> {
|
getString(R.string.database_data_compression_key) -> {
|
||||||
dialogFragment = DatabaseDataCompressionPreferenceDialogFragmentCompat.newInstance(preference.key)
|
dialogFragment = DatabaseDataCompressionPreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||||
}
|
}
|
||||||
|
getString(R.string.database_data_remove_unlinked_attachments_key) -> {
|
||||||
|
dialogFragment = DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||||
|
}
|
||||||
getString(R.string.max_history_items_key) -> {
|
getString(R.string.max_history_items_key) -> {
|
||||||
dialogFragment = MaxHistoryItemsPreferenceDialogFragmentCompat.newInstance(preference.key)
|
dialogFragment = MaxHistoryItemsPreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||||
}
|
}
|
||||||
@@ -546,7 +549,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
|||||||
|
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
settingActivity?.mProgressDialogThread?.startDatabaseSave(!mDatabaseReadOnly)
|
settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseSave(!mDatabaseReadOnly)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -231,12 +231,6 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.biometric_auto_open_prompt_default))
|
context.resources.getBoolean(R.bool.biometric_auto_open_prompt_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isFullFilePathEnable(context: Context): Boolean {
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
return prefs.getBoolean(context.getString(R.string.full_file_path_enable_key),
|
|
||||||
context.resources.getBoolean(R.bool.full_file_path_enable_default))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getListSort(context: Context): SortNodeEnum {
|
fun getListSort(context: Context): SortNodeEnum {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
prefs.getString(context.getString(R.string.sort_node_key),
|
prefs.getString(context.getString(R.string.sort_node_key),
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ import com.kunzisoft.keepass.view.showActionError
|
|||||||
open class SettingsActivity
|
open class SettingsActivity
|
||||||
: LockingActivity(),
|
: LockingActivity(),
|
||||||
MainPreferenceFragment.Callback,
|
MainPreferenceFragment.Callback,
|
||||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
AssignMasterKeyDialogFragment.AssignPasswordDialogListener,
|
||||||
|
PasswordEncodingDialogFragment.Listener {
|
||||||
|
|
||||||
private var backupManager: BackupManager? = null
|
private var backupManager: BackupManager? = null
|
||||||
|
|
||||||
@@ -50,23 +51,6 @@ open class SettingsActivity
|
|||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val SHOW_LOCK = "SHOW_LOCK"
|
|
||||||
private const val TAG_NESTED = "TAG_NESTED"
|
|
||||||
|
|
||||||
fun launch(activity: Activity, readOnly: Boolean, timeoutEnable: Boolean) {
|
|
||||||
val intent = Intent(activity, SettingsActivity::class.java)
|
|
||||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
|
||||||
intent.putExtra(TIMEOUT_ENABLE_KEY, timeoutEnable)
|
|
||||||
if (!timeoutEnable) {
|
|
||||||
activity.startActivity(intent)
|
|
||||||
} else if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
|
||||||
activity.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the main fragment to show in first
|
* Retrieve the main fragment to show in first
|
||||||
* @return The main fragment
|
* @return The main fragment
|
||||||
@@ -83,7 +67,11 @@ open class SettingsActivity
|
|||||||
|
|
||||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||||
toolbar = findViewById(R.id.toolbar)
|
toolbar = findViewById(R.id.toolbar)
|
||||||
toolbar?.setTitle(R.string.settings)
|
|
||||||
|
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
|
||||||
|
toolbar?.setTitle(R.string.settings)
|
||||||
|
else
|
||||||
|
toolbar?.title = savedInstanceState?.getString(TITLE_KEY)
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
@@ -105,7 +93,7 @@ open class SettingsActivity
|
|||||||
|
|
||||||
backupManager = BackupManager(this)
|
backupManager = BackupManager(this)
|
||||||
|
|
||||||
mProgressDialogThread?.onActionFinish = { actionTask, result ->
|
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||||
// Call result in fragment
|
// Call result in fragment
|
||||||
(supportFragmentManager
|
(supportFragmentManager
|
||||||
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
|
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
|
||||||
@@ -128,6 +116,22 @@ open class SettingsActivity
|
|||||||
super.onStop()
|
super.onStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||||
|
masterPasswordChecked: Boolean,
|
||||||
|
masterPassword: String?,
|
||||||
|
keyFileChecked: Boolean,
|
||||||
|
keyFile: Uri?) {
|
||||||
|
databaseUri?.let {
|
||||||
|
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
|
||||||
|
databaseUri,
|
||||||
|
masterPasswordChecked,
|
||||||
|
masterPassword,
|
||||||
|
keyFileChecked,
|
||||||
|
keyFile
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean,
|
override fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean,
|
||||||
masterPassword: String?,
|
masterPassword: String?,
|
||||||
keyFileChecked: Boolean,
|
keyFileChecked: Boolean,
|
||||||
@@ -136,7 +140,7 @@ open class SettingsActivity
|
|||||||
database.fileUri?.let { databaseUri ->
|
database.fileUri?.let { databaseUri ->
|
||||||
// Show the progress dialog now or after dialog confirmation
|
// Show the progress dialog now or after dialog confirmation
|
||||||
if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) {
|
if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) {
|
||||||
mProgressDialogThread?.startDatabaseAssignPassword(
|
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
masterPasswordChecked,
|
masterPasswordChecked,
|
||||||
masterPassword,
|
masterPassword,
|
||||||
@@ -144,18 +148,12 @@ open class SettingsActivity
|
|||||||
keyFile
|
keyFile
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
PasswordEncodingDialogFragment().apply {
|
PasswordEncodingDialogFragment.getInstance(databaseUri,
|
||||||
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
|
masterPasswordChecked,
|
||||||
mProgressDialogThread?.startDatabaseAssignPassword(
|
masterPassword,
|
||||||
databaseUri,
|
keyFileChecked,
|
||||||
masterPasswordChecked,
|
keyFile
|
||||||
masterPassword,
|
).show(supportFragmentManager, "passwordEncodingTag")
|
||||||
keyFileChecked,
|
|
||||||
keyFile
|
|
||||||
)
|
|
||||||
}
|
|
||||||
show(supportFragmentManager, "passwordEncodingTag")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,8 +162,7 @@ open class SettingsActivity
|
|||||||
override fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean,
|
override fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean,
|
||||||
masterPassword: String?,
|
masterPassword: String?,
|
||||||
keyFileChecked: Boolean,
|
keyFileChecked: Boolean,
|
||||||
keyFile: Uri?) {
|
keyFile: Uri?) {}
|
||||||
}
|
|
||||||
|
|
||||||
private fun hideOrShowLockButton(key: NestedSettingsFragment.Screen) {
|
private fun hideOrShowLockButton(key: NestedSettingsFragment.Screen) {
|
||||||
if (PreferencesUtil.showLockDatabaseButton(this)) {
|
if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
@@ -220,5 +217,24 @@ open class SettingsActivity
|
|||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
outState.putBoolean(SHOW_LOCK, lockView?.visibility == View.VISIBLE)
|
outState.putBoolean(SHOW_LOCK, lockView?.visibility == View.VISIBLE)
|
||||||
|
outState.putString(TITLE_KEY, toolbar?.title?.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val SHOW_LOCK = "SHOW_LOCK"
|
||||||
|
private const val TITLE_KEY = "TITLE_KEY"
|
||||||
|
private const val TAG_NESTED = "TAG_NESTED"
|
||||||
|
|
||||||
|
fun launch(activity: Activity, readOnly: Boolean, timeoutEnable: Boolean) {
|
||||||
|
val intent = Intent(activity, SettingsActivity::class.java)
|
||||||
|
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||||
|
intent.putExtra(TIMEOUT_ENABLE_KEY, timeoutEnable)
|
||||||
|
if (!timeoutEnable) {
|
||||||
|
activity.startActivity(intent)
|
||||||
|
} else if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.settings.preference
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.preference.DialogPreference
|
||||||
|
import android.util.AttributeSet
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
open class TextPreference @JvmOverloads constructor(context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
|
||||||
|
defStyleRes: Int = defStyleAttr)
|
||||||
|
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
|
|
||||||
|
override fun getDialogLayoutResource(): Int {
|
||||||
|
return R.layout.pref_dialog_text
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,7 +86,7 @@ class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialog
|
|||||||
}
|
}
|
||||||
val oldColor = database.customColor
|
val oldColor = database.customColor
|
||||||
database.customColor = newColor
|
database.customColor = newColor
|
||||||
mProgressDialogThread?.startDatabaseSaveColor(oldColor, newColor, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveColor(oldColor, newColor, mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
onDialogClosed(true)
|
onDialogClosed(true)
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class DatabaseDataCompressionPreferenceDialogFragmentCompat
|
|||||||
database.compressionAlgorithm = newCompression
|
database.compressionAlgorithm = newCompression
|
||||||
|
|
||||||
if (oldCompression != null && newCompression != null)
|
if (oldCompression != null && newCompression != null)
|
||||||
mProgressDialogThread?.startDatabaseSaveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveCompression(oldCompression, newCompression, mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePrefer
|
|||||||
val newDefaultUsername = inputText
|
val newDefaultUsername = inputText
|
||||||
val oldDefaultUsername = database.defaultUsername
|
val oldDefaultUsername = database.defaultUsername
|
||||||
database.defaultUsername = newDefaultUsername
|
database.defaultUsername = newDefaultUsername
|
||||||
mProgressDialogThread?.startDatabaseSaveDefaultUsername(oldDefaultUsername, newDefaultUsername, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(oldDefaultUsername, newDefaultUsername, mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreference
|
|||||||
val newDescription = inputText
|
val newDescription = inputText
|
||||||
val oldDescription = database.description
|
val oldDescription = database.description
|
||||||
database.description = newDescription
|
database.description = newDescription
|
||||||
mProgressDialogThread?.startDatabaseSaveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveDescription(oldDescription, newDescription, mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
|||||||
database.encryptionAlgorithm = newAlgorithm
|
database.encryptionAlgorithm = newAlgorithm
|
||||||
|
|
||||||
if (oldAlgorithm != null && newAlgorithm != null)
|
if (oldAlgorithm != null && newAlgorithm != null)
|
||||||
mProgressDialogThread?.startDatabaseSaveEncryption(oldAlgorithm, newAlgorithm, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveEncryption(oldAlgorithm, newAlgorithm, mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
|
|||||||
val oldKdfEngine = database.kdfEngine
|
val oldKdfEngine = database.kdfEngine
|
||||||
if (newKdfEngine != null && oldKdfEngine != null) {
|
if (newKdfEngine != null && oldKdfEngine != null) {
|
||||||
database.kdfEngine = newKdfEngine
|
database.kdfEngine = newKdfEngine
|
||||||
mProgressDialogThread?.startDatabaseSaveKeyDerivation(oldKdfEngine, newKdfEngine, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(oldKdfEngine, newKdfEngine, mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogF
|
|||||||
val newName = inputText
|
val newName = inputText
|
||||||
val oldName = database.name
|
val oldName = database.name
|
||||||
database.name = newName
|
database.name = newName
|
||||||
mProgressDialogThread?.startDatabaseSaveName(oldName, newName, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveName(oldName, newName, mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.view.View
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
class DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||||
|
|
||||||
|
override fun onBindDialogView(view: View) {
|
||||||
|
super.onBindDialogView(view)
|
||||||
|
|
||||||
|
explanationText = SpannableStringBuilder().apply {
|
||||||
|
append(getString(R.string.warning_remove_unlinked_attachment))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_remove_data))
|
||||||
|
}.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDialogClosed(positiveResult: Boolean) {
|
||||||
|
database?.let { _ ->
|
||||||
|
if (positiveResult) {
|
||||||
|
mProgressDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(mDatabaseAutoSaveEnable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun newInstance(key: String): DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat {
|
||||||
|
val fragment = DatabaseRemoveUnlinkedDataPreferenceDialogFragmentCompat()
|
||||||
|
val bundle = Bundle(1)
|
||||||
|
bundle.putString(ARG_KEY, key)
|
||||||
|
fragment.arguments = bundle
|
||||||
|
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.settings.SettingsActivity
|
import com.kunzisoft.keepass.settings.SettingsActivity
|
||||||
@@ -30,7 +30,7 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat : InputPreferenceDialo
|
|||||||
|
|
||||||
protected var database: Database? = null
|
protected var database: Database? = null
|
||||||
protected var mDatabaseAutoSaveEnable = true
|
protected var mDatabaseAutoSaveEnable = true
|
||||||
protected var mProgressDialogThread: ProgressDialogThread? = null
|
protected var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -42,12 +42,17 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat : InputPreferenceDialo
|
|||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
// Attach dialog thread to start action
|
// Attach dialog thread to start action
|
||||||
if (context is SettingsActivity) {
|
if (context is SettingsActivity) {
|
||||||
mProgressDialogThread = context.mProgressDialogThread
|
mProgressDatabaseTaskProvider = context.mProgressDatabaseTaskProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mDatabaseAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(context)
|
this.mDatabaseAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mProgressDatabaseTaskProvider = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "DbSavePrefDialog"
|
private const val TAG = "DbSavePrefDialog"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class MaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDial
|
|||||||
// Remove all history items
|
// Remove all history items
|
||||||
database.removeOldestHistoryForEachEntry()
|
database.removeOldestHistoryForEachEntry()
|
||||||
|
|
||||||
mProgressDialogThread?.startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems, maxHistoryItems, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems, maxHistoryItems, mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialo
|
|||||||
val oldMaxHistorySize = database.historyMaxSize
|
val oldMaxHistorySize = database.historyMaxSize
|
||||||
database.historyMaxSize = maxHistorySize
|
database.historyMaxSize = maxHistorySize
|
||||||
|
|
||||||
mProgressDialogThread?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, maxHistorySize, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, maxHistorySize, mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFr
|
|||||||
val oldMemoryUsage = database.memoryUsage
|
val oldMemoryUsage = database.memoryUsage
|
||||||
database.memoryUsage = memoryUsage
|
database.memoryUsage = memoryUsage
|
||||||
|
|
||||||
mProgressDialogThread?.startDatabaseSaveMemoryUsage(oldMemoryUsage, memoryUsage, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(oldMemoryUsage, memoryUsage, mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class ParallelismPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFr
|
|||||||
val oldParallelism = database.parallelism
|
val oldParallelism = database.parallelism
|
||||||
database.parallelism = parallelism
|
database.parallelism = parallelism
|
||||||
|
|
||||||
mProgressDialogThread?.startDatabaseSaveParallelism(
|
mProgressDatabaseTaskProvider?.startDatabaseSaveParallelism(
|
||||||
oldParallelism,
|
oldParallelism,
|
||||||
parallelism,
|
parallelism,
|
||||||
mDatabaseAutoSaveEnable)
|
mDatabaseAutoSaveEnable)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class RoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmen
|
|||||||
database.numberKeyEncryptionRounds = Long.MAX_VALUE
|
database.numberKeyEncryptionRounds = Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
|
|
||||||
mProgressDialogThread?.startDatabaseSaveIterations(oldRounds, rounds, mDatabaseAutoSaveEnable)
|
mProgressDatabaseTaskProvider?.startDatabaseSaveIterations(oldRounds, rounds, mDatabaseAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.stream
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.io.RandomAccessFile
|
|
||||||
|
|
||||||
class RandomFileOutputStream internal constructor(private val mFile: RandomAccessFile) : OutputStream() {
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun close() {
|
|
||||||
super.close()
|
|
||||||
|
|
||||||
mFile.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun write(buffer: ByteArray, offset: Int, count: Int) {
|
|
||||||
super.write(buffer, offset, count)
|
|
||||||
|
|
||||||
mFile.write(buffer, offset, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun write(buffer: ByteArray) {
|
|
||||||
super.write(buffer)
|
|
||||||
|
|
||||||
mFile.write(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun write(oneByte: Int) {
|
|
||||||
mFile.write(oneByte)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun seek(pos: Long) {
|
|
||||||
mFile.seek(pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.tasks
|
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.AsyncTask
|
|
||||||
import android.util.Log
|
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
|
||||||
import com.kunzisoft.keepass.model.EntryAttachment
|
|
||||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
|
||||||
|
|
||||||
class AttachmentFileAsyncTask(
|
|
||||||
private val fileUri: Uri,
|
|
||||||
private val attachmentNotification: AttachmentFileNotificationService.AttachmentNotification,
|
|
||||||
private val contentResolver: ContentResolver)
|
|
||||||
: AsyncTask<Void, Int, Boolean>() {
|
|
||||||
|
|
||||||
private val updateMinFrequency = 1000
|
|
||||||
private var previousSaveTime = System.currentTimeMillis()
|
|
||||||
var onUpdate: ((Uri, EntryAttachment, Int)->Unit)? = null
|
|
||||||
|
|
||||||
override fun onPreExecute() {
|
|
||||||
super.onPreExecute()
|
|
||||||
attachmentNotification.attachmentTask = this
|
|
||||||
attachmentNotification.entryAttachment.apply {
|
|
||||||
downloadState = AttachmentState.START
|
|
||||||
downloadProgression = 0
|
|
||||||
}
|
|
||||||
onUpdate?.invoke(fileUri, attachmentNotification.entryAttachment, attachmentNotification.notificationId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doInBackground(vararg params: Void?): Boolean {
|
|
||||||
try {
|
|
||||||
attachmentNotification.entryAttachment.apply {
|
|
||||||
downloadState = AttachmentState.IN_PROGRESS
|
|
||||||
binaryAttachment.download(fileUri, contentResolver, 1024) { percent ->
|
|
||||||
publishProgress(percent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onProgressUpdate(vararg values: Int?) {
|
|
||||||
super.onProgressUpdate(*values)
|
|
||||||
val percent = values[0] ?: 0
|
|
||||||
|
|
||||||
val currentTime = System.currentTimeMillis()
|
|
||||||
if (previousSaveTime + updateMinFrequency < currentTime) {
|
|
||||||
attachmentNotification.entryAttachment.apply {
|
|
||||||
downloadState = AttachmentState.IN_PROGRESS
|
|
||||||
downloadProgression = percent
|
|
||||||
}
|
|
||||||
onUpdate?.invoke(fileUri, attachmentNotification.entryAttachment, attachmentNotification.notificationId)
|
|
||||||
Log.d(TAG, "Download file $fileUri : $percent%")
|
|
||||||
previousSaveTime = currentTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostExecute(result: Boolean) {
|
|
||||||
super.onPostExecute(result)
|
|
||||||
attachmentNotification.attachmentTask = null
|
|
||||||
attachmentNotification.entryAttachment.apply {
|
|
||||||
downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR
|
|
||||||
downloadProgression = 100
|
|
||||||
}
|
|
||||||
onUpdate?.invoke(fileUri, attachmentNotification.entryAttachment, attachmentNotification.notificationId)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG = AttachmentFileAsyncTask::class.java.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,9 +28,13 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.model.EntryAttachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD
|
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD
|
||||||
|
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD
|
||||||
|
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_REMOVE
|
||||||
|
|
||||||
class AttachmentFileBinderManager(private val activity: FragmentActivity) {
|
class AttachmentFileBinderManager(private val activity: FragmentActivity) {
|
||||||
|
|
||||||
@@ -43,8 +47,18 @@ class AttachmentFileBinderManager(private val activity: FragmentActivity) {
|
|||||||
private var mServiceConnection: ServiceConnection? = null
|
private var mServiceConnection: ServiceConnection? = null
|
||||||
|
|
||||||
private val mActionTaskListener = object: AttachmentFileNotificationService.ActionTaskListener {
|
private val mActionTaskListener = object: AttachmentFileNotificationService.ActionTaskListener {
|
||||||
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
onActionTaskListener?.onAttachmentProgress(fileUri, attachment)
|
onActionTaskListener?.let {
|
||||||
|
it.onAttachmentAction(fileUri, entryAttachmentState)
|
||||||
|
when (entryAttachmentState.downloadState) {
|
||||||
|
AttachmentState.COMPLETE,
|
||||||
|
AttachmentState.ERROR -> {
|
||||||
|
// Finish the action when capture by activity
|
||||||
|
consumeAttachmentAction(entryAttachmentState)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,22 +99,38 @@ class AttachmentFileBinderManager(private val activity: FragmentActivity) {
|
|||||||
mServiceConnection = null
|
mServiceConnection = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun consumeAttachmentAction(attachment: EntryAttachmentState) {
|
||||||
|
mBinder?.getService()?.removeAttachmentAction(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun start(bundle: Bundle? = null, actionTask: String) {
|
private fun start(bundle: Bundle? = null, actionTask: String) {
|
||||||
activity.stopService(mIntentTask)
|
|
||||||
if (bundle != null)
|
if (bundle != null)
|
||||||
mIntentTask.putExtras(bundle)
|
mIntentTask.putExtras(bundle)
|
||||||
activity.runOnUiThread {
|
mIntentTask.action = actionTask
|
||||||
mIntentTask.action = actionTask
|
activity.startService(mIntentTask)
|
||||||
activity.startService(mIntentTask)
|
}
|
||||||
}
|
|
||||||
|
fun startUploadAttachment(uploadFileUri: Uri,
|
||||||
|
attachment: Attachment) {
|
||||||
|
start(Bundle().apply {
|
||||||
|
putParcelable(AttachmentFileNotificationService.FILE_URI_KEY, uploadFileUri)
|
||||||
|
putParcelable(AttachmentFileNotificationService.ATTACHMENT_KEY, attachment)
|
||||||
|
}, ACTION_ATTACHMENT_FILE_START_UPLOAD)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDownloadAttachment(downloadFileUri: Uri,
|
fun startDownloadAttachment(downloadFileUri: Uri,
|
||||||
entryAttachment: EntryAttachment) {
|
attachment: Attachment) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(AttachmentFileNotificationService.DOWNLOAD_FILE_URI_KEY, downloadFileUri)
|
putParcelable(AttachmentFileNotificationService.FILE_URI_KEY, downloadFileUri)
|
||||||
putParcelable(AttachmentFileNotificationService.ATTACHMENT_KEY, entryAttachment)
|
putParcelable(AttachmentFileNotificationService.ATTACHMENT_KEY, attachment)
|
||||||
}, ACTION_ATTACHMENT_FILE_START_DOWNLOAD)
|
}, ACTION_ATTACHMENT_FILE_START_DOWNLOAD)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeBinaryAttachment(attachment: Attachment) {
|
||||||
|
start(Bundle().apply {
|
||||||
|
putParcelable(AttachmentFileNotificationService.ATTACHMENT_KEY, attachment)
|
||||||
|
}, ACTION_ATTACHMENT_REMOVE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.utils
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.LinkedHashMap
|
||||||
|
|
||||||
object ParcelableUtil {
|
object ParcelableUtil {
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ object ParcelableUtil {
|
|||||||
|
|
||||||
// For writing map with string key to a Parcel
|
// For writing map with string key to a Parcel
|
||||||
fun <V : Parcelable> writeStringParcelableMap(
|
fun <V : Parcelable> writeStringParcelableMap(
|
||||||
parcel: Parcel, flags: Int, map: Map<String, V>) {
|
parcel: Parcel, flags: Int, map: LinkedHashMap<String, V>) {
|
||||||
parcel.writeInt(map.size)
|
parcel.writeInt(map.size)
|
||||||
for ((key, value) in map) {
|
for ((key, value) in map) {
|
||||||
parcel.writeString(key)
|
parcel.writeString(key)
|
||||||
@@ -59,11 +60,20 @@ object ParcelableUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For writing map with string key and Int value to a Parcel
|
||||||
|
fun writeStringIntMap(parcel: Parcel, map: LinkedHashMap<String, Int>) {
|
||||||
|
parcel.writeInt(map.size)
|
||||||
|
for ((key, value) in map) {
|
||||||
|
parcel.writeString(key)
|
||||||
|
parcel.writeInt(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For reading map with string key from a Parcel
|
// For reading map with string key from a Parcel
|
||||||
fun <V : Parcelable> readStringParcelableMap(
|
fun <V : Parcelable> readStringParcelableMap(
|
||||||
parcel: Parcel, vClass: Class<V>): HashMap<String, V> {
|
parcel: Parcel, vClass: Class<V>): LinkedHashMap<String, V> {
|
||||||
val size = parcel.readInt()
|
val size = parcel.readInt()
|
||||||
val map = HashMap<String, V>(size)
|
val map = LinkedHashMap<String, V>(size)
|
||||||
for (i in 0 until size) {
|
for (i in 0 until size) {
|
||||||
val key: String? = parcel.readString()
|
val key: String? = parcel.readString()
|
||||||
val value: V? = vClass.cast(parcel.readParcelable(vClass.classLoader))
|
val value: V? = vClass.cast(parcel.readParcelable(vClass.classLoader))
|
||||||
@@ -73,9 +83,22 @@ object ParcelableUtil {
|
|||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For reading map with string key and Int value from a Parcel
|
||||||
|
fun readStringIntMap(parcel: Parcel): LinkedHashMap<String, Int> {
|
||||||
|
val size = parcel.readInt()
|
||||||
|
val map = LinkedHashMap<String, Int>(size)
|
||||||
|
for (i in 0 until size) {
|
||||||
|
val key: String? = parcel.readString()
|
||||||
|
val value: Int? = parcel.readInt()
|
||||||
|
if (key != null && value != null)
|
||||||
|
map[key] = value
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// For writing map with string key and string value to a Parcel
|
// For writing map with string key and string value to a Parcel
|
||||||
fun writeStringParcelableMap(dest: Parcel, map: Map<String, String>) {
|
fun writeStringParcelableMap(dest: Parcel, map: LinkedHashMap<String, String>) {
|
||||||
dest.writeInt(map.size)
|
dest.writeInt(map.size)
|
||||||
for ((key, value) in map) {
|
for ((key, value) in map) {
|
||||||
dest.writeString(key)
|
dest.writeString(key)
|
||||||
@@ -84,9 +107,9 @@ object ParcelableUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For reading map with string key and string value from a Parcel
|
// For reading map with string key and string value from a Parcel
|
||||||
fun readStringParcelableMap(parcel: Parcel): HashMap<String, String> {
|
fun readStringParcelableMap(parcel: Parcel): LinkedHashMap<String, String> {
|
||||||
val size = parcel.readInt()
|
val size = parcel.readInt()
|
||||||
val map = HashMap<String, String>(size)
|
val map = LinkedHashMap<String, String>(size)
|
||||||
for (i in 0 until size) {
|
for (i in 0 until size) {
|
||||||
val key: String? = parcel.readString()
|
val key: String? = parcel.readString()
|
||||||
val value: String? = parcel.readString()
|
val value: String? = parcel.readString()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user