mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
398 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82450c0ae8 | ||
|
|
8dd6c33901 | ||
|
|
f920d40db5 | ||
|
|
19be6c1acc | ||
|
|
7d9d8ad0e4 | ||
|
|
85f8237d5f | ||
|
|
c542894734 | ||
|
|
d348987077 | ||
|
|
3718610595 | ||
|
|
9c36ec0623 | ||
|
|
c6917b5d74 | ||
|
|
4eaa179789 | ||
|
|
9008cd4549 | ||
|
|
cc3204453e | ||
|
|
5ef8d3b7b9 | ||
|
|
2a9de97a19 | ||
|
|
9cecfed417 | ||
|
|
319715918a | ||
|
|
a3bf6e8b6d | ||
|
|
c4062658ce | ||
|
|
01a5de413e | ||
|
|
e4c22b1f29 | ||
|
|
b10e60126f | ||
|
|
ef1f27f421 | ||
|
|
0ed208675c | ||
|
|
00f7a0a194 | ||
|
|
935d4f4a64 | ||
|
|
dc4d88260d | ||
|
|
18934601da | ||
|
|
4ea811aeda | ||
|
|
f8fdecdc8f | ||
|
|
5467c61137 | ||
|
|
9c72b4cc56 | ||
|
|
9102217bc3 | ||
|
|
0e8fd7b2c4 | ||
|
|
a06ea8fe55 | ||
|
|
31eb0fb48a | ||
|
|
d6a012e85f | ||
|
|
11c1cc7c72 | ||
|
|
6b7acb7bd5 | ||
|
|
bdebf19d7b | ||
|
|
cb1973ffb5 | ||
|
|
c6e2342ab4 | ||
|
|
2447599364 | ||
|
|
6a2cda74f1 | ||
|
|
8385d55d69 | ||
|
|
85e3464a15 | ||
|
|
6680039de7 | ||
|
|
9935826877 | ||
|
|
b977792168 | ||
|
|
2595cf87d8 | ||
|
|
f4342f1448 | ||
|
|
84c26b7c40 | ||
|
|
1cd7940a17 | ||
|
|
9514032f25 | ||
|
|
c7d6da2373 | ||
|
|
41b822fb6c | ||
|
|
69bf098c84 | ||
|
|
b4283ed4dc | ||
|
|
0fa0cac9e6 | ||
|
|
c71ef24052 | ||
|
|
cf0f665b14 | ||
|
|
2034e3ab78 | ||
|
|
89cfeec1b3 | ||
|
|
d8ae212df0 | ||
|
|
39b817bc69 | ||
|
|
09d79d52ae | ||
|
|
5c4b98d0e9 | ||
|
|
e5d6fc0604 | ||
|
|
5e656ebfba | ||
|
|
58fb75e55d | ||
|
|
e01621e658 | ||
|
|
b4aee17f53 | ||
|
|
dc70918648 | ||
|
|
69772edfa3 | ||
|
|
dd224cab05 | ||
|
|
b62873129e | ||
|
|
5052a1f564 | ||
|
|
2c36163e7a | ||
|
|
1bf912d6f0 | ||
|
|
d290259075 | ||
|
|
1689672faf | ||
|
|
8196e05679 | ||
|
|
fe0235da43 | ||
|
|
0895a73546 | ||
|
|
f06821e35b | ||
|
|
9cce5f645f | ||
|
|
c1a46408e9 | ||
|
|
21c3ccd637 | ||
|
|
3b9b034d80 | ||
|
|
348a5c3eb7 | ||
|
|
e3adaba3b3 | ||
|
|
4c5be658c3 | ||
|
|
b6517a449b | ||
|
|
ae0b8db0b0 | ||
|
|
56f0f8a299 | ||
|
|
c9af786b79 | ||
|
|
f34f615b80 | ||
|
|
aef2ef8479 | ||
|
|
7afbc9f5a4 | ||
|
|
df51b62041 | ||
|
|
045abc54fb | ||
|
|
9b2d9683eb | ||
|
|
3b0dd4a36c | ||
|
|
5e15f82313 | ||
|
|
d841c25bd3 | ||
|
|
8d3f1fe179 | ||
|
|
130ec130cc | ||
|
|
5e7a95eac0 | ||
|
|
a8cb49d12d | ||
|
|
c179ac626a | ||
|
|
041583bf96 | ||
|
|
ed710335b3 | ||
|
|
b556581a87 | ||
|
|
77a1b7918c | ||
|
|
45149e1b28 | ||
|
|
932338a25a | ||
|
|
925509e5a0 | ||
|
|
25646fbad7 | ||
|
|
e1733512c4 | ||
|
|
8379ffe1ce | ||
|
|
c77537ecee | ||
|
|
2192d97c69 | ||
|
|
a0dc76bda8 | ||
|
|
7fe177edc6 | ||
|
|
1f5e6f1e17 | ||
|
|
bf0aa295b0 | ||
|
|
649dffc3e0 | ||
|
|
a0f5ed66e2 | ||
|
|
7df3b95c22 | ||
|
|
0756474d40 | ||
|
|
60747db945 | ||
|
|
afcfad162e | ||
|
|
63f15bdc9e | ||
|
|
3b826869e9 | ||
|
|
af0256add0 | ||
|
|
b8d8cba12c | ||
|
|
616e9a0ec2 | ||
|
|
366434cbd7 | ||
|
|
f6d4046af6 | ||
|
|
82932f002e | ||
|
|
7593a05953 | ||
|
|
3026a9e3e4 | ||
|
|
362939eab9 | ||
|
|
61d52731a5 | ||
|
|
6aecc6521c | ||
|
|
ef5829593e | ||
|
|
4a8f67093f | ||
|
|
9cbe0664f6 | ||
|
|
965d6e4e8e | ||
|
|
eefdeb0bb7 | ||
|
|
a904a51293 | ||
|
|
cce377d70d | ||
|
|
5721bca5a3 | ||
|
|
bcd5b024f0 | ||
|
|
571f257c17 | ||
|
|
3451135800 | ||
|
|
f426a78a94 | ||
|
|
3d65236e63 | ||
|
|
7b51b5005a | ||
|
|
3d9cf16960 | ||
|
|
35def53666 | ||
|
|
5c46a89ddc | ||
|
|
4e429025bf | ||
|
|
95fae11eee | ||
|
|
9a22a9fb8b | ||
|
|
f60e2e2ca6 | ||
|
|
deb9101335 | ||
|
|
407f93ac43 | ||
|
|
78c39edceb | ||
|
|
c8445fb711 | ||
|
|
7c0e7347c8 | ||
|
|
12f37d0931 | ||
|
|
9a5086d9ba | ||
|
|
3222c7e677 | ||
|
|
632e0648d4 | ||
|
|
e3198031e3 | ||
|
|
0ced9c8e26 | ||
|
|
65f4a708cd | ||
|
|
36e7b00d9a | ||
|
|
8b2c48f5ca | ||
|
|
9f7a0d4f17 | ||
|
|
fa5ae17621 | ||
|
|
7a2536c559 | ||
|
|
96d2edb641 | ||
|
|
8a2bd23c32 | ||
|
|
d3b935ea7f | ||
|
|
e53bc3b048 | ||
|
|
5f1cfc9dda | ||
|
|
43207b316f | ||
|
|
96ed4c419a | ||
|
|
840a2253e2 | ||
|
|
18db9b0a77 | ||
|
|
6c7a5292a4 | ||
|
|
bef1c74226 | ||
|
|
176ec8bace | ||
|
|
c62064002f | ||
|
|
45b7800a68 | ||
|
|
fa761ac69b | ||
|
|
cc11e98aa6 | ||
|
|
8f1c71137a | ||
|
|
8fdf2dcb7a | ||
|
|
d4cd5b73bd | ||
|
|
77975aed2a | ||
|
|
a7700ce27e | ||
|
|
726ff1a126 | ||
|
|
e24269c452 | ||
|
|
686f4656ec | ||
|
|
55fe10d2dc | ||
|
|
422984ac41 | ||
|
|
706d117d80 | ||
|
|
13f8df4e0d | ||
|
|
263d433193 | ||
|
|
c15c11f3b1 | ||
|
|
524c8ccfc5 | ||
|
|
902392ea30 | ||
|
|
bef179187f | ||
|
|
ea7221c39a | ||
|
|
edaf9f6296 | ||
|
|
0d83725b77 | ||
|
|
6ce31305c6 | ||
|
|
90935c033d | ||
|
|
b4c3f831a7 | ||
|
|
f0e25e8198 | ||
|
|
d800082621 | ||
|
|
653d3da718 | ||
|
|
0f39409386 | ||
|
|
ccae0d1a57 | ||
|
|
257992d314 | ||
|
|
5eb843b63d | ||
|
|
3929b478a7 | ||
|
|
18734ed822 | ||
|
|
876e749b31 | ||
|
|
32b8c505d9 | ||
|
|
37a0dce7c5 | ||
|
|
2332f36b56 | ||
|
|
21cc9cc026 | ||
|
|
db4d76502e | ||
|
|
1578ea7590 | ||
|
|
7405de01fe | ||
|
|
77dc5943e5 | ||
|
|
f8a2748ede | ||
|
|
a99ca00bb3 | ||
|
|
6eb80eea2f | ||
|
|
82828f7f82 | ||
|
|
23c9a5963a | ||
|
|
7595f113ec | ||
|
|
e9e5a4ee0d | ||
|
|
1947fc3e83 | ||
|
|
8844689482 | ||
|
|
0d2ba54c10 | ||
|
|
a4bb5137ea | ||
|
|
b8aea1f97a | ||
|
|
120e1893bd | ||
|
|
836df52a50 | ||
|
|
00498aaeac | ||
|
|
f4f47cff75 | ||
|
|
40dc3d45fc | ||
|
|
b89d2a6da1 | ||
|
|
55a4af9f00 | ||
|
|
719d45e75e | ||
|
|
8703684740 | ||
|
|
e95e7218f6 | ||
|
|
e9d4711978 | ||
|
|
9309506e97 | ||
|
|
00d2a80e95 | ||
|
|
c1e62b7d90 | ||
|
|
84775d36dc | ||
|
|
fc4eb11fd8 | ||
|
|
ce70ce6c76 | ||
|
|
ffd404ec1b | ||
|
|
6f943db012 | ||
|
|
12342ac426 | ||
|
|
489ddc3f56 | ||
|
|
02a266cbea | ||
|
|
bdc9facd41 | ||
|
|
7d679aac0b | ||
|
|
ae1719e795 | ||
|
|
6cea05e9f4 | ||
|
|
cda84d4e64 | ||
|
|
b3f63a85d5 | ||
|
|
196903cb12 | ||
|
|
f13c8d7884 | ||
|
|
a10fdb260d | ||
|
|
0f8b1790f3 | ||
|
|
531e345dd9 | ||
|
|
c1942759d4 | ||
|
|
eca9e573de | ||
|
|
61376f8d68 | ||
|
|
292e2c60e2 | ||
|
|
2ffaf81109 | ||
|
|
da37381678 | ||
|
|
3236bf6122 | ||
|
|
a352ae6922 | ||
|
|
f3631a6a09 | ||
|
|
0d376bd5b7 | ||
|
|
fd57bd0378 | ||
|
|
5a11882d1b | ||
|
|
0f041da548 | ||
|
|
00a23119c5 | ||
|
|
cdeb49473b | ||
|
|
b55b3731a5 | ||
|
|
4afbb688ba | ||
|
|
f289a921f1 | ||
|
|
f2165bc4c1 | ||
|
|
73b20bfe4a | ||
|
|
fbf2006e3f | ||
|
|
358b701396 | ||
|
|
d3caae3a2d | ||
|
|
300062d3ac | ||
|
|
bb0aaad383 | ||
|
|
e9aa8609f1 | ||
|
|
37cbb18626 | ||
|
|
99e76f2254 | ||
|
|
a7801e376b | ||
|
|
90000aa1fb | ||
|
|
a2a26cd058 | ||
|
|
e3cbefc5b6 | ||
|
|
c6ceb25ee8 | ||
|
|
022777888c | ||
|
|
ad5e644362 | ||
|
|
bf6cb04fe8 | ||
|
|
dbde23fb7b | ||
|
|
c9cb469d65 | ||
|
|
2e37c20e55 | ||
|
|
ade665d228 | ||
|
|
a62f0cfd3b | ||
|
|
5afbfbfd43 | ||
|
|
0ec72bb013 | ||
|
|
cdfbcd873c | ||
|
|
82f5ab1446 | ||
|
|
c97f8c31ce | ||
|
|
92e5b5e9c3 | ||
|
|
a70fca493d | ||
|
|
42c8f0c345 | ||
|
|
d3d5a1745d | ||
|
|
8392d8b684 | ||
|
|
1abd13c6e0 | ||
|
|
e126b66e19 | ||
|
|
47449d93db | ||
|
|
1985a035be | ||
|
|
cdfc7e4158 | ||
|
|
5050052710 | ||
|
|
48e39d2ffa | ||
|
|
a8d053e82a | ||
|
|
2154945c3c | ||
|
|
8d83a0a86a | ||
|
|
c196cdf405 | ||
|
|
b6a5f43176 | ||
|
|
e4c3c224d9 | ||
|
|
69bc697568 | ||
|
|
36b37b3b8f | ||
|
|
ce9931d8a3 | ||
|
|
66f5ff35d3 | ||
|
|
80c0152d46 | ||
|
|
fac727cd3d | ||
|
|
9c30183068 | ||
|
|
9c28d5c5c5 | ||
|
|
3c55e3a3f0 | ||
|
|
ba6e3b801d | ||
|
|
4064ab47ac | ||
|
|
b6005fcb56 | ||
|
|
71de526366 | ||
|
|
a3e5d8448b | ||
|
|
f65b6e5484 | ||
|
|
05d0d9e501 | ||
|
|
d107beadf2 | ||
|
|
9c2fd26579 | ||
|
|
d796ea6324 | ||
|
|
8386c9e729 | ||
|
|
00ca4524b5 | ||
|
|
f4d4853319 | ||
|
|
00678cc9ca | ||
|
|
2845486a3f | ||
|
|
a815e6447d | ||
|
|
5b71a30ee9 | ||
|
|
ac2d94420b | ||
|
|
e344cafd9b | ||
|
|
4b0d16cad1 | ||
|
|
da4d8629bd | ||
|
|
68ae3b79ab | ||
|
|
78e0336c1b | ||
|
|
e439f4d643 | ||
|
|
39d95105e1 | ||
|
|
4c99923467 | ||
|
|
38dac3803b | ||
|
|
c0d4ad2042 | ||
|
|
c669d5657a | ||
|
|
98c44fa578 | ||
|
|
839375fcf1 | ||
|
|
ed82c36628 | ||
|
|
3536629dd9 | ||
|
|
c87696696e | ||
|
|
b5ba03df4d | ||
|
|
0d37a59a5c | ||
|
|
2d528db054 | ||
|
|
80c4ba6723 | ||
|
|
2ba8702787 |
@@ -1,3 +1,12 @@
|
||||
KeePassDX(3.0.0)
|
||||
* Add / Manage dynamic templates #191
|
||||
* Manually select RecycleBin group and Templates group #191
|
||||
* Setting to display OTP Token in list #655
|
||||
* Fix timeout in dialogs #716
|
||||
* Check URI permissions #626
|
||||
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
|
||||
* Improvements #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong)
|
||||
|
||||
KeePassDX(2.10.5)
|
||||
* Increase the saving speed of database #1028
|
||||
* Fix advanced unlocking by device credential #1029
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 30
|
||||
versionCode = 83
|
||||
versionName = "2.10.5"
|
||||
versionCode = 87
|
||||
versionName = "3.0.0"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.kunzisoft.keepass.tests.template
|
||||
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateAttributeOption
|
||||
import junit.framework.TestCase
|
||||
import org.junit.Assert
|
||||
|
||||
class TemplateAttributeOptionTest: TestCase() {
|
||||
|
||||
fun testSerializeOptions() {
|
||||
val options = TemplateAttributeOption().apply {
|
||||
put("TestA", "TestB")
|
||||
put("{D", "}C")
|
||||
put("E,gyu", "15,jk")
|
||||
put("ù*:**", "78:96?545")
|
||||
}
|
||||
|
||||
val strings = TemplateAttributeOption.getStringFromOptions(options)
|
||||
val optionsAfterSerialization = TemplateAttributeOption.getOptionsFromString(strings)
|
||||
val otherString = TemplateAttributeOption.getStringFromOptions(optionsAfterSerialization)
|
||||
|
||||
Assert.assertEquals("Output not equal to input", strings, otherString)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -45,7 +45,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="stateHidden" >
|
||||
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@@ -112,8 +112,7 @@
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustPan"
|
||||
android:launchMode="singleTask">
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
<meta-data
|
||||
android:name="android.app.default_searchable"
|
||||
android:value="com.kunzisoft.keepass.search.SearchResults"
|
||||
@@ -209,7 +208,7 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikIME"
|
||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
||||
android:label="@string/keyboard_label"
|
||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||
<meta-data android:name="android.view.im"
|
||||
|
||||
@@ -30,6 +30,7 @@ package com.igreenwood.loupe
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
@@ -108,6 +109,8 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION
|
||||
// drag distance threshold in dp for swipe to dismiss
|
||||
var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP
|
||||
// on view touched
|
||||
var onViewTouchedListener: View.OnTouchListener? = null
|
||||
// on view translate listener
|
||||
var onViewTranslateListener: OnViewTranslateListener? = null
|
||||
// on scale changed
|
||||
@@ -272,7 +275,10 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
private var imageViewRef: WeakReference<ImageView> = WeakReference(imageView)
|
||||
private var containerRef: WeakReference<ViewGroup> = WeakReference(container)
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
|
||||
onViewTouchedListener?.onTouch(view, event)
|
||||
|
||||
event ?: return false
|
||||
val imageView = imageViewRef.get() ?: return false
|
||||
val container = containerRef.get() ?: return false
|
||||
|
||||
@@ -25,14 +25,13 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
|
||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||
@@ -44,9 +43,18 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class AutofillLauncherActivity : AppCompatActivity() {
|
||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
// Retrieve selection mode
|
||||
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||
@@ -57,10 +65,11 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
||||
manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false)
|
||||
}
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchSelection(searchInfo)
|
||||
launchSelection(database, searchInfo)
|
||||
}
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
@@ -69,7 +78,7 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchRegistration(searchInfo, registerInfo)
|
||||
launchRegistration(database, searchInfo, registerInfo)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
@@ -79,11 +88,10 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun launchSelection(searchInfo: SearchInfo) {
|
||||
private fun launchSelection(database: Database?,
|
||||
searchInfo: SearchInfo) {
|
||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
|
||||
|
||||
@@ -98,24 +106,22 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
} else {
|
||||
val database = Database.getInstance()
|
||||
val readOnly = database.isReadOnly
|
||||
// If database is open
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
Database.getInstance(),
|
||||
database,
|
||||
searchInfo,
|
||||
{ items ->
|
||||
{ openedDatabase, items ->
|
||||
// Items found
|
||||
AutofillHelper.buildResponseAndSetResult(this, items)
|
||||
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
||||
finish()
|
||||
},
|
||||
{
|
||||
{ openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForAutofillResult(this,
|
||||
readOnly,
|
||||
autofillComponent,
|
||||
searchInfo,
|
||||
false)
|
||||
openedDatabase,
|
||||
autofillComponent,
|
||||
searchInfo,
|
||||
false)
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
@@ -127,7 +133,9 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) {
|
||||
private fun launchRegistration(database: Database?,
|
||||
searchInfo: SearchInfo,
|
||||
registerInfo: RegisterInfo?) {
|
||||
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||
PreferencesUtil.applicationIdBlocklist(this))
|
||||
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||
@@ -135,25 +143,26 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
showBlockRestartMessage()
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
} else {
|
||||
val database = Database.getInstance()
|
||||
val readOnly = database.isReadOnly
|
||||
val readOnly = database?.isReadOnly != false
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ _ ->
|
||||
{ openedDatabase, _ ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(this,
|
||||
registerInfo)
|
||||
openedDatabase,
|
||||
registerInfo)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
{
|
||||
{ openedDatabase ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(this,
|
||||
registerInfo)
|
||||
openedDatabase,
|
||||
registerInfo)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
@@ -190,15 +199,16 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION"
|
||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
||||
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
|
||||
|
||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||
|
||||
fun getAuthIntentSenderForSelection(context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender {
|
||||
fun getPendingIntentForSelection(context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
// Doesn't work with Parcelable (don't know why?)
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
@@ -206,6 +216,7 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
||||
putExtra(KEY_MANUAL_SELECTION, it.manualSelection)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
@@ -213,17 +224,17 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
}
|
||||
|
||||
fun getAuthIntentSenderForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo): IntentSender {
|
||||
fun getPendingIntentForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo): PendingIntent {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
}
|
||||
|
||||
fun launchForRegistration(context: Context,
|
||||
|
||||
@@ -32,71 +32,63 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.view.EntryContentsView
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
class EntryActivity : LockingActivity() {
|
||||
class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var titleIconView: ImageView? = null
|
||||
private var historyView: View? = null
|
||||
private var entryContentsView: EntryContentsView? = null
|
||||
private var entryProgress: ProgressBar? = null
|
||||
private var lockView: View? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
private var loadingView: ProgressBar? = null
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
private val mEntryViewModel: EntryViewModel by viewModels()
|
||||
|
||||
private var mEntry: Entry? = null
|
||||
|
||||
private var mIsHistory: Boolean = false
|
||||
private var mEntryLastVersion: Entry? = null
|
||||
private var mEntryHistoryPosition: Int = -1
|
||||
|
||||
private var mShowPassword: Boolean = false
|
||||
private var mMainEntryId: NodeId<UUID>? = null
|
||||
private var mHistoryPosition: Int = -1
|
||||
private var mEntryIsHistory: Boolean = false
|
||||
private var mUrl: String? = null
|
||||
private var mEntryLoaded = false
|
||||
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
private var mFirstLaunchOfActivity: Boolean = false
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var iconColor: Int = 0
|
||||
private var mIcon: IconImage? = null
|
||||
private var mIconColor: Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -108,63 +100,168 @@ class EntryActivity : LockingActivity() {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
mDatabase = Database.getInstance()
|
||||
mReadOnly = mDatabase!!.isReadOnly || mReadOnly
|
||||
|
||||
mShowPassword = !PreferencesUtil.isPasswordMask(this)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
iconColor = taIconColor.getColor(0, Color.BLACK)
|
||||
taIconColor.recycle()
|
||||
|
||||
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
|
||||
invalidateOptionsMenu()
|
||||
|
||||
// Get views
|
||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||
titleIconView = findViewById(R.id.entry_icon)
|
||||
historyView = findViewById(R.id.history_container)
|
||||
entryContentsView = findViewById(R.id.entry_contents)
|
||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||
entryContentsView?.setAttachmentCipherKey(mDatabase)
|
||||
entryProgress = findViewById(R.id.entry_progress)
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
loadingView = findViewById(R.id.loading)
|
||||
|
||||
// Empty title
|
||||
collapsingToolbarLayout?.title = " "
|
||||
toolbar?.title = " "
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
mIconColor = taIconColor.getColor(0, Color.BLACK)
|
||||
taIconColor.recycle()
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
|
||||
intent.removeExtra(KEY_ENTRY)
|
||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
|
||||
|
||||
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
|
||||
}
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
}
|
||||
|
||||
// Init SAF manager
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
// Init attachment service binder manager
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
|
||||
lockView?.setOnClickListener {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
// Focus view to reinitialize timeout
|
||||
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
|
||||
if (entryInfoHistory != null) {
|
||||
this.mMainEntryId = entryInfoHistory.mainEntryId
|
||||
|
||||
// Init the clipboard helper
|
||||
clipboardHelper = ClipboardHelper(this)
|
||||
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
||||
|
||||
// Init SAF manager
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
|
||||
// Init attachment service binder manager
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
|
||||
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
||||
// Close the current activity after an history action
|
||||
if (result.isSuccess)
|
||||
finish()
|
||||
// Manage history position
|
||||
val historyPosition = entryInfoHistory.historyPosition
|
||||
this.mHistoryPosition = historyPosition
|
||||
val entryIsHistory = historyPosition > -1
|
||||
this.mEntryIsHistory = entryIsHistory
|
||||
// Assign history dedicated view
|
||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||
if (entryIsHistory) {
|
||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
collapsingToolbarLayout?.contentScrim =
|
||||
ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||
taColorAccent.recycle()
|
||||
}
|
||||
ACTION_DATABASE_RELOAD_TASK -> {
|
||||
// Close the current activity
|
||||
this.showActionErrorIfNeeded(result)
|
||||
finish()
|
||||
|
||||
val entryInfo = entryInfoHistory.entryInfo
|
||||
// Manage entry copy to start notification if allowed (at the first start)
|
||||
if (savedInstanceState == null) {
|
||||
// Manage entry to launch copying notification if allowed
|
||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||
}
|
||||
}
|
||||
// Assign title icon
|
||||
mIcon = entryInfo.icon
|
||||
titleIconView?.let { iconView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
|
||||
}
|
||||
// Assign title text
|
||||
val entryTitle =
|
||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString()
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
mUrl = entryInfo.url
|
||||
|
||||
loadingView?.hideByFading()
|
||||
mEntryLoaded = true
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
// Refresh Menu
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
||||
if (otpElement == null)
|
||||
entryProgress?.visibility = View.GONE
|
||||
when (otpElement?.type) {
|
||||
// Only add token if HOTP
|
||||
OtpType.HOTP -> {
|
||||
entryProgress?.visibility = View.GONE
|
||||
}
|
||||
// Refresh view if TOTP
|
||||
OtpType.TOTP -> {
|
||||
entryProgress?.apply {
|
||||
max = otpElement.period
|
||||
progress = otpElement.secondsRemaining
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||
}
|
||||
|
||||
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
|
||||
mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentSelected
|
||||
}
|
||||
}
|
||||
|
||||
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||
mDatabase?.let { database ->
|
||||
launch(
|
||||
this,
|
||||
database,
|
||||
historySelected.nodeId,
|
||||
historySelected.historyPosition
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun viewToInvalidateTimeout(): View? {
|
||||
return coordinatorLayout
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mEntryViewModel.loadDatabase(database)
|
||||
|
||||
// Assign title icon
|
||||
mIcon?.let { icon ->
|
||||
titleIconView?.let { iconView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, icon, mIconColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
||||
// Close the current activity after an history action
|
||||
if (result.isSuccess)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -177,63 +274,14 @@ class EntryActivity : LockingActivity() {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
|
||||
if (keyEntry != null) {
|
||||
mEntry = mDatabase?.getEntryById(keyEntry)
|
||||
mEntryLastVersion = mEntry
|
||||
}
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
}
|
||||
|
||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
|
||||
mEntryHistoryPosition = historyPosition
|
||||
if (historyPosition >= 0) {
|
||||
mIsHistory = true
|
||||
mEntry = mEntry?.getHistory()?.get(historyPosition)
|
||||
}
|
||||
|
||||
if (mEntry == null) {
|
||||
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Update last access time.
|
||||
mEntry?.touch(modified = false, touchParents = false)
|
||||
|
||||
mEntry?.let { entry ->
|
||||
// Fill data in resume to update from EntryEditActivity
|
||||
fillEntryDataInContentsView(entry)
|
||||
// Refresh Menu
|
||||
invalidateOptionsMenu()
|
||||
|
||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||
// Manage entry copy to start notification if allowed
|
||||
if (mFirstLaunchOfActivity) {
|
||||
// Manage entry to launch copying notification if allowed
|
||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mAttachmentFileBinderManager?.apply {
|
||||
registerProgressTask()
|
||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||
if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) {
|
||||
entryContentsView?.putAttachment(entryAttachmentState)
|
||||
}
|
||||
mEntryViewModel.onAttachmentAction(entryAttachmentState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mFirstLaunchOfActivity = false
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -242,148 +290,14 @@ class EntryActivity : LockingActivity() {
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun fillEntryDataInContentsView(entry: Entry) {
|
||||
|
||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||
|
||||
// Assign title icon
|
||||
titleIconView?.let { iconView ->
|
||||
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor)
|
||||
}
|
||||
|
||||
// Assign title text
|
||||
val entryTitle = entryInfo.title
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
|
||||
// Assign basic fields
|
||||
entryContentsView?.assignUserName(entryInfo.username) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(entryInfo.username,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_user_name)))
|
||||
}
|
||||
|
||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||
val allowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
|
||||
|
||||
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
|
||||
AlertDialog.Builder(this@EntryActivity)
|
||||
.setMessage(getString(R.string.allow_copy_password_warning) +
|
||||
"\n\n" +
|
||||
getString(R.string.clipboard_warning))
|
||||
.create().apply {
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
|
||||
dialog.dismiss()
|
||||
fillEntryDataInContentsView(entry)
|
||||
}
|
||||
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
|
||||
dialog.dismiss()
|
||||
fillEntryDataInContentsView(entry)
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
||||
View.OnClickListener {
|
||||
clipboardHelper?.timeoutCopyToClipboard(entryInfo.password,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
}
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
showWarningClipboardDialogOnClickListener
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
entryContentsView?.assignPassword(entryInfo.password,
|
||||
allowCopyPasswordAndProtectedFields,
|
||||
onPasswordCopyClickListener)
|
||||
|
||||
//Assign OTP field
|
||||
entry.getOtpElement()?.let { otpElement ->
|
||||
entryContentsView?.assignOtp(otpElement, entryProgress) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
otpElement.token,
|
||||
getString(R.string.copy_field, getString(R.string.entry_otp))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView?.assignURL(entryInfo.url)
|
||||
entryContentsView?.assignNotes(entryInfo.notes)
|
||||
|
||||
// Assign custom fields
|
||||
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||
entryContentsView?.clearExtraFields()
|
||||
entryInfo.customFields.forEach { field ->
|
||||
val label = field.name
|
||||
// OTP field is already managed in dedicated view
|
||||
if (label != OtpEntryFields.OTP_TOKEN_FIELD) {
|
||||
val value = field.protectedValue
|
||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||
if (allowCopyProtectedField) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
value.toString(),
|
||||
getString(R.string.copy_field, label)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
||||
} else {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||
|
||||
// Manage attachments
|
||||
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||
mExternalFileHelper?.createDocument(attachmentItem.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||
}
|
||||
}
|
||||
|
||||
// Assign dates
|
||||
entryContentsView?.assignCreationDate(entryInfo.creationTime)
|
||||
entryContentsView?.assignModificationDate(entryInfo.lastModificationTime)
|
||||
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
|
||||
|
||||
// Manage history
|
||||
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
||||
if (mIsHistory) {
|
||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||
taColorAccent.recycle()
|
||||
}
|
||||
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
|
||||
launch(this, historyItem, mReadOnly, position)
|
||||
}
|
||||
|
||||
// Assign special data
|
||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
|
||||
// Not directly get the entry from intent data but from database
|
||||
mEntry?.let {
|
||||
fillEntryDataInContentsView(it)
|
||||
}
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
// Reload the current id from database
|
||||
mEntryViewModel.loadDatabase(mDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||
@@ -398,56 +312,57 @@ class EntryActivity : LockingActivity() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
if (mEntryLoaded) {
|
||||
val inflater = menuInflater
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
|
||||
val inflater = menuInflater
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
if (mIsHistory && !mReadOnly) {
|
||||
inflater.inflate(R.menu.entry_history, menu)
|
||||
}
|
||||
if (mIsHistory || mReadOnly) {
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||
}
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
|
||||
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
||||
gotoUrl?.apply {
|
||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
||||
// so mEntry may not be set
|
||||
if (mEntry == null) {
|
||||
isVisible = false
|
||||
} else {
|
||||
if (mEntry?.url?.isEmpty() != false) {
|
||||
// disable button if url is not available
|
||||
isVisible = false
|
||||
}
|
||||
if (mEntryIsHistory && !mDatabaseReadOnly) {
|
||||
inflater.inflate(R.menu.entry_history, menu)
|
||||
}
|
||||
|
||||
// Show education views
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation(
|
||||
EntryActivityEducation(
|
||||
this
|
||||
), menu
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Show education views
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
if (mUrl?.isEmpty() != false) {
|
||||
menu?.findItem(R.id.menu_goto_url)?.isVisible = false
|
||||
}
|
||||
if (mEntryIsHistory || mDatabaseReadOnly) {
|
||||
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||
menu: Menu) {
|
||||
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
||||
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
|
||||
as? EntryFragment?
|
||||
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
|
||||
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||
entryFieldCopyView,
|
||||
{
|
||||
val appNameString = getString(R.string.app_name)
|
||||
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||
getString(R.string.copy_field, appNameString))
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
})
|
||||
entryFieldCopyView,
|
||||
{
|
||||
entryFragment.launchEntryCopyEducationAction()
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
})
|
||||
|
||||
if (!entryCopyEducationPerformed) {
|
||||
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||
@@ -471,60 +386,53 @@ class EntryActivity : LockingActivity() {
|
||||
return true
|
||||
}
|
||||
R.id.menu_edit -> {
|
||||
mEntry?.let {
|
||||
EntryEditActivity.launch(this@EntryActivity, it)
|
||||
mDatabase?.let { database ->
|
||||
mMainEntryId?.let { entryId ->
|
||||
EntryEditActivity.launchToUpdate(
|
||||
this,
|
||||
database,
|
||||
entryId
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.menu_goto_url -> {
|
||||
var url: String = mEntry?.url ?: ""
|
||||
|
||||
// Default http:// if no protocol specified
|
||||
if (!url.contains("://")) {
|
||||
url = "http://$url"
|
||||
mUrl?.let { url ->
|
||||
UriUtil.gotoUrl(this, url)
|
||||
}
|
||||
|
||||
UriUtil.gotoUrl(this, url)
|
||||
return true
|
||||
}
|
||||
R.id.menu_restore_entry_history -> {
|
||||
mEntryLastVersion?.let { mainEntry ->
|
||||
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
|
||||
mainEntry,
|
||||
mEntryHistoryPosition,
|
||||
!mReadOnly && mAutoSaveEnable)
|
||||
mMainEntryId?.let { mainEntryId ->
|
||||
restoreEntryHistory(
|
||||
mainEntryId,
|
||||
mHistoryPosition)
|
||||
}
|
||||
}
|
||||
R.id.menu_delete_entry_history -> {
|
||||
mEntryLastVersion?.let { mainEntry ->
|
||||
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
|
||||
mainEntry,
|
||||
mEntryHistoryPosition,
|
||||
!mReadOnly && mAutoSaveEnable)
|
||||
mMainEntryId?.let { mainEntryId ->
|
||||
deleteEntryHistory(
|
||||
mainEntryId,
|
||||
mHistoryPosition)
|
||||
}
|
||||
}
|
||||
R.id.menu_save_database -> {
|
||||
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||
saveDatabase()
|
||||
}
|
||||
R.id.menu_reload_database -> {
|
||||
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
|
||||
reloadDatabase()
|
||||
}
|
||||
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putBoolean(KEY_FIRST_LAUNCH_ACTIVITY, mFirstLaunchOfActivity)
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
// Transit data in previous Activity after an update
|
||||
Intent().apply {
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
|
||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||
setResult(EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE, this)
|
||||
}
|
||||
super.finish()
|
||||
}
|
||||
@@ -532,19 +440,46 @@ class EntryActivity : LockingActivity() {
|
||||
companion object {
|
||||
private val TAG = EntryActivity::class.java.name
|
||||
|
||||
private const val KEY_FIRST_LAUNCH_ACTIVITY = "KEY_FIRST_LAUNCH_ACTIVITY"
|
||||
|
||||
const val KEY_ENTRY = "KEY_ENTRY"
|
||||
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
||||
|
||||
fun launch(activity: Activity, entry: Entry, readOnly: Boolean, historyPosition: Int? = null) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||
if (historyPosition != null)
|
||||
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
||||
|
||||
/**
|
||||
* Open standard Entry activity
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
database: Database,
|
||||
entryId: NodeId<UUID>) {
|
||||
if (database.loaded) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
activity.startActivityForResult(
|
||||
intent,
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open history Entry activity
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
database: Database,
|
||||
entryId: NodeId<UUID>,
|
||||
historyPosition: Int) {
|
||||
if (database.loaded) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||
activity.startActivityForResult(
|
||||
intent,
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,14 +22,13 @@ package com.kunzisoft.keepass.activities
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
@@ -39,10 +38,18 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
* Activity to search or select entry in database,
|
||||
* Commonly used with Magikeyboard
|
||||
*/
|
||||
class EntrySelectionLauncherActivity : AppCompatActivity() {
|
||||
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
var sharedWebDomain: String? = null
|
||||
var otpString: String? = null
|
||||
|
||||
@@ -68,39 +75,39 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
||||
else -> {}
|
||||
}
|
||||
|
||||
|
||||
// Build domain search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
this.webDomain = sharedWebDomain
|
||||
this.otpString = otpString
|
||||
}
|
||||
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launch(searchInfo)
|
||||
launch(database, searchInfo)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun launch(searchInfo: SearchInfo) {
|
||||
private fun launch(database: Database?,
|
||||
searchInfo: SearchInfo) {
|
||||
|
||||
if (!searchInfo.containsOnlyNullValues()) {
|
||||
// Setting to integrate Magikeyboard
|
||||
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
||||
|
||||
// If database is open
|
||||
val database = Database.getInstance()
|
||||
val readOnly = database.isReadOnly
|
||||
val readOnly = database?.isReadOnly != false
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ items ->
|
||||
{ openedDatabase, items ->
|
||||
// Items found
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(this,
|
||||
searchInfo,
|
||||
false)
|
||||
GroupActivity.launchForSaveResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
@@ -111,30 +118,32 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
||||
if (items.size == 1) {
|
||||
// Automatically populate keyboard
|
||||
val entryPopulate = items[0]
|
||||
populateKeyboardAndMoveAppToBackground(this,
|
||||
populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entryPopulate,
|
||||
intent)
|
||||
} else {
|
||||
// Select the one we want
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
readOnly,
|
||||
searchInfo,
|
||||
true)
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
true)
|
||||
}
|
||||
} else {
|
||||
GroupActivity.launchForSearchResult(this,
|
||||
readOnly,
|
||||
searchInfo,
|
||||
true)
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
true)
|
||||
}
|
||||
},
|
||||
{
|
||||
{ openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(this,
|
||||
searchInfo,
|
||||
false)
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
@@ -143,13 +152,14 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
||||
}
|
||||
} else if (readOnly || searchShareForMagikeyboard) {
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
readOnly,
|
||||
searchInfo,
|
||||
false)
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
GroupActivity.launchForSaveResult(this,
|
||||
searchInfo,
|
||||
false)
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -183,7 +193,7 @@ fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||
intent: Intent,
|
||||
toast: Boolean = true) {
|
||||
// Populate Magikeyboard with entry
|
||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||
// Consume the selection mode
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
activity.moveTaskToBack(true)
|
||||
|
||||
@@ -45,12 +45,11 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
@@ -61,12 +60,13 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||
|
||||
// Views
|
||||
@@ -85,8 +85,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -127,7 +125,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
}
|
||||
}
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||
// Remove from app database
|
||||
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
||||
true
|
||||
}
|
||||
@@ -190,39 +187,62 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
// Retrieve settings for default database
|
||||
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||
}
|
||||
}
|
||||
|
||||
// Attach the dialog thread to this activity
|
||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||
onActionFinish = { actionTask, result ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK -> {
|
||||
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
|
||||
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
|
||||
}
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
||||
}
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
val database = Database.getInstance()
|
||||
if (result.isSuccess
|
||||
&& database.loaded) {
|
||||
launchGroupActivity(database)
|
||||
} else {
|
||||
var resultError = ""
|
||||
val resultMessage = result.message
|
||||
// Show error message
|
||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||
resultError = "$resultError $resultMessage"
|
||||
}
|
||||
Log.e(TAG, resultError)
|
||||
Snackbar.make(coordinatorLayout,
|
||||
resultError,
|
||||
Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
if (database != null) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Update list
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK,
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||
val mainCredential =
|
||||
result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
|
||||
?: MainCredential()
|
||||
databaseFilesViewModel.addDatabaseFile(
|
||||
databaseUri,
|
||||
mainCredential.keyFileUri
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Launch activity
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK -> {
|
||||
GroupActivity.launch(
|
||||
this@FileDatabaseSelectActivity,
|
||||
database,
|
||||
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)
|
||||
)
|
||||
}
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var resultError = ""
|
||||
val resultMessage = result.message
|
||||
// Show error message
|
||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||
resultError = "$resultError $resultMessage"
|
||||
}
|
||||
Log.e(TAG, resultError)
|
||||
Snackbar.make(coordinatorLayout,
|
||||
resultError,
|
||||
Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,12 +271,14 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
{ onLaunchActivitySpecialMode() })
|
||||
}
|
||||
|
||||
private fun launchGroupActivity(database: Database) {
|
||||
GroupActivity.launch(this,
|
||||
database.isReadOnly,
|
||||
private fun launchGroupActivityIfLoaded(database: Database) {
|
||||
if (database.loaded) {
|
||||
GroupActivity.launch(this,
|
||||
database,
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() })
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValidateSpecialMode() {
|
||||
@@ -296,28 +318,16 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
val database = Database.getInstance()
|
||||
if (database.loaded) {
|
||||
launchGroupActivity(database)
|
||||
} else {
|
||||
// Construct adapter with listeners
|
||||
if (PreferencesUtil.showRecentFiles(this)) {
|
||||
databaseFilesViewModel.loadListOfDatabases()
|
||||
} else {
|
||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
// Register progress task
|
||||
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||
mDatabase?.let { database ->
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
// Unregister progress task
|
||||
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
// Show recent files if allowed
|
||||
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
||||
databaseFilesViewModel.loadListOfDatabases()
|
||||
} else {
|
||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -329,15 +339,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
||||
|
||||
try {
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
|
||||
// Create the new database
|
||||
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
||||
databaseUri,
|
||||
mainCredential
|
||||
)
|
||||
createDatabase(databaseUri, mainCredential)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
val error = getString(R.string.error_create_database_file)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,8 +36,7 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
@@ -50,7 +49,7 @@ import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
|
||||
class IconPickerActivity : LockingActivity() {
|
||||
class IconPickerActivity : DatabaseLockActivity() {
|
||||
|
||||
private lateinit var toolbar: Toolbar
|
||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||
@@ -65,8 +64,6 @@ class IconPickerActivity : LockingActivity() {
|
||||
private var mCustomIconsSelectionMode = false
|
||||
private var mIconsSelected: List<IconImageCustom> = ArrayList()
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -74,8 +71,6 @@ class IconPickerActivity : LockingActivity() {
|
||||
|
||||
setContentView(R.layout.activity_icon_picker)
|
||||
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
toolbar.title = " "
|
||||
setSupportActionBar(toolbar)
|
||||
@@ -88,11 +83,6 @@ class IconPickerActivity : LockingActivity() {
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
|
||||
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||
if (mDatabase?.allowCustomIcons == true) {
|
||||
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
} else {
|
||||
uploadButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
lockView?.setOnClickListener {
|
||||
@@ -118,9 +108,6 @@ class IconPickerActivity : LockingActivity() {
|
||||
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
|
||||
}
|
||||
|
||||
// Focus view to reinitialize timeout
|
||||
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||
|
||||
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
||||
mIconImage.standard = iconStandard
|
||||
// Remove the custom icon if a standard one is selected
|
||||
@@ -154,6 +141,24 @@ class IconPickerActivity : LockingActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun viewToInvalidateTimeout(): View? {
|
||||
return findViewById<ViewGroup>(R.id.icon_picker_container)
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
if (database?.allowCustomIcons == true) {
|
||||
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
} else {
|
||||
uploadButton.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateIconsSelectedViews() {
|
||||
if (mIconsSelected.isEmpty()) {
|
||||
mCustomIconsSelectionMode = false
|
||||
@@ -187,13 +192,18 @@ class IconPickerActivity : LockingActivity() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
if (mCustomIconsSelectionMode) {
|
||||
menuInflater.inflate(R.menu.icon, menu)
|
||||
}
|
||||
menuInflater.inflate(R.menu.icon, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
menu?.findItem(R.id.menu_delete)?.apply {
|
||||
isEnabled = mCustomIconsSelectionMode
|
||||
isVisible = isEnabled
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
@@ -208,6 +218,9 @@ class IconPickerActivity : LockingActivity() {
|
||||
removeCustomIcon(iconToRemove)
|
||||
}
|
||||
}
|
||||
R.id.menu_external_icon -> {
|
||||
UriUtil.gotoUrl(this, R.string.external_icon_url)
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
@@ -31,16 +32,19 @@ import android.widget.ImageView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.igreenwood.loupe.Loupe
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||
import kotlin.math.max
|
||||
|
||||
class ImageViewerActivity : LockingActivity() {
|
||||
class ImageViewerActivity : DatabaseLockActivity() {
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
private var imageContainerView: ViewGroup? = null
|
||||
private lateinit var imageView: ImageView
|
||||
private lateinit var progressView: View
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -50,49 +54,21 @@ class ImageViewerActivity : LockingActivity() {
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
val imageContainerView: ViewGroup = findViewById(R.id.image_viewer_container)
|
||||
val imageView: ImageView = findViewById(R.id.image_viewer_image)
|
||||
val progressView: View = findViewById(R.id.image_viewer_progress)
|
||||
|
||||
// Approximately, to not OOM and allow a zoom
|
||||
val mImagePreviewMaxWidth = max(
|
||||
resources.displayMetrics.widthPixels * 2,
|
||||
resources.displayMetrics.heightPixels * 2
|
||||
)
|
||||
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
try {
|
||||
progressView.visibility = View.VISIBLE
|
||||
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||
|
||||
supportActionBar?.title = attachment.name
|
||||
|
||||
val size = attachment.binaryData.getSize()
|
||||
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
|
||||
|
||||
mDatabase?.let { database ->
|
||||
BinaryDatabaseManager.loadBitmap(
|
||||
database,
|
||||
attachment.binaryData,
|
||||
mImagePreviewMaxWidth
|
||||
) { bitmapLoaded ->
|
||||
if (bitmapLoaded == null) {
|
||||
finish()
|
||||
} else {
|
||||
progressView.visibility = View.GONE
|
||||
imageView.setImageBitmap(bitmapLoaded)
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: finish()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to view the binary", e)
|
||||
finish()
|
||||
toolbar.setOnTouchListener { _, _ ->
|
||||
resetAppTimeout()
|
||||
false
|
||||
}
|
||||
|
||||
Loupe.create(imageView, imageContainerView) {
|
||||
imageContainerView = findViewById(R.id.image_viewer_container)
|
||||
imageView = findViewById(R.id.image_viewer_image)
|
||||
progressView = findViewById(R.id.image_viewer_progress)
|
||||
|
||||
Loupe.create(imageView, imageContainerView!!) {
|
||||
onViewTouchedListener = View.OnTouchListener { _, _ ->
|
||||
// to reset timeout when Loupe image view touched
|
||||
resetAppTimeout()
|
||||
false
|
||||
}
|
||||
onViewTranslateListener = object : Loupe.OnViewTranslateListener {
|
||||
|
||||
override fun onStart(view: ImageView) {
|
||||
@@ -115,6 +91,54 @@ class ImageViewerActivity : LockingActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun viewToInvalidateTimeout(): View? {
|
||||
// Null to manually manage events
|
||||
return null
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
try {
|
||||
progressView.visibility = View.VISIBLE
|
||||
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||
|
||||
supportActionBar?.title = attachment.name
|
||||
|
||||
val size = attachment.binaryData.getSize()
|
||||
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
|
||||
|
||||
// Approximately, to not OOM and allow a zoom
|
||||
val mImagePreviewMaxWidth = max(
|
||||
resources.displayMetrics.widthPixels * 2,
|
||||
resources.displayMetrics.heightPixels * 2
|
||||
)
|
||||
|
||||
database?.let { database ->
|
||||
BinaryDatabaseManager.loadBitmap(
|
||||
database,
|
||||
attachment.binaryData,
|
||||
mImagePreviewMaxWidth
|
||||
) { bitmapLoaded ->
|
||||
if (bitmapLoaded == null) {
|
||||
finish()
|
||||
} else {
|
||||
progressView.visibility = View.GONE
|
||||
imageView.setImageBitmap(bitmapLoaded)
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: finish()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to view the binary", e)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> finish()
|
||||
|
||||
@@ -19,36 +19,41 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
|
||||
/**
|
||||
* Activity to select entry in database and populate it in Magikeyboard
|
||||
*/
|
||||
class MagikeyboardLauncherActivity : AppCompatActivity() {
|
||||
class MagikeyboardLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val database = Database.getInstance()
|
||||
val readOnly = database.isReadOnly
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
null,
|
||||
{
|
||||
// Not called
|
||||
// if items found directly returns before calling this activity
|
||||
},
|
||||
{
|
||||
// Select if not found
|
||||
GroupActivity.launchForKeyboardSelectionResult(this, readOnly)
|
||||
},
|
||||
{
|
||||
// Pass extra to get entry
|
||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
|
||||
}
|
||||
database,
|
||||
null,
|
||||
{ _, _ ->
|
||||
// Not called
|
||||
// if items found directly returns before calling this activity
|
||||
},
|
||||
{ openedDatabase ->
|
||||
// Select if not found
|
||||
GroupActivity.launchForKeyboardSelectionResult(this, openedDatabase)
|
||||
},
|
||||
{
|
||||
// Pass extra to get entry
|
||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
|
||||
}
|
||||
)
|
||||
finish()
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,11 @@ import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.KeyEvent.KEYCODE_ENTER
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
@@ -43,13 +46,12 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||
import com.kunzisoft.keepass.activities.helpers.*
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||
@@ -63,6 +65,7 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
@@ -71,7 +74,8 @@ import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||
|
||||
open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
@@ -95,11 +99,11 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mPermissionAsked = false
|
||||
private var readOnly: Boolean = false
|
||||
private var mReadOnly: Boolean = false
|
||||
private var mForceReadOnly: Boolean = false
|
||||
set(value) {
|
||||
infoContainerView?.visibility = if (value) {
|
||||
readOnly = true
|
||||
mReadOnly = true
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
@@ -107,8 +111,6 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
field = value
|
||||
}
|
||||
|
||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||
|
||||
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -132,7 +134,11 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
||||
|
||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
||||
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
||||
} else {
|
||||
PreferencesUtil.enableReadOnlyDatabase(this)
|
||||
}
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
|
||||
@@ -149,6 +155,15 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
checkboxPasswordView?.isChecked = true
|
||||
}
|
||||
})
|
||||
passwordView?.setOnKeyListener { _, _, keyEvent ->
|
||||
var handled = false
|
||||
if (keyEvent.action == KeyEvent.ACTION_DOWN
|
||||
&& keyEvent?.keyCode == KEYCODE_ENTER) {
|
||||
verifyCheckboxesAndLoadDatabase()
|
||||
handled = true
|
||||
}
|
||||
handled
|
||||
}
|
||||
|
||||
// If is a view intent
|
||||
getUriFromIntent(intent)
|
||||
@@ -204,72 +219,114 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
|
||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
||||
}
|
||||
}
|
||||
|
||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||
onActionFinish = { actionTask, result ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
// Recheck advanced unlock if error
|
||||
advancedUnlockFragment?.initAdvancedUnlockMode()
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (result.isSuccess) {
|
||||
mDatabaseKeyFileUri = null
|
||||
clearCredentialsViews(true)
|
||||
launchGroupActivity()
|
||||
} else {
|
||||
var resultError = ""
|
||||
val resultException = result.exception
|
||||
val resultMessage = result.message
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@PasswordActivity)
|
||||
|
||||
if (resultException != null) {
|
||||
resultError = resultException.getLocalizedMessage(resources)
|
||||
// Back to previous keyboard is setting activated
|
||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@PasswordActivity)) {
|
||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||
}
|
||||
|
||||
when (resultException) {
|
||||
is DuplicateUuidDatabaseException -> {
|
||||
// Relaunch loading if we need to fix UUID
|
||||
showLoadDatabaseDuplicateUuidMessage {
|
||||
// Don't allow auto open prompt if lock become when UI visible
|
||||
mAllowAutoOpenBiometricPrompt = if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
||||
false
|
||||
else
|
||||
mAllowAutoOpenBiometricPrompt
|
||||
mDatabaseFileUri?.let { databaseFileUri ->
|
||||
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||
}
|
||||
|
||||
var databaseUri: Uri? = null
|
||||
var mainCredential: MainCredential = MainCredential()
|
||||
var readOnly = true
|
||||
var cipherEntity: CipherDatabaseEntity? = null
|
||||
checkPermission()
|
||||
|
||||
result.data?.let { resultData ->
|
||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||
mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential
|
||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||
}
|
||||
mDatabase?.let { database ->
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
|
||||
databaseUri?.let { databaseFileUri ->
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseFileUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEntity,
|
||||
true)
|
||||
}
|
||||
}
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
if (database != null) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
// Recheck advanced unlock if error
|
||||
advancedUnlockFragment?.initAdvancedUnlockMode()
|
||||
|
||||
if (result.isSuccess) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
} else {
|
||||
passwordView?.requestFocusFromTouch()
|
||||
|
||||
var resultError = ""
|
||||
val resultException = result.exception
|
||||
val resultMessage = result.message
|
||||
|
||||
if (resultException != null) {
|
||||
resultError = resultException.getLocalizedMessage(resources)
|
||||
|
||||
when (resultException) {
|
||||
is DuplicateUuidDatabaseException -> {
|
||||
// Relaunch loading if we need to fix UUID
|
||||
showLoadDatabaseDuplicateUuidMessage {
|
||||
|
||||
var databaseUri: Uri? = null
|
||||
var mainCredential = MainCredential()
|
||||
var readOnly = true
|
||||
var cipherEntity: CipherDatabaseEntity? = null
|
||||
|
||||
result.data?.let { resultData ->
|
||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||
mainCredential =
|
||||
resultData.getParcelable(MAIN_CREDENTIAL_KEY)
|
||||
?: mainCredential
|
||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||
cipherEntity =
|
||||
resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||
}
|
||||
is FileNotFoundDatabaseException -> {
|
||||
// Remove this default database inaccessible
|
||||
if (mDefaultDatabase) {
|
||||
databaseFileViewModel.removeDefaultDatabase()
|
||||
}
|
||||
|
||||
databaseUri?.let { databaseFileUri ->
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseFileUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEntity,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||
resultError = "$resultError $resultMessage"
|
||||
is FileNotFoundDatabaseException -> {
|
||||
// Remove this default database inaccessible
|
||||
if (mDefaultDatabase) {
|
||||
databaseFileViewModel.removeDefaultDatabase()
|
||||
}
|
||||
}
|
||||
Log.e(TAG, resultError)
|
||||
Snackbar.make(coordinatorLayout,
|
||||
resultError,
|
||||
Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||
resultError = "$resultError $resultMessage"
|
||||
}
|
||||
Log.e(TAG, resultError)
|
||||
Snackbar.make(
|
||||
coordinatorLayout,
|
||||
resultError,
|
||||
Snackbar.LENGTH_LONG
|
||||
).asError().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,13 +353,17 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
getUriFromIntent(intent)
|
||||
}
|
||||
|
||||
private fun launchGroupActivity() {
|
||||
GroupActivity.launch(this,
|
||||
readOnly,
|
||||
private fun launchGroupActivityIfLoaded(database: Database) {
|
||||
// Check if database really loaded
|
||||
if (database.loaded) {
|
||||
clearCredentialsViews(true)
|
||||
GroupActivity.launch(this,
|
||||
database,
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValidateSpecialMode() {
|
||||
@@ -352,40 +413,6 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (Database.getInstance().loaded) {
|
||||
launchGroupActivity()
|
||||
} else {
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
|
||||
// If the database isn't accessible make sure to clear the password field, if it
|
||||
// was saved in the instance state
|
||||
if (Database.getInstance().loaded) {
|
||||
clearCredentialsViews()
|
||||
}
|
||||
|
||||
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||
|
||||
// Back to previous keyboard is setting activated
|
||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
|
||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||
}
|
||||
|
||||
// Don't allow auto open prompt if lock become when UI visible
|
||||
mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
||||
false
|
||||
else
|
||||
mAllowAutoOpenBiometricPrompt
|
||||
mDatabaseFileUri?.let { databaseFileUri ->
|
||||
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||
}
|
||||
|
||||
checkPermission()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||
// Define Key File text
|
||||
if (mRememberKeyFile) {
|
||||
@@ -409,11 +436,17 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
} else {
|
||||
// Init Biometric elements
|
||||
advancedUnlockFragment?.loadDatabase(databaseFileUri,
|
||||
mAllowAutoOpenBiometricPrompt
|
||||
&& mProgressDatabaseTaskProvider?.isBinded() != true)
|
||||
mAllowAutoOpenBiometricPrompt)
|
||||
}
|
||||
|
||||
enableOrNotTheConfirmationButton()
|
||||
|
||||
// Auto select the password field and open keyboard
|
||||
passwordView?.postDelayed({
|
||||
passwordView?.requestFocusFromTouch()
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager?
|
||||
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
private fun enableOrNotTheConfirmationButton() {
|
||||
@@ -431,6 +464,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||
populatePasswordTextView(null)
|
||||
if (clearKeyFile) {
|
||||
mDatabaseKeyFileUri = null
|
||||
populateKeyFileTextView(null)
|
||||
}
|
||||
}
|
||||
@@ -460,10 +494,8 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||
|
||||
// Reinit locking activity UI variable
|
||||
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||
mAllowAutoOpenBiometricPrompt = true
|
||||
|
||||
super.onPause()
|
||||
@@ -474,7 +506,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
mDatabaseKeyFileUri?.let {
|
||||
outState.putString(KEY_KEYFILE, it.toString())
|
||||
}
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
||||
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
@@ -512,7 +544,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
clearCredentialsViews()
|
||||
}
|
||||
|
||||
if (readOnly && (
|
||||
if (mReadOnly && (
|
||||
mSpecialMode == SpecialMode.SAVE
|
||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||
) {
|
||||
@@ -526,7 +558,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseUri,
|
||||
MainCredential(password, keyFileUri),
|
||||
readOnly,
|
||||
mReadOnly,
|
||||
cipherDatabaseEntity,
|
||||
false)
|
||||
}
|
||||
@@ -538,7 +570,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
readOnly: Boolean,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||
fixDuplicateUUID: Boolean) {
|
||||
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
||||
loadDatabase(
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
@@ -577,7 +609,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
// Check permission
|
||||
private fun checkPermission() {
|
||||
if (Build.VERSION.SDK_INT in 23..28
|
||||
&& !readOnly
|
||||
&& !mReadOnly
|
||||
&& !mPermissionAsked) {
|
||||
mPermissionAsked = true
|
||||
// Check self permission to show or not the dialog
|
||||
@@ -654,7 +686,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
}
|
||||
|
||||
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
||||
if (readOnly) {
|
||||
if (mReadOnly) {
|
||||
togglePassword.setTitle(R.string.menu_file_selection_read_only)
|
||||
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
|
||||
} else {
|
||||
@@ -668,7 +700,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> finish()
|
||||
R.id.menu_open_file_read_mode_key -> {
|
||||
readOnly = !readOnly
|
||||
mReadOnly = !mReadOnly
|
||||
changeOpenFileReadIcon(item)
|
||||
}
|
||||
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||
@@ -695,8 +727,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
|
||||
var keyFileResult = false
|
||||
mExternalFileHelper?.let {
|
||||
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
if (uri != null) {
|
||||
mDatabaseKeyFileUri = uri
|
||||
populateKeyFileTextView(uri)
|
||||
@@ -706,9 +737,9 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
if (!keyFileResult) {
|
||||
// this block if not a key file response
|
||||
when (resultCode) {
|
||||
LockingActivity.RESULT_EXIT_LOCK -> {
|
||||
DatabaseLockActivity.RESULT_EXIT_LOCK -> {
|
||||
clearCredentialsViews()
|
||||
Database.getInstance().clearAndClose(this)
|
||||
closeDatabase()
|
||||
}
|
||||
Activity.RESULT_CANCELED -> {
|
||||
clearCredentialsViews()
|
||||
@@ -727,6 +758,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
private const val KEY_KEYFILE = "keyFile"
|
||||
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||
|
||||
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
||||
|
||||
@@ -32,7 +32,6 @@ import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
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.activities.helpers.ExternalFileHelper
|
||||
@@ -41,7 +40,7 @@ import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
|
||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mMasterPassword: String? = null
|
||||
private var mKeyFile: Uri? = null
|
||||
|
||||
@@ -23,12 +23,11 @@ import android.app.Dialog
|
||||
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.model.SnapFileDatabaseInfo
|
||||
|
||||
|
||||
class DatabaseChangedDialogFragment : DialogFragment() {
|
||||
class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
var actionDatabaseListener: ActionDatabaseChangedListener? = null
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
|
||||
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mDatabaseViewModel.database.observe(this) { database ->
|
||||
this.mDatabase = database
|
||||
resetAppTimeoutOnTouchOrFocus()
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.actionFinished.observe(this) { result ->
|
||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
resetAppTimeoutOnTouchOrFocus()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
// Can be overridden by a subclass
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
// Can be overridden by a subclass
|
||||
}
|
||||
|
||||
fun resetAppTimeout() {
|
||||
context?.let {
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(it,
|
||||
mDatabase?.loaded ?: false)
|
||||
}
|
||||
}
|
||||
|
||||
open fun overrideTimeoutTouchAndFocusEvents(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
private fun resetAppTimeoutOnTouchOrFocus() {
|
||||
if (!overrideTimeoutTouchAndFocusEvents()) {
|
||||
context?.let {
|
||||
dialog?.window?.decorView?.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||
it,
|
||||
mDatabase?.loaded
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
||||
// Not as DatabaseDialogFragment because crash on KitKat
|
||||
class DatePickerFragment : DialogFragment() {
|
||||
|
||||
private var mDefaultYear: Int = 2000
|
||||
|
||||
@@ -20,61 +20,38 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
||||
|
||||
open class DeleteNodesDialogFragment : DialogFragment() {
|
||||
class DeleteNodesDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mNodesToDelete: List<Node> = ArrayList()
|
||||
private var mListener: DeleteNodeListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as DeleteNodeListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + DeleteNodeListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
protected open fun retrieveMessage(): String {
|
||||
return getString(R.string.warning_permanently_delete_nodes)
|
||||
}
|
||||
private var mNodesToDelete: List<Node> = listOf()
|
||||
private val mNodesViewModel: NodesViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
mNodesViewModel.nodesToDelete.observe(this) { nodes ->
|
||||
this.mNodesToDelete = nodes
|
||||
}
|
||||
var recycleBin = false
|
||||
arguments?.apply {
|
||||
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
||||
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
||||
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), this)
|
||||
}
|
||||
} ?: savedInstanceState?.apply {
|
||||
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
||||
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
||||
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), savedInstanceState)
|
||||
if (containsKey(RECYCLE_BIN_TAG)) {
|
||||
recycleBin = this.getBoolean(RECYCLE_BIN_TAG)
|
||||
}
|
||||
}
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
builder.setMessage(retrieveMessage())
|
||||
builder.setMessage(if (recycleBin)
|
||||
getString(R.string.warning_empty_recycle_bin)
|
||||
else
|
||||
getString(R.string.warning_permanently_delete_nodes))
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
||||
mNodesViewModel.permanentlyDeleteNodes(mNodesToDelete)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
// Create the AlertDialog object and return it
|
||||
@@ -83,19 +60,14 @@ open class DeleteNodesDialogFragment : DialogFragment() {
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putAll(getBundleFromListNodes(mNodesToDelete))
|
||||
}
|
||||
|
||||
interface DeleteNodeListener {
|
||||
fun permanentlyDeleteNodes(nodes: List<Node>)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getInstance(nodesToDelete: List<Node>): DeleteNodesDialogFragment {
|
||||
private const val RECYCLE_BIN_TAG = "RECYCLE_BIN_TAG"
|
||||
|
||||
fun getInstance(recycleBin: Boolean): DeleteNodesDialogFragment {
|
||||
return DeleteNodesDialogFragment().apply {
|
||||
arguments = getBundleFromListNodes(nodesToDelete)
|
||||
arguments = Bundle().apply {
|
||||
putBoolean(RECYCLE_BIN_TAG, recycleBin)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,14 +31,13 @@ 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.Field
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
|
||||
|
||||
class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||
class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
|
||||
|
||||
private var oldField: Field? = null
|
||||
|
||||
|
||||
@@ -22,18 +22,18 @@ package com.kunzisoft.keepass.activities.dialogs
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
|
||||
class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mListener: GeneratePasswordListener? = null
|
||||
|
||||
@@ -42,6 +42,8 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
private var passwordInputLayoutView: TextInputLayout? = null
|
||||
private var passwordView: EditText? = null
|
||||
|
||||
private var mPasswordField: Field? = null
|
||||
|
||||
private var uppercaseBox: CompoundButton? = null
|
||||
private var lowercaseBox: CompoundButton? = null
|
||||
private var digitsBox: CompoundButton? = null
|
||||
@@ -77,7 +79,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
passwordView = root?.findViewById(R.id.password)
|
||||
passwordView?.applyFontVisibility()
|
||||
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
|
||||
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyPasswordAndProtectedFields(activity))
|
||||
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(activity))
|
||||
View.VISIBLE else View.GONE
|
||||
val clipboardHelper = ClipboardHelper(activity)
|
||||
passwordCopyView?.setOnClickListener {
|
||||
@@ -98,6 +100,8 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
bracketsBox = root?.findViewById(R.id.cb_brackets)
|
||||
extendedBox = root?.findViewById(R.id.cb_extended)
|
||||
|
||||
mPasswordField = arguments?.getParcelable(KEY_PASSWORD_FIELD)
|
||||
|
||||
assignDefaultCharacters()
|
||||
|
||||
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
|
||||
@@ -120,16 +124,18 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
|
||||
builder.setView(root)
|
||||
.setPositiveButton(R.string.accept) { _, _ ->
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_PASSWORD_ID, passwordView!!.text.toString())
|
||||
mListener?.acceptPassword(bundle)
|
||||
|
||||
mPasswordField?.let { passwordField ->
|
||||
passwordView?.text?.toString()?.let { passwordValue ->
|
||||
passwordField.protectedValue.stringValue = passwordValue
|
||||
}
|
||||
mListener?.acceptPassword(passwordField)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
val bundle = Bundle()
|
||||
mListener?.cancelPassword(bundle)
|
||||
|
||||
mPasswordField?.let { passwordField ->
|
||||
mListener?.cancelPassword(passwordField)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@@ -200,11 +206,19 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
interface GeneratePasswordListener {
|
||||
fun acceptPassword(bundle: Bundle)
|
||||
fun cancelPassword(bundle: Bundle)
|
||||
fun acceptPassword(passwordField: Field)
|
||||
fun cancelPassword(passwordField: Field)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
|
||||
private const val KEY_PASSWORD_FIELD = "KEY_PASSWORD_FIELD"
|
||||
|
||||
fun getInstance(field: Field): GeneratePasswordDialogFragment {
|
||||
return GeneratePasswordDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(KEY_PASSWORD_FIELD, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
@@ -28,35 +27,34 @@ import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.IconPickerActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.*
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.view.ExpirationView
|
||||
import com.kunzisoft.keepass.view.DateTimeEditFieldView
|
||||
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||
import org.joda.time.DateTime
|
||||
|
||||
class GroupEditDialogFragment : DialogFragment() {
|
||||
class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
private val mGroupEditViewModel: GroupEditViewModel by activityViewModels()
|
||||
|
||||
private var mEditGroupListener: EditGroupListener? = null
|
||||
|
||||
private var mEditGroupDialogAction = EditGroupDialogAction.NONE
|
||||
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||
private var mEditGroupDialogAction = NONE
|
||||
private var mGroupInfo = GroupInfo()
|
||||
private var mGroupNamesNotAllowed: List<String>? = null
|
||||
|
||||
private lateinit var iconButtonView: ImageView
|
||||
private var iconColor: Int = 0
|
||||
private var mIconColor: Int = 0
|
||||
private lateinit var nameTextLayoutView: TextInputLayout
|
||||
private lateinit var nameTextView: TextView
|
||||
private lateinit var notesTextLayoutView: TextInputLayout
|
||||
private lateinit var notesTextView: TextView
|
||||
private lateinit var expirationView: ExpirationView
|
||||
private lateinit var expirationView: DateTimeEditFieldView
|
||||
|
||||
enum class EditGroupDialogAction {
|
||||
CREATION, UPDATE, NONE;
|
||||
@@ -68,22 +66,51 @@ class GroupEditDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
// Verify that the host activity implements the callback interface
|
||||
try {
|
||||
// Instantiate the NoticeDialogListener so we can send events to the host
|
||||
mEditGroupListener = context as EditGroupListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mGroupEditViewModel.onIconSelected.observe(this) { iconImage ->
|
||||
mGroupInfo.icon = iconImage
|
||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||
}
|
||||
|
||||
mGroupEditViewModel.onDateSelected.observe(this) { viewModelDate ->
|
||||
// Save the date
|
||||
mGroupInfo.expiryTime = DateInstant(
|
||||
DateTime(mGroupInfo.expiryTime.date)
|
||||
.withYear(viewModelDate.year)
|
||||
.withMonthOfYear(viewModelDate.month + 1)
|
||||
.withDayOfMonth(viewModelDate.day)
|
||||
.toDate())
|
||||
expirationView.dateTime = mGroupInfo.expiryTime
|
||||
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
|
||||
val instantTime = DateInstant(mGroupInfo.expiryTime.date, DateInstant.Type.TIME)
|
||||
// Trick to recall selection with time
|
||||
mGroupEditViewModel.requestDateTimeSelection(instantTime)
|
||||
}
|
||||
}
|
||||
|
||||
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
|
||||
// Save the time
|
||||
mGroupInfo.expiryTime = DateInstant(
|
||||
DateTime(mGroupInfo.expiryTime.date)
|
||||
.withHourOfDay(viewModelTime.hours)
|
||||
.withMinuteOfHour(viewModelTime.minutes)
|
||||
.toDate(), mGroupInfo.expiryTime.type)
|
||||
expirationView.dateTime = mGroupInfo.expiryTime
|
||||
}
|
||||
|
||||
mGroupEditViewModel.groupNamesNotAllowed.observe(this) { namesNotAllowed ->
|
||||
this.mGroupNamesNotAllowed = namesNotAllowed
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mEditGroupListener = null
|
||||
super.onDetach()
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
@@ -98,12 +125,9 @@ class GroupEditDialogFragment : DialogFragment() {
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
iconColor = ta.getColor(0, Color.WHITE)
|
||||
mIconColor = ta.getColor(0, Color.WHITE)
|
||||
ta.recycle()
|
||||
|
||||
// Init elements
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
||||
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||
@@ -120,32 +144,22 @@ class GroupEditDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
// populate info in views
|
||||
populateInfoToViews()
|
||||
expirationView.setOnDateClickListener = {
|
||||
expirationView.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(parentFragmentManager, "DatePickerFragment")
|
||||
}
|
||||
populateInfoToViews(mGroupInfo)
|
||||
|
||||
iconButtonView.setOnClickListener { _ ->
|
||||
mGroupEditViewModel.requestIconSelection(mGroupInfo.icon)
|
||||
}
|
||||
expirationView.setOnDateClickListener = { dateInstant ->
|
||||
mGroupEditViewModel.requestDateTimeSelection(dateInstant)
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
retrieveGroupInfoFromViews()
|
||||
mEditGroupListener?.cancelEditGroup(
|
||||
mEditGroupDialogAction,
|
||||
mGroupInfo)
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
iconButtonView.setOnClickListener { _ ->
|
||||
IconPickerActivity.launch(activity, mGroupInfo.icon)
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
@@ -155,40 +169,34 @@ class GroupEditDialogFragment : DialogFragment() {
|
||||
super.onResume()
|
||||
|
||||
// To prevent auto dismiss
|
||||
val d = dialog as AlertDialog?
|
||||
if (d != null) {
|
||||
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||
val alertDialog = dialog as AlertDialog?
|
||||
if (alertDialog != null) {
|
||||
val positiveButton = alertDialog.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||
positiveButton.setOnClickListener {
|
||||
retrieveGroupInfoFromViews()
|
||||
if (isValid()) {
|
||||
mEditGroupListener?.approveEditGroup(
|
||||
mEditGroupDialogAction,
|
||||
mGroupInfo)
|
||||
d.dismiss()
|
||||
when (mEditGroupDialogAction) {
|
||||
CREATION ->
|
||||
mGroupEditViewModel.approveGroupCreation(mGroupInfo)
|
||||
UPDATE ->
|
||||
mGroupEditViewModel.approveGroupUpdate(mGroupInfo)
|
||||
NONE -> {}
|
||||
}
|
||||
alertDialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getExpiryTime(): DateInstant {
|
||||
retrieveGroupInfoFromViews()
|
||||
return mGroupInfo.expiryTime
|
||||
}
|
||||
|
||||
fun setExpiryTime(expiryTime: DateInstant) {
|
||||
mGroupInfo.expiryTime = expiryTime
|
||||
populateInfoToViews()
|
||||
}
|
||||
|
||||
private fun populateInfoToViews() {
|
||||
assignIconView()
|
||||
nameTextView.text = mGroupInfo.title
|
||||
notesTextLayoutView.visibility = if (mGroupInfo.notes == null) View.GONE else View.VISIBLE
|
||||
mGroupInfo.notes?.let {
|
||||
private fun populateInfoToViews(groupInfo: GroupInfo) {
|
||||
mGroupEditViewModel.selectIcon(groupInfo.icon)
|
||||
nameTextView.text = groupInfo.title
|
||||
notesTextLayoutView.visibility = if (groupInfo.notes == null) View.GONE else View.VISIBLE
|
||||
groupInfo.notes?.let {
|
||||
notesTextView.text = it
|
||||
}
|
||||
expirationView.expires = mGroupInfo.expires
|
||||
expirationView.expiryTime = mGroupInfo.expiryTime
|
||||
expirationView.activation = groupInfo.expires
|
||||
expirationView.dateTime = groupInfo.expiryTime
|
||||
}
|
||||
|
||||
private fun retrieveGroupInfoFromViews() {
|
||||
@@ -198,17 +206,8 @@ class GroupEditDialogFragment : DialogFragment() {
|
||||
if (newNotes.isNotEmpty()) {
|
||||
mGroupInfo.notes = newNotes
|
||||
}
|
||||
mGroupInfo.expires = expirationView.expires
|
||||
mGroupInfo.expiryTime = expirationView.expiryTime
|
||||
}
|
||||
|
||||
private fun assignIconView() {
|
||||
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconButtonView, mGroupInfo.icon, iconColor)
|
||||
}
|
||||
|
||||
fun setIcon(icon: IconImage) {
|
||||
mGroupInfo.icon = icon
|
||||
assignIconView()
|
||||
mGroupInfo.expires = expirationView.activation
|
||||
mGroupInfo.expiryTime = expirationView.dateTime
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -219,7 +218,21 @@ class GroupEditDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
private fun isValid(): Boolean {
|
||||
val error = mEditGroupListener?.isValidGroupName(nameTextView.text.toString()) ?: Error(false, null)
|
||||
val name = nameTextView.text.toString()
|
||||
val error = when {
|
||||
name.isEmpty() -> {
|
||||
Error(true, R.string.error_no_name)
|
||||
}
|
||||
mGroupNamesNotAllowed == null -> {
|
||||
Error(true, R.string.error_word_reserved)
|
||||
}
|
||||
mGroupNamesNotAllowed?.find { it.equals(name, ignoreCase = true) } != null -> {
|
||||
Error(true, R.string.error_word_reserved)
|
||||
}
|
||||
else -> {
|
||||
Error(false, null)
|
||||
}
|
||||
}
|
||||
error.messageId?.let { messageId ->
|
||||
nameTextLayoutView.error = getString(messageId)
|
||||
} ?: kotlin.run {
|
||||
@@ -230,14 +243,6 @@ class GroupEditDialogFragment : DialogFragment() {
|
||||
|
||||
data class Error(val isError: Boolean, val messageId: Int?)
|
||||
|
||||
interface EditGroupListener {
|
||||
fun isValidGroupName(name: String): Error
|
||||
fun approveEditGroup(action: EditGroupDialogAction,
|
||||
groupInfo: GroupInfo)
|
||||
fun cancelEditGroup(action: EditGroupDialogAction,
|
||||
groupInfo: GroupInfo)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
|
||||
@@ -25,14 +25,13 @@ 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() {
|
||||
class ReplaceFileDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mActionChooseListener: ActionChooseListener? = null
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -49,7 +48,7 @@ import com.kunzisoft.keepass.otp.TokenCalculator
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import java.util.*
|
||||
|
||||
class SetOTPDialogFragment : DialogFragment() {
|
||||
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mCreateOTPElementListener: CreateOtpListener? = null
|
||||
|
||||
@@ -80,11 +79,15 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
||||
if (!isFocus)
|
||||
mManualEvent = true
|
||||
else
|
||||
resetAppTimeout()
|
||||
}
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private var mOnTouchListener = View.OnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
mManualEvent = true
|
||||
resetAppTimeout()
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -95,6 +98,10 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
private var mPeriodWellFormed = false
|
||||
private var mDigitsWellFormed = false
|
||||
|
||||
override fun overrideTimeoutTouchAndFocusEvents(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
// Verify that the host activity implements the callback interface
|
||||
@@ -225,8 +232,11 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.apply {
|
||||
setView(root)
|
||||
.setPositiveButton(android.R.string.ok) {_, _ -> }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
resetAppTimeout()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
resetAppTimeout()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,16 +22,15 @@ package com.kunzisoft.keepass.activities.dialogs
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.RadioGroup
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
|
||||
class SortDialogFragment : DialogFragment() {
|
||||
class SortDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mListener: SortSelectionListener? = null
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.Bundle
|
||||
import android.text.format.DateFormat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
||||
// Not as DatabaseDialogFragment because crash on KitKat
|
||||
class TimePickerFragment : DialogFragment() {
|
||||
|
||||
private var defaultHour: Int = 0
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
|
||||
abstract class DatabaseFragment : StylishFragment(), DatabaseRetrieval {
|
||||
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
protected var mDatabase: Database? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
||||
if (mDatabase == null || mDatabase != database) {
|
||||
this.mDatabase = database
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
|
||||
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
|
||||
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
|
||||
context?.let {
|
||||
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
// Can be overridden by a subclass
|
||||
}
|
||||
|
||||
protected fun buildNewBinaryAttachment(): BinaryData? {
|
||||
return mDatabase?.buildNewBinaryAttachment()
|
||||
}
|
||||
}
|
||||
@@ -19,431 +19,264 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
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.EditText
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
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.EntryEditActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
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.model.*
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.ExpirationView
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.view.TemplateEditView
|
||||
import com.kunzisoft.keepass.view.collapse
|
||||
import com.kunzisoft.keepass.view.expand
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||
|
||||
class EntryEditFragment: StylishFragment() {
|
||||
class EntryEditFragment: DatabaseFragment() {
|
||||
|
||||
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 entryExpirationView: ExpirationView
|
||||
private lateinit var entryNotesView: EditText
|
||||
private lateinit var extraFieldsContainerView: View
|
||||
private lateinit var extraFieldsListView: ViewGroup
|
||||
private lateinit var attachmentsContainerView: View
|
||||
private val mEntryEditViewModel: EntryEditViewModel by activityViewModels()
|
||||
|
||||
private lateinit var rootView: View
|
||||
private lateinit var templateView: TemplateEditView
|
||||
private lateinit var attachmentsContainerView: ViewGroup
|
||||
private lateinit var attachmentsListView: RecyclerView
|
||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||
|
||||
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
|
||||
private var mTemplate: Template? = null
|
||||
private var mAllowMultipleAttachments: Boolean = false
|
||||
|
||||
private var fontInVisibility: Boolean = false
|
||||
private var iconColor: Int = 0
|
||||
private var mIconColor: Int = 0
|
||||
|
||||
var drawFactory: IconDrawableFactory? = null
|
||||
var setOnDateClickListener: (() -> Unit)? = null
|
||||
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
||||
var setOnIconViewClickListener: ((IconImage) -> Unit)? = 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? {
|
||||
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)
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
mIconColor = taIconColor?.getColor(0, Color.BLACK) ?: Color.BLACK
|
||||
taIconColor?.recycle()
|
||||
|
||||
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
|
||||
return inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_entry_edit, container, false)
|
||||
}
|
||||
|
||||
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?.invoke(mEntryInfo.icon)
|
||||
override fun onViewCreated(view: View,
|
||||
savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
rootView = view
|
||||
// Hide only the first time
|
||||
if (savedInstanceState == null) {
|
||||
view.isVisible = false
|
||||
}
|
||||
templateView = view.findViewById(R.id.template_view)
|
||||
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
||||
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
|
||||
|
||||
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)
|
||||
}
|
||||
entryExpirationView = rootView.findViewById(R.id.entry_edit_expiration)
|
||||
entryExpirationView.setOnDateClickListener = setOnDateClickListener
|
||||
|
||||
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())
|
||||
// TODO retrieve current database with its unique key
|
||||
attachmentsAdapter.database = Database.getInstance()
|
||||
//attachmentsAdapter.database = arguments?.getInt(KEY_DATABASE)
|
||||
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
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
rootView?.resetAppTimeoutWhenViewFocusedOrChanged(requireContext())
|
||||
|
||||
// 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
|
||||
templateView.apply {
|
||||
setOnIconClickListener {
|
||||
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
|
||||
}
|
||||
setOnCustomEditionActionClickListener { field ->
|
||||
mEntryEditViewModel.requestCustomFieldEdition(field)
|
||||
}
|
||||
setOnPasswordGenerationActionClickListener { field ->
|
||||
mEntryEditViewModel.requestPasswordSelection(field)
|
||||
}
|
||||
setOnDateInstantClickListener { dateInstant ->
|
||||
mEntryEditViewModel.requestDateTimeSelection(dateInstant)
|
||||
}
|
||||
}
|
||||
|
||||
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
|
||||
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
|
||||
if (savedInstanceState != null) {
|
||||
val attachments: List<Attachment> =
|
||||
savedInstanceState.getParcelableArrayList(ATTACHMENTS_TAG) ?: listOf()
|
||||
setAttachments(attachments)
|
||||
}
|
||||
|
||||
populateViewsWithEntry()
|
||||
mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template ->
|
||||
this.mTemplate = template
|
||||
templateView.setTemplate(template)
|
||||
}
|
||||
|
||||
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) {}
|
||||
mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry ->
|
||||
if (templateEntry != null) {
|
||||
val selectedTemplate = if (mTemplate != null)
|
||||
mTemplate
|
||||
else
|
||||
templateEntry.defaultTemplate
|
||||
templateView.setTemplate(selectedTemplate)
|
||||
// Load entry info only the first time to keep change locally
|
||||
if (savedInstanceState == null) {
|
||||
assignEntryInfo(templateEntry.entryInfo)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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?.assignDatabaseIcon(entryIconView, 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()
|
||||
// To prevent flickering
|
||||
rootView.showByFading()
|
||||
// Apply timeout reset
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||
}
|
||||
}
|
||||
|
||||
var expires: Boolean
|
||||
get() {
|
||||
return entryExpirationView.expires
|
||||
}
|
||||
set(value) {
|
||||
entryExpirationView.expires = value
|
||||
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
|
||||
mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, retrieveEntryInfo())
|
||||
}
|
||||
|
||||
var expiryTime: DateInstant
|
||||
get() {
|
||||
return entryExpirationView.expiryTime
|
||||
}
|
||||
set(value) {
|
||||
entryExpirationView.expiryTime = value
|
||||
mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { iconImage ->
|
||||
templateView.setIcon(iconImage)
|
||||
}
|
||||
|
||||
var notes: String
|
||||
get() {
|
||||
return entryNotesView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryNotesView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryNotesView.applyFontVisibility()
|
||||
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
|
||||
templateView.setPasswordField(passwordField)
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Extra Fields
|
||||
* -------------
|
||||
*/
|
||||
mEntryEditViewModel.onDateSelected.observe(viewLifecycleOwner) { viewModelDate ->
|
||||
// Save the date
|
||||
templateView.setCurrentDateTimeValue(viewModelDate)
|
||||
}
|
||||
|
||||
private var mExtraFieldsList: MutableList<Field> = ArrayList()
|
||||
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
|
||||
mEntryEditViewModel.onTimeSelected.observe(viewLifecycleOwner) { viewModelTime ->
|
||||
// Save the time
|
||||
templateView.setCurrentTimeValue(viewModelTime)
|
||||
}
|
||||
|
||||
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?.endIconMode = if (extraField.protectedValue.isProtected)
|
||||
TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE
|
||||
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
|
||||
mEntryEditViewModel.onCustomFieldEdited.observe(viewLifecycleOwner) { fieldAction ->
|
||||
val oldField = fieldAction.oldField
|
||||
val newField = fieldAction.newField
|
||||
// Field to add
|
||||
if (oldField == null) {
|
||||
newField?.let {
|
||||
if (!templateView.putCustomField(it)) {
|
||||
mEntryEditViewModel.showCustomFieldEditionError()
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
// Field to replace
|
||||
oldField?.let {
|
||||
newField?.let {
|
||||
if (!templateView.replaceCustomField(oldField, newField)) {
|
||||
mEntryEditViewModel.showCustomFieldEditionError()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Field to remove
|
||||
if (newField == null) {
|
||||
oldField?.let {
|
||||
templateView.removeCustomField(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
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))
|
||||
mEntryEditViewModel.requestSetupOtp.observe(viewLifecycleOwner) {
|
||||
// Retrieve the current otpElement if exists
|
||||
// and open the dialog to set up the OTP
|
||||
SetOTPDialogFragment.build(templateView.getEntryInfo().otpModel)
|
||||
.show(parentFragmentManager, "addOTPDialog")
|
||||
}
|
||||
// Request last focus
|
||||
mLastFocusedEditField?.let { focusField ->
|
||||
mExtraViewToRequestFocus?.apply {
|
||||
requestFocus()
|
||||
setSelection(focusField.cursorSelectionStart,
|
||||
focusField.cursorSelectionEnd)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.onOtpCreated.observe(viewLifecycleOwner) {
|
||||
// Update the otp field with otpauth:// url
|
||||
templateView.putOtpElement(it)
|
||||
}
|
||||
mLastFocusedEditField = null
|
||||
mOnEditButtonClickListener = onEditButtonClickListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an extra field or create a new one if doesn't exists, the old value is lost
|
||||
*/
|
||||
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()
|
||||
}
|
||||
}
|
||||
mEntryEditViewModel.onBuildNewAttachment.observe(viewLifecycleOwner) {
|
||||
val attachmentToUploadUri = it.attachmentToUploadUri
|
||||
val fileName = it.fileName
|
||||
|
||||
/**
|
||||
* Update an extra field and keep the old value
|
||||
*/
|
||||
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
|
||||
extraFieldsContainerView.visibility = View.VISIBLE
|
||||
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||
val oldValueEditText: EditText = extraFieldsListView.getChildAt(index)
|
||||
.findViewWithTag("FIELD_VALUE_TAG")
|
||||
val oldValue = oldValueEditText.text.toString()
|
||||
val newExtraFieldWithOldValue = Field(newExtraField).apply {
|
||||
this.protectedValue.stringValue = oldValue
|
||||
}
|
||||
mExtraFieldsList.removeAt(index)
|
||||
mExtraFieldsList.add(index, newExtraFieldWithOldValue)
|
||||
extraFieldsListView.removeViewAt(index)
|
||||
val newView = buildViewFromField(newExtraFieldWithOldValue)
|
||||
extraFieldsListView.addView(newView, index)
|
||||
newView?.requestFocus()
|
||||
}
|
||||
|
||||
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)
|
||||
buildNewBinaryAttachment()?.let { binaryAttachment ->
|
||||
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||
// Ask to replace the current attachment
|
||||
if ((!mAllowMultipleAttachments
|
||||
&& containsAttachment()) ||
|
||||
containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD))) {
|
||||
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
|
||||
.show(parentFragmentManager, "replacementFileFragment")
|
||||
} else {
|
||||
mEntryEditViewModel.startUploadAttachment(attachmentToUploadUri, entryAttachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mEntryEditViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
|
||||
when (entryAttachmentState?.downloadState) {
|
||||
AttachmentState.START -> {
|
||||
putAttachment(entryAttachmentState)
|
||||
getAttachmentViewPosition(entryAttachmentState) { attachment, position ->
|
||||
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
|
||||
}
|
||||
}
|
||||
AttachmentState.IN_PROGRESS -> {
|
||||
putAttachment(entryAttachmentState)
|
||||
}
|
||||
AttachmentState.COMPLETE -> {
|
||||
putAttachment(entryAttachmentState) { entryAttachment ->
|
||||
getAttachmentViewPosition(entryAttachment) { attachment, position ->
|
||||
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
|
||||
}
|
||||
}
|
||||
mEntryEditViewModel.onAttachmentAction(null)
|
||||
}
|
||||
AttachmentState.CANCELED,
|
||||
AttachmentState.ERROR -> {
|
||||
removeAttachment(entryAttachmentState)
|
||||
mEntryEditViewModel.onAttachmentAction(null)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
|
||||
templateView.populateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
|
||||
mAllowMultipleAttachments = database?.allowMultipleAttachments == true
|
||||
|
||||
attachmentsAdapter?.database = database
|
||||
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
||||
if (previousSize > 0 && newSize == 0) {
|
||||
attachmentsContainerView.collapse(true)
|
||||
} else if (previousSize == 0 && newSize == 1) {
|
||||
attachmentsContainerView.expand(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||
// Populate entry views
|
||||
templateView.setEntryInfo(entryInfo)
|
||||
|
||||
// Manage attachments
|
||||
setAttachments(entryInfo?.attachments ?: listOf())
|
||||
}
|
||||
|
||||
private fun retrieveEntryInfo(): EntryInfo {
|
||||
val entryInfo = templateView.getEntryInfo()
|
||||
entryInfo.attachments = getAttachments().toMutableList()
|
||||
return entryInfo
|
||||
}
|
||||
|
||||
/* -------------
|
||||
@@ -451,78 +284,84 @@ class EntryEditFragment: StylishFragment() {
|
||||
* -------------
|
||||
*/
|
||||
|
||||
fun getAttachments(): List<Attachment> {
|
||||
return attachmentsAdapter.itemsList.map { it.attachment }
|
||||
private fun getAttachments(): List<Attachment> {
|
||||
return attachmentsAdapter?.itemsList?.map { it.attachment } ?: listOf()
|
||||
}
|
||||
|
||||
fun assignAttachments(attachments: List<Attachment>,
|
||||
streamDirection: StreamDirection,
|
||||
onDeleteItem: (attachment: Attachment)->Unit) {
|
||||
private fun setAttachments(attachments: List<Attachment>) {
|
||||
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
||||
onDeleteItem.invoke(item.attachment)
|
||||
attachmentsAdapter?.assignItems(attachments.map {
|
||||
EntryAttachmentState(it, StreamDirection.UPLOAD)
|
||||
})
|
||||
attachmentsAdapter?.onDeleteButtonClickListener = { item ->
|
||||
val attachment = item.attachment
|
||||
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
|
||||
mEntryEditViewModel.deleteAttachment(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun containsAttachment(): Boolean {
|
||||
return !attachmentsAdapter.isEmpty()
|
||||
private fun containsAttachment(): Boolean {
|
||||
return attachmentsAdapter?.isEmpty() != true
|
||||
}
|
||||
|
||||
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||
return attachmentsAdapter.contains(attachment)
|
||||
private fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||
return attachmentsAdapter?.contains(attachment) ?: false
|
||||
}
|
||||
|
||||
fun putAttachment(attachment: EntryAttachmentState,
|
||||
onPreviewLoaded: (()-> Unit)? = null) {
|
||||
private fun putAttachment(attachment: EntryAttachmentState,
|
||||
onPreviewLoaded: ((attachment: EntryAttachmentState) -> Unit)? = null) {
|
||||
// When only one attachment is allowed
|
||||
if (!mAllowMultipleAttachments
|
||||
&& attachment.downloadState == AttachmentState.START) {
|
||||
attachmentsAdapter?.clear()
|
||||
}
|
||||
attachmentsContainerView.visibility = View.VISIBLE
|
||||
attachmentsAdapter.putItem(attachment)
|
||||
attachmentsAdapter.onBinaryPreviewLoaded = {
|
||||
onPreviewLoaded?.invoke()
|
||||
attachmentsAdapter?.putItem(attachment)
|
||||
attachmentsAdapter?.onBinaryPreviewLoaded = {
|
||||
onPreviewLoaded?.invoke(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAttachment(attachment: EntryAttachmentState) {
|
||||
attachmentsAdapter.removeItem(attachment)
|
||||
private fun removeAttachment(attachment: EntryAttachmentState) {
|
||||
attachmentsAdapter?.removeItem(attachment)
|
||||
}
|
||||
|
||||
fun clearAttachments() {
|
||||
attachmentsAdapter.clear()
|
||||
}
|
||||
|
||||
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
|
||||
private fun getAttachmentViewPosition(attachment: EntryAttachmentState,
|
||||
position: (attachment: EntryAttachmentState, Float) -> Unit) {
|
||||
attachmentsListView.postDelayed({
|
||||
position.invoke(attachmentsContainerView.y
|
||||
+ attachmentsListView.y
|
||||
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
|
||||
?: 0F)
|
||||
)
|
||||
attachmentsAdapter?.indexOf(attachment)?.let { index ->
|
||||
position.invoke(attachment,
|
||||
attachmentsContainerView.y
|
||||
+ attachmentsListView.y
|
||||
+ (attachmentsListView.getChildAt(index)?.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)
|
||||
outState.putParcelableArrayList(ATTACHMENTS_TAG, ArrayList(getAttachments()))
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Education
|
||||
* -------------
|
||||
*/
|
||||
|
||||
fun getActionImageView(): View? {
|
||||
return templateView.getActionImageView()
|
||||
}
|
||||
|
||||
fun launchGeneratePasswordEductionAction() {
|
||||
mEntryEditViewModel.requestPasswordSelection(templateView.getPasswordField())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
||||
const val KEY_DATABASE = "KEY_DATABASE"
|
||||
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
||||
private val TAG = EntryEditFragment::class.java.name
|
||||
|
||||
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
||||
//database: Database?): EntryEditFragment {
|
||||
return EntryEditFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
||||
// TODO Unique database key database.key
|
||||
putInt(KEY_DATABASE, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
private const val ATTACHMENTS_TAG = "ATTACHMENTS_TAG"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.view.TemplateView
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
import java.util.*
|
||||
|
||||
class EntryFragment: DatabaseFragment() {
|
||||
|
||||
private lateinit var rootView: View
|
||||
private lateinit var templateView: TemplateView
|
||||
|
||||
private lateinit var creationDateView: TextView
|
||||
private lateinit var modificationDateView: TextView
|
||||
|
||||
private lateinit var attachmentsContainerView: View
|
||||
private lateinit var attachmentsListView: RecyclerView
|
||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||
|
||||
private lateinit var uuidContainerView: View
|
||||
private lateinit var uuidView: TextView
|
||||
private lateinit var uuidReferenceView: TextView
|
||||
|
||||
private var mClipboardHelper: ClipboardHelper? = null
|
||||
|
||||
private val mEntryViewModel: EntryViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
return inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_entry, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View,
|
||||
savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
context?.let { context ->
|
||||
mClipboardHelper = ClipboardHelper(context)
|
||||
}
|
||||
|
||||
rootView = view
|
||||
// Hide only the first time
|
||||
if (savedInstanceState == null) {
|
||||
view.isVisible = false
|
||||
}
|
||||
templateView = view.findViewById(R.id.entry_template)
|
||||
loadTemplateSettings()
|
||||
|
||||
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
||||
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
|
||||
attachmentsListView.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
|
||||
creationDateView = view.findViewById(R.id.entry_created)
|
||||
modificationDateView = view.findViewById(R.id.entry_modified)
|
||||
|
||||
uuidContainerView = view.findViewById(R.id.entry_UUID_container)
|
||||
uuidContainerView.apply {
|
||||
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||
}
|
||||
uuidView = view.findViewById(R.id.entry_UUID)
|
||||
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
||||
|
||||
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
|
||||
if (entryInfoHistory != null) {
|
||||
templateView.setTemplate(entryInfoHistory.template)
|
||||
assignEntryInfo(entryInfoHistory.entryInfo)
|
||||
// Smooth appearing
|
||||
rootView.showByFading()
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||
}
|
||||
}
|
||||
|
||||
mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
|
||||
entryAttachmentState?.let {
|
||||
if (it.streamDirection != StreamDirection.UPLOAD) {
|
||||
putAttachment(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
context?.let { context ->
|
||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||
attachmentsAdapter?.database = database
|
||||
}
|
||||
|
||||
attachmentsListView.adapter = attachmentsAdapter
|
||||
}
|
||||
|
||||
private fun loadTemplateSettings() {
|
||||
context?.let { context ->
|
||||
templateView.setFirstTimeAskAllowCopyProtectedFields(PreferencesUtil.isFirstTimeAskAllowCopyProtectedFields(context))
|
||||
templateView.setAllowCopyProtectedFields(PreferencesUtil.allowCopyProtectedFields(context))
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||
// Set copy buttons
|
||||
templateView.apply {
|
||||
setOnAskCopySafeClickListener {
|
||||
showClipboardDialog()
|
||||
}
|
||||
|
||||
setOnCopyActionClickListener { field ->
|
||||
mClipboardHelper?.timeoutCopyToClipboard(
|
||||
field.protectedValue.stringValue,
|
||||
getString(
|
||||
R.string.copy_field,
|
||||
TemplateField.getLocalizedName(context, field.name)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Populate entry views
|
||||
templateView.setEntryInfo(entryInfo)
|
||||
|
||||
// OTP timer updated
|
||||
templateView.setOnOtpElementUpdated { otpElementUpdated ->
|
||||
mEntryViewModel.onOtpElementUpdated(otpElementUpdated)
|
||||
}
|
||||
|
||||
// Manage attachments
|
||||
assignAttachments(entryInfo?.attachments ?: listOf())
|
||||
|
||||
// Assign dates
|
||||
assignCreationDate(entryInfo?.creationTime)
|
||||
assignModificationDate(entryInfo?.lastModificationTime)
|
||||
|
||||
// Assign special data
|
||||
assignUUID(entryInfo?.id)
|
||||
}
|
||||
|
||||
private fun showClipboardDialog() {
|
||||
context?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setMessage(
|
||||
getString(R.string.allow_copy_password_warning) +
|
||||
"\n\n" +
|
||||
getString(R.string.clipboard_warning)
|
||||
)
|
||||
.create().apply {
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, true)
|
||||
finishDialog(dialog)
|
||||
}
|
||||
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, false)
|
||||
finishDialog(dialog)
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishDialog(dialog: DialogInterface) {
|
||||
dialog.dismiss()
|
||||
loadTemplateSettings()
|
||||
templateView.reload()
|
||||
}
|
||||
|
||||
private fun assignCreationDate(date: DateInstant?) {
|
||||
creationDateView.text = date?.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
private fun assignModificationDate(date: DateInstant?) {
|
||||
modificationDateView.text = date?.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
private fun assignUUID(uuid: UUID?) {
|
||||
uuidView.text = uuid?.toString()
|
||||
uuidReferenceView.text = UuidUtil.toHexString(uuid)
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Attachments
|
||||
* -------------
|
||||
*/
|
||||
|
||||
private fun assignAttachments(attachments: List<Attachment>) {
|
||||
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||
attachmentsAdapter?.assignItems(attachments.map {
|
||||
EntryAttachmentState(it, StreamDirection.DOWNLOAD)
|
||||
})
|
||||
attachmentsAdapter?.onItemClickListener = { item ->
|
||||
mEntryViewModel.onAttachmentSelected(item.attachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun putAttachment(attachmentToDownload: EntryAttachmentState) {
|
||||
attachmentsAdapter?.putItem(attachmentToDownload)
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Education
|
||||
* -------------
|
||||
*/
|
||||
|
||||
fun firstEntryFieldCopyView(): View? {
|
||||
return try {
|
||||
templateView.getActionImageView()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun launchEntryCopyEducationAction() {
|
||||
val appNameString = getString(R.string.app_name)
|
||||
mClipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||
getString(R.string.copy_field, appNameString))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun getInstance(): EntryFragment {
|
||||
return EntryFragment().apply {
|
||||
arguments = Bundle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
|
||||
class EntryHistoryFragment: StylishFragment() {
|
||||
|
||||
private lateinit var historyContainerView: View
|
||||
private lateinit var historyListView: RecyclerView
|
||||
private var historyAdapter: EntryHistoryAdapter? = null
|
||||
|
||||
private val mEntryViewModel: EntryViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
return inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_entry_history, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
context?.let { context ->
|
||||
historyAdapter = EntryHistoryAdapter(context)
|
||||
}
|
||||
|
||||
historyContainerView = view.findViewById(R.id.entry_history_container)
|
||||
historyListView = view.findViewById(R.id.entry_history_list)
|
||||
historyListView.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
|
||||
adapter = historyAdapter
|
||||
}
|
||||
|
||||
mEntryViewModel.entryHistory.observe(viewLifecycleOwner) {
|
||||
assignHistory(it)
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* History
|
||||
* -------------
|
||||
*/
|
||||
private fun assignHistory(history: List<EntryInfo>?) {
|
||||
historyAdapter?.clear()
|
||||
history?.let {
|
||||
historyAdapter?.entryHistoryList?.addAll(history)
|
||||
}
|
||||
historyAdapter?.onItemClickListener = { item, position ->
|
||||
mEntryViewModel.onHistorySelected(item, position)
|
||||
}
|
||||
historyContainerView.visibility = if (historyAdapter?.entryHistoryList?.isEmpty() != false)
|
||||
View.GONE
|
||||
else
|
||||
View.VISIBLE
|
||||
historyAdapter?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -25,34 +25,40 @@ import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.EntryEditActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.viewmodels.GroupViewModel
|
||||
import java.util.*
|
||||
|
||||
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
||||
class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListener {
|
||||
|
||||
private var nodeClickListener: NodeClickListener? = null
|
||||
private var onScrollListener: OnScrollListener? = null
|
||||
|
||||
private var mNodesRecyclerView: RecyclerView? = null
|
||||
var mainGroup: Group? = null
|
||||
private set
|
||||
private var mLayoutManager: LinearLayoutManager? = null
|
||||
private var mAdapter: NodeAdapter? = null
|
||||
|
||||
private val mGroupViewModel: GroupViewModel by activityViewModels()
|
||||
|
||||
private var mCurrentGroup: Group? = null
|
||||
|
||||
var nodeActionSelectionMode = false
|
||||
private set
|
||||
var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED
|
||||
@@ -63,12 +69,23 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
private var notFoundView: View? = null
|
||||
private var isASearchResult: Boolean = false
|
||||
|
||||
|
||||
private var readOnly: Boolean = false
|
||||
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
||||
private var mRecycleBinEnable: Boolean = false
|
||||
private var mRecycleBin: Group? = null
|
||||
|
||||
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
if (newState == SCROLL_STATE_IDLE) {
|
||||
mGroupViewModel.assignPosition(getFirstVisiblePosition())
|
||||
}
|
||||
}
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
onScrollListener?.onScrolled(dy)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
@@ -100,128 +117,135 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
|
||||
|
||||
arguments?.let { args ->
|
||||
// Contains all the group in element
|
||||
if (args.containsKey(GROUP_KEY)) {
|
||||
mainGroup = args.getParcelable(GROUP_KEY)
|
||||
}
|
||||
if (args.containsKey(IS_SEARCH)) {
|
||||
isASearchResult = args.getBoolean(IS_SEARCH)
|
||||
}
|
||||
}
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
mRecycleBinEnable = database?.isRecycleBinEnabled == true
|
||||
mRecycleBin = database?.recycleBin
|
||||
|
||||
contextThemed?.let { context ->
|
||||
mAdapter = NodeAdapter(context)
|
||||
mAdapter?.apply {
|
||||
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
||||
override fun onNodeClick(node: Node) {
|
||||
if (nodeActionSelectionMode) {
|
||||
if (listActionNodes.contains(node)) {
|
||||
// Remove selected item if already selected
|
||||
listActionNodes.remove(node)
|
||||
database?.let { database ->
|
||||
mAdapter = NodeAdapter(context, database).apply {
|
||||
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
||||
override fun onNodeClick(database: Database, node: Node) {
|
||||
if (nodeActionSelectionMode) {
|
||||
if (listActionNodes.contains(node)) {
|
||||
// Remove selected item if already selected
|
||||
listActionNodes.remove(node)
|
||||
} else {
|
||||
// Add selected item if not already selected
|
||||
listActionNodes.add(node)
|
||||
}
|
||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
} else {
|
||||
// Add selected item if not already selected
|
||||
listActionNodes.add(node)
|
||||
nodeClickListener?.onNodeClick(database, node)
|
||||
}
|
||||
nodeClickListener?.onNodeSelected(listActionNodes)
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
} else {
|
||||
nodeClickListener?.onNodeClick(node)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNodeLongClick(node: Node): Boolean {
|
||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||
// Select the first item after a long click
|
||||
if (!listActionNodes.contains(node))
|
||||
listActionNodes.add(node)
|
||||
override fun onNodeLongClick(database: Database, node: Node): Boolean {
|
||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||
// Select the first item after a long click
|
||||
if (!listActionNodes.contains(node))
|
||||
listActionNodes.add(node)
|
||||
|
||||
nodeClickListener?.onNodeSelected(listActionNodes)
|
||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
mNodesRecyclerView?.adapter = mAdapter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||
super.onSaveInstanceState(outState)
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
|
||||
// Too many special cases to make specific additions or deletions,
|
||||
// rebuilt the list works well.
|
||||
if (result.isSuccess) {
|
||||
rebuildList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// To apply theme
|
||||
val rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_list_nodes, container, false)
|
||||
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list)
|
||||
notFoundView = rootView.findViewById(R.id.not_found_container)
|
||||
return inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_group, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
mNodesRecyclerView = view.findViewById(R.id.nodes_list)
|
||||
notFoundView = view.findViewById(R.id.not_found_container)
|
||||
|
||||
mLayoutManager = LinearLayoutManager(context)
|
||||
mNodesRecyclerView?.apply {
|
||||
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
layoutManager = mLayoutManager
|
||||
adapter = mAdapter
|
||||
}
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||
|
||||
onScrollListener?.let { onScrollListener ->
|
||||
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
onScrollListener.onScrolled(dy)
|
||||
}
|
||||
})
|
||||
mGroupViewModel.group.observe(viewLifecycleOwner) {
|
||||
mCurrentGroup = it.group
|
||||
isASearchResult = it.group.isVirtual
|
||||
rebuildList()
|
||||
it.showFromPosition?.let { position ->
|
||||
mNodesRecyclerView?.scrollToPosition(position)
|
||||
}
|
||||
}
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
|
||||
activity?.intent?.let {
|
||||
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
||||
}
|
||||
|
||||
// Refresh data
|
||||
try {
|
||||
rebuildList()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to rebuild the list during resume")
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
||||
// To show the " no search entry found "
|
||||
mNodesRecyclerView?.visibility = View.GONE
|
||||
notFoundView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
mNodesRecyclerView?.visibility = View.VISIBLE
|
||||
notFoundView?.visibility = View.GONE
|
||||
}
|
||||
rebuildList()
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun rebuildList() {
|
||||
// Add elements to the list
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.apply {
|
||||
override fun onPause() {
|
||||
|
||||
mNodesRecyclerView?.removeOnScrollListener(mRecycleViewScrollListener)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
fun getFirstVisiblePosition(): Int {
|
||||
return mLayoutManager?.findFirstVisibleItemPosition() ?: 0
|
||||
}
|
||||
|
||||
private fun rebuildList() {
|
||||
try {
|
||||
// Add elements to the list
|
||||
mCurrentGroup?.let { mainGroup ->
|
||||
// Thrown an exception when sort cannot be performed
|
||||
rebuildList(mainGroup)
|
||||
// To visually change the elements
|
||||
if (PreferencesUtil.APPEARANCE_CHANGED) {
|
||||
notifyDataSetChanged()
|
||||
PreferencesUtil.APPEARANCE_CHANGED = false
|
||||
}
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
}
|
||||
} catch (e:Exception) {
|
||||
Log.e(TAG, "Unable to rebuild the list", e)
|
||||
}
|
||||
|
||||
if (isASearchResult && mAdapter != null && mAdapter!!.isEmpty) {
|
||||
// To show the " no search entry found "
|
||||
notFoundView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
notFoundView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,8 +261,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
|
||||
rebuildList()
|
||||
} catch (e:Exception) {
|
||||
Log.e(TAG, "Unable to rebuild the list with the sort")
|
||||
e.printStackTrace()
|
||||
Log.e(TAG, "Unable to sort the list", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +277,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
R.id.menu_sort -> {
|
||||
context?.let { context ->
|
||||
val sortDialogFragment: SortDialogFragment =
|
||||
if (Database.getInstance().isRecycleBinEnabled) {
|
||||
if (mRecycleBinEnable) {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
@@ -276,34 +299,32 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
}
|
||||
|
||||
fun actionNodesCallback(nodes: List<Node>,
|
||||
fun actionNodesCallback(database: Database,
|
||||
nodes: List<Node>,
|
||||
menuListener: NodesActionMenuListener?,
|
||||
actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
|
||||
onDestroyActionMode: (mode: ActionMode?) -> Unit) : ActionMode.Callback {
|
||||
|
||||
return object : ActionMode.Callback {
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
nodeActionSelectionMode = false
|
||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||
return actionModeCallback.onCreateActionMode(mode, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
menu?.clear()
|
||||
|
||||
if (nodeActionPasteMode != PasteMode.UNDEFINED) {
|
||||
mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu)
|
||||
} else {
|
||||
nodeActionSelectionMode = true
|
||||
mode?.menuInflater?.inflate(R.menu.node_menu, menu)
|
||||
|
||||
val database = Database.getInstance()
|
||||
|
||||
// Open and Edit for a single item
|
||||
if (nodes.size == 1) {
|
||||
// Edition
|
||||
if (readOnly
|
||||
|| (database.isRecycleBinEnabled && nodes[0] == database.recycleBin)) {
|
||||
if (database.isReadOnly
|
||||
|| (mRecycleBinEnable && nodes[0] == mRecycleBin)) {
|
||||
menu?.removeItem(R.id.menu_edit)
|
||||
}
|
||||
} else {
|
||||
@@ -312,59 +333,58 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
|
||||
// Move
|
||||
if (readOnly
|
||||
if (database.isReadOnly
|
||||
|| isASearchResult) {
|
||||
menu?.removeItem(R.id.menu_move)
|
||||
}
|
||||
|
||||
// Copy (not allowed for group)
|
||||
if (readOnly
|
||||
if (database.isReadOnly
|
||||
|| isASearchResult
|
||||
|| nodes.any { it.type == Type.GROUP }) {
|
||||
menu?.removeItem(R.id.menu_copy)
|
||||
}
|
||||
|
||||
// Deletion
|
||||
if (readOnly
|
||||
|| (database.isRecycleBinEnabled && nodes.any { it == database.recycleBin })) {
|
||||
if (database.isReadOnly
|
||||
|| (mRecycleBinEnable && nodes.any { it == mRecycleBin })) {
|
||||
menu?.removeItem(R.id.menu_delete)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the number of items selected in title
|
||||
mode?.title = nodes.size.toString()
|
||||
|
||||
return actionModeCallback.onPrepareActionMode(mode, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
if (menuListener == null)
|
||||
return false
|
||||
return when (item?.itemId) {
|
||||
R.id.menu_open -> menuListener.onOpenMenuClick(nodes[0])
|
||||
R.id.menu_edit -> menuListener.onEditMenuClick(nodes[0])
|
||||
R.id.menu_open -> menuListener.onOpenMenuClick(database, nodes[0])
|
||||
R.id.menu_edit -> menuListener.onEditMenuClick(database, nodes[0])
|
||||
R.id.menu_copy -> {
|
||||
nodeActionPasteMode = PasteMode.PASTE_FROM_COPY
|
||||
mAdapter?.unselectActionNodes()
|
||||
val returnValue = menuListener.onCopyMenuClick(nodes)
|
||||
val returnValue = menuListener.onCopyMenuClick(database, nodes)
|
||||
nodeActionSelectionMode = false
|
||||
returnValue
|
||||
}
|
||||
R.id.menu_move -> {
|
||||
nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE
|
||||
mAdapter?.unselectActionNodes()
|
||||
val returnValue = menuListener.onMoveMenuClick(nodes)
|
||||
val returnValue = menuListener.onMoveMenuClick(database, nodes)
|
||||
nodeActionSelectionMode = false
|
||||
returnValue
|
||||
}
|
||||
R.id.menu_delete -> menuListener.onDeleteMenuClick(nodes)
|
||||
R.id.menu_delete -> menuListener.onDeleteMenuClick(database, nodes)
|
||||
R.id.menu_paste -> {
|
||||
val returnValue = menuListener.onPasteMenuClick(nodeActionPasteMode, nodes)
|
||||
val returnValue = menuListener.onPasteMenuClick(database, nodeActionPasteMode, nodes)
|
||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||
nodeActionSelectionMode = false
|
||||
returnValue
|
||||
}
|
||||
else -> actionModeCallback.onActionItemClicked(mode, item)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +394,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
mAdapter?.unselectActionNodes()
|
||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||
nodeActionSelectionMode = false
|
||||
actionModeCallback.onDestroyActionMode(mode)
|
||||
onDestroyActionMode(mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,73 +404,40 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|
||||
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
||||
addNode(changedNode)
|
||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
|
||||
mAdapter?.notifyDataSetChanged()
|
||||
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
||||
if (resultCode == EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE) {
|
||||
data?.getParcelableExtra<NodeId<UUID>>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let {
|
||||
// Simply refresh the list
|
||||
rebuildList()
|
||||
// Scroll to the new entry
|
||||
mDatabase?.getEntryById(it)?.let { entry ->
|
||||
mAdapter?.indexOf(entry)?.let { position ->
|
||||
mNodesRecyclerView?.scrollToPosition(position)
|
||||
}
|
||||
}
|
||||
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun contains(node: Node): Boolean {
|
||||
return mAdapter?.contains(node) ?: false
|
||||
}
|
||||
|
||||
fun addNode(newNode: Node) {
|
||||
mAdapter?.addNode(newNode)
|
||||
}
|
||||
|
||||
fun addNodes(newNodes: List<Node>) {
|
||||
mAdapter?.addNodes(newNodes)
|
||||
}
|
||||
|
||||
fun updateNode(oldNode: Node, newNode: Node? = null) {
|
||||
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
|
||||
}
|
||||
|
||||
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
||||
mAdapter?.updateNodes(oldNodes, newNodes)
|
||||
}
|
||||
|
||||
fun removeNode(pwNode: Node) {
|
||||
mAdapter?.removeNode(pwNode)
|
||||
}
|
||||
|
||||
fun removeNodes(nodes: List<Node>) {
|
||||
mAdapter?.removeNodes(nodes)
|
||||
}
|
||||
|
||||
fun removeNodeAt(position: Int) {
|
||||
mAdapter?.removeNodeAt(position)
|
||||
}
|
||||
|
||||
fun removeNodesAt(positions: IntArray) {
|
||||
mAdapter?.removeNodesAt(positions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
interface NodeClickListener {
|
||||
fun onNodeClick(node: Node)
|
||||
fun onNodeSelected(nodes: List<Node>): Boolean
|
||||
fun onNodeClick(database: Database, node: Node)
|
||||
fun onNodeSelected(database: Database, nodes: List<Node>): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu listener to redefine to do an action in menu
|
||||
*/
|
||||
interface NodesActionMenuListener {
|
||||
fun onOpenMenuClick(node: Node): Boolean
|
||||
fun onEditMenuClick(node: Node): Boolean
|
||||
fun onCopyMenuClick(nodes: List<Node>): Boolean
|
||||
fun onMoveMenuClick(nodes: List<Node>): Boolean
|
||||
fun onDeleteMenuClick(nodes: List<Node>): Boolean
|
||||
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
||||
fun onOpenMenuClick(database: Database, node: Node): Boolean
|
||||
fun onEditMenuClick(database: Database, node: Node): Boolean
|
||||
fun onCopyMenuClick(database: Database, nodes: List<Node>): Boolean
|
||||
fun onMoveMenuClick(database: Database, nodes: List<Node>): Boolean
|
||||
fun onDeleteMenuClick(database: Database, nodes: List<Node>): Boolean
|
||||
fun onPasteMenuClick(database: Database, pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
||||
}
|
||||
|
||||
enum class PasteMode {
|
||||
@@ -469,22 +456,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = ListNodesFragment::class.java.name
|
||||
|
||||
private const val GROUP_KEY = "GROUP_KEY"
|
||||
private const val IS_SEARCH = "IS_SEARCH"
|
||||
|
||||
fun newInstance(group: Group?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
|
||||
val bundle = Bundle()
|
||||
if (group != null) {
|
||||
bundle.putParcelable(GROUP_KEY, group)
|
||||
}
|
||||
bundle.putBoolean(IS_SEARCH, isASearch)
|
||||
ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly)
|
||||
val listNodesFragment = ListNodesFragment()
|
||||
listNodesFragment.arguments = bundle
|
||||
return listNodesFragment
|
||||
}
|
||||
private val TAG = GroupFragment::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.activities.fragments
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
|
||||
|
||||
@@ -31,8 +32,8 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
|
||||
return R.layout.fragment_icon_grid
|
||||
}
|
||||
|
||||
override fun defineIconList() {
|
||||
mDatabase?.doForEachCustomIcons { customIcon, _ ->
|
||||
override fun defineIconList(database: Database?) {
|
||||
database?.doForEachCustomIcons { customIcon, _ ->
|
||||
iconPickerAdapter.addIcon(customIcon, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@@ -28,7 +27,6 @@ import android.view.ViewGroup
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.adapters.IconPickerAdapter
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
||||
@@ -38,39 +36,48 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
|
||||
abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
||||
IconPickerAdapter.IconPickerListener<T> {
|
||||
|
||||
protected lateinit var iconsGridView: RecyclerView
|
||||
protected lateinit var iconPickerAdapter: IconPickerAdapter<T>
|
||||
protected var iconActionSelectionMode = false
|
||||
|
||||
protected var mDatabase: Database? = null
|
||||
|
||||
protected val iconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||
|
||||
abstract fun retrieveMainLayoutId(): Int
|
||||
|
||||
abstract fun defineIconList()
|
||||
abstract fun defineIconList(database: Database?)
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
override fun onCreateView(inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(retrieveMainLayoutId(), container, false)
|
||||
}
|
||||
|
||||
mDatabase = Database.getInstance()
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
|
||||
ta?.recycle()
|
||||
|
||||
iconPickerAdapter = IconPickerAdapter<T>(context, tintColor).apply {
|
||||
iconDrawableFactory = mDatabase?.iconDrawableFactory
|
||||
}
|
||||
iconsGridView = view.findViewById(R.id.icons_grid_view)
|
||||
iconPickerAdapter = IconPickerAdapter(requireContext(), tintColor)
|
||||
iconPickerAdapter.iconPickerListener = this
|
||||
iconsGridView.adapter = iconPickerAdapter
|
||||
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val populateList = launch {
|
||||
iconPickerAdapter.clear()
|
||||
defineIconList()
|
||||
defineIconList(database)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
populateList.join()
|
||||
@@ -79,21 +86,6 @@ abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View {
|
||||
val root = inflater.inflate(retrieveMainLayoutId(), container, false)
|
||||
iconsGridView = root.findViewById(R.id.icons_grid_view)
|
||||
iconsGridView.adapter = iconPickerAdapter
|
||||
return root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
iconPickerAdapter.iconPickerListener = this
|
||||
}
|
||||
|
||||
fun onIconDeleteClicked() {
|
||||
iconActionSelectionMode = false
|
||||
}
|
||||
|
||||
@@ -9,20 +9,18 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||
|
||||
class IconPickerFragment : StylishFragment() {
|
||||
class IconPickerFragment : DatabaseFragment() {
|
||||
|
||||
private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null
|
||||
private lateinit var viewPager: ViewPager2
|
||||
private lateinit var tabLayout: TabLayout
|
||||
|
||||
private val iconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -32,19 +30,11 @@ class IconPickerFragment : StylishFragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
mDatabase = Database.getInstance()
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewPager = view.findViewById(R.id.icon_picker_pager)
|
||||
val tabLayout = view.findViewById<TabLayout>(R.id.icon_picker_tabs)
|
||||
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||
if (mDatabase?.allowCustomIcons == true) 2 else 1)
|
||||
viewPager.adapter = iconPickerPagerAdapter
|
||||
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||
tab.text = when (position) {
|
||||
1 -> getString(R.string.icon_section_custom)
|
||||
else -> getString(R.string.icon_section_standard)
|
||||
}
|
||||
}.attach()
|
||||
tabLayout = view.findViewById(R.id.icon_picker_tabs)
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||
|
||||
arguments?.apply {
|
||||
if (containsKey(ICON_TAB_ARG)) {
|
||||
@@ -58,6 +48,18 @@ class IconPickerFragment : StylishFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||
if (database?.allowCustomIcons == true) 2 else 1)
|
||||
viewPager.adapter = iconPickerPagerAdapter
|
||||
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||
tab.text = when (position) {
|
||||
1 -> getString(R.string.icon_section_custom)
|
||||
else -> getString(R.string.icon_section_standard)
|
||||
}
|
||||
}.attach()
|
||||
}
|
||||
|
||||
enum class IconTab {
|
||||
STANDARD, CUSTOM
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
|
||||
|
||||
@@ -29,8 +30,8 @@ class IconStandardFragment : IconFragment<IconImageStandard>() {
|
||||
return R.layout.fragment_icon_grid
|
||||
}
|
||||
|
||||
override fun defineIconList() {
|
||||
mDatabase?.doForEachStandardIcons { standardIcon ->
|
||||
override fun defineIconList(database: Database?) {
|
||||
database?.doForEachStandardIcons { standardIcon ->
|
||||
iconPickerAdapter.addIcon(standardIcon, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,17 +127,7 @@ class ExternalFileHelper {
|
||||
if (data != null) {
|
||||
val uri = data.data
|
||||
if (uri != null) {
|
||||
try {
|
||||
// try to persist read and write permissions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
activity?.contentResolver?.apply {
|
||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// nop
|
||||
}
|
||||
UriUtil.takeUriPermission(activity?.contentResolver, uri)
|
||||
onFileSelected?.invoke(uri)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +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.activities.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
object ReadOnlyHelper {
|
||||
|
||||
private const val READ_ONLY_KEY = "READ_ONLY_KEY"
|
||||
|
||||
const val READ_ONLY_DEFAULT = false
|
||||
|
||||
fun retrieveReadOnlyFromIntent(intent: Intent): Boolean {
|
||||
return intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
|
||||
}
|
||||
|
||||
fun retrieveReadOnlyFromInstanceStateOrPreference(context: Context, savedInstanceState: Bundle?): Boolean {
|
||||
return if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
||||
savedInstanceState.getBoolean(READ_ONLY_KEY)
|
||||
} else {
|
||||
PreferencesUtil.enableReadOnlyDatabase(context)
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState: Bundle?, arguments: Bundle?): Boolean {
|
||||
var readOnly = READ_ONLY_DEFAULT
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
||||
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
|
||||
} else if (arguments != null && arguments.containsKey(READ_ONLY_KEY)) {
|
||||
readOnly = arguments.getBoolean(READ_ONLY_KEY)
|
||||
}
|
||||
return readOnly
|
||||
}
|
||||
|
||||
fun retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState: Bundle?, intent: Intent?): Boolean {
|
||||
var readOnly = READ_ONLY_DEFAULT
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
||||
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
|
||||
} else {
|
||||
if (intent != null)
|
||||
readOnly = intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
|
||||
}
|
||||
return readOnly
|
||||
}
|
||||
|
||||
fun putReadOnlyInIntent(intent: Intent, readOnly: Boolean) {
|
||||
intent.putExtra(READ_ONLY_KEY, readOnly)
|
||||
}
|
||||
|
||||
fun putReadOnlyInBundle(bundle: Bundle, readOnly: Boolean) {
|
||||
bundle.putBoolean(READ_ONLY_KEY, readOnly)
|
||||
}
|
||||
|
||||
fun onSaveInstanceState(outState: Bundle, readOnly: Boolean) {
|
||||
outState.putBoolean(READ_ONLY_KEY, readOnly)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.kunzisoft.keepass.activities.legacy
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
|
||||
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
|
||||
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
||||
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||
protected var mDatabase: Database? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||
|
||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||
val databaseWasReloaded = database?.wasReloaded == true
|
||||
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
|
||||
finish()
|
||||
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
|
||||
database?.wasReloaded = false
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
|
||||
onDatabaseActionFinished(database, actionTask, result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
mDatabase = database
|
||||
mDatabaseViewModel.defineDatabase(database)
|
||||
// optional method implementation
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
mDatabaseViewModel.onActionFinished(database, actionTask, result)
|
||||
// optional method implementation
|
||||
}
|
||||
|
||||
fun createDatabase(databaseUri: Uri,
|
||||
mainCredential: MainCredential) {
|
||||
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
|
||||
}
|
||||
|
||||
fun loadDatabase(databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherEntity: CipherDatabaseEntity?,
|
||||
fixDuplicateUuid: Boolean) {
|
||||
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEntity, fixDuplicateUuid)
|
||||
}
|
||||
|
||||
protected fun closeDatabase() {
|
||||
mDatabase?.clearAndClose(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
mDatabaseTaskProvider?.registerProgressTask()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||
super.onPause()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
/*
|
||||
* 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.legacy
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
||||
import java.util.*
|
||||
|
||||
abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
PasswordEncodingDialogFragment.Listener {
|
||||
|
||||
private val mNodesViewModel: NodesViewModel by viewModels()
|
||||
|
||||
protected var mTimeoutEnable: Boolean = true
|
||||
|
||||
private var mLockReceiver: LockReceiver? = null
|
||||
private var mExitLock: Boolean = false
|
||||
|
||||
protected var mDatabaseReadOnly: Boolean = true
|
||||
private var mAutoSaveEnable: Boolean = true
|
||||
|
||||
protected var mIconDrawableFactory: IconDrawableFactory? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)
|
||||
) {
|
||||
mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
|
||||
} else {
|
||||
if (intent != null)
|
||||
mTimeoutEnable =
|
||||
intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
|
||||
}
|
||||
|
||||
mNodesViewModel.nodesToPermanentlyDelete.observe(this) { nodes ->
|
||||
deleteDatabaseNodes(nodes)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveDatabase.observe(this) { save ->
|
||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
||||
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveName.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveDescription.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveDefaultUsername.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveColor.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveCompression.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.removeUnlinkData.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveRecycleBin.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveTemplatesGroup.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveMaxHistoryItems.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveMaxHistorySize.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveEncryption.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveKeyDerivation.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveIterations.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveMemoryUsage.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveParallelism.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mExitLock = false
|
||||
}
|
||||
|
||||
open fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
// End activity if database not loaded
|
||||
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) {
|
||||
finish()
|
||||
}
|
||||
|
||||
// Focus view to reinitialize timeout,
|
||||
// view is not necessary loaded so retry later in resume
|
||||
viewToInvalidateTimeout()
|
||||
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
|
||||
|
||||
database?.let {
|
||||
// check timeout
|
||||
if (mTimeoutEnable) {
|
||||
if (mLockReceiver == null) {
|
||||
mLockReceiver = LockReceiver {
|
||||
mDatabase = null
|
||||
closeDatabase(database)
|
||||
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||
// Add onActivityForResult response
|
||||
setResult(RESULT_EXIT_LOCK)
|
||||
closeOptionsMenu()
|
||||
finish()
|
||||
}
|
||||
registerLockReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
// After the first creation
|
||||
// or If simply swipe with another application
|
||||
// If the time is out -> close the Activity
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||
// If onCreate already record time
|
||||
if (!mExitLock)
|
||||
TimeoutHelper.recordTime(this, database.loaded)
|
||||
}
|
||||
|
||||
mDatabaseReadOnly = database.isReadOnly
|
||||
mIconDrawableFactory = database.iconDrawableFactory
|
||||
|
||||
checkRegister()
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun viewToInvalidateTimeout(): View?
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
when (actionTask) {
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||
// Reload the current activity
|
||||
if (result.isSuccess) {
|
||||
reloadActivity()
|
||||
} else {
|
||||
this.showActionErrorIfNeeded(result)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||
mainCredential: MainCredential) {
|
||||
assignDatabasePassword(databaseUri, mainCredential)
|
||||
}
|
||||
|
||||
private fun assignDatabasePassword(databaseUri: Uri?,
|
||||
mainCredential: MainCredential) {
|
||||
if (databaseUri != null) {
|
||||
mDatabaseTaskProvider?.startDatabaseAssignPassword(databaseUri, mainCredential)
|
||||
}
|
||||
}
|
||||
|
||||
fun assignPassword(mainCredential: MainCredential) {
|
||||
mDatabase?.let { database ->
|
||||
database.fileUri?.let { databaseUri ->
|
||||
// Show the progress dialog now or after dialog confirmation
|
||||
if (database.validatePasswordEncoding(mainCredential)) {
|
||||
assignDatabasePassword(databaseUri, mainCredential)
|
||||
} else {
|
||||
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
||||
.show(supportFragmentManager, "passwordEncodingTag")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDatabase() {
|
||||
mDatabaseTaskProvider?.startDatabaseSave(true)
|
||||
}
|
||||
|
||||
fun reloadDatabase() {
|
||||
mDatabaseTaskProvider?.startDatabaseReload(false)
|
||||
}
|
||||
|
||||
fun createEntry(newEntry: Entry,
|
||||
parent: Group) {
|
||||
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun updateEntry(oldEntry: Entry,
|
||||
entryToUpdate: Entry) {
|
||||
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun copyNodes(nodesToCopy: List<Node>,
|
||||
newParent: Group) {
|
||||
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun moveNodes(nodesToMove: List<Node>,
|
||||
newParent: Group) {
|
||||
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
private fun eachNodeRecyclable(database: Database, nodes: List<Node>): Boolean {
|
||||
return nodes.find { node ->
|
||||
var cannotRecycle = true
|
||||
if (node is Entry) {
|
||||
cannotRecycle = !database.canRecycle(node)
|
||||
} else if (node is Group) {
|
||||
cannotRecycle = !database.canRecycle(node)
|
||||
}
|
||||
cannotRecycle
|
||||
} == null
|
||||
}
|
||||
|
||||
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
|
||||
mDatabase?.let { database ->
|
||||
// If recycle bin enabled, ensure it exists
|
||||
if (database.isRecycleBinEnabled) {
|
||||
database.ensureRecycleBinExists(resources)
|
||||
}
|
||||
|
||||
// If recycle bin enabled and not in recycle bin, move in recycle bin
|
||||
if (eachNodeRecyclable(database, nodes)) {
|
||||
deleteDatabaseNodes(nodes)
|
||||
}
|
||||
// else open the dialog to confirm deletion
|
||||
else {
|
||||
DeleteNodesDialogFragment.getInstance(recycleBin)
|
||||
.show(supportFragmentManager, "deleteNodesDialogFragment")
|
||||
mNodesViewModel.deleteNodes(nodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteDatabaseNodes(nodes: List<Node>) {
|
||||
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun createGroup(parent: Group,
|
||||
groupInfo: GroupInfo?) {
|
||||
// Build the group
|
||||
mDatabase?.createGroup()?.let { newGroup ->
|
||||
groupInfo?.let { info ->
|
||||
newGroup.setGroupInfo(info)
|
||||
}
|
||||
// Not really needed here because added in runnable but safe
|
||||
newGroup.parent = parent
|
||||
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGroup(oldGroup: Group,
|
||||
groupInfo: GroupInfo) {
|
||||
// If group updated save it in the database
|
||||
val updateGroup = Group(oldGroup).let { updateGroup ->
|
||||
updateGroup.apply {
|
||||
// WARNING remove parent and children to keep memory
|
||||
removeParent()
|
||||
removeChildren()
|
||||
this.setGroupInfo(groupInfo)
|
||||
}
|
||||
}
|
||||
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun restoreEntryHistory(mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int) {
|
||||
mDatabaseTaskProvider
|
||||
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun deleteEntryHistory(mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int) {
|
||||
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (resultCode == RESULT_EXIT_LOCK) {
|
||||
mExitLock = true
|
||||
lockAndExit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkRegister() {
|
||||
// If in ave or registration mode, don't allow read only
|
||||
if ((mSpecialMode == SpecialMode.SAVE
|
||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||
&& mDatabaseReadOnly) {
|
||||
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// To refresh when back to normal workflow from selection workflow
|
||||
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
|
||||
|
||||
// Invalidate timeout by touch
|
||||
mDatabase?.let { database ->
|
||||
viewToInvalidateTimeout()
|
||||
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
|
||||
}
|
||||
|
||||
invalidateOptionsMenu()
|
||||
|
||||
LOCKING_ACTIVITY_UI_VISIBLE = true
|
||||
}
|
||||
|
||||
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
|
||||
mDatabase?.loaded == true,
|
||||
action)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||
|
||||
super.onPause()
|
||||
|
||||
if (mTimeoutEnable) {
|
||||
// If the time is out during our navigation in activity -> close the Activity
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterLockReceiver(mLockReceiver)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
protected fun lockAndExit() {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
|
||||
fun resetAppTimeout() {
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
|
||||
mDatabase?.loaded ?: false)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (mTimeoutEnable) {
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
|
||||
mDatabase?.loaded == true) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG = "LockingActivity"
|
||||
|
||||
const val RESULT_EXIT_LOCK = 1450
|
||||
|
||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||
|
||||
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To reset the app timeout when a view is focused or changed
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
|
||||
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
|
||||
setOnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
||||
databaseLoaded ?: false)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
setOnFocusChangeListener { _, _ ->
|
||||
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
||||
databaseLoaded ?: false)
|
||||
}
|
||||
if (this is ViewGroup) {
|
||||
for (i in 0..childCount) {
|
||||
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.kunzisoft.keepass.activities.selection
|
||||
package com.kunzisoft.keepass.activities.legacy
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
@@ -7,15 +7,14 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.SpecialModeView
|
||||
|
||||
/**
|
||||
* Activity to manage special mode (ie: selection mode)
|
||||
* Activity to manage database special mode (ie: selection mode)
|
||||
*/
|
||||
abstract class SpecialModeActivity : StylishActivity() {
|
||||
abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
|
||||
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||
private var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.kunzisoft.keepass.activities.legacy
|
||||
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
interface DatabaseRetrieval {
|
||||
fun onDatabaseRetrieved(database: Database?)
|
||||
fun onDatabaseActionFinished(database: Database,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result)
|
||||
}
|
||||
@@ -1,215 +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.activities.lock
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
|
||||
abstract class LockingActivity : SpecialModeActivity() {
|
||||
|
||||
protected var mTimeoutEnable: Boolean = true
|
||||
|
||||
private var mLockReceiver: LockReceiver? = null
|
||||
private var mExitLock: Boolean = false
|
||||
|
||||
// Force readOnly if Entry Selection mode
|
||||
protected var mReadOnly: Boolean
|
||||
get() {
|
||||
return mReadOnlyToSave
|
||||
}
|
||||
set(value) {
|
||||
mReadOnlyToSave = value
|
||||
}
|
||||
private var mReadOnlyToSave: Boolean = false
|
||||
protected var mAutoSaveEnable: Boolean = true
|
||||
|
||||
var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||
private set
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)) {
|
||||
mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
|
||||
} else {
|
||||
if (intent != null)
|
||||
mTimeoutEnable = intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
|
||||
}
|
||||
|
||||
if (mTimeoutEnable) {
|
||||
mLockReceiver = LockReceiver {
|
||||
closeDatabase()
|
||||
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||
// Add onActivityForResult response
|
||||
setResult(RESULT_EXIT_LOCK)
|
||||
closeOptionsMenu()
|
||||
finish()
|
||||
}
|
||||
registerLockReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
mExitLock = false
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (resultCode == RESULT_EXIT_LOCK) {
|
||||
mExitLock = true
|
||||
if (Database.getInstance().loaded) {
|
||||
lockAndExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// If in ave or registration mode, don't allow read only
|
||||
if ((mSpecialMode == SpecialMode.SAVE
|
||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||
&& mReadOnly) {
|
||||
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||
|
||||
// To refresh when back to normal workflow from selection workflow
|
||||
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
|
||||
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
|
||||
|
||||
invalidateOptionsMenu()
|
||||
|
||||
if (mTimeoutEnable) {
|
||||
// End activity if database not loaded
|
||||
if (!Database.getInstance().loaded) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// After the first creation
|
||||
// or If simply swipe with another application
|
||||
// If the time is out -> close the Activity
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||
// If onCreate already record time
|
||||
if (!mExitLock)
|
||||
TimeoutHelper.recordTime(this)
|
||||
}
|
||||
|
||||
LOCKING_ACTIVITY_UI_VISIBLE = true
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||
|
||||
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
|
||||
if (mTimeoutEnable) {
|
||||
// If the time is out during our navigation in activity -> close the Activity
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterLockReceiver(mLockReceiver)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
protected fun lockAndExit() {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (mTimeoutEnable) {
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG = "LockingActivity"
|
||||
|
||||
const val RESULT_EXIT_LOCK = 1450
|
||||
|
||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||
|
||||
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To reset the app timeout when a view is focused or changed
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context) {
|
||||
setOnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
//Log.d(LockingActivity.TAG, "View touched, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
setOnFocusChangeListener { _, _ ->
|
||||
//Log.d(LockingActivity.TAG, "View focused, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
|
||||
}
|
||||
if (this is ViewGroup) {
|
||||
for (i in 0..childCount) {
|
||||
getChildAt(i)?.resetAppTimeoutWhenViewFocusedOrChanged(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,13 @@ package com.kunzisoft.keepass.activities.stylish
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_APPEARANCE_PREFERENCE_CHANGED
|
||||
|
||||
/**
|
||||
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
||||
@@ -35,6 +38,7 @@ abstract class StylishActivity : AppCompatActivity() {
|
||||
|
||||
@StyleRes
|
||||
private var themeId: Int = 0
|
||||
private var customStyle = true
|
||||
|
||||
/* (non-Javadoc) Workaround for HTC Linkify issues
|
||||
* @see android.app.Activity#startActivity(android.content.Intent)
|
||||
@@ -52,10 +56,30 @@ abstract class StylishActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
open fun applyCustomStyle(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
open fun finishActivityIfReloadRequested(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
open fun reloadActivity() {
|
||||
if (!finishActivityIfReloadRequested()) {
|
||||
startActivity(intent)
|
||||
}
|
||||
finish()
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
this.themeId = Stylish.getThemeId(this)
|
||||
setTheme(themeId)
|
||||
|
||||
customStyle = applyCustomStyle()
|
||||
if (customStyle) {
|
||||
this.themeId = Stylish.getThemeId(this)
|
||||
setTheme(themeId)
|
||||
}
|
||||
|
||||
// Several gingerbread devices have problems with FLAG_SECURE
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
@@ -63,9 +87,17 @@ abstract class StylishActivity : AppCompatActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (Stylish.getThemeId(this) != this.themeId) {
|
||||
|
||||
if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|
||||
|| DATABASE_APPEARANCE_PREFERENCE_CHANGED) {
|
||||
DATABASE_APPEARANCE_PREFERENCE_CHANGED = false
|
||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||
this.recreate()
|
||||
recreateActivity()
|
||||
}
|
||||
}
|
||||
|
||||
private fun recreateActivity() {
|
||||
// To prevent KitKat bugs
|
||||
Handler(Looper.getMainLooper()).post { recreate() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,13 +26,13 @@ import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
|
||||
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
var entryHistoryList: MutableList<Entry> = ArrayList()
|
||||
var onItemClickListener: ((item: Entry, position: Int)->Unit)? = null
|
||||
var entryHistoryList: MutableList<EntryInfo> = ArrayList()
|
||||
var onItemClickListener: ((item: EntryInfo, position: Int)->Unit)? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder {
|
||||
return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false))
|
||||
@@ -44,7 +44,6 @@ class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHist
|
||||
holder.lastModifiedView.text = entryHistory.lastModificationTime.getDateTimeString(context.resources)
|
||||
holder.titleView.text = entryHistory.title
|
||||
holder.usernameView.text = entryHistory.username
|
||||
holder.urlView.text = entryHistory.url
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClickListener?.invoke(entryHistory, position)
|
||||
@@ -64,6 +63,5 @@ class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHist
|
||||
var lastModifiedView: TextView = itemView.findViewById(R.id.entry_history_last_modified)
|
||||
var titleView: TextView = itemView.findViewById(R.id.entry_history_title)
|
||||
var usernameView: TextView = itemView.findViewById(R.id.entry_history_username)
|
||||
var urlView: TextView = itemView.findViewById(R.id.entry_history_url)
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
|
||||
import java.util.ArrayList
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.DatabaseFile
|
||||
import com.kunzisoft.keepass.view.collapse
|
||||
@@ -44,11 +46,43 @@ class FileDatabaseHistoryAdapter(context: Context)
|
||||
private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null
|
||||
private var saveAliasListener: ((DatabaseFile)->Unit)? = null
|
||||
|
||||
private val listDatabaseFiles = ArrayList<DatabaseFile>()
|
||||
private var mDefaultDatabase: DatabaseFile? = null
|
||||
private var mExpandedDatabaseFile: SuperDatabaseFile? = null
|
||||
private var mPreviousExpandedDatabaseFile: SuperDatabaseFile? = null
|
||||
|
||||
private var mDefaultDatabaseFile: DatabaseFile? = null
|
||||
private var mExpandedDatabaseFile: DatabaseFile? = null
|
||||
private var mPreviousExpandedDatabaseFile: DatabaseFile? = null
|
||||
private val mListPosition = mutableListOf<SuperDatabaseFile>()
|
||||
private val mSortedListDatabaseFiles = SortedList(SuperDatabaseFile::class.java,
|
||||
object: SortedListAdapterCallback<SuperDatabaseFile>(this) {
|
||||
override fun compare(item1: SuperDatabaseFile, item2: SuperDatabaseFile): Int {
|
||||
val indexItem1 = mListPosition.indexOf(item1)
|
||||
val indexItem2 = mListPosition.indexOf(item2)
|
||||
return if (indexItem1 == -1 && indexItem2 == -1)
|
||||
-1
|
||||
else if (indexItem1 < indexItem2)
|
||||
-1
|
||||
else if (indexItem1 > indexItem2)
|
||||
1
|
||||
else
|
||||
0
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: SuperDatabaseFile, newItem: SuperDatabaseFile): Boolean {
|
||||
val oldDatabaseFile = oldItem.databaseFile
|
||||
val newDatabaseFile = newItem.databaseFile
|
||||
return oldDatabaseFile.databaseUri == newDatabaseFile.databaseUri
|
||||
&& oldDatabaseFile.databaseDecodedPath == newDatabaseFile.databaseDecodedPath
|
||||
&& oldDatabaseFile.databaseAlias == newDatabaseFile.databaseAlias
|
||||
&& oldDatabaseFile.databaseFileExists == newDatabaseFile.databaseFileExists
|
||||
&& oldDatabaseFile.databaseLastModified == newDatabaseFile.databaseLastModified
|
||||
&& oldDatabaseFile.databaseSize == newDatabaseFile.databaseSize
|
||||
&& oldItem.default == newItem.default
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: SuperDatabaseFile, item2: SuperDatabaseFile): Boolean {
|
||||
return item1.databaseFile == item2.databaseFile
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ColorInt
|
||||
private val defaultColor: Int
|
||||
@@ -71,7 +105,8 @@ class FileDatabaseHistoryAdapter(context: Context)
|
||||
|
||||
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
||||
// Get info from position
|
||||
val databaseFile = listDatabaseFiles[position]
|
||||
val superDatabaseFile = mSortedListDatabaseFiles[position]
|
||||
val databaseFile = superDatabaseFile.databaseFile
|
||||
|
||||
// Click item to open file
|
||||
holder.fileContainer.setOnClickListener {
|
||||
@@ -80,7 +115,7 @@ class FileDatabaseHistoryAdapter(context: Context)
|
||||
|
||||
// Default database
|
||||
holder.defaultFileButton.apply {
|
||||
this.isChecked = mDefaultDatabaseFile == databaseFile
|
||||
this.isChecked = superDatabaseFile.default
|
||||
setOnClickListener {
|
||||
defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null)
|
||||
}
|
||||
@@ -115,7 +150,7 @@ class FileDatabaseHistoryAdapter(context: Context)
|
||||
}
|
||||
|
||||
// Click on information
|
||||
val isExpanded = databaseFile == mExpandedDatabaseFile
|
||||
val isExpanded = superDatabaseFile == mExpandedDatabaseFile
|
||||
// Hides or shows info
|
||||
holder.fileExpandContainer.apply {
|
||||
if (isExpanded) {
|
||||
@@ -151,16 +186,16 @@ class FileDatabaseHistoryAdapter(context: Context)
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
mPreviousExpandedDatabaseFile = databaseFile
|
||||
mPreviousExpandedDatabaseFile = superDatabaseFile
|
||||
}
|
||||
holder.fileInformationButton.apply {
|
||||
animate().rotation(if (isExpanded) 180F else 0F).start()
|
||||
setOnClickListener {
|
||||
mExpandedDatabaseFile = if (isExpanded) null else databaseFile
|
||||
mExpandedDatabaseFile = if (isExpanded) null else superDatabaseFile
|
||||
// Notify change
|
||||
val previousExpandedPosition = listDatabaseFiles.indexOf(mPreviousExpandedDatabaseFile)
|
||||
val previousExpandedPosition = mListPosition.indexOf(mPreviousExpandedDatabaseFile)
|
||||
notifyItemChanged(previousExpandedPosition)
|
||||
val expandedPosition = listDatabaseFiles.indexOf(mExpandedDatabaseFile)
|
||||
val expandedPosition = mListPosition.indexOf(mExpandedDatabaseFile)
|
||||
notifyItemChanged(expandedPosition)
|
||||
}
|
||||
}
|
||||
@@ -172,50 +207,67 @@ class FileDatabaseHistoryAdapter(context: Context)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return listDatabaseFiles.size
|
||||
return mSortedListDatabaseFiles.size()
|
||||
}
|
||||
|
||||
fun clearDatabaseFileHistoryList() {
|
||||
listDatabaseFiles.clear()
|
||||
mListPosition.clear()
|
||||
mSortedListDatabaseFiles.clear()
|
||||
}
|
||||
|
||||
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
|
||||
listDatabaseFiles.add(0, fileDatabaseHistoryToAdd)
|
||||
notifyItemInserted(0)
|
||||
val superToAdd = SuperDatabaseFile(fileDatabaseHistoryToAdd)
|
||||
mListPosition.add(0, superToAdd)
|
||||
mSortedListDatabaseFiles.add(superToAdd)
|
||||
}
|
||||
|
||||
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
|
||||
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToUpdate)
|
||||
if (listDatabaseFiles.remove(fileDatabaseHistoryToUpdate)) {
|
||||
listDatabaseFiles.add(index, fileDatabaseHistoryToUpdate)
|
||||
notifyItemChanged(index)
|
||||
val superToUpdate = SuperDatabaseFile(fileDatabaseHistoryToUpdate)
|
||||
val index = mListPosition.indexOf(superToUpdate)
|
||||
if (mListPosition.remove(superToUpdate)) {
|
||||
mListPosition.add(index, superToUpdate)
|
||||
}
|
||||
mSortedListDatabaseFiles.updateItemAt(index, superToUpdate)
|
||||
}
|
||||
|
||||
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) {
|
||||
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToDelete)
|
||||
if (listDatabaseFiles.remove(fileDatabaseHistoryToDelete)) {
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
val superToDelete = SuperDatabaseFile(fileDatabaseHistoryToDelete)
|
||||
val index = mListPosition.indexOf(superToDelete)
|
||||
mListPosition.remove(superToDelete)
|
||||
mSortedListDatabaseFiles.removeItemAt(index)
|
||||
}
|
||||
|
||||
fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) {
|
||||
if (listDatabaseFiles.isEmpty()) {
|
||||
listFileDatabaseHistoryToAdd.forEach {
|
||||
listDatabaseFiles.add(it)
|
||||
notifyItemInserted(listDatabaseFiles.size)
|
||||
}
|
||||
} else {
|
||||
listDatabaseFiles.clear()
|
||||
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
||||
notifyDataSetChanged()
|
||||
val superMapToReplace = listFileDatabaseHistoryToAdd.map {
|
||||
SuperDatabaseFile(it)
|
||||
}
|
||||
mListPosition.clear()
|
||||
mListPosition.addAll(superMapToReplace)
|
||||
mSortedListDatabaseFiles.replaceAll(superMapToReplace)
|
||||
}
|
||||
|
||||
fun setDefaultDatabase(databaseUri: Uri?) {
|
||||
val defaultDatabaseFile = listDatabaseFiles.firstOrNull { it.databaseUri == databaseUri }
|
||||
mDefaultDatabaseFile = defaultDatabaseFile
|
||||
notifyDataSetChanged()
|
||||
// Remove default from last item
|
||||
val oldDefaultDatabasePosition = mListPosition.indexOfFirst {
|
||||
it.default
|
||||
}
|
||||
if (oldDefaultDatabasePosition >= 0) {
|
||||
val oldDefaultDatabase = mListPosition[oldDefaultDatabasePosition].apply {
|
||||
default = false
|
||||
}
|
||||
mSortedListDatabaseFiles.updateItemAt(oldDefaultDatabasePosition, oldDefaultDatabase)
|
||||
}
|
||||
// Add default to new item
|
||||
val newDefaultDatabaseFilePosition = mListPosition.indexOfFirst {
|
||||
it.databaseFile.databaseUri == databaseUri
|
||||
}
|
||||
if (newDefaultDatabaseFilePosition >= 0) {
|
||||
val newDefaultDatabase = mListPosition[newDefaultDatabaseFilePosition].apply {
|
||||
default = true
|
||||
}
|
||||
mDefaultDatabase = newDefaultDatabase.databaseFile
|
||||
mSortedListDatabaseFiles.updateItemAt(newDefaultDatabaseFilePosition, newDefaultDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) {
|
||||
@@ -234,6 +286,30 @@ class FileDatabaseHistoryAdapter(context: Context)
|
||||
this.saveAliasListener = listener
|
||||
}
|
||||
|
||||
private inner class SuperDatabaseFile(
|
||||
var databaseFile: DatabaseFile,
|
||||
var default: Boolean = false
|
||||
) {
|
||||
|
||||
init {
|
||||
if (mDefaultDatabase == databaseFile)
|
||||
this.default = true
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is SuperDatabaseFile) return false
|
||||
|
||||
if (databaseFile != other.databaseFile) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return databaseFile.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
|
||||
|
||||
@@ -26,7 +26,9 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -40,7 +42,11 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.view.setTextSize
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
import java.util.*
|
||||
@@ -49,7 +55,8 @@ import java.util.*
|
||||
* Create node list adapter with contextMenu or not
|
||||
* @param context Context to use
|
||||
*/
|
||||
class NodeAdapter (private val context: Context)
|
||||
class NodeAdapter (private val context: Context,
|
||||
private val database: Database)
|
||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||
|
||||
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||
@@ -67,12 +74,13 @@ class NodeAdapter (private val context: Context)
|
||||
|
||||
private var mShowUserNames: Boolean = true
|
||||
private var mShowNumberEntries: Boolean = true
|
||||
private var mShowOTP: Boolean = false
|
||||
private var mShowUUID: Boolean = false
|
||||
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
||||
|
||||
private var mActionNodesList = LinkedList<Node>()
|
||||
private var mNodeClickCallback: NodeClickCallback? = null
|
||||
|
||||
private val mDatabase: Database
|
||||
private var mClipboardHelper = ClipboardHelper(context)
|
||||
|
||||
@ColorInt
|
||||
private val mContentSelectionColor: Int
|
||||
@@ -96,9 +104,6 @@ class NodeAdapter (private val context: Context)
|
||||
this.mNodeSortedListCallback = NodeSortedListCallback()
|
||||
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
|
||||
|
||||
// Database
|
||||
this.mDatabase = Database.getInstance()
|
||||
|
||||
// Color of content selection
|
||||
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
|
||||
// Retrieve the color to tint the icon
|
||||
@@ -111,7 +116,7 @@ class NodeAdapter (private val context: Context)
|
||||
taTextColor.recycle()
|
||||
}
|
||||
|
||||
fun assignPreferences() {
|
||||
private fun assignPreferences() {
|
||||
this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context)
|
||||
|
||||
notifyChangeSort(
|
||||
@@ -125,6 +130,8 @@ class NodeAdapter (private val context: Context)
|
||||
|
||||
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||
this.mShowOTP = PreferencesUtil.showOTPToken(context)
|
||||
this.mShowUUID = PreferencesUtil.showUUID(context)
|
||||
|
||||
this.mEntryFilters = Group.ChildFilter.getDefaults(context)
|
||||
|
||||
@@ -146,9 +153,21 @@ class NodeAdapter (private val context: Context)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||
return oldItem.type == newItem.type
|
||||
var typeContentTheSame = true
|
||||
if (oldItem is Entry && newItem is Entry) {
|
||||
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
|
||||
&& oldItem.username == newItem.username
|
||||
&& oldItem.getOtpElement() == newItem.getOtpElement()
|
||||
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
||||
} else if (oldItem is Group && newItem is Group) {
|
||||
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
||||
}
|
||||
return typeContentTheSame
|
||||
&& oldItem.nodeId == newItem.nodeId
|
||||
&& oldItem.type == newItem.type
|
||||
&& oldItem.title == newItem.title
|
||||
&& oldItem.icon == newItem.icon
|
||||
&& oldItem.isCurrentlyExpires == newItem.isCurrentlyExpires
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
|
||||
@@ -241,6 +260,10 @@ class NodeAdapter (private val context: Context)
|
||||
mNodeSortedList.endBatchedUpdates()
|
||||
}
|
||||
|
||||
fun indexOf(node: Node): Int {
|
||||
return mNodeSortedList.indexOf(node)
|
||||
}
|
||||
|
||||
fun notifyNodeChanged(node: Node) {
|
||||
notifyItemChanged(mNodeSortedList.indexOf(node))
|
||||
}
|
||||
@@ -266,7 +289,7 @@ class NodeAdapter (private val context: Context)
|
||||
*/
|
||||
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
|
||||
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||
this.mNodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
|
||||
this.mNodeComparator = sortNodeEnum.getNodeComparator(database, sortNodeParameters)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
@@ -303,7 +326,7 @@ class NodeAdapter (private val context: Context)
|
||||
}
|
||||
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||
holder.icon.apply {
|
||||
mDatabase.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
|
||||
database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
|
||||
// Relative size of the icon
|
||||
layoutParams?.apply {
|
||||
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||
@@ -323,11 +346,16 @@ class NodeAdapter (private val context: Context)
|
||||
strikeOut(subNode.isCurrentlyExpires)
|
||||
visibility = View.GONE
|
||||
}
|
||||
// Add meta text to show UUID
|
||||
holder.meta.apply {
|
||||
text = subNode.nodeId.toString()
|
||||
visibility = if (mShowUUID) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
// Specific elements for entry
|
||||
if (subNode.type == Type.ENTRY) {
|
||||
val entry = subNode as Entry
|
||||
mDatabase.startManageEntry(entry)
|
||||
database.startManageEntry(entry)
|
||||
|
||||
holder.text.text = entry.getVisualTitle()
|
||||
holder.subText.apply {
|
||||
@@ -339,10 +367,29 @@ class NodeAdapter (private val context: Context)
|
||||
}
|
||||
}
|
||||
|
||||
val otpElement = entry.getOtpElement()
|
||||
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
|
||||
if (otpElement != null
|
||||
&& mShowOTP
|
||||
&& otpElement.token.isNotEmpty()) {
|
||||
|
||||
// Execute runnable to show progress
|
||||
holder.otpRunnable.action = {
|
||||
populateOtpView(holder, otpElement)
|
||||
}
|
||||
if (otpElement.type == OtpType.TOTP) {
|
||||
holder.otpRunnable.postDelayed()
|
||||
}
|
||||
populateOtpView(holder, otpElement)
|
||||
|
||||
holder.otpContainer?.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.otpContainer?.visibility = View.GONE
|
||||
}
|
||||
holder.attachmentIcon?.visibility =
|
||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||
|
||||
mDatabase.stopManageEntry(entry)
|
||||
database.stopManageEntry(entry)
|
||||
}
|
||||
|
||||
// Add number of entries in groups
|
||||
@@ -350,7 +397,7 @@ class NodeAdapter (private val context: Context)
|
||||
if (mShowNumberEntries) {
|
||||
holder.numberChildren?.apply {
|
||||
text = (subNode as Group)
|
||||
.getNumberOfChildEntries(mEntryFilters)
|
||||
.numberOfChildEntries
|
||||
.toString()
|
||||
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
||||
visibility = View.VISIBLE
|
||||
@@ -362,13 +409,56 @@ class NodeAdapter (private val context: Context)
|
||||
|
||||
// Assign click
|
||||
holder.container.setOnClickListener {
|
||||
mNodeClickCallback?.onNodeClick(subNode)
|
||||
mNodeClickCallback?.onNodeClick(database, subNode)
|
||||
}
|
||||
holder.container.setOnLongClickListener {
|
||||
mNodeClickCallback?.onNodeLongClick(subNode) ?: false
|
||||
mNodeClickCallback?.onNodeLongClick(database, subNode) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun populateOtpView(holder: NodeViewHolder?, otpElement: OtpElement?) {
|
||||
when (otpElement?.type) {
|
||||
OtpType.HOTP -> {
|
||||
holder?.otpProgress?.apply {
|
||||
max = 100
|
||||
progress = 100
|
||||
}
|
||||
}
|
||||
OtpType.TOTP -> {
|
||||
holder?.otpProgress?.apply {
|
||||
max = otpElement.period
|
||||
progress = otpElement.secondsRemaining
|
||||
}
|
||||
}
|
||||
}
|
||||
holder?.otpToken?.text = otpElement?.token
|
||||
holder?.otpContainer?.setOnClickListener {
|
||||
otpElement?.token?.let { token ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.copy_field,
|
||||
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
mClipboardHelper.copyToClipboard(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OtpRunnable(val view: View?): Runnable {
|
||||
|
||||
var action: (() -> Unit)? = null
|
||||
|
||||
override fun run() {
|
||||
action?.invoke()
|
||||
postDelayed()
|
||||
}
|
||||
|
||||
fun postDelayed() {
|
||||
view?.postDelayed(this, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return mNodeSortedList.size()
|
||||
}
|
||||
@@ -384,8 +474,8 @@ class NodeAdapter (private val context: Context)
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
interface NodeClickCallback {
|
||||
fun onNodeClick(node: Node)
|
||||
fun onNodeLongClick(node: Node): Boolean
|
||||
fun onNodeClick(database: Database, node: Node)
|
||||
fun onNodeLongClick(database: Database, node: Node): Boolean
|
||||
}
|
||||
|
||||
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
@@ -394,6 +484,11 @@ class NodeAdapter (private val context: Context)
|
||||
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
||||
var meta: TextView = itemView.findViewById(R.id.node_meta)
|
||||
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
|
||||
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress)
|
||||
var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token)
|
||||
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
|
||||
|
||||
class TemplatesSelectorAdapter(private val context: Context,
|
||||
private val iconDrawableFactory: IconDrawableFactory?,
|
||||
private var templates: List<Template>): BaseAdapter() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var mIconColor = Color.BLACK
|
||||
|
||||
init {
|
||||
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
mIconColor = taIconColor.getColor(0, Color.BLACK)
|
||||
taIconColor.recycle()
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val template: Template = getItem(position)
|
||||
|
||||
val holder: TemplateSelectorViewHolder
|
||||
var templateView = convertView
|
||||
if (templateView == null) {
|
||||
holder = TemplateSelectorViewHolder()
|
||||
templateView = inflater.inflate(R.layout.item_template, parent, false)
|
||||
holder.icon = templateView?.findViewById(R.id.template_image)
|
||||
holder.name = templateView?.findViewById(R.id.template_name)
|
||||
templateView?.tag = holder
|
||||
} else {
|
||||
holder = templateView.tag as TemplateSelectorViewHolder
|
||||
}
|
||||
|
||||
holder.icon?.let { icon ->
|
||||
iconDrawableFactory?.assignDatabaseIcon(icon, template.icon, mIconColor)
|
||||
}
|
||||
holder.name?.text = TemplateField.getLocalizedName(context, template.title)
|
||||
|
||||
return templateView!!
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return templates.size
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Template {
|
||||
return templates[position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
inner class TemplateSelectorViewHolder {
|
||||
var icon: ImageView? = null
|
||||
var name: TextView? = null
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.app
|
||||
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
|
||||
class App : MultiDexApplication() {
|
||||
|
||||
@@ -31,9 +30,4 @@ class App : MultiDexApplication() {
|
||||
Stylish.load(this)
|
||||
PRNGFixes.apply()
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
Database.getInstance().clearAndClose(this)
|
||||
super.onTerminate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,26 +189,36 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
||||
fun deleteKeyFileByDatabaseUri(databaseUri: Uri,
|
||||
result: (() ->Unit)? = null) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||
},
|
||||
{
|
||||
result?.invoke()
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteAllKeyFiles() {
|
||||
fun deleteAllKeyFiles(result: (() ->Unit)? = null) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteAllKeyFiles()
|
||||
},
|
||||
{
|
||||
result?.invoke()
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteAll() {
|
||||
fun deleteAll(result: (() ->Unit)? = null) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteAll()
|
||||
},
|
||||
{
|
||||
result?.invoke()
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.app.PendingIntent
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
@@ -37,19 +38,23 @@ import android.view.autofill.AutofillValue
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.Toast
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@@ -85,13 +90,14 @@ object AutofillHelper {
|
||||
}
|
||||
|
||||
private fun newRemoteViews(context: Context,
|
||||
database: Database,
|
||||
remoteViewsText: String,
|
||||
remoteViewsIcon: IconImage? = null): RemoteViews {
|
||||
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
||||
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
||||
if (remoteViewsIcon != null) {
|
||||
try {
|
||||
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
|
||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||
presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
|
||||
}
|
||||
@@ -103,19 +109,96 @@ object AutofillHelper {
|
||||
}
|
||||
|
||||
private fun buildDataset(context: Context,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result,
|
||||
inlinePresentation: InlinePresentation?): Dataset? {
|
||||
database: Database,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result,
|
||||
inlinePresentation: InlinePresentation?): Dataset? {
|
||||
val title = makeEntryTitle(entryInfo)
|
||||
val views = newRemoteViews(context, title, entryInfo.icon)
|
||||
val views = newRemoteViews(context, database, title, entryInfo.icon)
|
||||
val builder = Dataset.Builder(views)
|
||||
builder.setId(entryInfo.id)
|
||||
builder.setId(entryInfo.id.toString())
|
||||
|
||||
struct.usernameId?.let { usernameId ->
|
||||
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
|
||||
}
|
||||
struct.passwordId?.let { password ->
|
||||
builder.setValue(password, AutofillValue.forText(entryInfo.password))
|
||||
struct.passwordId?.let { passwordId ->
|
||||
builder.setValue(passwordId, AutofillValue.forText(entryInfo.password))
|
||||
}
|
||||
|
||||
if (entryInfo.expires) {
|
||||
val year = entryInfo.expiryTime.getYearInt()
|
||||
val month = entryInfo.expiryTime.getMonthInt()
|
||||
val monthString = month.toString().padStart(2, '0')
|
||||
val day = entryInfo.expiryTime.getDay()
|
||||
val dayString = day.toString().padStart(2, '0')
|
||||
|
||||
struct.creditCardExpirationDateId?.let {
|
||||
if (struct.isWebView) {
|
||||
// set date string as defined in https://html.spec.whatwg.org
|
||||
builder.setValue(it, AutofillValue.forText("$year\u002D$monthString"))
|
||||
} else {
|
||||
builder.setValue(it, AutofillValue.forDate(entryInfo.expiryTime.date.time))
|
||||
}
|
||||
}
|
||||
struct.creditCardExpirationYearId?.let {
|
||||
var autofillValue: AutofillValue? = null
|
||||
|
||||
struct.creditCardExpirationYearOptions?.let { options ->
|
||||
var yearIndex = options.indexOf(year.toString().substring(0, 2))
|
||||
|
||||
if (yearIndex == -1) {
|
||||
yearIndex = options.indexOf(year.toString())
|
||||
}
|
||||
if (yearIndex != -1) {
|
||||
autofillValue = AutofillValue.forList(yearIndex)
|
||||
builder.setValue(it, autofillValue)
|
||||
}
|
||||
}
|
||||
|
||||
if (autofillValue == null) {
|
||||
builder.setValue(it, AutofillValue.forText(year.toString()))
|
||||
}
|
||||
}
|
||||
struct.creditCardExpirationMonthId?.let {
|
||||
if (struct.isWebView) {
|
||||
builder.setValue(it, AutofillValue.forText(monthString))
|
||||
} else {
|
||||
if (struct.creditCardExpirationMonthOptions != null) {
|
||||
// index starts at 0
|
||||
builder.setValue(it, AutofillValue.forList(month - 1))
|
||||
} else {
|
||||
builder.setValue(it, AutofillValue.forText(monthString))
|
||||
}
|
||||
}
|
||||
}
|
||||
struct.creditCardExpirationDayId?.let {
|
||||
if (struct.isWebView) {
|
||||
builder.setValue(it, AutofillValue.forText(dayString))
|
||||
} else {
|
||||
if (struct.creditCardExpirationDayOptions != null) {
|
||||
builder.setValue(it, AutofillValue.forList(day - 1))
|
||||
} else {
|
||||
builder.setValue(it, AutofillValue.forText(dayString))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (field in entryInfo.customFields) {
|
||||
if (field.name == TemplateField.LABEL_HOLDER) {
|
||||
struct.creditCardHolderId?.let { ccNameId ->
|
||||
builder.setValue(ccNameId, AutofillValue.forText(field.protectedValue.stringValue))
|
||||
}
|
||||
}
|
||||
if (field.name == TemplateField.LABEL_NUMBER) {
|
||||
struct.creditCardNumberId?.let { ccnId ->
|
||||
builder.setValue(ccnId, AutofillValue.forText(field.protectedValue.stringValue))
|
||||
}
|
||||
}
|
||||
if (field.name == TemplateField.LABEL_CVV) {
|
||||
struct.cardVerificationValueId?.let { cvvId ->
|
||||
builder.setValue(cvvId, AutofillValue.forText(field.protectedValue.stringValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
@@ -126,8 +209,8 @@ object AutofillHelper {
|
||||
|
||||
return try {
|
||||
builder.build()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// if not value be set
|
||||
} catch (e: Exception) {
|
||||
// at least one value must be set
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -135,9 +218,11 @@ object AutofillHelper {
|
||||
/**
|
||||
* Method to assign a drawable to a new icon from a database icon
|
||||
*/
|
||||
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
|
||||
private fun buildIconFromEntry(context: Context,
|
||||
database: Database,
|
||||
entryInfo: EntryInfo): Icon? {
|
||||
try {
|
||||
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
|
||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||
return Icon.createWithBitmap(bitmap)
|
||||
}
|
||||
@@ -150,13 +235,14 @@ object AutofillHelper {
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun buildInlinePresentationForEntry(context: Context,
|
||||
database: Database,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest,
|
||||
positionItem: Int,
|
||||
entryInfo: EntryInfo): InlinePresentation? {
|
||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||
|
||||
if (positionItem <= maxSuggestion-1
|
||||
if (positionItem <= maxSuggestion - 1
|
||||
&& inlinePresentationSpecs.size > positionItem) {
|
||||
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
||||
|
||||
@@ -178,7 +264,7 @@ object AutofillHelper {
|
||||
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
buildIconFromEntry(context, entryInfo)?.let { icon ->
|
||||
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
||||
setEndIcon(icon.apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
@@ -188,10 +274,32 @@ object AutofillHelper {
|
||||
return null
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun buildInlinePresentationForManualSelection(context: Context,
|
||||
inlinePresentationSpec: InlinePresentationSpec,
|
||||
pendingIntent: PendingIntent): InlinePresentation? {
|
||||
// Make sure that the IME spec claims support for v1 UI template.
|
||||
val imeStyle = inlinePresentationSpec.style
|
||||
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
||||
return null
|
||||
|
||||
// Build the content for IME UI
|
||||
return InlinePresentation(
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
||||
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
|
||||
setTitle(context.getString(R.string.autofill_select_entry))
|
||||
setStartIcon(Icon.createWithResource(context, R.drawable.ic_arrow_right_green_24dp).apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
}.build().slice, inlinePresentationSpec, false)
|
||||
}
|
||||
|
||||
fun buildResponse(context: Context,
|
||||
database: Database,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse {
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? {
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
// Add Header
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
@@ -208,31 +316,85 @@ object AutofillHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add inline suggestion for new IME and dataset
|
||||
entriesInfo.forEachIndexed { index, entryInfo ->
|
||||
val inlinePresentation = inlineSuggestionsRequest?.let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
buildInlinePresentationForEntry(context, inlineSuggestionsRequest, index, entryInfo)
|
||||
} else {
|
||||
null
|
||||
var numberInlineSuggestions = 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
||||
--numberInlineSuggestions
|
||||
}
|
||||
}
|
||||
}
|
||||
responseBuilder.addDataset(buildDataset(context, entryInfo, parseResult, inlinePresentation))
|
||||
|
||||
}
|
||||
|
||||
entriesInfo.forEachIndexed { _, entry ->
|
||||
val inlinePresentation = if (numberInlineSuggestions > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, numberInlineSuggestions--, entry)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult, inlinePresentation))
|
||||
}
|
||||
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
manualSelection = true
|
||||
}
|
||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
||||
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
|
||||
searchInfo, inlineSuggestionsRequest)
|
||||
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
autofillIds.forEach { id ->
|
||||
val builder = Dataset.Builder(manualSelectionView)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
||||
inlinePresentation?.let {
|
||||
builder.setInlinePresentation(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.setValue(id, null)
|
||||
builder.setAuthentication(pendingIntent.intentSender)
|
||||
responseBuilder.addDataset(builder.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
responseBuilder.build()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
return responseBuilder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Autofill response for one entry
|
||||
*/
|
||||
fun buildResponseAndSetResult(activity: Activity, entryInfo: EntryInfo) {
|
||||
buildResponseAndSetResult(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
||||
fun buildResponseAndSetResult(activity: Activity,
|
||||
database: Database,
|
||||
entryInfo: EntryInfo) {
|
||||
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Autofill response for many entry
|
||||
*/
|
||||
fun buildResponseAndSetResult(activity: Activity, entriesInfo: List<EntryInfo>) {
|
||||
fun buildResponseAndSetResult(activity: Activity,
|
||||
database: Database,
|
||||
entriesInfo: List<EntryInfo>) {
|
||||
if (entriesInfo.isEmpty()) {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
} else {
|
||||
@@ -245,9 +407,9 @@ object AutofillHelper {
|
||||
if (inlineSuggestionsRequest != null) {
|
||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest)
|
||||
buildResponse(activity, database, entriesInfo, result, inlineSuggestionsRequest)
|
||||
} else {
|
||||
buildResponse(activity, entriesInfo, result, null)
|
||||
buildResponse(activity, database, entriesInfo, result, null)
|
||||
}
|
||||
val mReplyIntent = Intent()
|
||||
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
||||
|
||||
@@ -36,29 +36,47 @@ import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.model.CreditCard
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import org.joda.time.DateTime
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class KeeAutofillService : AutofillService() {
|
||||
|
||||
var applicationIdBlocklist: Set<String>? = null
|
||||
var webDomainBlocklist: Set<String>? = null
|
||||
var askToSaveData: Boolean = false
|
||||
var autofillInlineSuggestionsEnabled: Boolean = false
|
||||
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||
private var mDatabase: Database? = null
|
||||
private var applicationIdBlocklist: Set<String>? = null
|
||||
private var webDomainBlocklist: Set<String>? = null
|
||||
private var askToSaveData: Boolean = false
|
||||
private var autofillInlineSuggestionsEnabled: Boolean = false
|
||||
private var mLock = AtomicBoolean()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||
mDatabaseTaskProvider?.registerProgressTask()
|
||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||
this.mDatabase = database
|
||||
}
|
||||
|
||||
getPreferences()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun getPreferences() {
|
||||
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
|
||||
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
|
||||
@@ -95,7 +113,8 @@ class KeeAutofillService : AutofillService() {
|
||||
} else {
|
||||
null
|
||||
}
|
||||
launchSelection(searchInfo,
|
||||
launchSelection(mDatabase,
|
||||
searchInfo,
|
||||
parseResult,
|
||||
inlineSuggestionsRequest,
|
||||
callback)
|
||||
@@ -105,27 +124,28 @@ class KeeAutofillService : AutofillService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSelection(searchInfo: SearchInfo,
|
||||
private fun launchSelection(database: Database?,
|
||||
searchInfo: SearchInfo,
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
Database.getInstance(),
|
||||
database,
|
||||
searchInfo,
|
||||
{ items ->
|
||||
{ openedDatabase, items ->
|
||||
callback.onSuccess(
|
||||
AutofillHelper.buildResponse(this,
|
||||
AutofillHelper.buildResponse(this, openedDatabase,
|
||||
items, parseResult, inlineSuggestionsRequest)
|
||||
)
|
||||
},
|
||||
{
|
||||
{ openedDatabase ->
|
||||
// Show UI if no search result
|
||||
showUIForEntrySelection(parseResult,
|
||||
showUIForEntrySelection(parseResult, openedDatabase,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
},
|
||||
{
|
||||
// Show UI if database not open
|
||||
showUIForEntrySelection(parseResult,
|
||||
showUIForEntrySelection(parseResult, null,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
}
|
||||
)
|
||||
@@ -133,6 +153,7 @@ class KeeAutofillService : AutofillService() {
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||
database: Database?,
|
||||
searchInfo: SearchInfo,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
@@ -140,38 +161,87 @@ class KeeAutofillService : AutofillService() {
|
||||
if (autofillIds.isNotEmpty()) {
|
||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||
// to generate Response.
|
||||
val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this,
|
||||
searchInfo, inlineSuggestionsRequest)
|
||||
val intentSender = AutofillLauncherActivity.getPendingIntentForSelection(this,
|
||||
searchInfo, inlineSuggestionsRequest).intentSender
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
|
||||
setTextViewText(R.id.autofill_web_domain_text, parseResult.webDomain)
|
||||
}
|
||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
||||
setTextViewText(R.id.autofill_app_id_text, parseResult.applicationId)
|
||||
val remoteViewsUnlock: RemoteViews = if (database == null) {
|
||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||
RemoteViews(
|
||||
packageName,
|
||||
R.layout.item_autofill_unlock_web_domain
|
||||
).apply {
|
||||
setTextViewText(
|
||||
R.id.autofill_web_domain_text,
|
||||
parseResult.webDomain
|
||||
)
|
||||
}
|
||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
||||
setTextViewText(
|
||||
R.id.autofill_app_id_text,
|
||||
parseResult.applicationId
|
||||
)
|
||||
}
|
||||
} else {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||
}
|
||||
} else {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||
RemoteViews(
|
||||
packageName,
|
||||
R.layout.item_autofill_select_entry_web_domain
|
||||
).apply {
|
||||
setTextViewText(
|
||||
R.id.autofill_web_domain_text,
|
||||
parseResult.webDomain
|
||||
)
|
||||
}
|
||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_select_entry_app_id).apply {
|
||||
setTextViewText(
|
||||
R.id.autofill_app_id_text,
|
||||
parseResult.applicationId
|
||||
)
|
||||
}
|
||||
} else {
|
||||
RemoteViews(packageName, R.layout.item_autofill_select_entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Tell to service the interest to save credentials
|
||||
// Tell the autofill framework the interest to save credentials
|
||||
if (askToSaveData) {
|
||||
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
|
||||
val info = ArrayList<AutofillId>()
|
||||
val requiredIds = ArrayList<AutofillId>()
|
||||
val optionalIds = ArrayList<AutofillId>()
|
||||
|
||||
// Only if at least a password
|
||||
parseResult.passwordId?.let { passwordInfo ->
|
||||
parseResult.usernameId?.let { usernameInfo ->
|
||||
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
|
||||
info.add(usernameInfo)
|
||||
requiredIds.add(usernameInfo)
|
||||
}
|
||||
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
|
||||
info.add(passwordInfo)
|
||||
requiredIds.add(passwordInfo)
|
||||
}
|
||||
if (info.isNotEmpty()) {
|
||||
responseBuilder.setSaveInfo(
|
||||
SaveInfo.Builder(types, info.toTypedArray()).build()
|
||||
)
|
||||
// or a credit card form
|
||||
if (requiredIds.isEmpty()) {
|
||||
parseResult.creditCardNumberId?.let { numberId ->
|
||||
types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
|
||||
requiredIds.add(numberId)
|
||||
Log.d(TAG, "Asking to save credit card number")
|
||||
}
|
||||
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
|
||||
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
|
||||
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
|
||||
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
|
||||
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
|
||||
}
|
||||
if (requiredIds.isNotEmpty()) {
|
||||
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
|
||||
if (optionalIds.isNotEmpty()) {
|
||||
builder.setOptionalIds(optionalIds.toTypedArray())
|
||||
}
|
||||
responseBuilder.setSaveInfo(builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,14 +293,35 @@ class KeeAutofillService : AutofillService() {
|
||||
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
||||
Log.d(TAG, "autofill onSaveRequest password")
|
||||
|
||||
// Build expiration from date or from year and month
|
||||
var expiration: DateTime? = parseResult.creditCardExpirationValue
|
||||
if (parseResult.creditCardExpirationValue == null
|
||||
&& parseResult.creditCardExpirationYearValue != 0
|
||||
&& parseResult.creditCardExpirationMonthValue != 0) {
|
||||
expiration = DateTime()
|
||||
.withYear(parseResult.creditCardExpirationYearValue)
|
||||
.withMonthOfYear(parseResult.creditCardExpirationMonthValue)
|
||||
if (parseResult.creditCardExpirationDayValue != 0) {
|
||||
expiration = expiration.withDayOfMonth(parseResult.creditCardExpirationDayValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Show UI to save data
|
||||
val registerInfo = RegisterInfo(SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
},
|
||||
val registerInfo = RegisterInfo(
|
||||
SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
},
|
||||
parseResult.usernameValue?.textValue?.toString(),
|
||||
parseResult.passwordValue?.textValue?.toString())
|
||||
parseResult.passwordValue?.textValue?.toString(),
|
||||
CreditCard(
|
||||
parseResult.creditCardHolder,
|
||||
parseResult.creditCardNumber,
|
||||
expiration,
|
||||
parseResult.cardVerificationValue
|
||||
))
|
||||
|
||||
// TODO Callback in each activity #765
|
||||
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
||||
|
||||
@@ -21,12 +21,14 @@ package com.kunzisoft.keepass.autofill
|
||||
import android.app.assist.AssistStructure
|
||||
import android.os.Build
|
||||
import android.text.InputType
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.autofill.AutofillId
|
||||
import android.view.autofill.AutofillValue
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
/**
|
||||
@@ -35,10 +37,8 @@ import java.util.*
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class StructureParser(private val structure: AssistStructure) {
|
||||
private var result: Result? = null
|
||||
|
||||
private var usernameNeeded = true
|
||||
|
||||
private var usernameCandidate: AutofillId? = null
|
||||
private var usernameIdCandidate: AutofillId? = null
|
||||
private var usernameValueCandidate: AutofillValue? = null
|
||||
|
||||
fun parse(saveValue: Boolean = false): Result? {
|
||||
@@ -46,37 +46,42 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
result = Result()
|
||||
result?.apply {
|
||||
allowSaveValues = saveValue
|
||||
usernameCandidate = null
|
||||
usernameIdCandidate = null
|
||||
usernameValueCandidate = null
|
||||
mainLoop@ for (i in 0 until structure.windowNodeCount) {
|
||||
val windowNode = structure.getWindowNodeAt(i)
|
||||
applicationId = windowNode.title.toString().split("/")[0]
|
||||
Log.d(TAG, "Autofill applicationId: $applicationId")
|
||||
|
||||
if (parseViewNode(windowNode.rootViewNode))
|
||||
break@mainLoop
|
||||
if (applicationId?.contains("PopupWindow:") == false) {
|
||||
if (parseViewNode(windowNode.rootViewNode))
|
||||
break@mainLoop
|
||||
}
|
||||
}
|
||||
// If not explicit username field found, add the field just before password field.
|
||||
if (usernameId == null && passwordId != null && usernameCandidate != null) {
|
||||
usernameId = usernameCandidate
|
||||
if (usernameId == null && passwordId != null && usernameIdCandidate != null) {
|
||||
usernameId = usernameIdCandidate
|
||||
if (allowSaveValues) {
|
||||
usernameValue = usernameValueCandidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the result only if password field is retrieved
|
||||
return if ((!usernameNeeded || result?.usernameId != null)
|
||||
&& result?.passwordId != null)
|
||||
result
|
||||
else
|
||||
null
|
||||
return if (result?.passwordId != null || result?.creditCardNumberId != null)
|
||||
result
|
||||
else
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
||||
// remember this
|
||||
if (node.className == "android.webkit.WebView") {
|
||||
result?.isWebView = true
|
||||
}
|
||||
|
||||
// Get the domain of a web app
|
||||
node.webDomain?.let { webDomain ->
|
||||
if (webDomain.isNotEmpty()) {
|
||||
@@ -97,8 +102,7 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
var returnValue = false
|
||||
// Only parse visible nodes
|
||||
if (node.visibility == View.VISIBLE) {
|
||||
if (node.autofillId != null
|
||||
&& node.autofillType == View.AUTOFILL_TYPE_TEXT) {
|
||||
if (node.autofillId != null) {
|
||||
// Parse methods
|
||||
val hints = node.autofillHints
|
||||
if (hints != null && hints.isNotEmpty()) {
|
||||
@@ -130,7 +134,7 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|
||||
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||
|| it.contains("email", true)
|
||||
|| it.contains(View.AUTOFILL_HINT_PHONE, true)-> {
|
||||
|| it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
|
||||
result?.usernameId = autofillId
|
||||
result?.usernameValue = node.autofillValue
|
||||
Log.d(TAG, "Autofill username hint")
|
||||
@@ -139,14 +143,123 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
result?.passwordId = autofillId
|
||||
result?.passwordValue = node.autofillValue
|
||||
Log.d(TAG, "Autofill password hint")
|
||||
// Username not needed in this case
|
||||
usernameNeeded = false
|
||||
return true
|
||||
}
|
||||
it.equals("cc-name", true) -> {
|
||||
Log.d(TAG, "Autofill credit card name hint")
|
||||
result?.creditCardHolderId = autofillId
|
||||
result?.creditCardHolder = node.autofillValue?.textValue?.toString()
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, true)
|
||||
|| it.equals("cc-number", true) -> {
|
||||
Log.d(TAG, "Autofill credit card number hint")
|
||||
result?.creditCardNumberId = autofillId
|
||||
result?.creditCardNumber = node.autofillValue?.textValue?.toString()
|
||||
}
|
||||
// expect date string as defined in https://html.spec.whatwg.org, e.g. 2014-12
|
||||
it.equals("cc-exp", true) -> {
|
||||
Log.d(TAG, "Autofill credit card expiration date hint")
|
||||
result?.creditCardExpirationDateId = autofillId
|
||||
node.autofillValue?.let { value ->
|
||||
if (value.isText && value.textValue.length == 7) {
|
||||
value.textValue.let { date ->
|
||||
try {
|
||||
result?.creditCardExpirationValue = DateTime()
|
||||
.withYear(date.substring(2, 4).toInt())
|
||||
.withMonthOfYear(date.substring(5, 7).toInt())
|
||||
} catch(e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve expiration", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE, true) -> {
|
||||
Log.d(TAG, "Autofill credit card expiration date hint")
|
||||
result?.creditCardExpirationDateId = autofillId
|
||||
node.autofillValue?.let { value ->
|
||||
if (value.isDate) {
|
||||
result?.creditCardExpirationValue = DateTime(value.dateValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, true)
|
||||
|| it.equals("cc-exp-year", true) -> {
|
||||
Log.d(TAG, "Autofill credit card expiration year hint")
|
||||
result?.creditCardExpirationYearId = autofillId
|
||||
if (node.autofillOptions != null) {
|
||||
result?.creditCardExpirationYearOptions = node.autofillOptions
|
||||
}
|
||||
node.autofillValue?.let { value ->
|
||||
var year = 0
|
||||
try {
|
||||
if (value.isText) {
|
||||
year = value.textValue.toString().toInt()
|
||||
}
|
||||
if (value.isList) {
|
||||
year = node.autofillOptions?.get(value.listValue).toString().toInt()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve expiration year", e)
|
||||
}
|
||||
result?.creditCardExpirationYearValue = year % 100
|
||||
}
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, true)
|
||||
|| it.equals("cc-exp-month", true) -> {
|
||||
Log.d(TAG, "Autofill credit card expiration month hint")
|
||||
result?.creditCardExpirationMonthId = autofillId
|
||||
if (node.autofillOptions != null) {
|
||||
result?.creditCardExpirationMonthOptions = node.autofillOptions
|
||||
}
|
||||
node.autofillValue?.let { value ->
|
||||
var month = 0
|
||||
try {
|
||||
if (value.isText) {
|
||||
month = value.textValue.toString().toInt()
|
||||
}
|
||||
if (value.isList) {
|
||||
// assume list starts with January (index 0)
|
||||
month = value.listValue + 1
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve expiration month", e)
|
||||
}
|
||||
result?.creditCardExpirationMonthValue = month
|
||||
}
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY, true)
|
||||
|| it.equals("cc-exp-day", true) -> {
|
||||
Log.d(TAG, "Autofill credit card expiration day hint")
|
||||
result?.creditCardExpirationDayId = autofillId
|
||||
if (node.autofillOptions != null) {
|
||||
result?.creditCardExpirationDayOptions = node.autofillOptions
|
||||
}
|
||||
node.autofillValue?.let { value ->
|
||||
var day = 0
|
||||
try {
|
||||
if (value.isText) {
|
||||
day = value.textValue.toString().toInt()
|
||||
}
|
||||
if (value.isList) {
|
||||
day = node.autofillOptions?.get(value.listValue).toString().toInt()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve expiration day", e)
|
||||
}
|
||||
result?.creditCardExpirationDayValue = day
|
||||
}
|
||||
}
|
||||
it.contains(View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE, true)
|
||||
|| it.contains("cc-csc", true) -> {
|
||||
Log.d(TAG, "Autofill card security code hint")
|
||||
result?.cardVerificationValueId = autofillId
|
||||
result?.cardVerificationValue = node.autofillValue?.textValue?.toString()
|
||||
}
|
||||
// Ignore autocomplete="off"
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
|
||||
it.equals("off", true) ||
|
||||
it.equals("on", true) -> {
|
||||
it.equals("on", true) -> {
|
||||
Log.d(TAG, "Autofill web hint")
|
||||
return parseNodeByHtmlAttributes(node)
|
||||
}
|
||||
@@ -171,7 +284,7 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||
}
|
||||
"text" -> {
|
||||
usernameCandidate = autofillId
|
||||
usernameIdCandidate = autofillId
|
||||
usernameValueCandidate = node.autofillValue
|
||||
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||
}
|
||||
@@ -219,15 +332,15 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
InputType.TYPE_TEXT_VARIATION_NORMAL,
|
||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
||||
usernameCandidate = autofillId
|
||||
usernameIdCandidate = autofillId
|
||||
usernameValueCandidate = node.autofillValue
|
||||
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||
}
|
||||
inputIsVariationType(inputType,
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
||||
// Some forms used visible password as username
|
||||
if (usernameCandidate == null && usernameValueCandidate == null) {
|
||||
usernameCandidate = autofillId
|
||||
if (usernameIdCandidate == null && usernameValueCandidate == null) {
|
||||
usernameIdCandidate = autofillId
|
||||
usernameValueCandidate = node.autofillValue
|
||||
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
|
||||
} else if (result?.passwordId == null && result?.passwordValue == null) {
|
||||
@@ -243,7 +356,6 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
result?.passwordId = autofillId
|
||||
result?.passwordValue = node.autofillValue
|
||||
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
|
||||
usernameNeeded = false
|
||||
return true
|
||||
}
|
||||
inputIsVariationType(inputType,
|
||||
@@ -265,16 +377,15 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
when {
|
||||
inputIsVariationType(inputType,
|
||||
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
||||
usernameCandidate = autofillId
|
||||
usernameIdCandidate = autofillId
|
||||
usernameValueCandidate = node.autofillValue
|
||||
Log.d(TAG, "Autofill usernale candidate android number type: ${showHexInputType(inputType)}")
|
||||
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
|
||||
}
|
||||
inputIsVariationType(inputType,
|
||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
||||
result?.passwordId = autofillId
|
||||
result?.passwordValue = node.autofillValue
|
||||
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
|
||||
usernameNeeded = false
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
@@ -288,8 +399,8 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class Result {
|
||||
var isWebView: Boolean = false
|
||||
var applicationId: String? = null
|
||||
|
||||
var webDomain: String? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
@@ -302,6 +413,12 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
field = value
|
||||
}
|
||||
|
||||
// if the user selects the credit card expiration date from a list of options
|
||||
// all options are stored here
|
||||
var creditCardExpirationYearOptions: Array<CharSequence>? = null
|
||||
var creditCardExpirationMonthOptions: Array<CharSequence>? = null
|
||||
var creditCardExpirationDayOptions: Array<CharSequence>? = null
|
||||
|
||||
var usernameId: AutofillId? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
@@ -314,6 +431,48 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
field = value
|
||||
}
|
||||
|
||||
var creditCardHolderId: AutofillId? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
var creditCardNumberId: AutofillId? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
var creditCardExpirationDateId: AutofillId? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
var creditCardExpirationYearId: AutofillId? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
var creditCardExpirationMonthId: AutofillId? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
var creditCardExpirationDayId: AutofillId? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
var cardVerificationValueId: AutofillId? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
fun allAutofillIds(): Array<AutofillId> {
|
||||
val all = ArrayList<AutofillId>()
|
||||
usernameId?.let {
|
||||
@@ -322,6 +481,15 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
passwordId?.let {
|
||||
all.add(it)
|
||||
}
|
||||
creditCardHolderId?.let {
|
||||
all.add(it)
|
||||
}
|
||||
creditCardNumberId?.let {
|
||||
all.add(it)
|
||||
}
|
||||
cardVerificationValueId?.let {
|
||||
all.add(it)
|
||||
}
|
||||
return all.toTypedArray()
|
||||
}
|
||||
|
||||
@@ -339,6 +507,52 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
if (allowSaveValues && field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
var creditCardHolder: String? = null
|
||||
set(value) {
|
||||
if (allowSaveValues)
|
||||
field = value
|
||||
}
|
||||
|
||||
var creditCardNumber: String? = null
|
||||
set(value) {
|
||||
if (allowSaveValues)
|
||||
field = value
|
||||
}
|
||||
|
||||
// format MMYY
|
||||
var creditCardExpirationValue: DateTime? = null
|
||||
set(value) {
|
||||
if (allowSaveValues)
|
||||
field = value
|
||||
}
|
||||
|
||||
// for year of CC expiration date: YY
|
||||
var creditCardExpirationYearValue = 0
|
||||
set(value) {
|
||||
if (allowSaveValues)
|
||||
field = value
|
||||
}
|
||||
|
||||
// for month of CC expiration date: MM
|
||||
var creditCardExpirationMonthValue = 0
|
||||
set(value) {
|
||||
if (allowSaveValues)
|
||||
field = value
|
||||
}
|
||||
|
||||
var creditCardExpirationDayValue = 0
|
||||
set(value) {
|
||||
if (allowSaveValues)
|
||||
field = value
|
||||
}
|
||||
|
||||
// the security code for the credit card (also called CVV)
|
||||
var cardVerificationValue: String? = null
|
||||
set(value) {
|
||||
if (allowSaveValues)
|
||||
field = value
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -32,6 +32,7 @@ class CreateDatabaseRunnable(context: Context,
|
||||
databaseUri: Uri,
|
||||
private val databaseName: String,
|
||||
private val rootName: String,
|
||||
private val templateGroupName: String?,
|
||||
mainCredential: MainCredential,
|
||||
private val createDatabaseResult: ((Result) -> Unit)?)
|
||||
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
|
||||
@@ -40,7 +41,7 @@ class CreateDatabaseRunnable(context: Context,
|
||||
try {
|
||||
// Create new database record
|
||||
mDatabase.apply {
|
||||
createData(mDatabaseUri, databaseName, rootName)
|
||||
createData(mDatabaseUri, databaseName, rootName, templateGroupName)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
mDatabase.clearAndClose(context)
|
||||
|
||||
@@ -19,21 +19,23 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.app.Service
|
||||
import android.content.*
|
||||
import android.content.Context.BIND_ABOVE_CLIENT
|
||||
import android.content.Context.BIND_NOT_FOREGROUND
|
||||
import android.content.Context.*
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
@@ -70,21 +72,35 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
/**
|
||||
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
|
||||
* Useful to retrieve a database instance and sending tasks commands
|
||||
*/
|
||||
class DatabaseTaskProvider {
|
||||
|
||||
var onActionFinish: ((actionTask: String,
|
||||
private var activity: FragmentActivity? = null
|
||||
private var service: Service? = null
|
||||
private var context: Context
|
||||
|
||||
var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null
|
||||
|
||||
var onActionFinish: ((database: Database,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result) -> Unit)? = null
|
||||
|
||||
private var intentDatabaseTask = Intent(activity.applicationContext, DatabaseTaskNotificationService::class.java)
|
||||
private var intentDatabaseTask: Intent
|
||||
|
||||
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
||||
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
||||
@@ -94,17 +110,31 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
||||
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
||||
|
||||
constructor(activity: FragmentActivity) {
|
||||
this.activity = activity
|
||||
this.context = activity
|
||||
this.intentDatabaseTask = Intent(activity.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java)
|
||||
}
|
||||
|
||||
constructor(service: Service) {
|
||||
this.service = service
|
||||
this.context = service
|
||||
this.intentDatabaseTask = Intent(service.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java)
|
||||
}
|
||||
|
||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
override fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
startDialog(titleId, messageId, warningId)
|
||||
}
|
||||
|
||||
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
updateDialog(titleId, messageId, warningId)
|
||||
}
|
||||
|
||||
override fun onStopAction(actionTask: String, result: ActionRunnable.Result) {
|
||||
onActionFinish?.invoke(actionTask, result)
|
||||
override fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) {
|
||||
onActionFinish?.invoke(database, actionTask, result)
|
||||
// Remove the progress task
|
||||
stopDialog()
|
||||
}
|
||||
@@ -119,31 +149,56 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
private var databaseInfoListener = object: DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||
override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newDatabaseInfo: SnapFileDatabaseInfo) {
|
||||
if (databaseChangedDialogFragment == null) {
|
||||
databaseChangedDialogFragment = activity.supportFragmentManager
|
||||
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
||||
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(previousDatabaseInfo, newDatabaseInfo)
|
||||
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener
|
||||
databaseChangedDialogFragment?.show(activity.supportFragmentManager, DATABASE_CHANGED_DIALOG_TAG)
|
||||
activity?.let { activity ->
|
||||
activity.lifecycleScope.launch {
|
||||
if (databaseChangedDialogFragment == null) {
|
||||
databaseChangedDialogFragment = activity.supportFragmentManager
|
||||
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
|
||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||
mActionDatabaseListener
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
||||
previousDatabaseInfo,
|
||||
newDatabaseInfo
|
||||
)
|
||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||
mActionDatabaseListener
|
||||
databaseChangedDialogFragment?.show(
|
||||
activity.supportFragmentManager,
|
||||
DATABASE_CHANGED_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var databaseListener = object: DatabaseTaskNotificationService.DatabaseListener {
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
onDatabaseRetrieved?.invoke(database)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDialog(titleId: Int? = null,
|
||||
messageId: Int? = null,
|
||||
warningId: Int? = null) {
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = activity.supportFragmentManager
|
||||
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
||||
activity?.let { activity ->
|
||||
activity.lifecycleScope.launch {
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = activity.supportFragmentManager
|
||||
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
||||
progressTaskDialogFragment?.show(
|
||||
activity.supportFragmentManager,
|
||||
PROGRESS_TASK_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
updateDialog(titleId, messageId, warningId)
|
||||
}
|
||||
}
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
||||
progressTaskDialogFragment?.show(activity.supportFragmentManager, PROGRESS_TASK_DIALOG_TAG)
|
||||
}
|
||||
updateDialog(titleId, messageId, warningId)
|
||||
}
|
||||
|
||||
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
@@ -170,16 +225,19 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||
addActionTaskListener(actionTaskListener)
|
||||
addDatabaseListener(databaseListener)
|
||||
addDatabaseFileInfoListener(databaseInfoListener)
|
||||
getService().checkAction()
|
||||
addActionTaskListener(actionTaskListener)
|
||||
getService().checkDatabase()
|
||||
getService().checkDatabaseInfo()
|
||||
getService().checkAction()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
mBinder?.removeDatabaseListener(databaseListener)
|
||||
mBinder = null
|
||||
}
|
||||
}
|
||||
@@ -189,7 +247,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
private fun bindService() {
|
||||
initServiceConnection()
|
||||
serviceConnection?.let {
|
||||
activity.bindService(intentDatabaseTask, it, BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
|
||||
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +256,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
*/
|
||||
private fun unBindService() {
|
||||
serviceConnection?.let {
|
||||
activity.unbindService(it)
|
||||
context.unbindService(it)
|
||||
}
|
||||
serviceConnection = null
|
||||
}
|
||||
@@ -226,7 +284,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
}
|
||||
}
|
||||
}
|
||||
activity.registerReceiver(databaseTaskBroadcastReceiver,
|
||||
context.registerReceiver(databaseTaskBroadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(DATABASE_START_TASK_ACTION)
|
||||
addAction(DATABASE_STOP_TASK_ACTION)
|
||||
@@ -240,14 +298,15 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
fun unregisterProgressTask() {
|
||||
stopDialog()
|
||||
|
||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
mBinder?.removeDatabaseListener(databaseListener)
|
||||
mBinder = null
|
||||
|
||||
unBindService()
|
||||
|
||||
try {
|
||||
activity.unregisterReceiver(databaseTaskBroadcastReceiver)
|
||||
context.unregisterReceiver(databaseTaskBroadcastReceiver)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// If receiver not register, do nothing
|
||||
}
|
||||
@@ -258,7 +317,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
if (bundle != null)
|
||||
intentDatabaseTask.putExtras(bundle)
|
||||
intentDatabaseTask.action = actionTask
|
||||
activity.startService(intentDatabaseTask)
|
||||
context.startService(intentDatabaseTask)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to perform database action", e)
|
||||
Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||
@@ -371,9 +430,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
nodesPaste.forEach { nodeVersioned ->
|
||||
when (nodeVersioned.type) {
|
||||
Type.GROUP -> {
|
||||
(nodeVersioned as Group).nodeId?.let { groupId ->
|
||||
groupsIdToCopy.add(groupId)
|
||||
}
|
||||
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
|
||||
@@ -416,22 +473,22 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
-----------------
|
||||
*/
|
||||
|
||||
fun startDatabaseRestoreEntryHistory(mainEntry: Entry,
|
||||
fun startDatabaseRestoreEntryHistory(mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int,
|
||||
save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId)
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
||||
}
|
||||
|
||||
fun startDatabaseDeleteEntryHistory(mainEntry: Entry,
|
||||
fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>,
|
||||
entryHistoryPosition: Int,
|
||||
save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntry.nodeId)
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
@@ -506,6 +563,28 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
|
||||
newRecycleBin: Group?,
|
||||
save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
|
||||
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveTemplatesGroup(oldTemplatesGroup: Group?,
|
||||
newTemplatesGroup: Group?,
|
||||
save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
|
||||
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
||||
newMaxHistoryItems: Int,
|
||||
save: Boolean) {
|
||||
@@ -600,6 +679,6 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ProgressDatabaseTaskProvider::class.java.name
|
||||
private val TAG = DatabaseTaskProvider::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -34,54 +34,52 @@ class UpdateEntryRunnable constructor(
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
|
||||
// Keep backup of original values in case save fails
|
||||
private var mBackupEntryHistory: Entry = Entry(mOldEntry)
|
||||
|
||||
override fun nodeAction() {
|
||||
// WARNING : Re attribute parent removed in entry edit activity to save memory
|
||||
mNewEntry.addParentFrom(mOldEntry)
|
||||
if (mOldEntry.nodeId == mNewEntry.nodeId) {
|
||||
// WARNING : Re attribute parent removed in entry edit activity to save memory
|
||||
mNewEntry.addParentFrom(mOldEntry)
|
||||
|
||||
// Build oldest attachments
|
||||
val oldEntryAttachments = mOldEntry.getAttachments(database.attachmentPool, true)
|
||||
val newEntryAttachments = mNewEntry.getAttachments(database.attachmentPool, 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.binaryData == newAttachment.binaryData)
|
||||
attachmentsToRemove.remove(oldAttachment)
|
||||
// Build oldest attachments
|
||||
val oldEntryAttachments = mOldEntry.getAttachments(database.attachmentPool, true)
|
||||
val newEntryAttachments = mNewEntry.getAttachments(database.attachmentPool, 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.binaryData == newAttachment.binaryData
|
||||
)
|
||||
attachmentsToRemove.remove(oldAttachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update entry with new values
|
||||
mOldEntry.updateWith(mNewEntry)
|
||||
mNewEntry.touch(modified = true, touchParents = true)
|
||||
// Update entry with new values
|
||||
mNewEntry.touch(modified = true, touchParents = true)
|
||||
|
||||
// Create an entry history (an entry history don't have history)
|
||||
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
|
||||
database.removeOldestEntryHistory(mOldEntry, database.attachmentPool)
|
||||
// Create an entry history (an entry history don't have history)
|
||||
mNewEntry.addEntryToHistory(Entry(mOldEntry, copyHistory = false))
|
||||
database.removeOldestEntryHistory(mNewEntry, database.attachmentPool)
|
||||
|
||||
// Only change data in index
|
||||
database.updateEntry(mOldEntry)
|
||||
// Only change data in index
|
||||
database.updateEntry(mNewEntry)
|
||||
|
||||
// Remove oldest attachments
|
||||
attachmentsToRemove.forEach {
|
||||
database.removeAttachmentIfNotUsed(it)
|
||||
// Remove oldest attachments
|
||||
attachmentsToRemove.forEach {
|
||||
database.removeAttachmentIfNotUsed(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFinish(): ActionNodesValues {
|
||||
if (!result.isSuccess) {
|
||||
mOldEntry.updateWith(mBackupEntryHistory)
|
||||
// If we fail to save, back out changes to global structure
|
||||
database.updateEntry(mOldEntry)
|
||||
}
|
||||
|
||||
val oldNodesReturn = ArrayList<Node>()
|
||||
oldNodesReturn.add(mBackupEntryHistory)
|
||||
oldNodesReturn.add(mOldEntry)
|
||||
val newNodesReturn = ArrayList<Node>()
|
||||
newNodesReturn.add(mOldEntry)
|
||||
newNodesReturn.add(mNewEntry)
|
||||
return ActionNodesValues(oldNodesReturn, newNodesReturn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,33 +33,30 @@ class UpdateGroupRunnable constructor(
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
|
||||
// Keep backup of original values in case save fails
|
||||
private val mBackupGroup: Group = Group(mOldGroup)
|
||||
|
||||
override fun nodeAction() {
|
||||
// WARNING : Re attribute parent and children removed in group activity to save memory
|
||||
mNewGroup.addParentFrom(mOldGroup)
|
||||
mNewGroup.addChildrenFrom(mOldGroup)
|
||||
if (mOldGroup.nodeId == mNewGroup.nodeId) {
|
||||
// WARNING : Re attribute parent and children removed in group activity to save memory
|
||||
mNewGroup.addParentFrom(mOldGroup)
|
||||
mNewGroup.addChildrenFrom(mOldGroup)
|
||||
|
||||
// Update group with new values
|
||||
mOldGroup.updateWith(mNewGroup)
|
||||
mOldGroup.touch(modified = true, touchParents = true)
|
||||
// Update group with new values
|
||||
mNewGroup.touch(modified = true, touchParents = true)
|
||||
|
||||
// Only change data in index
|
||||
database.updateGroup(mOldGroup)
|
||||
// Only change data in index
|
||||
database.updateGroup(mNewGroup)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFinish(): ActionNodesValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, back out changes to global structure
|
||||
mOldGroup.updateWith(mBackupGroup)
|
||||
database.updateGroup(mOldGroup)
|
||||
}
|
||||
|
||||
val oldNodesReturn = ArrayList<Node>()
|
||||
oldNodesReturn.add(mBackupGroup)
|
||||
oldNodesReturn.add(mOldGroup)
|
||||
val newNodesReturn = ArrayList<Node>()
|
||||
newNodesReturn.add(mOldGroup)
|
||||
newNodesReturn.add(mNewGroup)
|
||||
return ActionNodesValues(oldNodesReturn, newNodesReturn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
|
||||
entry.expires
|
||||
))
|
||||
|
||||
entry.doForEachDecodedCustomField { key, value ->
|
||||
extraFieldCursor.addExtraField(entryId, key, value)
|
||||
entry.doForEachDecodedCustomField { field ->
|
||||
extraFieldCursor.addExtraField(entryId, field)
|
||||
}
|
||||
|
||||
entryId++
|
||||
|
||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.cursor
|
||||
|
||||
import android.database.MatrixCursor
|
||||
import android.provider.BaseColumns
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
@@ -36,8 +37,12 @@ class ExtraFieldCursor : MatrixCursor(arrayOf(
|
||||
private var fieldId: Long = 0
|
||||
|
||||
@Synchronized
|
||||
fun addExtraField(entryId: Long, label: String, value: ProtectedString) {
|
||||
addRow(arrayOf(fieldId, entryId, label, if (value.isProtected) 1 else 0, value.toString()))
|
||||
fun addExtraField(entryId: Long, field: Field) {
|
||||
addRow(arrayOf(fieldId,
|
||||
entryId,
|
||||
field.name,
|
||||
if (field.protectedValue.isProtected) 1 else 0,
|
||||
field.protectedValue.toString()))
|
||||
fieldId++
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
@@ -40,6 +42,8 @@ import com.kunzisoft.keepass.database.element.icon.IconsManager
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateEngine
|
||||
import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
@@ -143,6 +147,57 @@ class Database {
|
||||
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
|
||||
}
|
||||
|
||||
fun getTemplates(templateCreation: Boolean): List<Template> {
|
||||
return mDatabaseKDBX?.getTemplates(templateCreation) ?: listOf()
|
||||
}
|
||||
|
||||
fun getTemplate(entry: Entry): Template? {
|
||||
if (entryIsTemplate(entry))
|
||||
return TemplateEngine.CREATION
|
||||
entry.entryKDBX?.let { entryKDBX ->
|
||||
return mDatabaseKDBX?.getTemplate(entryKDBX)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun entryIsTemplate(entry: Entry?): Boolean {
|
||||
// Define is current entry is a template (in direct template group)
|
||||
if (entry == null || templatesGroup == null)
|
||||
return false
|
||||
return templatesGroup == entry.parent
|
||||
}
|
||||
|
||||
// Not the same as decode, here remove in all cases the template link in the entry data
|
||||
fun removeTemplateConfiguration(entry: Entry): Entry {
|
||||
entry.entryKDBX?.let {
|
||||
mDatabaseKDBX?.decodeEntryWithTemplateConfiguration(it, false)?.let { decode ->
|
||||
return Entry(decode)
|
||||
}
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
// Remove the template link in the entry data if it's a basic entry
|
||||
// or compress the template fields (as pseudo language) if it's a template entry
|
||||
fun decodeEntryWithTemplateConfiguration(entry: Entry, lastEntryVersion: Entry? = null): Entry {
|
||||
entry.entryKDBX?.let {
|
||||
val lastEntry = lastEntryVersion ?: entry
|
||||
mDatabaseKDBX?.decodeEntryWithTemplateConfiguration(it, entryIsTemplate(lastEntry))?.let { decode ->
|
||||
return Entry(decode)
|
||||
}
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
fun encodeEntryWithTemplateConfiguration(entry: Entry, template: Template): Entry {
|
||||
entry.entryKDBX?.let {
|
||||
mDatabaseKDBX?.encodeEntryWithTemplateConfiguration(it, entryIsTemplate(entry), template)?.let { encode ->
|
||||
return Entry(encode)
|
||||
}
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
val allowName: Boolean
|
||||
get() = mDatabaseKDBX != null
|
||||
|
||||
@@ -316,6 +371,15 @@ class Database {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not modify groups here, used for read only
|
||||
*/
|
||||
fun getAllGroupsWithoutRoot(): List<Group> {
|
||||
return mDatabaseKDB?.getAllGroupsWithoutRoot()?.map { Group(it) }
|
||||
?: mDatabaseKDBX?.getAllGroupsWithoutRoot()?.map { Group(it) }
|
||||
?: listOf()
|
||||
}
|
||||
|
||||
val manageHistory: Boolean
|
||||
get() = mDatabaseKDBX != null
|
||||
|
||||
@@ -342,12 +406,18 @@ class Database {
|
||||
val allowConfigurableRecycleBin: Boolean
|
||||
get() = mDatabaseKDBX != null
|
||||
|
||||
var isRecycleBinEnabled: Boolean
|
||||
val isRecycleBinEnabled: Boolean
|
||||
// Backup is always enabled in KDB database
|
||||
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
|
||||
set(value) {
|
||||
mDatabaseKDBX?.isRecycleBinEnabled = value
|
||||
|
||||
fun enableRecycleBin(enable: Boolean, resources: Resources) {
|
||||
mDatabaseKDBX?.isRecycleBinEnabled = enable
|
||||
if (enable) {
|
||||
ensureRecycleBinExists(resources)
|
||||
} else {
|
||||
mDatabaseKDBX?.removeRecycleBin()
|
||||
}
|
||||
}
|
||||
|
||||
val recycleBin: Group?
|
||||
get() {
|
||||
@@ -360,6 +430,47 @@ class Database {
|
||||
return null
|
||||
}
|
||||
|
||||
fun setRecycleBin(group: Group?) {
|
||||
// Only the kdbx recycle bin can be changed
|
||||
if (group != null) {
|
||||
mDatabaseKDBX?.recycleBinUUID = group.nodeIdKDBX.id
|
||||
} else {
|
||||
mDatabaseKDBX?.removeTemplatesGroup()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a configurable templates group is available or not for this version of database
|
||||
* @return true if a configurable templates group available
|
||||
*/
|
||||
val allowConfigurableTemplatesGroup: Boolean
|
||||
get() = mDatabaseKDBX != null
|
||||
|
||||
// Maybe another templates method with KDBX5
|
||||
val isTemplatesEnabled: Boolean
|
||||
get() = mDatabaseKDBX?.isTemplatesGroupEnabled() ?: false
|
||||
|
||||
fun enableTemplates(enable: Boolean, templatesGroupName: String) {
|
||||
mDatabaseKDBX?.enableTemplatesGroup(enable, templatesGroupName)
|
||||
}
|
||||
|
||||
val templatesGroup: Group?
|
||||
get() {
|
||||
mDatabaseKDBX?.getTemplatesGroup()?.let {
|
||||
return Group(it)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun setTemplatesGroup(group: Group?) {
|
||||
// Only the kdbx templates group can be changed
|
||||
if (group != null) {
|
||||
mDatabaseKDBX?.entryTemplatesGroup = group.nodeIdKDBX.id
|
||||
} else {
|
||||
mDatabaseKDBX?.entryTemplatesGroup
|
||||
}
|
||||
}
|
||||
|
||||
val groupNamesNotAllowed: List<String>
|
||||
get() {
|
||||
return mDatabaseKDB?.groupNamesNotAllowed ?: ArrayList()
|
||||
@@ -375,8 +486,11 @@ class Database {
|
||||
this.mDatabaseKDBX = databaseKDBX
|
||||
}
|
||||
|
||||
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
||||
val newDatabase = DatabaseKDBX(databaseName, rootName)
|
||||
fun createData(databaseUri: Uri,
|
||||
databaseName: String,
|
||||
rootName: String,
|
||||
templateGroupName: String?) {
|
||||
val newDatabase = DatabaseKDBX(databaseName, rootName, templateGroupName)
|
||||
setDatabaseKDBX(newDatabase)
|
||||
this.fileUri = databaseUri
|
||||
// Set Database state
|
||||
@@ -558,6 +672,7 @@ class Database {
|
||||
searchInOther = true
|
||||
searchInUUIDs = false
|
||||
searchInTags = false
|
||||
searchInTemplates = false
|
||||
}, omitBackup, max)
|
||||
}
|
||||
|
||||
@@ -575,10 +690,11 @@ class Database {
|
||||
return false
|
||||
}
|
||||
|
||||
fun buildNewBinaryAttachment(compressed: Boolean = false,
|
||||
protected: Boolean = false): BinaryData? {
|
||||
fun buildNewBinaryAttachment(): BinaryData? {
|
||||
return mDatabaseKDB?.buildNewAttachment()
|
||||
?: mDatabaseKDBX?.buildNewAttachment( false, compressed, protected)
|
||||
?: mDatabaseKDBX?.buildNewAttachment( false,
|
||||
compressionForNewEntry(),
|
||||
false)
|
||||
}
|
||||
|
||||
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
||||
@@ -891,11 +1007,6 @@ class Database {
|
||||
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
||||
}
|
||||
|
||||
fun removeRecycleBin() {
|
||||
// Don't allow remove backup in KDB
|
||||
mDatabaseKDBX?.removeRecycleBin()
|
||||
}
|
||||
|
||||
fun canRecycle(entry: Entry): Boolean {
|
||||
var canRecycle: Boolean? = null
|
||||
entry.entryKDB?.let {
|
||||
|
||||
@@ -23,32 +23,85 @@ import android.content.res.Resources
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import org.joda.time.Duration
|
||||
import org.joda.time.Instant
|
||||
import com.kunzisoft.keepass.utils.readEnum
|
||||
import com.kunzisoft.keepass.utils.writeEnum
|
||||
import org.joda.time.*
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class DateInstant : Parcelable {
|
||||
|
||||
private var jDate: Date = Date()
|
||||
private var mType: Type = Type.DATE_TIME
|
||||
|
||||
val date: Date
|
||||
get() = jDate
|
||||
|
||||
var type: Type
|
||||
get() = mType
|
||||
set(value) {
|
||||
mType = value
|
||||
}
|
||||
|
||||
constructor(source: DateInstant) {
|
||||
this.jDate = Date(source.jDate.time)
|
||||
this.mType = source.mType
|
||||
}
|
||||
|
||||
constructor(date: Date) {
|
||||
constructor(date: Date, type: Type = Type.DATE_TIME) {
|
||||
jDate = Date(date.time)
|
||||
mType = type
|
||||
}
|
||||
|
||||
constructor(millis: Long) {
|
||||
constructor(millis: Long, type: Type = Type.DATE_TIME) {
|
||||
jDate = Date(millis)
|
||||
mType = type
|
||||
}
|
||||
|
||||
constructor(string: String) {
|
||||
jDate = dateFormat.parse(string) ?: jDate
|
||||
private fun parse(value: String, type: Type): Date {
|
||||
return when (type) {
|
||||
Type.DATE -> dateFormat.parse(value) ?: jDate
|
||||
Type.TIME -> timeFormat.parse(value) ?: jDate
|
||||
else -> dateTimeFormat.parse(value) ?: jDate
|
||||
}
|
||||
}
|
||||
|
||||
constructor(string: String, type: Type = Type.DATE_TIME) {
|
||||
try {
|
||||
jDate = parse(string, type)
|
||||
mType = type
|
||||
} catch (e: Exception) {
|
||||
// Retry with second format
|
||||
try {
|
||||
when (type) {
|
||||
Type.TIME -> {
|
||||
jDate = parse(string, Type.DATE)
|
||||
mType = Type.DATE
|
||||
}
|
||||
else -> {
|
||||
jDate = parse(string, Type.TIME)
|
||||
mType = Type.TIME
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Retry with third format
|
||||
when (type) {
|
||||
Type.DATE, Type.TIME -> {
|
||||
jDate = parse(string, Type.DATE_TIME)
|
||||
mType = Type.DATE_TIME
|
||||
}
|
||||
else -> {
|
||||
jDate = parse(string, Type.DATE)
|
||||
mType = Type.DATE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(type: Type) {
|
||||
mType = type
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@@ -56,62 +109,123 @@ class DateInstant : Parcelable {
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) {
|
||||
jDate = parcel.readSerializable() as Date
|
||||
jDate = parcel.readSerializable() as? Date? ?: jDate
|
||||
mType = parcel.readEnum<Type>() ?: mType
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
fun getDateTimeString(resources: Resources): String {
|
||||
return Companion.getDateTimeString(resources, this.date)
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeSerializable(date)
|
||||
dest.writeSerializable(jDate)
|
||||
dest.writeEnum(mType)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
fun getDateTimeString(resources: Resources): String {
|
||||
return when (mType) {
|
||||
Type.DATE -> DateFormat.getDateInstance(
|
||||
DateFormat.MEDIUM,
|
||||
ConfigurationCompat.getLocales(resources.configuration)[0])
|
||||
.format(jDate)
|
||||
Type.TIME -> DateFormat.getTimeInstance(
|
||||
DateFormat.SHORT,
|
||||
ConfigurationCompat.getLocales(resources.configuration)[0])
|
||||
.format(jDate)
|
||||
else -> DateFormat.getDateTimeInstance(
|
||||
DateFormat.MEDIUM,
|
||||
DateFormat.SHORT,
|
||||
ConfigurationCompat.getLocales(resources.configuration)[0])
|
||||
.format(jDate)
|
||||
}
|
||||
if (other == null) {
|
||||
return false
|
||||
}
|
||||
if (javaClass != other.javaClass) {
|
||||
return false
|
||||
}
|
||||
|
||||
val date = other as DateInstant
|
||||
return isSameDate(jDate, date.jDate)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return jDate.hashCode()
|
||||
fun getYearInt(): Int {
|
||||
val dateFormat = SimpleDateFormat("yyyy", Locale.ENGLISH)
|
||||
return dateFormat.format(date).toInt()
|
||||
}
|
||||
|
||||
fun getMonthInt(): Int {
|
||||
val dateFormat = SimpleDateFormat("MM", Locale.ENGLISH)
|
||||
return dateFormat.format(date).toInt()
|
||||
}
|
||||
|
||||
fun getDay(): Int {
|
||||
val dateFormat = SimpleDateFormat("dd", Locale.ENGLISH)
|
||||
return dateFormat.format(date).toInt()
|
||||
}
|
||||
|
||||
// If expireDate is before NEVER_EXPIRE date less 1 month (to be sure)
|
||||
// it is not expires
|
||||
fun isNeverExpires(): Boolean {
|
||||
return LocalDateTime(jDate)
|
||||
.isBefore(
|
||||
LocalDateTime.fromDateFields(NEVER_EXPIRES.date)
|
||||
.minusMonths(1))
|
||||
}
|
||||
|
||||
fun isCurrentlyExpire(): Boolean {
|
||||
return when (type) {
|
||||
Type.DATE -> LocalDate.fromDateFields(jDate).isBefore(LocalDate.now())
|
||||
Type.TIME -> LocalTime.fromDateFields(jDate).isBefore(LocalTime.now())
|
||||
else -> LocalDateTime.fromDateFields(jDate).isBefore(LocalDateTime.now())
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return dateFormat.format(jDate)
|
||||
return when (type) {
|
||||
Type.DATE -> dateFormat.format(jDate)
|
||||
Type.TIME -> timeFormat.format(jDate)
|
||||
else -> dateTimeFormat.format(jDate)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is DateInstant) return false
|
||||
|
||||
if (jDate != other.jDate) return false
|
||||
if (mType != other.mType) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = jDate.hashCode()
|
||||
result = 31 * result + mType.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
DATE_TIME, DATE, TIME
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val NEVER_EXPIRE = neverExpire
|
||||
val IN_ONE_MONTH = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
|
||||
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
|
||||
val NEVER_EXPIRES = DateInstant(Calendar.getInstance().apply {
|
||||
set(Calendar.YEAR, 2999)
|
||||
set(Calendar.MONTH, 11)
|
||||
set(Calendar.DAY_OF_MONTH, 28)
|
||||
set(Calendar.HOUR, 23)
|
||||
set(Calendar.MINUTE, 59)
|
||||
set(Calendar.SECOND, 59)
|
||||
}.time)
|
||||
val IN_ONE_MONTH_DATE_TIME = DateInstant(
|
||||
Instant.now().plus(Duration.standardDays(30)).toDate(), Type.DATE_TIME)
|
||||
val IN_ONE_MONTH_DATE = DateInstant(
|
||||
Instant.now().plus(Duration.standardDays(30)).toDate(), Type.DATE)
|
||||
val IN_ONE_HOUR_TIME = DateInstant(
|
||||
Instant.now().plus(Duration.standardHours(1)).toDate(), Type.TIME)
|
||||
|
||||
private val neverExpire: DateInstant
|
||||
get() {
|
||||
val cal = Calendar.getInstance()
|
||||
cal.set(Calendar.YEAR, 2999)
|
||||
cal.set(Calendar.MONTH, 11)
|
||||
cal.set(Calendar.DAY_OF_MONTH, 28)
|
||||
cal.set(Calendar.HOUR, 23)
|
||||
cal.set(Calendar.MINUTE, 59)
|
||||
cal.set(Calendar.SECOND, 59)
|
||||
|
||||
return DateInstant(cal.time)
|
||||
}
|
||||
private val dateTimeFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.ROOT).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'Z'", Locale.ROOT).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
private val timeFormat = SimpleDateFormat("HH:mm'Z'", Locale.ROOT).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<DateInstant> = object : Parcelable.Creator<DateInstant> {
|
||||
@@ -123,31 +237,5 @@ class DateInstant : Parcelable {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSameDate(d1: Date, d2: Date): Boolean {
|
||||
val cal1 = Calendar.getInstance()
|
||||
cal1.time = d1
|
||||
cal1.set(Calendar.MILLISECOND, 0)
|
||||
|
||||
val cal2 = Calendar.getInstance()
|
||||
cal2.time = d2
|
||||
cal2.set(Calendar.MILLISECOND, 0)
|
||||
|
||||
return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
|
||||
cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH) &&
|
||||
cal1.get(Calendar.DAY_OF_MONTH) == cal2.get(Calendar.DAY_OF_MONTH) &&
|
||||
cal1.get(Calendar.HOUR) == cal2.get(Calendar.HOUR) &&
|
||||
cal1.get(Calendar.MINUTE) == cal2.get(Calendar.MINUTE) &&
|
||||
cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND)
|
||||
|
||||
}
|
||||
|
||||
fun getDateTimeString(resources: Resources, date: Date): String {
|
||||
return java.text.DateFormat.getDateTimeInstance(
|
||||
java.text.DateFormat.MEDIUM,
|
||||
java.text.DateFormat.SHORT,
|
||||
ConfigurationCompat.getLocales(resources.configuration)[0])
|
||||
.format(date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import java.util.*
|
||||
@@ -46,15 +45,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
var entryKDBX: EntryKDBX? = null
|
||||
private set
|
||||
|
||||
fun updateWith(entry: Entry, copyHistory: Boolean = true) {
|
||||
entry.entryKDB?.let {
|
||||
this.entryKDB?.updateWith(it)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
this.entryKDBX?.updateWith(it, copyHistory)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this constructor to copy an Entry with exact same values
|
||||
*/
|
||||
@@ -65,7 +55,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
if (entry.entryKDBX != null) {
|
||||
this.entryKDBX = EntryKDBX()
|
||||
}
|
||||
updateWith(entry, copyHistory)
|
||||
entry.entryKDB?.let {
|
||||
this.entryKDB?.updateWith(it)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
this.entryKDBX?.updateWith(it, copyHistory)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(entry: EntryKDB) {
|
||||
@@ -283,8 +278,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
fun getExtraFields(): List<Field> {
|
||||
val extraFields = ArrayList<Field>()
|
||||
entryKDBX?.let {
|
||||
it.doForEachDecodedCustomField { key, value ->
|
||||
extraFields.add(Field(key, value))
|
||||
it.doForEachDecodedCustomField { field ->
|
||||
extraFields.add(field)
|
||||
}
|
||||
}
|
||||
return extraFields
|
||||
@@ -294,7 +289,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
* Update or add an extra field to the list (standard or custom)
|
||||
*/
|
||||
fun putExtraField(field: Field) {
|
||||
entryKDBX?.putField(field.name, field.protectedValue)
|
||||
entryKDBX?.putField(field)
|
||||
}
|
||||
|
||||
private fun addExtraFields(fields: List<Field>) {
|
||||
@@ -310,7 +305,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
fun getOtpElement(): OtpElement? {
|
||||
entryKDBX?.let {
|
||||
return OtpEntryFields.parseFields { key ->
|
||||
it.getField(key)?.toString()
|
||||
it.getFieldValue(key)?.toString()
|
||||
}
|
||||
}
|
||||
return null
|
||||
@@ -398,37 +393,46 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
* Retrieve generated entry info.
|
||||
* If are not [raw] data, remove parameter fields and add auto generated elements in auto custom fields
|
||||
*/
|
||||
fun getEntryInfo(database: Database?, raw: Boolean = false): EntryInfo {
|
||||
fun getEntryInfo(database: Database?,
|
||||
raw: Boolean = false,
|
||||
removeTemplateConfiguration: Boolean = true): EntryInfo {
|
||||
val entryInfo = EntryInfo()
|
||||
if (raw)
|
||||
database?.stopManageEntry(this)
|
||||
// Remove unwanted template fields
|
||||
val baseInfo = if (removeTemplateConfiguration)
|
||||
database?.removeTemplateConfiguration(this) ?: this
|
||||
else
|
||||
database?.startManageEntry(this)
|
||||
this
|
||||
baseInfo.apply {
|
||||
if (raw)
|
||||
database?.stopManageEntry(this)
|
||||
else
|
||||
database?.startManageEntry(this)
|
||||
|
||||
entryInfo.id = nodeId.toString()
|
||||
entryInfo.title = title
|
||||
entryInfo.icon = icon
|
||||
entryInfo.username = username
|
||||
entryInfo.password = password
|
||||
entryInfo.creationTime = creationTime
|
||||
entryInfo.lastModificationTime = lastModificationTime
|
||||
entryInfo.expires = expires
|
||||
entryInfo.expiryTime = expiryTime
|
||||
entryInfo.url = url
|
||||
entryInfo.notes = notes
|
||||
entryInfo.customFields = getExtraFields()
|
||||
// Add otpElement to generate token
|
||||
entryInfo.otpModel = getOtpElement()?.otpModel
|
||||
if (!raw) {
|
||||
// Replace parameter fields by generated OTP fields
|
||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||
}
|
||||
database?.attachmentPool?.let { binaryPool ->
|
||||
entryInfo.attachments = getAttachments(binaryPool)
|
||||
}
|
||||
entryInfo.id = nodeId.id
|
||||
entryInfo.title = title
|
||||
entryInfo.icon = icon
|
||||
entryInfo.username = username
|
||||
entryInfo.password = password
|
||||
entryInfo.creationTime = creationTime
|
||||
entryInfo.lastModificationTime = lastModificationTime
|
||||
entryInfo.expires = expires
|
||||
entryInfo.expiryTime = expiryTime
|
||||
entryInfo.url = url
|
||||
entryInfo.notes = notes
|
||||
entryInfo.customFields = getExtraFields().toMutableList()
|
||||
// Add otpElement to generate token
|
||||
entryInfo.otpModel = getOtpElement()?.otpModel
|
||||
if (!raw) {
|
||||
// Replace parameter fields by generated OTP fields
|
||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||
}
|
||||
database?.attachmentPool?.let { binaryPool ->
|
||||
entryInfo.attachments = getAttachments(binaryPool).toMutableList()
|
||||
}
|
||||
|
||||
if (!raw)
|
||||
database?.stopManageEntry(this)
|
||||
if (!raw)
|
||||
database?.stopManageEntry(this)
|
||||
}
|
||||
return entryInfo
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.model
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
@@ -44,14 +44,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
// Virtual group is used to defined a detached database group
|
||||
var isVirtual = false
|
||||
|
||||
fun updateWith(group: Group) {
|
||||
group.groupKDB?.let {
|
||||
this.groupKDB?.updateWith(it)
|
||||
}
|
||||
group.groupKDBX?.let {
|
||||
this.groupKDBX?.updateWith(it)
|
||||
}
|
||||
}
|
||||
var numberOfChildEntries: Int = 0
|
||||
|
||||
/**
|
||||
* Use this constructor to copy a Group
|
||||
@@ -65,7 +58,12 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
if (this.groupKDBX == null)
|
||||
this.groupKDBX = GroupKDBX()
|
||||
}
|
||||
updateWith(group)
|
||||
group.groupKDB?.let {
|
||||
this.groupKDB?.updateWith(it)
|
||||
}
|
||||
group.groupKDBX?.let {
|
||||
this.groupKDBX?.updateWith(it)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(group: GroupKDB) {
|
||||
@@ -118,8 +116,8 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
dest.writeByte((if (isVirtual) 1 else 0).toByte())
|
||||
}
|
||||
|
||||
override val nodeId: NodeId<*>?
|
||||
get() = groupKDBX?.nodeId ?: groupKDB?.nodeId
|
||||
override val nodeId: NodeId<*>
|
||||
get() = groupKDBX?.nodeId ?: groupKDB?.nodeId ?: NodeIdUUID()
|
||||
|
||||
override var title: String
|
||||
get() = groupKDB?.title ?: groupKDBX?.title ?: ""
|
||||
@@ -270,6 +268,20 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
ArrayList()
|
||||
}
|
||||
|
||||
fun getFilteredChildGroups(filters: Array<ChildFilter>): List<Group> {
|
||||
return groupKDB?.getChildGroups()?.map {
|
||||
Group(it).apply {
|
||||
this.refreshNumberOfChildEntries(filters)
|
||||
}
|
||||
} ?:
|
||||
groupKDBX?.getChildGroups()?.map {
|
||||
Group(it).apply {
|
||||
this.refreshNumberOfChildEntries(filters)
|
||||
}
|
||||
} ?:
|
||||
ArrayList()
|
||||
}
|
||||
|
||||
override fun getChildEntries(): List<Entry> {
|
||||
return groupKDB?.getChildEntries()?.map {
|
||||
Entry(it)
|
||||
@@ -306,8 +318,8 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
ArrayList()
|
||||
}
|
||||
|
||||
fun getNumberOfChildEntries(filters: Array<ChildFilter> = emptyArray()): Int {
|
||||
return getFilteredChildEntries(filters).size
|
||||
fun refreshNumberOfChildEntries(filters: Array<ChildFilter> = emptyArray()) {
|
||||
this.numberOfChildEntries = getFilteredChildEntries(filters).size
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,7 +331,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
}
|
||||
|
||||
fun getFilteredChildren(filters: Array<ChildFilter>): List<Node> {
|
||||
return getChildGroups() + getFilteredChildEntries(filters)
|
||||
val nodes = getFilteredChildGroups(filters) + getFilteredChildEntries(filters)
|
||||
refreshNumberOfChildEntries(filters)
|
||||
return nodes
|
||||
}
|
||||
|
||||
override fun addChildGroup(group: Group) {
|
||||
@@ -340,6 +354,24 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateChildGroup(group: Group) {
|
||||
group.groupKDB?.let {
|
||||
groupKDB?.updateChildGroup(it)
|
||||
}
|
||||
group.groupKDBX?.let {
|
||||
groupKDBX?.updateChildGroup(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateChildEntry(entry: Entry) {
|
||||
entry.entryKDB?.let {
|
||||
groupKDB?.updateChildEntry(it)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
groupKDBX?.updateChildEntry(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeChildGroup(group: Group) {
|
||||
group.groupKDB?.let {
|
||||
groupKDB?.removeChildGroup(it)
|
||||
@@ -455,4 +487,10 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
result = 31 * result + (groupKDBX?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return groupKDB?.toString() ?: groupKDBX?.toString() ?: "Undefined"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -28,15 +28,17 @@ import java.util.*
|
||||
enum class SortNodeEnum {
|
||||
DB, TITLE, USERNAME, CREATION_TIME, LAST_MODIFY_TIME, LAST_ACCESS_TIME;
|
||||
|
||||
fun <G: GroupVersionedInterface<G, *>> getNodeComparator(sortNodeParameters: SortNodeParameters)
|
||||
fun <G: GroupVersionedInterface<G, *>> getNodeComparator(
|
||||
database: Database,
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: Comparator<NodeVersionedInterface<G>> {
|
||||
return when (this) {
|
||||
DB -> NodeNaturalComparator(sortNodeParameters) // Force false because natural order contains recycle bin
|
||||
TITLE -> NodeTitleComparator(sortNodeParameters)
|
||||
USERNAME -> NodeUsernameComparator(sortNodeParameters)
|
||||
CREATION_TIME -> NodeCreationComparator(sortNodeParameters)
|
||||
LAST_MODIFY_TIME -> NodeLastModificationComparator(sortNodeParameters)
|
||||
LAST_ACCESS_TIME -> NodeLastAccessComparator(sortNodeParameters)
|
||||
DB -> NodeNaturalComparator(database, sortNodeParameters) // Force false because natural order contains recycle bin
|
||||
TITLE -> NodeTitleComparator(database, sortNodeParameters)
|
||||
USERNAME -> NodeUsernameComparator(database, sortNodeParameters)
|
||||
CREATION_TIME -> NodeCreationComparator(database, sortNodeParameters)
|
||||
LAST_MODIFY_TIME -> NodeLastModificationComparator(database, sortNodeParameters)
|
||||
LAST_ACCESS_TIME -> NodeLastAccessComparator(database, sortNodeParameters)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,11 +50,9 @@ enum class SortNodeEnum {
|
||||
<
|
||||
G: GroupVersionedInterface<*, *>,
|
||||
T: NodeVersionedInterface<G>
|
||||
>(var sortNodeParameters: SortNodeParameters)
|
||||
>(var database: Database, var sortNodeParameters: SortNodeParameters)
|
||||
: Comparator<T> {
|
||||
|
||||
val database = Database.getInstance()
|
||||
|
||||
abstract fun compareBySpecificOrder(object1: T, object2: T): Int
|
||||
|
||||
private fun specificOrderOrHashIfEquals(object1: T, object2: T): Int {
|
||||
@@ -110,8 +110,9 @@ enum class SortNodeEnum {
|
||||
* Comparator of node by natural database placement
|
||||
*/
|
||||
class NodeNaturalComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
database: Database,
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
: NodeComparator<G, T>(database, sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
return object1.nodeIndexInParentForNaturalOrder()
|
||||
@@ -123,13 +124,14 @@ enum class SortNodeEnum {
|
||||
* Comparator of Node by Title
|
||||
*/
|
||||
class NodeTitleComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
database: Database,
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
: NodeComparator<G, T>(database, sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val titleCompare = object1.title.compareTo(object2.title, ignoreCase = true)
|
||||
return if (titleCompare == 0)
|
||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
||||
NodeNaturalComparator<G, T>(database, sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
titleCompare
|
||||
@@ -140,8 +142,9 @@ enum class SortNodeEnum {
|
||||
* Comparator of Node by Username, Groups by title
|
||||
*/
|
||||
class NodeUsernameComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
database: Database,
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
: NodeComparator<G, T>(database, sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
return if (object1.type == Type.ENTRY && object2.type == Type.ENTRY) {
|
||||
@@ -150,12 +153,12 @@ enum class SortNodeEnum {
|
||||
.compareTo((object2 as Entry).getEntryInfo(database).username,
|
||||
ignoreCase = true)
|
||||
if (usernameCompare == 0)
|
||||
NodeTitleComparator<G, T>(sortNodeParameters)
|
||||
NodeTitleComparator<G, T>(database, sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
usernameCompare
|
||||
} else {
|
||||
NodeTitleComparator<G, T>(sortNodeParameters)
|
||||
NodeTitleComparator<G, T>(database, sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
}
|
||||
}
|
||||
@@ -165,14 +168,15 @@ enum class SortNodeEnum {
|
||||
* Comparator of node by creation
|
||||
*/
|
||||
class NodeCreationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
database: Database,
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
: NodeComparator<G, T>(database, sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val creationCompare = object1.creationTime.date
|
||||
.compareTo(object2.creationTime.date)
|
||||
return if (creationCompare == 0)
|
||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
||||
NodeNaturalComparator<G, T>(database, sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
creationCompare
|
||||
@@ -183,14 +187,15 @@ enum class SortNodeEnum {
|
||||
* Comparator of node by last modification
|
||||
*/
|
||||
class NodeLastModificationComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
database: Database,
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
: NodeComparator<G, T>(database, sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val lastModificationCompare = object1.lastModificationTime.date
|
||||
.compareTo(object2.lastModificationTime.date)
|
||||
return if (lastModificationCompare == 0)
|
||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
||||
NodeNaturalComparator<G, T>(database, sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
lastModificationCompare
|
||||
@@ -201,14 +206,15 @@ enum class SortNodeEnum {
|
||||
* Comparator of node by last access
|
||||
*/
|
||||
class NodeLastAccessComparator<G: GroupVersionedInterface<*, *>, T: NodeVersionedInterface<G>>(
|
||||
database: Database,
|
||||
sortNodeParameters: SortNodeParameters)
|
||||
: NodeComparator<G, T>(sortNodeParameters) {
|
||||
: NodeComparator<G, T>(database, sortNodeParameters) {
|
||||
|
||||
override fun compareBySpecificOrder(object1: T, object2: T): Int {
|
||||
val lastAccessCompare = object1.lastAccessTime.date
|
||||
.compareTo(object2.lastAccessTime.date)
|
||||
return if (lastAccessCompare == 0)
|
||||
NodeNaturalComparator<G, T>(sortNodeParameters)
|
||||
NodeNaturalComparator<G, T>(database, sortNodeParameters)
|
||||
.compare(object1, object2)
|
||||
else
|
||||
lastAccessCompare
|
||||
|
||||
@@ -45,6 +45,8 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
|
||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||
@@ -79,6 +81,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
private var numKeyEncRounds: Long = 0
|
||||
var publicCustomData = VariantDictionary()
|
||||
private val mFieldReferenceEngine = FieldReferencesEngine(this)
|
||||
private val mTemplateEngine = TemplateEngineCompatible(this)
|
||||
|
||||
var kdbxVersion = UnsignedInt(0)
|
||||
var name = ""
|
||||
@@ -128,7 +131,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
/**
|
||||
* Create a new database with a root group
|
||||
*/
|
||||
constructor(databaseName: String, rootName: String) {
|
||||
constructor(databaseName: String,
|
||||
rootName: String,
|
||||
templatesGroupName: String? = null) {
|
||||
name = databaseName
|
||||
kdbxVersion = FILE_VERSION_31
|
||||
val group = createGroup().apply {
|
||||
@@ -136,6 +141,11 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID)
|
||||
}
|
||||
rootGroup = group
|
||||
if (templatesGroupName != null) {
|
||||
val templatesGroup = mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
|
||||
entryTemplatesGroup = templatesGroup.id
|
||||
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
|
||||
}
|
||||
}
|
||||
|
||||
override val version: String
|
||||
@@ -332,10 +342,77 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
return this.iconsManager.getIcon(iconUuid)
|
||||
}
|
||||
|
||||
fun isTemplatesGroupEnabled(): Boolean {
|
||||
return entryTemplatesGroup != UUID_ZERO
|
||||
}
|
||||
|
||||
fun enableTemplatesGroup(enable: Boolean, templatesGroupName: String) {
|
||||
// Create templates group only if a group with a valid name don't already exists
|
||||
val firstGroupWithValidName = getGroupIndexes().firstOrNull {
|
||||
it.title == templatesGroupName
|
||||
}
|
||||
if (enable) {
|
||||
val templatesGroup = firstGroupWithValidName
|
||||
?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
|
||||
entryTemplatesGroup = templatesGroup.id
|
||||
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
|
||||
} else {
|
||||
removeTemplatesGroup()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeTemplatesGroup() {
|
||||
entryTemplatesGroup = UUID_ZERO
|
||||
entryTemplatesGroupChanged = DateInstant()
|
||||
mTemplateEngine.clearCache()
|
||||
}
|
||||
|
||||
fun getTemplatesGroup(): GroupKDBX? {
|
||||
if (isTemplatesGroupEnabled()) {
|
||||
return getGroupById(entryTemplatesGroup)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getTemplates(templateCreation: Boolean): List<Template> {
|
||||
return if (templateCreation)
|
||||
listOf(mTemplateEngine.getTemplateCreation())
|
||||
else
|
||||
mTemplateEngine.getTemplates()
|
||||
}
|
||||
|
||||
fun getTemplate(entry: EntryKDBX): Template? {
|
||||
return mTemplateEngine.getTemplate(entry)
|
||||
}
|
||||
|
||||
fun decodeEntryWithTemplateConfiguration(entryKDBX: EntryKDBX, entryIsTemplate: Boolean): EntryKDBX {
|
||||
return if (entryIsTemplate) {
|
||||
mTemplateEngine.decodeTemplateEntry(entryKDBX)
|
||||
} else {
|
||||
mTemplateEngine.removeMetaTemplateRecognitionFromEntry(entryKDBX)
|
||||
}
|
||||
}
|
||||
|
||||
fun encodeEntryWithTemplateConfiguration(entryKDBX: EntryKDBX, entryIsTemplate: Boolean, template: Template): EntryKDBX {
|
||||
return if (entryIsTemplate) {
|
||||
mTemplateEngine.encodeTemplateEntry(entryKDBX)
|
||||
} else {
|
||||
mTemplateEngine.addMetaTemplateRecognitionToEntry(template, entryKDBX)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Search methods
|
||||
*/
|
||||
|
||||
fun getGroupById(id: UUID): GroupKDBX? {
|
||||
return this.getGroupById(NodeIdUUID(id))
|
||||
}
|
||||
|
||||
fun getEntryById(id: UUID): EntryKDBX? {
|
||||
return this.getEntryById(NodeIdUUID(id))
|
||||
}
|
||||
|
||||
fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? {
|
||||
return this.entryIndexes.values.find { entry ->
|
||||
entry.decodeTitleKey(recursionLevel).equals(title, true)
|
||||
@@ -629,15 +706,23 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
*/
|
||||
fun ensureRecycleBinExists(resources: Resources) {
|
||||
if (recycleBin == null) {
|
||||
// Create recycle bin
|
||||
val recycleBinGroup = createGroup().apply {
|
||||
title = resources.getString(R.string.recycle_bin)
|
||||
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
|
||||
enableAutoType = false
|
||||
enableSearching = false
|
||||
isExpanded = false
|
||||
// Create recycle bin only if a group with a valid name don't already exists
|
||||
val firstGroupWithValidName = getGroupIndexes().firstOrNull {
|
||||
it.title == resources.getString(R.string.recycle_bin)
|
||||
}
|
||||
val recycleBinGroup = if (firstGroupWithValidName == null) {
|
||||
val newRecycleBinGroup = createGroup().apply {
|
||||
title = resources.getString(R.string.recycle_bin)
|
||||
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
|
||||
enableAutoType = false
|
||||
enableSearching = false
|
||||
isExpanded = false
|
||||
}
|
||||
addGroupTo(newRecycleBinGroup, rootGroup)
|
||||
newRecycleBinGroup
|
||||
} else {
|
||||
firstGroupWithValidName
|
||||
}
|
||||
addGroupTo(recycleBinGroup, rootGroup)
|
||||
recycleBinUUID = recycleBinGroup.id
|
||||
recycleBinChanged = recycleBinGroup.lastModificationTime
|
||||
}
|
||||
|
||||
@@ -93,6 +93,10 @@ abstract class DatabaseVersioned<
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllGroupsWithoutRoot(): List<Group> {
|
||||
return getGroupIndexes().filter { it != rootGroup }
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
|
||||
|
||||
@@ -238,13 +242,6 @@ abstract class DatabaseVersioned<
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGroupIndex(group: Group) {
|
||||
val groupId = group.nodeId
|
||||
if (groupIndexes.containsKey(groupId)) {
|
||||
groupIndexes[groupId] = group
|
||||
}
|
||||
}
|
||||
|
||||
fun removeGroupIndex(group: Group) {
|
||||
this.groupIndexes.remove(group.nodeId)
|
||||
}
|
||||
@@ -287,13 +284,6 @@ abstract class DatabaseVersioned<
|
||||
}
|
||||
}
|
||||
|
||||
fun updateEntryIndex(entry: Entry) {
|
||||
val entryId = entry.nodeId
|
||||
if (entryIndexes.containsKey(entryId)) {
|
||||
entryIndexes[entryId] = entry
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEntryIndex(entry: Entry) {
|
||||
this.entryIndexes.remove(entry.nodeId)
|
||||
}
|
||||
@@ -325,7 +315,11 @@ abstract class DatabaseVersioned<
|
||||
}
|
||||
|
||||
fun updateGroup(group: Group) {
|
||||
updateGroupIndex(group)
|
||||
group.parent?.updateChildGroup(group)
|
||||
val groupId = group.nodeId
|
||||
if (groupIndexes.containsKey(groupId)) {
|
||||
groupIndexes[groupId] = group
|
||||
}
|
||||
}
|
||||
|
||||
fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
|
||||
@@ -342,7 +336,11 @@ abstract class DatabaseVersioned<
|
||||
}
|
||||
|
||||
open fun updateEntry(entry: Entry) {
|
||||
updateEntryIndex(entry)
|
||||
entry.parent?.updateChildEntry(entry)
|
||||
val entryId = entry.nodeId
|
||||
if (entryIndexes.containsKey(entryId)) {
|
||||
entryIndexes[entryId] = entry
|
||||
}
|
||||
}
|
||||
|
||||
open fun removeEntryFrom(entryToRemove: Entry, parent: Group?) {
|
||||
|
||||
@@ -22,10 +22,7 @@ package com.kunzisoft.keepass.database.element.entry
|
||||
import android.os.Parcel
|
||||
import android.os.ParcelUuid
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.CustomData
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.Tags
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||
@@ -52,7 +49,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
override var usageCount = UnsignedLong(0)
|
||||
override var locationChanged = DateInstant()
|
||||
override var customData = CustomData()
|
||||
var fields = LinkedHashMap<String, ProtectedString>()
|
||||
private var fields = LinkedHashMap<String, ProtectedString>()
|
||||
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
|
||||
var foregroundColor = ""
|
||||
var backgroundColor = ""
|
||||
@@ -81,7 +78,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
|
||||
autoType = parcel.readParcelable(AutoType::class.java.classLoader) ?: autoType
|
||||
parcel.readTypedList(history, CREATOR)
|
||||
url = parcel.readString() ?: url
|
||||
additional = parcel.readString() ?: additional
|
||||
}
|
||||
|
||||
@@ -107,7 +103,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
|
||||
dest.writeParcelable(autoType, flags)
|
||||
dest.writeTypedList(history)
|
||||
dest.writeString(url)
|
||||
dest.writeString(additional)
|
||||
}
|
||||
|
||||
@@ -133,7 +128,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
history.clear()
|
||||
if (copyHistory)
|
||||
history.addAll(source.history)
|
||||
url = source.url
|
||||
additional = source.additional
|
||||
}
|
||||
|
||||
@@ -227,6 +221,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
fields[STR_NOTES] = ProtectedString(protect, value)
|
||||
}
|
||||
|
||||
fun getCustomFieldValue(label: String): String {
|
||||
return decodeRefKey(mDecodeRef, label, 0)
|
||||
}
|
||||
|
||||
fun getSize(attachmentPool: AttachmentPool): Long {
|
||||
var size = FIXED_LENGTH_SIZE
|
||||
|
||||
@@ -265,28 +263,41 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
|| key == STR_NOTES)
|
||||
}
|
||||
|
||||
fun doForEachDecodedCustomField(action: (key: String, value: ProtectedString) -> Unit) {
|
||||
fun doForEachDecodedCustomField(action: (field: Field) -> Unit) {
|
||||
val iterator = fields.entries.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val mapEntry = iterator.next()
|
||||
if (!isStandardField(mapEntry.key)) {
|
||||
action.invoke(mapEntry.key,
|
||||
action.invoke(Field(mapEntry.key,
|
||||
ProtectedString(mapEntry.value.isProtected,
|
||||
decodeRefKey(mDecodeRef, mapEntry.key, 0)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getField(key: String): ProtectedString? {
|
||||
return fields[key]
|
||||
fun getFieldValue(label: String): ProtectedString? {
|
||||
return fields[label]
|
||||
}
|
||||
|
||||
fun getFields(): List<Field> {
|
||||
return fields.map { Field(it.key, it.value) }
|
||||
}
|
||||
|
||||
fun putField(field: Field) {
|
||||
putField(field.name, field.protectedValue)
|
||||
}
|
||||
|
||||
fun putField(label: String, value: ProtectedString) {
|
||||
fields[label] = value
|
||||
}
|
||||
|
||||
fun removeField(name: String) {
|
||||
fields.remove(name)
|
||||
}
|
||||
|
||||
fun removeAllFields() {
|
||||
fields.clear()
|
||||
}
|
||||
|
||||
@@ -98,6 +98,24 @@ abstract class GroupVersioned
|
||||
this.childEntries.add(entry)
|
||||
}
|
||||
|
||||
override fun updateChildGroup(group: Group) {
|
||||
val index = this.childGroups.indexOfFirst { it.nodeId == group.nodeId }
|
||||
if (index >= 0) {
|
||||
val oldGroup = this.childGroups.removeAt(index)
|
||||
group.nodeIndexInParentForNaturalOrder = oldGroup.nodeIndexInParentForNaturalOrder
|
||||
this.childGroups.add(index, group)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateChildEntry(entry: Entry) {
|
||||
val index = this.childEntries.indexOfFirst { it.nodeId == entry.nodeId }
|
||||
if (index >= 0) {
|
||||
val oldEntry = this.childEntries.removeAt(index)
|
||||
entry.nodeIndexInParentForNaturalOrder = oldEntry.nodeIndexInParentForNaturalOrder
|
||||
this.childEntries.add(index, entry)
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeChildGroup(group: Group) {
|
||||
this.childGroups.remove(group)
|
||||
}
|
||||
@@ -117,8 +135,4 @@ abstract class GroupVersioned
|
||||
else
|
||||
nodeIndexInParentForNaturalOrder
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return titleGroup
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ interface GroupVersionedInterface<Group: GroupVersionedInterface<Group, Entry>,
|
||||
|
||||
fun addChildEntry(entry: Entry)
|
||||
|
||||
fun updateChildGroup(group: Group)
|
||||
|
||||
fun updateChildEntry(entry: Entry)
|
||||
|
||||
fun removeChildGroup(group: Group)
|
||||
|
||||
fun removeChildEntry(entry: Entry)
|
||||
|
||||
@@ -75,8 +75,16 @@ class IconImageStandard : IconImageDraw {
|
||||
companion object {
|
||||
|
||||
const val KEY_ID = 0
|
||||
const val ID_CARD_ID = 9
|
||||
const val WIRELESS_ID = 12
|
||||
const val EMAIL_ID = 19
|
||||
const val CREDIT_CARD_ID = 37
|
||||
const val TRASH_ID = 43
|
||||
const val FOLDER_ID = 48
|
||||
const val LIST_ID = 57
|
||||
const val BUILD_ID = 59
|
||||
const val STAR_ID = 61
|
||||
const val DOLLAR_ID = 66
|
||||
|
||||
fun isCorrectIconId(iconId: Int): Boolean {
|
||||
return iconId in 0 until NB_ICONS
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.element.node
|
||||
import android.os.Parcel
|
||||
import android.os.ParcelUuid
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import java.util.*
|
||||
|
||||
class NodeIdUUID : NodeId<UUID> {
|
||||
@@ -60,7 +61,7 @@ class NodeIdUUID : NodeId<UUID> {
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return id.toString()
|
||||
return UuidUtil.toHexString(id) ?: id.toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -20,19 +20,16 @@
|
||||
package com.kunzisoft.keepass.database.element.node
|
||||
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
interface NodeKDBInterface : NodeTimeInterface {
|
||||
|
||||
override var expires: Boolean
|
||||
// If expireDate is before NEVER_EXPIRE date less 1 month (to be sure)
|
||||
// it is not expires
|
||||
get() = LocalDateTime(expiryTime.date)
|
||||
.isBefore(LocalDateTime.fromDateFields(DateInstant.NEVER_EXPIRE.date)
|
||||
.minusMonths(1))
|
||||
|
||||
get() = expiryTime.isNeverExpires()
|
||||
|
||||
set(value) {
|
||||
if (!value)
|
||||
expiryTime = DateInstant.NEVER_EXPIRE
|
||||
expiryTime = DateInstant.NEVER_EXPIRES
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Abstract class who manage Groups and Entries
|
||||
@@ -95,11 +94,10 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
|
||||
|
||||
final override var lastAccessTime: DateInstant = DateInstant()
|
||||
|
||||
final override var expiryTime: DateInstant = DateInstant.NEVER_EXPIRE
|
||||
final override var expiryTime: DateInstant = DateInstant.NEVER_EXPIRES
|
||||
|
||||
final override val isCurrentlyExpires: Boolean
|
||||
get() = expires
|
||||
&& LocalDateTime.fromDateFields(expiryTime.date).isBefore(LocalDateTime.now())
|
||||
get() = expires && expiryTime.isCurrentlyExpire()
|
||||
|
||||
/**
|
||||
* @return true if parent is present (false if not present, can be a root or a detach element)
|
||||
@@ -150,4 +148,8 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
|
||||
override fun hashCode(): Int {
|
||||
return nodeId.hashCode()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$title ($nodeId)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2021 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.template
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.ParcelUuid
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.LinkedHashMap
|
||||
|
||||
class Template : Parcelable {
|
||||
|
||||
var version = 1
|
||||
var uuid: UUID = DatabaseVersioned.UUID_ZERO
|
||||
var title = ""
|
||||
var icon = IconImage()
|
||||
var sections: MutableList<TemplateSection> = ArrayList()
|
||||
private set
|
||||
|
||||
constructor(uuid: UUID,
|
||||
title: String,
|
||||
icon: IconImage,
|
||||
section: TemplateSection,
|
||||
version: Int = 1): this(uuid, title, icon, ArrayList<TemplateSection>().apply {
|
||||
add(section)
|
||||
}, version)
|
||||
|
||||
constructor(uuid: UUID,
|
||||
title: String,
|
||||
icon: IconImage,
|
||||
sections: List<TemplateSection>,
|
||||
version: Int = 1) {
|
||||
this.version = version
|
||||
this.uuid = uuid
|
||||
this.title = title
|
||||
this.icon = icon
|
||||
this.sections.clear()
|
||||
this.sections.addAll(sections)
|
||||
}
|
||||
|
||||
constructor(template: Template) {
|
||||
this.version = template.version
|
||||
this.uuid = template.uuid
|
||||
this.title = template.title
|
||||
this.icon = template.icon
|
||||
this.sections.clear()
|
||||
this.sections.addAll(template.sections)
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) {
|
||||
version = parcel.readInt()
|
||||
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: uuid
|
||||
title = parcel.readString() ?: title
|
||||
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
|
||||
parcel.readList(sections, TemplateSection::class.java.classLoader)
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeInt(version)
|
||||
parcel.writeParcelable(ParcelUuid(uuid), flags)
|
||||
parcel.writeString(title)
|
||||
parcel.writeParcelable(icon, flags)
|
||||
parcel.writeList(sections)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Template) return false
|
||||
|
||||
if (uuid != other.uuid) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return uuid.hashCode()
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<Template> {
|
||||
override fun createFromParcel(parcel: Parcel): Template {
|
||||
return Template(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<Template?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
|
||||
val TITLE_ATTRIBUTE = TemplateAttribute(
|
||||
TemplateField.LABEL_TITLE,
|
||||
TemplateAttributeType.TEXT)
|
||||
val USERNAME_ATTRIBUTE = TemplateAttribute(
|
||||
TemplateField.LABEL_USERNAME,
|
||||
TemplateAttributeType.TEXT)
|
||||
val PASSWORD_ATTRIBUTE = TemplateAttribute(
|
||||
TemplateField.LABEL_PASSWORD,
|
||||
TemplateAttributeType.TEXT,
|
||||
true,
|
||||
TemplateAttributeOption().apply {
|
||||
setNumberLines(3)
|
||||
associatePasswordGenerator()
|
||||
}
|
||||
)
|
||||
val URL_ATTRIBUTE = TemplateAttribute(
|
||||
TemplateField.LABEL_URL,
|
||||
TemplateAttributeType.TEXT,
|
||||
false,
|
||||
TemplateAttributeOption().apply {
|
||||
setLink(true)
|
||||
})
|
||||
val EXPIRATION_ATTRIBUTE = TemplateAttribute(
|
||||
TemplateField.LABEL_EXPIRATION,
|
||||
TemplateAttributeType.DATETIME,
|
||||
false)
|
||||
val NOTES_ATTRIBUTE = TemplateAttribute(
|
||||
TemplateField.LABEL_NOTES,
|
||||
TemplateAttributeType.TEXT,
|
||||
false,
|
||||
TemplateAttributeOption().apply {
|
||||
setNumberLinesToMany()
|
||||
})
|
||||
|
||||
val STANDARD: Template
|
||||
get() {
|
||||
val sections = mutableListOf<TemplateSection>()
|
||||
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
|
||||
add(USERNAME_ATTRIBUTE)
|
||||
add(PASSWORD_ATTRIBUTE)
|
||||
add(URL_ATTRIBUTE)
|
||||
add(EXPIRATION_ATTRIBUTE)
|
||||
add(NOTES_ATTRIBUTE)
|
||||
})
|
||||
sections.add(mainSection)
|
||||
return Template(
|
||||
DatabaseVersioned.UUID_ZERO,
|
||||
TemplateField.LABEL_STANDARD,
|
||||
IconImage(),
|
||||
sections)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2021 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.template
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.utils.ParcelableUtil
|
||||
import com.kunzisoft.keepass.utils.readEnum
|
||||
import com.kunzisoft.keepass.utils.writeEnum
|
||||
|
||||
data class TemplateAttribute(var label: String,
|
||||
var type: TemplateAttributeType,
|
||||
var protected: Boolean = false,
|
||||
var options: TemplateAttributeOption = TemplateAttributeOption(),
|
||||
var action: TemplateAttributeAction = TemplateAttributeAction.NONE): Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString() ?: "",
|
||||
parcel.readEnum<TemplateAttributeType>() ?: TemplateAttributeType.TEXT,
|
||||
parcel.readByte() != 0.toByte(),
|
||||
parcel.readParcelable(TemplateAttributeOption::class.java.classLoader) ?: TemplateAttributeOption(),
|
||||
parcel.readEnum<TemplateAttributeAction>() ?: TemplateAttributeAction.NONE)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(label)
|
||||
parcel.writeEnum(type)
|
||||
parcel.writeByte(if (protected) 1 else 0)
|
||||
parcel.writeParcelable(options, flags)
|
||||
parcel.writeEnum(action)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
var alias: String?
|
||||
get() {
|
||||
return options.alias
|
||||
}
|
||||
set(value) {
|
||||
options.alias = value
|
||||
}
|
||||
|
||||
var default: String
|
||||
get() {
|
||||
return this.options.default
|
||||
}
|
||||
set(value) {
|
||||
this.options.default = value
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<TemplateAttribute> {
|
||||
override fun createFromParcel(parcel: Parcel): TemplateAttribute {
|
||||
return TemplateAttribute(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<TemplateAttribute?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
enum class TemplateAttributeAction {
|
||||
NONE, CUSTOM_EDITION
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.utils.ParcelableUtil
|
||||
|
||||
class TemplateAttributeOption() : Parcelable {
|
||||
|
||||
private val mOptions: MutableMap<String, String> = mutableMapOf()
|
||||
|
||||
constructor(parcel: Parcel) : this() {
|
||||
ParcelableUtil.readStringParcelableMap(parcel)
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
ParcelableUtil.writeStringParcelableMap(parcel, LinkedHashMap(mOptions))
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
var alias: String?
|
||||
get() {
|
||||
val tempAlias = mOptions[ALIAS_ATTR]
|
||||
if (tempAlias.isNullOrEmpty())
|
||||
return null
|
||||
return tempAlias
|
||||
}
|
||||
set(value) {
|
||||
if (value == null)
|
||||
mOptions.remove(ALIAS_ATTR)
|
||||
else
|
||||
mOptions[ALIAS_ATTR] = value
|
||||
}
|
||||
|
||||
var default: String
|
||||
get() {
|
||||
return mOptions[DEFAULT_ATTR] ?: DEFAULT_VALUE
|
||||
}
|
||||
set(value) {
|
||||
mOptions[DEFAULT_ATTR] = value
|
||||
}
|
||||
|
||||
fun getNumberChars(): Int {
|
||||
return try {
|
||||
if (mOptions[TEXT_NUMBER_CHARS_ATTR].equals(TEXT_NUMBER_CHARS_VALUE_MANY_STRING, true))
|
||||
TEXT_NUMBER_CHARS_VALUE_MANY
|
||||
else
|
||||
mOptions[TEXT_NUMBER_CHARS_ATTR]?.toInt() ?: TEXT_NUMBER_CHARS_VALUE_DEFAULT
|
||||
} catch (e: Exception) {
|
||||
TEXT_NUMBER_CHARS_VALUE_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
fun setNumberChars(numberChars: Int) {
|
||||
mOptions[TEXT_NUMBER_CHARS_ATTR] = numberChars.toString()
|
||||
}
|
||||
|
||||
fun setNumberCharsToMany() {
|
||||
mOptions[TEXT_NUMBER_CHARS_ATTR] = TEXT_NUMBER_CHARS_VALUE_MANY_STRING
|
||||
}
|
||||
|
||||
fun getNumberLines(): Int {
|
||||
return try {
|
||||
if (mOptions[TEXT_NUMBER_LINES_ATTR].equals(TEXT_NUMBER_LINES_VALUE_MANY_STRING, true))
|
||||
TEXT_NUMBER_LINES_VALUE_MANY
|
||||
else
|
||||
mOptions[TEXT_NUMBER_LINES_ATTR]?.toInt() ?: TEXT_NUMBER_LINES_VALUE_DEFAULT
|
||||
} catch (e: Exception) {
|
||||
TEXT_NUMBER_LINES_VALUE_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
fun setNumberLines(numberLines: Int) {
|
||||
val lines = if (numberLines == 0) 1 else numberLines
|
||||
mOptions[TEXT_NUMBER_LINES_ATTR] = lines.toString()
|
||||
}
|
||||
|
||||
fun setNumberLinesToMany() {
|
||||
mOptions[TEXT_NUMBER_LINES_ATTR] = TEXT_NUMBER_LINES_VALUE_MANY_STRING
|
||||
}
|
||||
|
||||
fun isLink(): Boolean {
|
||||
return try {
|
||||
mOptions[TEXT_LINK_ATTR]?.toBoolean() ?: TEXT_LINK_VALUE_DEFAULT
|
||||
} catch (e: Exception) {
|
||||
TEXT_LINK_VALUE_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
fun setLink(isLink: Boolean) {
|
||||
mOptions[TEXT_LINK_ATTR] = isLink.toString()
|
||||
}
|
||||
|
||||
fun isAssociatedWithPasswordGenerator(): Boolean {
|
||||
return try {
|
||||
mOptions[PASSWORD_GENERATOR_ATTR]?.toBoolean() ?: PASSWORD_GENERATOR_VALUE_DEFAULT
|
||||
} catch (e: Exception) {
|
||||
PASSWORD_GENERATOR_VALUE_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
fun associatePasswordGenerator() {
|
||||
mOptions[PASSWORD_GENERATOR_ATTR] = true.toString()
|
||||
}
|
||||
|
||||
fun getListItems(): List<String> {
|
||||
return mOptions[LIST_ITEMS]?.split(LIST_ITEMS_SEPARATOR) ?: listOf()
|
||||
}
|
||||
|
||||
fun setListItems(vararg items: String) {
|
||||
mOptions[LIST_ITEMS] = items.joinToString(LIST_ITEMS_SEPARATOR)
|
||||
}
|
||||
|
||||
fun getDateFormat(): DateInstant.Type {
|
||||
return when (mOptions[DATETIME_FORMAT_ATTR]) {
|
||||
DATETIME_FORMAT_VALUE_DATE -> DateInstant.Type.DATE
|
||||
DATETIME_FORMAT_VALUE_TIME -> DateInstant.Type.TIME
|
||||
else -> DateInstant.Type.DATE_TIME
|
||||
}
|
||||
}
|
||||
|
||||
fun setDateFormatToDate() {
|
||||
mOptions[DATETIME_FORMAT_ATTR] = DATETIME_FORMAT_VALUE_DATE
|
||||
}
|
||||
|
||||
fun setDateFormatToTime() {
|
||||
mOptions[DATETIME_FORMAT_ATTR] = DATETIME_FORMAT_VALUE_TIME
|
||||
}
|
||||
|
||||
fun get(label: String): String? {
|
||||
return mOptions[label]
|
||||
}
|
||||
|
||||
fun put(label: String, value: String) {
|
||||
mOptions[label] = value
|
||||
}
|
||||
|
||||
fun remove(label: String) {
|
||||
mOptions.remove(label)
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<TemplateAttributeOption> {
|
||||
override fun createFromParcel(parcel: Parcel): TemplateAttributeOption {
|
||||
return TemplateAttributeOption(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<TemplateAttributeOption?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applicable to each type
|
||||
* Define a text replacement for a label,
|
||||
* Useful to keep compatibility with old keepass apps by replacing standard field label
|
||||
*/
|
||||
private const val ALIAS_ATTR = "alias"
|
||||
|
||||
/**
|
||||
* Applicable to each type
|
||||
* Define a default string element representation
|
||||
* For a type LIST, represents a single string element representation
|
||||
*/
|
||||
private const val DEFAULT_ATTR = "default"
|
||||
private const val DEFAULT_VALUE = ""
|
||||
|
||||
/**
|
||||
* Applicable to type TEXT
|
||||
* Define a number of chars
|
||||
* Integer, can be "many" or "-1" to infinite value
|
||||
* "1" if not defined
|
||||
*/
|
||||
private const val TEXT_NUMBER_CHARS_ATTR = "chars"
|
||||
private const val TEXT_NUMBER_CHARS_VALUE_MANY = -1
|
||||
private const val TEXT_NUMBER_CHARS_VALUE_MANY_STRING = "many"
|
||||
private const val TEXT_NUMBER_CHARS_VALUE_DEFAULT = -1
|
||||
|
||||
/**
|
||||
* Applicable to type TEXT
|
||||
* Define a number of lines
|
||||
* Integer, can be "-1" to infinite value
|
||||
* "1" if not defined
|
||||
*/
|
||||
private const val TEXT_NUMBER_LINES_ATTR = "lines"
|
||||
private const val TEXT_NUMBER_LINES_VALUE_MANY = -1
|
||||
private const val TEXT_NUMBER_LINES_VALUE_MANY_STRING = "many"
|
||||
private const val TEXT_NUMBER_LINES_VALUE_DEFAULT = 1
|
||||
|
||||
/**
|
||||
* Applicable to type TEXT
|
||||
* Define if a text is a link
|
||||
* Boolean ("true" or "false")
|
||||
* "true" if not defined
|
||||
*/
|
||||
private const val TEXT_LINK_ATTR = "link"
|
||||
private const val TEXT_LINK_VALUE_DEFAULT = false
|
||||
|
||||
/**
|
||||
* Applicable to type TEXT
|
||||
* Define if a password generator is associated with the text
|
||||
* Boolean ("true" or "false")
|
||||
* "false" if not defined
|
||||
*/
|
||||
private const val PASSWORD_GENERATOR_ATTR = "generator"
|
||||
private const val PASSWORD_GENERATOR_VALUE_DEFAULT = false
|
||||
|
||||
/**
|
||||
* Applicable to type LIST
|
||||
* Define items of a list
|
||||
* List of items, separator is '|'
|
||||
*/
|
||||
private const val LIST_ITEMS = "items"
|
||||
private const val LIST_ITEMS_SEPARATOR = "|"
|
||||
|
||||
/**
|
||||
* Applicable to type DATETIME
|
||||
* Define the type of date
|
||||
* String ("date" or "time" or "datetime" or based on https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html)
|
||||
* "datetime" if not defined
|
||||
*/
|
||||
private const val DATETIME_FORMAT_ATTR = "format"
|
||||
private const val DATETIME_FORMAT_VALUE_DATE = "date"
|
||||
private const val DATETIME_FORMAT_VALUE_TIME = "time"
|
||||
|
||||
private fun removeSpecialChars(string: String): String {
|
||||
return string.filterNot { "{,:}".indexOf(it) > -1 }
|
||||
}
|
||||
|
||||
fun getOptionsFromString(label: String): TemplateAttributeOption {
|
||||
val options = TemplateAttributeOption()
|
||||
val optionsMap = if (label.contains("{") || label.contains("}")) {
|
||||
try {
|
||||
label.trim().substringAfter("{").substringBefore("}")
|
||||
.split(",").associate {
|
||||
val keyValue = it.trim()
|
||||
val (left, right) = keyValue.split(":")
|
||||
left to right
|
||||
}.toMutableMap()
|
||||
} catch (e: Exception) {
|
||||
mutableMapOf()
|
||||
}
|
||||
} else {
|
||||
mutableMapOf()
|
||||
}
|
||||
options.mOptions.apply {
|
||||
clear()
|
||||
putAll(optionsMap)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
fun getStringFromOptions(options: TemplateAttributeOption): String {
|
||||
var optionsString = ""
|
||||
if (options.mOptions.isNotEmpty()) {
|
||||
optionsString += " {"
|
||||
var first = true
|
||||
for ((key, value) in options.mOptions) {
|
||||
if (!first)
|
||||
optionsString += ", "
|
||||
first = false
|
||||
optionsString += "${removeSpecialChars(key)}:${removeSpecialChars(value)}"
|
||||
}
|
||||
optionsString += "}"
|
||||
}
|
||||
return optionsString
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
@@ -15,25 +15,24 @@
|
||||
*
|
||||
* 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
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||
|
||||
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {
|
||||
|
||||
override fun retrieveMessage(): String {
|
||||
return getString(R.string.warning_empty_recycle_bin)
|
||||
}
|
||||
enum class TemplateAttributeType(val typeString: String) {
|
||||
TEXT("text"),
|
||||
LIST("list"),
|
||||
DATETIME("datetime"),
|
||||
DIVIDER("divider");
|
||||
|
||||
companion object {
|
||||
fun getInstance(nodesToDelete: List<Node>): EmptyRecycleBinDialogFragment {
|
||||
return EmptyRecycleBinDialogFragment().apply {
|
||||
arguments = getBundleFromListNodes(nodesToDelete)
|
||||
fun getFromString(label: String): TemplateAttributeType {
|
||||
return when {
|
||||
label.contains(TEXT.typeString, true) -> TEXT
|
||||
label.contains(LIST.typeString, true) -> LIST
|
||||
label.contains(DATETIME.typeString, true) -> DATETIME
|
||||
label.contains(DIVIDER.typeString, true) -> DIVIDER
|
||||
else -> TEXT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import java.util.*
|
||||
import kotlin.collections.LinkedHashMap
|
||||
|
||||
class TemplateBuilder {
|
||||
|
||||
private val urlAttribute = TemplateAttribute(TemplateField.LABEL_URL, TemplateAttributeType.TEXT)
|
||||
private val usernameAttribute = TemplateAttribute(TemplateField.LABEL_USERNAME, TemplateAttributeType.TEXT)
|
||||
private val notesAttribute = TemplateAttribute(
|
||||
TemplateField.LABEL_NOTES,
|
||||
TemplateAttributeType.TEXT,
|
||||
false,
|
||||
TemplateAttributeOption().apply {
|
||||
setNumberLinesToMany()
|
||||
})
|
||||
private val holderAttribute = TemplateAttribute(TemplateField.LABEL_HOLDER, TemplateAttributeType.TEXT)
|
||||
private val numberAttribute = TemplateAttribute(TemplateField.LABEL_NUMBER, TemplateAttributeType.TEXT)
|
||||
private val cvvAttribute = TemplateAttribute(TemplateField.LABEL_CVV, TemplateAttributeType.TEXT, true)
|
||||
private val pinAttribute = TemplateAttribute(TemplateField.LABEL_PIN, TemplateAttributeType.TEXT, true)
|
||||
private val nameAttribute = TemplateAttribute(TemplateField.LABEL_NAME, TemplateAttributeType.TEXT)
|
||||
private val placeOfIssueAttribute = TemplateAttribute(TemplateField.LABEL_PLACE_OF_ISSUE, TemplateAttributeType.TEXT)
|
||||
private val dateOfIssueAttribute = TemplateAttribute(
|
||||
TemplateField.LABEL_DATE_OF_ISSUE,
|
||||
TemplateAttributeType.DATETIME,
|
||||
false,
|
||||
TemplateAttributeOption().apply {
|
||||
setDateFormatToDate()
|
||||
})
|
||||
private val expirationDateAttribute = TemplateAttribute(
|
||||
TemplateField.LABEL_EXPIRATION,
|
||||
TemplateAttributeType.DATETIME,
|
||||
false,
|
||||
TemplateAttributeOption().apply {
|
||||
setDateFormatToDate()
|
||||
})
|
||||
private val emailAddressAttribute = TemplateAttribute(TemplateField.LABEL_EMAIL_ADDRESS, TemplateAttributeType.TEXT)
|
||||
private val passwordAttribute = TemplateAttribute(TemplateField.LABEL_PASSWORD, TemplateAttributeType.TEXT, true)
|
||||
private val ssidAttribute = TemplateAttribute(TemplateField.LABEL_SSID, TemplateAttributeType.TEXT)
|
||||
private val wirelessTypeAttribute = TemplateAttribute(
|
||||
TemplateField.LABEL_TYPE,
|
||||
TemplateAttributeType.LIST,
|
||||
false,
|
||||
TemplateAttributeOption().apply{
|
||||
setListItems("WPA3", "WPA2", "WPA", "WEP")
|
||||
default = "WPA2"
|
||||
})
|
||||
private val tokenAttribute = TemplateAttribute(TemplateField.LABEL_TOKEN, TemplateAttributeType.TEXT)
|
||||
private val publicKeyAttribute = TemplateAttribute(TemplateField.LABEL_PUBLIC_KEY, TemplateAttributeType.TEXT)
|
||||
private val privateKeyAttribute = TemplateAttribute(TemplateField.LABEL_PRIVATE_KEY, TemplateAttributeType.TEXT, true)
|
||||
private val seedAttribute = TemplateAttribute(TemplateField.LABEL_SEED, TemplateAttributeType.TEXT, true)
|
||||
private val bicAttribute = TemplateAttribute(TemplateField.LABEL_BIC, TemplateAttributeType.TEXT)
|
||||
private val ibanAttribute = TemplateAttribute(TemplateField.LABEL_IBAN, TemplateAttributeType.TEXT)
|
||||
|
||||
val email: Template
|
||||
get() {
|
||||
val sections = mutableListOf<TemplateSection>()
|
||||
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
|
||||
add(emailAddressAttribute)
|
||||
add(urlAttribute)
|
||||
add(passwordAttribute)
|
||||
})
|
||||
sections.add(mainSection)
|
||||
return Template(
|
||||
UUID.randomUUID(),
|
||||
TemplateField.LABEL_EMAIL,
|
||||
IconImage(IconImageStandard(IconImageStandard.EMAIL_ID)),
|
||||
sections)
|
||||
}
|
||||
|
||||
val wifi: Template
|
||||
get() {
|
||||
val sections = mutableListOf<TemplateSection>()
|
||||
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
|
||||
add(ssidAttribute)
|
||||
add(passwordAttribute)
|
||||
add(wirelessTypeAttribute)
|
||||
})
|
||||
sections.add(mainSection)
|
||||
return Template(
|
||||
UUID.randomUUID(),
|
||||
TemplateField.LABEL_WIRELESS,
|
||||
IconImage(IconImageStandard(IconImageStandard.WIRELESS_ID)),
|
||||
sections)
|
||||
}
|
||||
|
||||
val notes: Template
|
||||
get() {
|
||||
val sections = mutableListOf<TemplateSection>()
|
||||
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
|
||||
add(notesAttribute)
|
||||
})
|
||||
sections.add(mainSection)
|
||||
return Template(
|
||||
UUID.randomUUID(),
|
||||
TemplateField.LABEL_NOTES,
|
||||
IconImage(IconImageStandard(IconImageStandard.LIST_ID)),
|
||||
sections)
|
||||
}
|
||||
|
||||
val idCard: Template
|
||||
get() {
|
||||
val sections = mutableListOf<TemplateSection>()
|
||||
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
|
||||
add(numberAttribute)
|
||||
add(nameAttribute)
|
||||
add(placeOfIssueAttribute)
|
||||
add(dateOfIssueAttribute)
|
||||
add(expirationDateAttribute)
|
||||
})
|
||||
sections.add(mainSection)
|
||||
return Template(
|
||||
UUID.randomUUID(),
|
||||
TemplateField.LABEL_ID_CARD,
|
||||
IconImage(IconImageStandard(IconImageStandard.ID_CARD_ID)),
|
||||
sections)
|
||||
}
|
||||
|
||||
val creditCard: Template
|
||||
get() {
|
||||
val sections = mutableListOf<TemplateSection>()
|
||||
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
|
||||
add(numberAttribute)
|
||||
add(cvvAttribute)
|
||||
add(pinAttribute)
|
||||
add(holderAttribute)
|
||||
add(expirationDateAttribute)
|
||||
})
|
||||
sections.add(mainSection)
|
||||
return Template(
|
||||
UUID.randomUUID(),
|
||||
TemplateField.LABEL_DEBIT_CREDIT_CARD,
|
||||
IconImage(IconImageStandard(IconImageStandard.CREDIT_CARD_ID)),
|
||||
sections)
|
||||
}
|
||||
|
||||
val bank: Template
|
||||
get() {
|
||||
val sections = mutableListOf<TemplateSection>()
|
||||
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
|
||||
add(nameAttribute)
|
||||
add(urlAttribute)
|
||||
add(usernameAttribute)
|
||||
add(passwordAttribute)
|
||||
})
|
||||
val ibanSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
|
||||
add(holderAttribute)
|
||||
add(bicAttribute)
|
||||
add(ibanAttribute)
|
||||
}, TemplateField.LABEL_ACCOUNT)
|
||||
sections.add(mainSection)
|
||||
sections.add(ibanSection)
|
||||
return Template(
|
||||
UUID.randomUUID(),
|
||||
TemplateField.LABEL_BANK,
|
||||
IconImage(IconImageStandard(IconImageStandard.DOLLAR_ID)),
|
||||
sections)
|
||||
}
|
||||
|
||||
val cryptocurrency: Template
|
||||
get() {
|
||||
val sections = mutableListOf<TemplateSection>()
|
||||
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
|
||||
add(tokenAttribute)
|
||||
add(publicKeyAttribute)
|
||||
add(privateKeyAttribute)
|
||||
add(seedAttribute)
|
||||
})
|
||||
sections.add(mainSection)
|
||||
return Template(
|
||||
UUID.randomUUID(),
|
||||
TemplateField.LABEL_CRYPTOCURRENCY,
|
||||
IconImage(IconImageStandard(IconImageStandard.STAR_ID)),
|
||||
sections)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
|
||||
|
||||
private val mCacheTemplates = HashMap<UUID, Template>()
|
||||
|
||||
fun getTemplates(): List<Template> {
|
||||
val templates = mutableListOf<Template>()
|
||||
try {
|
||||
val templateGroup = mDatabase.getTemplatesGroup()
|
||||
mCacheTemplates.clear()
|
||||
if (templateGroup != null) {
|
||||
templates.add(Template.STANDARD)
|
||||
templateGroup.getChildEntries().forEach { templateEntry ->
|
||||
getTemplateFromTemplateEntry(templateEntry)?.let {
|
||||
mCacheTemplates[templateEntry.id] = it
|
||||
templates.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to get templates from group", e)
|
||||
}
|
||||
return templates
|
||||
}
|
||||
|
||||
fun getTemplateCreation(): Template {
|
||||
return Template(CREATION)
|
||||
}
|
||||
|
||||
fun createNewTemplatesGroup(templatesGroupName: String): GroupKDBX {
|
||||
val newTemplatesGroup = mDatabase.createGroup().apply {
|
||||
title = templatesGroupName
|
||||
icon.standard = mDatabase.getStandardIcon(IconImageStandard.BUILD_ID)
|
||||
enableAutoType = false
|
||||
enableSearching = false
|
||||
isExpanded = false
|
||||
}
|
||||
mDatabase.addGroupTo(newTemplatesGroup, mDatabase.rootGroup)
|
||||
// Build default templates
|
||||
getDefaults().forEach { defaultTemplate ->
|
||||
createTemplateEntry(defaultTemplate).also {
|
||||
mDatabase.addEntryTo(it, newTemplatesGroup)
|
||||
}
|
||||
}
|
||||
return newTemplatesGroup
|
||||
}
|
||||
|
||||
fun clearCache() {
|
||||
mCacheTemplates.clear()
|
||||
}
|
||||
|
||||
protected fun getTemplateByCache(uuid: UUID): Template? {
|
||||
try {
|
||||
if (mCacheTemplates.containsKey(uuid))
|
||||
return mCacheTemplates[uuid]
|
||||
else {
|
||||
mDatabase.getEntryById(uuid)?.let { templateEntry ->
|
||||
getTemplateFromTemplateEntry(templateEntry)?.let { newTemplate ->
|
||||
mCacheTemplates[uuid] = newTemplate
|
||||
return newTemplate
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to get Entry by $uuid", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
abstract fun getVersion(): Int
|
||||
|
||||
abstract fun getTemplate(entryKDBX: EntryKDBX): Template?
|
||||
|
||||
abstract fun removeMetaTemplateRecognitionFromEntry(entry: EntryKDBX): EntryKDBX
|
||||
|
||||
abstract fun addMetaTemplateRecognitionToEntry(template: Template, entry: EntryKDBX): EntryKDBX
|
||||
|
||||
abstract fun buildTemplateEntryField(attribute: TemplateAttribute): Field
|
||||
|
||||
abstract fun decodeTemplateEntry(templateEntry: EntryKDBX): EntryKDBX
|
||||
|
||||
abstract fun encodeTemplateEntry(templateEntry: EntryKDBX): EntryKDBX
|
||||
|
||||
fun createTemplateEntry(template: Template): EntryKDBX {
|
||||
val newEntry = EntryKDBX().apply {
|
||||
nodeId = NodeIdUUID(template.uuid)
|
||||
title = template.title
|
||||
icon = template.icon
|
||||
template.sections.forEachIndexed { index, section ->
|
||||
section.attributes.forEach { attribute ->
|
||||
if (index > 0) {
|
||||
// Label is not important with section => [Section_X]: Divider
|
||||
val sectionName = if (section.name.isEmpty())
|
||||
"$SECTION_DECODED_TEMPLATE_PREFIX${index-1}"
|
||||
else
|
||||
section.name
|
||||
putField(Field(addTemplateDecorator(sectionName),
|
||||
ProtectedString(false, TemplateAttributeType.DIVIDER.typeString))
|
||||
)
|
||||
}
|
||||
|
||||
putField(buildTemplateEntryField(attribute))
|
||||
}
|
||||
}
|
||||
}
|
||||
return encodeTemplateEntry(newEntry)
|
||||
}
|
||||
|
||||
private fun buildTemplateSectionFromFields(fields: List<Field>): TemplateSection {
|
||||
val sectionAttributes = mutableListOf<TemplateAttribute>()
|
||||
fields.forEach { field ->
|
||||
sectionAttributes.add(TemplateAttribute(
|
||||
removeTemplateDecorator(field.name),
|
||||
TemplateAttributeType.getFromString(field.protectedValue.stringValue),
|
||||
field.protectedValue.isProtected,
|
||||
TemplateAttributeOption.getOptionsFromString(field.protectedValue.stringValue))
|
||||
)
|
||||
}
|
||||
return TemplateSection(sectionAttributes)
|
||||
}
|
||||
|
||||
private fun getTemplateFromTemplateEntry(templateEntry: EntryKDBX): Template? {
|
||||
|
||||
val templateEntryDecoded = decodeTemplateEntry(templateEntry)
|
||||
val templateSections = mutableListOf<TemplateSection>()
|
||||
val sectionFields = mutableListOf<Field>()
|
||||
templateEntryDecoded.doForEachDecodedCustomField { field ->
|
||||
if (field.protectedValue.stringValue.contains(TemplateAttributeType.DIVIDER.typeString)) {
|
||||
templateSections.add(buildTemplateSectionFromFields(sectionFields))
|
||||
sectionFields.clear()
|
||||
} else {
|
||||
sectionFields.add(field)
|
||||
}
|
||||
}
|
||||
templateSections.add(buildTemplateSectionFromFields(sectionFields))
|
||||
|
||||
return Template(templateEntry.id, templateEntry.title, templateEntry.icon, templateSections, getVersion())
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = TemplateEngine::class.java.name
|
||||
|
||||
private const val PREFIX_DECODED_TEMPLATE = "["
|
||||
private const val SUFFIX_DECODED_TEMPLATE = "]"
|
||||
private const val SECTION_DECODED_TEMPLATE_PREFIX = "Section "
|
||||
|
||||
val CREATION: Template
|
||||
get() {
|
||||
val sections = mutableListOf<TemplateSection>()
|
||||
val mainSection = TemplateSection(mutableListOf<TemplateAttribute>().apply {
|
||||
// Dynamic part
|
||||
})
|
||||
sections.add(mainSection)
|
||||
return Template(UUID(0, 1),
|
||||
TemplateField.LABEL_TEMPLATE,
|
||||
IconImage(IconImageStandard(IconImageStandard.BUILD_ID)),
|
||||
sections)
|
||||
}
|
||||
|
||||
fun getDefaultTemplateGroupName(resources: Resources): String {
|
||||
return resources.getString(R.string.templates)
|
||||
}
|
||||
|
||||
fun getDefaults(): List<Template> {
|
||||
val templateBuilder = TemplateBuilder()
|
||||
return listOf(
|
||||
templateBuilder.email,
|
||||
templateBuilder.wifi,
|
||||
templateBuilder.notes,
|
||||
templateBuilder.idCard,
|
||||
templateBuilder.creditCard,
|
||||
templateBuilder.bank,
|
||||
templateBuilder.cryptocurrency)
|
||||
}
|
||||
|
||||
fun containsTemplateDecorator(name: String): Boolean {
|
||||
return name.startsWith(PREFIX_DECODED_TEMPLATE)
|
||||
&& name.endsWith(SUFFIX_DECODED_TEMPLATE)
|
||||
}
|
||||
|
||||
fun addTemplateDecorator(name: String): String {
|
||||
return "$PREFIX_DECODED_TEMPLATE${name}$SUFFIX_DECODED_TEMPLATE"
|
||||
}
|
||||
|
||||
fun removeTemplateDecorator(name: String): String {
|
||||
return name
|
||||
.removePrefix(PREFIX_DECODED_TEMPLATE)
|
||||
.removeSuffix(SUFFIX_DECODED_TEMPLATE)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
|
||||
class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database) {
|
||||
|
||||
override fun getVersion(): Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
override fun getTemplate(entryKDBX: EntryKDBX): Template? {
|
||||
UuidUtil.fromHexString(entryKDBX.getCustomFieldValue(TEMPLATE_ENTRY_UUID))?.let { templateUUID ->
|
||||
return getTemplateByCache(templateUUID)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun removeMetaTemplateRecognitionFromEntry(entry: EntryKDBX): EntryKDBX {
|
||||
val entryCopy = EntryKDBX().apply {
|
||||
updateWith(entry)
|
||||
}
|
||||
entryCopy.removeField(TEMPLATE_ENTRY_UUID)
|
||||
return entryCopy
|
||||
}
|
||||
|
||||
private fun getTemplateUUIDField(template: Template): Field? {
|
||||
UuidUtil.toHexString(template.uuid)?.let { uuidString ->
|
||||
return Field(TEMPLATE_ENTRY_UUID,
|
||||
ProtectedString(false, uuidString))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun addMetaTemplateRecognitionToEntry(template: Template, entry: EntryKDBX): EntryKDBX {
|
||||
val entryCopy = EntryKDBX().apply {
|
||||
updateWith(entry)
|
||||
}
|
||||
// Add template field
|
||||
if (template != Template.STANDARD
|
||||
&& template != CREATION) {
|
||||
getTemplateUUIDField(template)?.let { templateField ->
|
||||
entryCopy.putField(templateField)
|
||||
}
|
||||
} else {
|
||||
entryCopy.removeField(TEMPLATE_ENTRY_UUID)
|
||||
}
|
||||
return entryCopy
|
||||
}
|
||||
|
||||
private fun getOrRetrieveAttributeFromName(attributes: HashMap<String, TemplateAttributePosition>, name: String): TemplateAttributePosition {
|
||||
return if (attributes.containsKey(name)) {
|
||||
attributes[name]!!
|
||||
} else {
|
||||
val newAttribute = TemplateAttributePosition(
|
||||
-1,
|
||||
TemplateAttribute(name, TemplateAttributeType.TEXT)
|
||||
)
|
||||
attributes[name] = newAttribute
|
||||
newAttribute
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildTemplateEntryField(attribute: TemplateAttribute): Field {
|
||||
val typeAndOptions = attribute.type.typeString +
|
||||
TemplateAttributeOption.getStringFromOptions(attribute.options)
|
||||
// PREFIX_DECODED_TEMPLATE to fix same label as standard fields
|
||||
return Field(addTemplateDecorator(decodeTemplateAttribute(attribute.label)),
|
||||
ProtectedString(attribute.protected, typeAndOptions))
|
||||
}
|
||||
|
||||
override fun decodeTemplateEntry(templateEntry: EntryKDBX): EntryKDBX {
|
||||
val attributes = HashMap<String, TemplateAttributePosition>()
|
||||
val defaultValues = HashMap<String, String>()
|
||||
val entryCopy = EntryKDBX().apply {
|
||||
updateWith(templateEntry)
|
||||
}
|
||||
// Remove template version
|
||||
entryCopy.getFieldValue(TEMPLATE_LABEL_VERSION)
|
||||
try {
|
||||
// value.toIntOrNull()
|
||||
// At the moment, only the version 1 is known
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve template version", e)
|
||||
}
|
||||
entryCopy.removeField(TEMPLATE_LABEL_VERSION)
|
||||
// Dynamic attributes
|
||||
templateEntry.doForEachDecodedCustomField { field ->
|
||||
|
||||
val label = field.name
|
||||
val value = field.protectedValue.stringValue
|
||||
when {
|
||||
label.startsWith(TEMPLATE_ATTRIBUTE_POSITION_PREFIX, true) -> {
|
||||
try {
|
||||
val attributeName = label.substring(TEMPLATE_ATTRIBUTE_POSITION_PREFIX.length)
|
||||
val attribute = getOrRetrieveAttributeFromName(attributes, attributeName)
|
||||
attribute.position = value.toInt()
|
||||
entryCopy.removeField(field.name)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve template position", e)
|
||||
}
|
||||
}
|
||||
label.startsWith(TEMPLATE_ATTRIBUTE_TITLE_PREFIX, true) -> {
|
||||
try {
|
||||
val attributeName = label.substring(TEMPLATE_ATTRIBUTE_TITLE_PREFIX.length)
|
||||
val attribute = getOrRetrieveAttributeFromName(attributes, attributeName)
|
||||
// Here title is an alias if different (often the same)
|
||||
if (attributeName != value) {
|
||||
attribute.attribute.alias = value
|
||||
}
|
||||
entryCopy.removeField(field.name)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve template title", e)
|
||||
}
|
||||
}
|
||||
label.startsWith(TEMPLATE_ATTRIBUTE_TYPE_PREFIX, true) -> {
|
||||
try {
|
||||
val attributeName = label.substring(TEMPLATE_ATTRIBUTE_TYPE_PREFIX.length)
|
||||
val attribute = getOrRetrieveAttributeFromName(attributes, attributeName)
|
||||
if (value.contains(TEMPLATE_ATTRIBUTE_TYPE_PROTECTED, true)) {
|
||||
attribute.attribute.protected = true
|
||||
}
|
||||
when {
|
||||
value.contains(TEMPLATE_ATTRIBUTE_TYPE_INLINE_URL, true) -> {
|
||||
attribute.attribute.type = TemplateAttributeType.TEXT
|
||||
attribute.attribute.options.setLink(true)
|
||||
}
|
||||
value.contains(TEMPLATE_ATTRIBUTE_TYPE_INLINE, true) ||
|
||||
value.contains(TEMPLATE_ATTRIBUTE_TYPE_POPOUT, true) -> {
|
||||
attribute.attribute.type = TemplateAttributeType.TEXT
|
||||
}
|
||||
value.contains(TEMPLATE_ATTRIBUTE_TYPE_MULTILINE, true) -> {
|
||||
attribute.attribute.type = TemplateAttributeType.TEXT
|
||||
attribute.attribute.options.setNumberLinesToMany()
|
||||
}
|
||||
value.contains(TEMPLATE_ATTRIBUTE_TYPE_RICH_TEXTBOX, true) -> {
|
||||
attribute.attribute.type = TemplateAttributeType.TEXT
|
||||
attribute.attribute.options.setNumberLinesToMany()
|
||||
attribute.attribute.options.setLink(true)
|
||||
}
|
||||
value.contains(TEMPLATE_ATTRIBUTE_TYPE_LISTBOX, true) -> {
|
||||
attribute.attribute.type = TemplateAttributeType.LIST
|
||||
}
|
||||
value.contains(TEMPLATE_ATTRIBUTE_TYPE_DATE_TIME, true) -> {
|
||||
attribute.attribute.type = TemplateAttributeType.DATETIME
|
||||
}
|
||||
value.contains(TEMPLATE_ATTRIBUTE_TYPE_DATE, true) -> {
|
||||
attribute.attribute.type = TemplateAttributeType.DATETIME
|
||||
attribute.attribute.options.setDateFormatToDate()
|
||||
}
|
||||
value.contains(TEMPLATE_ATTRIBUTE_TYPE_TIME, true) -> {
|
||||
attribute.attribute.type = TemplateAttributeType.DATETIME
|
||||
attribute.attribute.options.setDateFormatToTime()
|
||||
}
|
||||
value.contains(TEMPLATE_ATTRIBUTE_TYPE_DIVIDER, true) -> {
|
||||
attribute.attribute.type = TemplateAttributeType.DIVIDER
|
||||
}
|
||||
}
|
||||
entryCopy.removeField(field.name)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve template type", e)
|
||||
}
|
||||
}
|
||||
label.startsWith(TEMPLATE_ATTRIBUTE_OPTIONS_PREFIX, true) -> {
|
||||
try {
|
||||
val attributeName = label.substring(TEMPLATE_ATTRIBUTE_OPTIONS_PREFIX.length)
|
||||
val attribute = getOrRetrieveAttributeFromName(attributes, attributeName)
|
||||
if (value.isNotEmpty()) {
|
||||
attribute.attribute.options.put(TEMPLATE_ATTRIBUTE_OPTIONS_TEMP, value)
|
||||
}
|
||||
entryCopy.removeField(field.name)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve template options", e)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// To retrieve default values
|
||||
if (value.isNotEmpty()) {
|
||||
defaultValues[label] = value
|
||||
}
|
||||
entryCopy.removeField(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val newFields = arrayOfNulls<Field>(attributes.size)
|
||||
attributes.values.forEach {
|
||||
|
||||
val attribute = it.attribute
|
||||
|
||||
// Add password generator
|
||||
if (attribute.label.equals(TEMPLATE_ATTRIBUTE_PASSWORD, true)) {
|
||||
attribute.options.associatePasswordGenerator()
|
||||
}
|
||||
|
||||
// Add default value
|
||||
if (defaultValues.containsKey(attribute.label)) {
|
||||
attribute.options.default = defaultValues[attribute.label]!!
|
||||
}
|
||||
|
||||
// Recognize each temp option
|
||||
attribute.options.get(TEMPLATE_ATTRIBUTE_OPTIONS_TEMP)?.let { defaultOption ->
|
||||
when (attribute.type) {
|
||||
TemplateAttributeType.TEXT -> {
|
||||
try {
|
||||
when (attribute.options.getNumberLines()) {
|
||||
1 -> {
|
||||
// If one line, default attribute option is number of chars
|
||||
attribute.options.setNumberChars(defaultOption.toInt())
|
||||
}
|
||||
else -> {
|
||||
// else it's number of lines
|
||||
attribute.options.setNumberLines(defaultOption.toInt())
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to transform default text option", e)
|
||||
}
|
||||
}
|
||||
TemplateAttributeType.LIST -> {
|
||||
try {
|
||||
// Default attribute option is items of the list
|
||||
val items = defaultOption.split(",")
|
||||
attribute.options.setListItems(*items.toTypedArray())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to transform default list option", e)
|
||||
}
|
||||
}
|
||||
TemplateAttributeType.DATETIME -> {
|
||||
try {
|
||||
// Default attribute option is datetime, date or time
|
||||
when {
|
||||
defaultOption.equals(TEMPLATE_ATTRIBUTE_TYPE_DATE_TIME, true) -> {
|
||||
// Do not add option if it's datetime
|
||||
}
|
||||
defaultOption.equals(TEMPLATE_ATTRIBUTE_TYPE_DATE, true) -> {
|
||||
attribute.options.setDateFormatToDate()
|
||||
}
|
||||
defaultOption.equals(TEMPLATE_ATTRIBUTE_TYPE_TIME, true) -> {
|
||||
attribute.options.setDateFormatToTime()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to transform default datetime", e)
|
||||
}
|
||||
}
|
||||
TemplateAttributeType.DIVIDER -> {
|
||||
// No option here
|
||||
}
|
||||
}
|
||||
attribute.options.remove(TEMPLATE_ATTRIBUTE_OPTIONS_TEMP)
|
||||
}
|
||||
|
||||
// Add position for each attribute
|
||||
newFields[it.position] = Field(buildTemplateEntryField(attribute))
|
||||
}
|
||||
// Add custom fields to entry
|
||||
newFields.forEach { field ->
|
||||
field?.let {
|
||||
entryCopy.putField(field)
|
||||
}
|
||||
}
|
||||
|
||||
return entryCopy
|
||||
}
|
||||
|
||||
override fun encodeTemplateEntry(templateEntry: EntryKDBX): EntryKDBX {
|
||||
val entryCopy = EntryKDBX().apply {
|
||||
updateWith(templateEntry)
|
||||
}
|
||||
// Add template version
|
||||
entryCopy.putField(TEMPLATE_LABEL_VERSION, ProtectedString(false, "1"))
|
||||
// Dynamic attributes
|
||||
var index = 0
|
||||
templateEntry.doForEachDecodedCustomField { field ->
|
||||
val label = removeTemplateDecorator(encodeTemplateAttribute(field.name))
|
||||
val value = field.protectedValue
|
||||
when {
|
||||
label.equals(TEMPLATE_LABEL_VERSION, true) -> {
|
||||
// Keep template version as is
|
||||
}
|
||||
else -> {
|
||||
entryCopy.removeField(field.name)
|
||||
val options = TemplateAttributeOption.getOptionsFromString(value.stringValue)
|
||||
|
||||
// Add Position attribute
|
||||
entryCopy.putField(
|
||||
TEMPLATE_ATTRIBUTE_POSITION_PREFIX + label,
|
||||
ProtectedString(false, index.toString())
|
||||
)
|
||||
// Add Title attribute (or alias if defined)
|
||||
val title = options.alias ?: label
|
||||
entryCopy.putField(
|
||||
TEMPLATE_ATTRIBUTE_TITLE_PREFIX + label,
|
||||
ProtectedString(false, title)
|
||||
)
|
||||
// Add Type attribute
|
||||
var typeString: String = when {
|
||||
value.stringValue.contains(TemplateAttributeType.TEXT.typeString, true) -> {
|
||||
when (options.getNumberLines()) {
|
||||
1 -> TEMPLATE_ATTRIBUTE_TYPE_INLINE
|
||||
else -> TEMPLATE_ATTRIBUTE_TYPE_MULTILINE
|
||||
}
|
||||
}
|
||||
value.stringValue.contains(TemplateAttributeType.LIST.typeString, true) -> {
|
||||
TEMPLATE_ATTRIBUTE_TYPE_LISTBOX
|
||||
|
||||
}
|
||||
value.stringValue.contains(TemplateAttributeType.DATETIME.typeString, true) -> {
|
||||
when (options.getDateFormat()) {
|
||||
DateInstant.Type.DATE -> TEMPLATE_ATTRIBUTE_TYPE_DATE
|
||||
DateInstant.Type.TIME -> TEMPLATE_ATTRIBUTE_TYPE_TIME
|
||||
else -> TEMPLATE_ATTRIBUTE_TYPE_DATE_TIME
|
||||
}
|
||||
}
|
||||
value.stringValue.contains(TemplateAttributeType.DIVIDER.typeString, true) -> {
|
||||
TEMPLATE_ATTRIBUTE_TYPE_DIVIDER
|
||||
}
|
||||
else -> TEMPLATE_ATTRIBUTE_TYPE_INLINE
|
||||
}
|
||||
// Add protected string if needed
|
||||
if (value.isProtected) {
|
||||
typeString = "$TEMPLATE_ATTRIBUTE_TYPE_PROTECTED $typeString"
|
||||
}
|
||||
entryCopy.putField(
|
||||
TEMPLATE_ATTRIBUTE_TYPE_PREFIX + label,
|
||||
ProtectedString(false, typeString)
|
||||
)
|
||||
// Add Options attribute
|
||||
// Here only number of chars, lines and list items are supported
|
||||
var defaultOption = ""
|
||||
try {
|
||||
val listItems = options.getListItems()
|
||||
if (listItems.isNotEmpty()) {
|
||||
defaultOption = listItems.joinToString(",")
|
||||
} else {
|
||||
val numberChars = options.getNumberChars()
|
||||
if (numberChars > 1) {
|
||||
defaultOption = numberChars.toString()
|
||||
} else {
|
||||
val numberLines = options.getNumberLines()
|
||||
if (numberLines > 1) {
|
||||
defaultOption = numberLines.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Ignore, default option is defined
|
||||
}
|
||||
entryCopy.putField(
|
||||
TEMPLATE_ATTRIBUTE_OPTIONS_PREFIX + label,
|
||||
ProtectedString(false, defaultOption)
|
||||
)
|
||||
// Add default elements
|
||||
if (options.default.isNotEmpty()) {
|
||||
entryCopy.putField(
|
||||
label,
|
||||
ProtectedString(value.isProtected, options.default)
|
||||
)
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
return entryCopy
|
||||
}
|
||||
|
||||
private data class TemplateAttributePosition(var position: Int, var attribute: TemplateAttribute)
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = TemplateEngineCompatible::class.java.name
|
||||
|
||||
// Custom template ref
|
||||
private const val TEMPLATE_ATTRIBUTE_TITLE = "Title"
|
||||
private const val TEMPLATE_ATTRIBUTE_USERNAME = "UserName"
|
||||
private const val TEMPLATE_ATTRIBUTE_PASSWORD = "Password"
|
||||
private const val TEMPLATE_ATTRIBUTE_URL = "URL"
|
||||
private const val TEMPLATE_ATTRIBUTE_EXP_DATE = "@exp_date"
|
||||
private const val TEMPLATE_ATTRIBUTE_EXPIRES = "Expires"
|
||||
private const val TEMPLATE_ATTRIBUTE_NOTES = "Notes"
|
||||
|
||||
private const val TEMPLATE_LABEL_VERSION = "_etm_template"
|
||||
private const val TEMPLATE_ENTRY_UUID = "_etm_template_uuid"
|
||||
private const val TEMPLATE_ATTRIBUTE_POSITION_PREFIX = "_etm_position_"
|
||||
private const val TEMPLATE_ATTRIBUTE_TITLE_PREFIX = "_etm_title_"
|
||||
private const val TEMPLATE_ATTRIBUTE_TYPE_PREFIX = "_etm_type_"
|
||||
private const val TEMPLATE_ATTRIBUTE_OPTIONS_PREFIX = "_etm_options_"
|
||||
private const val TEMPLATE_ATTRIBUTE_OPTIONS_TEMP = "temp_option"
|
||||
private const val TEMPLATE_ATTRIBUTE_TYPE_PROTECTED = "Protected"
|
||||
private const val TEMPLATE_ATTRIBUTE_TYPE_INLINE = "Inline"
|
||||
private const val TEMPLATE_ATTRIBUTE_TYPE_INLINE_URL = "Inline URL"
|
||||
private const val TEMPLATE_ATTRIBUTE_TYPE_MULTILINE = "Multiline"
|
||||
private const val TEMPLATE_ATTRIBUTE_TYPE_LISTBOX = "Listbox"
|
||||
private const val TEMPLATE_ATTRIBUTE_TYPE_DATE_TIME = "Date Time"
|
||||
private const val TEMPLATE_ATTRIBUTE_TYPE_DATE = "Date"
|
||||
private const val TEMPLATE_ATTRIBUTE_TYPE_TIME = "Time"
|
||||
private const val TEMPLATE_ATTRIBUTE_TYPE_DIVIDER = "Divider"
|
||||
private const val TEMPLATE_ATTRIBUTE_TYPE_POPOUT = "Popout"
|
||||
private const val TEMPLATE_ATTRIBUTE_TYPE_RICH_TEXTBOX = "Rich Textbox"
|
||||
|
||||
private fun decodeTemplateAttribute(name: String): String {
|
||||
return when {
|
||||
TEMPLATE_LABEL_VERSION.equals(name, true) -> TemplateField.LABEL_VERSION
|
||||
TEMPLATE_ATTRIBUTE_TITLE.equals(name, true) -> TemplateField.LABEL_TITLE
|
||||
TEMPLATE_ATTRIBUTE_USERNAME.equals(name, true) -> TemplateField.LABEL_USERNAME
|
||||
TEMPLATE_ATTRIBUTE_PASSWORD.equals(name, true) -> TemplateField.LABEL_PASSWORD
|
||||
TEMPLATE_ATTRIBUTE_URL.equals(name, true) -> TemplateField.LABEL_URL
|
||||
TEMPLATE_ATTRIBUTE_EXP_DATE.equals(name, true) -> TemplateField.LABEL_EXPIRATION
|
||||
TEMPLATE_ATTRIBUTE_EXPIRES.equals(name, true) -> TemplateField.LABEL_EXPIRATION
|
||||
TEMPLATE_ATTRIBUTE_NOTES.equals(name, true) -> TemplateField.LABEL_NOTES
|
||||
else -> name
|
||||
}
|
||||
}
|
||||
|
||||
private fun encodeTemplateAttribute(name: String): String {
|
||||
return when {
|
||||
TemplateField.LABEL_VERSION.equals(name, true) -> TEMPLATE_LABEL_VERSION
|
||||
TemplateField.LABEL_TITLE.equals(name, true) -> TEMPLATE_ATTRIBUTE_TITLE
|
||||
TemplateField.LABEL_USERNAME.equals(name, true) -> TEMPLATE_ATTRIBUTE_USERNAME
|
||||
TemplateField.LABEL_PASSWORD.equals(name, true) -> TEMPLATE_ATTRIBUTE_PASSWORD
|
||||
TemplateField.LABEL_URL.equals(name, true) -> TEMPLATE_ATTRIBUTE_URL
|
||||
TemplateField.LABEL_EXPIRATION.equals(name, true) -> TEMPLATE_ATTRIBUTE_EXP_DATE
|
||||
TemplateField.LABEL_NOTES.equals(name, true) -> TEMPLATE_ATTRIBUTE_NOTES
|
||||
else -> name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
object TemplateField {
|
||||
|
||||
const val LABEL_STANDARD = "Standard"
|
||||
const val LABEL_TEMPLATE = "Template"
|
||||
const val LABEL_VERSION = "Version"
|
||||
|
||||
const val LABEL_TITLE = "Title"
|
||||
const val LABEL_USERNAME = "Username"
|
||||
const val LABEL_PASSWORD = "Password"
|
||||
const val LABEL_URL = "URL"
|
||||
const val LABEL_EXPIRATION = "Expires"
|
||||
const val LABEL_NOTES = "Notes"
|
||||
|
||||
const val LABEL_DEBIT_CREDIT_CARD = "Debit / Credit Card"
|
||||
const val LABEL_HOLDER = "Holder"
|
||||
const val LABEL_NUMBER = "Number"
|
||||
const val LABEL_CVV = "CVV"
|
||||
const val LABEL_PIN = "PIN"
|
||||
const val LABEL_ID_CARD = "ID Card"
|
||||
const val LABEL_NAME = "Name"
|
||||
const val LABEL_PLACE_OF_ISSUE = "Place of issue"
|
||||
const val LABEL_DATE_OF_ISSUE = "Date of issue"
|
||||
const val LABEL_EMAIL = "Email"
|
||||
const val LABEL_EMAIL_ADDRESS = "Email address"
|
||||
const val LABEL_WIRELESS = "Wifi"
|
||||
const val LABEL_SSID = "SSID"
|
||||
const val LABEL_TYPE = "Type"
|
||||
const val LABEL_CRYPTOCURRENCY = "Cryptocurrency wallet"
|
||||
const val LABEL_TOKEN = "Token"
|
||||
const val LABEL_PUBLIC_KEY = "Public key"
|
||||
const val LABEL_PRIVATE_KEY = "Private key"
|
||||
const val LABEL_SEED = "Seed"
|
||||
const val LABEL_ACCOUNT = "Account"
|
||||
const val LABEL_BANK = "Bank"
|
||||
const val LABEL_BIC = "BIC"
|
||||
const val LABEL_IBAN = "IBAN"
|
||||
const val LABEL_SECURE_NOTE = "Secure Note"
|
||||
const val LABEL_MEMBERSHIP = "Membership"
|
||||
|
||||
fun isStandardFieldName(name: String): Boolean {
|
||||
return arrayOf(
|
||||
LABEL_TITLE,
|
||||
LABEL_USERNAME,
|
||||
LABEL_PASSWORD,
|
||||
LABEL_URL,
|
||||
LABEL_EXPIRATION,
|
||||
LABEL_NOTES
|
||||
).firstOrNull { it.equals(name, true) } != null
|
||||
}
|
||||
|
||||
fun getLocalizedName(context: Context?, name: String): String {
|
||||
if (context == null
|
||||
|| TemplateEngine.containsTemplateDecorator(name))
|
||||
return name
|
||||
|
||||
return when {
|
||||
LABEL_STANDARD.equals(name, true) -> context.getString(R.string.standard)
|
||||
LABEL_TEMPLATE.equals(name, true) -> context.getString(R.string.template)
|
||||
LABEL_VERSION.equals(name, true) -> context.getString(R.string.version)
|
||||
|
||||
LABEL_TITLE.equals(name, true) -> context.getString(R.string.entry_title)
|
||||
LABEL_USERNAME.equals(name, true) -> context.getString(R.string.entry_user_name)
|
||||
LABEL_PASSWORD.equals(name, true) -> context.getString(R.string.entry_password)
|
||||
LABEL_URL.equals(name, true) -> context.getString(R.string.entry_url)
|
||||
LABEL_EXPIRATION.equals(name, true) -> context.getString(R.string.entry_expires)
|
||||
LABEL_NOTES.equals(name, true) -> context.getString(R.string.entry_notes)
|
||||
|
||||
LABEL_DEBIT_CREDIT_CARD.equals(name, true) -> context.getString(R.string.debit_credit_card)
|
||||
LABEL_HOLDER.equals(name, true) -> context.getString(R.string.holder)
|
||||
LABEL_NUMBER.equals(name, true) -> context.getString(R.string.number)
|
||||
LABEL_CVV.equals(name, true) -> context.getString(R.string.card_verification_value)
|
||||
LABEL_PIN.equals(name, true) -> context.getString(R.string.personal_identification_number)
|
||||
LABEL_ID_CARD.equals(name, true) -> context.getString(R.string.id_card)
|
||||
LABEL_NAME.equals(name, true) -> context.getString(R.string.name)
|
||||
LABEL_PLACE_OF_ISSUE.equals(name, true) -> context.getString(R.string.place_of_issue)
|
||||
LABEL_DATE_OF_ISSUE.equals(name, true) -> context.getString(R.string.date_of_issue)
|
||||
LABEL_EMAIL.equals(name, true) -> context.getString(R.string.email)
|
||||
LABEL_EMAIL_ADDRESS.equals(name, true) -> context.getString(R.string.email_address)
|
||||
LABEL_WIRELESS.equals(name, true) -> context.getString(R.string.wireless)
|
||||
LABEL_SSID.equals(name, true) -> context.getString(R.string.ssid)
|
||||
LABEL_TYPE.equals(name, true) -> context.getString(R.string.type)
|
||||
LABEL_CRYPTOCURRENCY.equals(name, true) -> context.getString(R.string.cryptocurrency)
|
||||
LABEL_TOKEN.equals(name, true) -> context.getString(R.string.token)
|
||||
LABEL_PUBLIC_KEY.equals(name, true) -> context.getString(R.string.public_key)
|
||||
LABEL_PRIVATE_KEY.equals(name, true) -> context.getString(R.string.private_key)
|
||||
LABEL_SEED.equals(name, true) -> context.getString(R.string.seed)
|
||||
LABEL_ACCOUNT.equals(name, true) -> context.getString(R.string.account)
|
||||
LABEL_BANK.equals(name, true) -> context.getString(R.string.bank)
|
||||
LABEL_BIC.equals(name, true) -> context.getString(R.string.bank_identifier_code)
|
||||
LABEL_IBAN.equals(name, true) -> context.getString(R.string.international_bank_account_number)
|
||||
LABEL_SECURE_NOTE.equals(name, true) -> context.getString(R.string.secure_note)
|
||||
LABEL_MEMBERSHIP.equals(name, true) -> context.getString(R.string.membership)
|
||||
|
||||
else -> name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.kunzisoft.keepass.database.element.template
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
class TemplateSection: Parcelable {
|
||||
|
||||
var name: String = ""
|
||||
var attributes: List<TemplateAttribute> = ArrayList()
|
||||
private set
|
||||
|
||||
constructor(attributes: List<TemplateAttribute>, name: String = "") {
|
||||
this.name = name
|
||||
this.attributes = attributes
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) {
|
||||
this.name = parcel.readString() ?: name
|
||||
parcel.readList(this.attributes, TemplateAttribute::class.java.classLoader)
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(this.name)
|
||||
parcel.writeList(this.attributes)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<TemplateSection> {
|
||||
override fun createFromParcel(parcel: Parcel): TemplateSection {
|
||||
return TemplateSection(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<TemplateSection?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,8 @@ object DateKDBXUtil {
|
||||
} else dt.toDate()
|
||||
}
|
||||
|
||||
fun convertDateToKDBX4Time(dt: DateTime?): Long {
|
||||
val duration = Duration(javaEpoch, dt)
|
||||
fun convertDateToKDBX4Time(date: Date): Long {
|
||||
val duration = Duration(javaEpoch, DateTime(date))
|
||||
val seconds = duration.millis / 1000L
|
||||
return seconds + epochOffset
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
@@ -401,7 +400,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
writeTags(entry.tags)
|
||||
writePreviousParentGroup(entry.previousParentGroup)
|
||||
writeTimes(entry)
|
||||
writeFields(entry.fields)
|
||||
writeFields(entry.getFields())
|
||||
writeEntryBinaries(entry.binaries)
|
||||
writeCustomData(entry.customData)
|
||||
writeAutoType(entry.autoType)
|
||||
@@ -433,7 +432,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
if (header!!.version.isBefore(FILE_VERSION_40)) {
|
||||
writeString(name, DatabaseKDBXXML.DateFormatter.format(date))
|
||||
} else {
|
||||
val buf = longTo8Bytes(DateKDBXUtil.convertDateToKDBX4Time(DateTime(date)))
|
||||
val buf = longTo8Bytes(DateKDBXUtil.convertDateToKDBX4Time(date))
|
||||
val b64 = String(Base64.encode(buf, BASE_64_FLAG))
|
||||
writeString(name, b64)
|
||||
}
|
||||
@@ -548,25 +547,26 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeFields(fields: Map<String, ProtectedString>) {
|
||||
|
||||
for ((key, value) in fields) {
|
||||
writeField(key, value)
|
||||
private fun writeFields(fields: List<Field>) {
|
||||
for (field in fields) {
|
||||
writeField(field)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeField(key: String, value: ProtectedString) {
|
||||
private fun writeField(field: Field) {
|
||||
val label = field.name
|
||||
val value = field.protectedValue
|
||||
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemString)
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemKey)
|
||||
xml.text(safeXmlString(key))
|
||||
xml.text(safeXmlString(label))
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemKey)
|
||||
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemValue)
|
||||
var protect = value.isProtected
|
||||
|
||||
when (key) {
|
||||
when (label) {
|
||||
MemoryProtectionConfig.ProtectDefinition.TITLE_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectTitle
|
||||
MemoryProtectionConfig.ProtectDefinition.USERNAME_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectUserName
|
||||
MemoryProtectionConfig.ProtectDefinition.PASSWORD_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectPassword
|
||||
|
||||
@@ -51,6 +51,8 @@ class SearchHelper {
|
||||
override fun operate(node: Entry): Boolean {
|
||||
if (incrementEntry >= max)
|
||||
return false
|
||||
if (database.entryIsTemplate(node) && !searchParameters.searchInTemplates)
|
||||
return false
|
||||
if (entryContainsString(database, node, searchParameters)) {
|
||||
searchGroup?.addChildEntry(node)
|
||||
incrementEntry++
|
||||
@@ -70,6 +72,7 @@ class SearchHelper {
|
||||
},
|
||||
false)
|
||||
|
||||
searchGroup?.refreshNumberOfChildEntries()
|
||||
return searchGroup
|
||||
}
|
||||
|
||||
@@ -92,15 +95,18 @@ class SearchHelper {
|
||||
* Utility method to perform actions if item is found or not after an auto search in [database]
|
||||
*/
|
||||
fun checkAutoSearchInfo(context: Context,
|
||||
database: Database,
|
||||
database: Database?,
|
||||
searchInfo: SearchInfo?,
|
||||
onItemsFound: (items: List<EntryInfo>) -> Unit,
|
||||
onItemNotFound: () -> Unit,
|
||||
onItemsFound: (openedDatabase: Database,
|
||||
items: List<EntryInfo>) -> Unit,
|
||||
onItemNotFound: (openedDatabase: Database) -> Unit,
|
||||
onDatabaseClosed: () -> Unit) {
|
||||
if (database.loaded && TimeoutHelper.checkTime(context)) {
|
||||
if (database == null || !database.loaded) {
|
||||
onDatabaseClosed.invoke()
|
||||
} else if (TimeoutHelper.checkTime(context)) {
|
||||
var searchWithoutUI = false
|
||||
if (PreferencesUtil.isAutofillAutoSearchEnable(context)
|
||||
&& searchInfo != null
|
||||
&& searchInfo != null && !searchInfo.manualSelection
|
||||
&& !searchInfo.containsOnlyNullValues()) {
|
||||
// If search provide results
|
||||
database.createVirtualGroupFromSearchInfo(
|
||||
@@ -108,18 +114,16 @@ class SearchHelper {
|
||||
PreferencesUtil.omitBackup(context),
|
||||
MAX_SEARCH_ENTRY
|
||||
)?.let { searchGroup ->
|
||||
if (searchGroup.getNumberOfChildEntries() > 0) {
|
||||
if (searchGroup.numberOfChildEntries > 0) {
|
||||
searchWithoutUI = true
|
||||
onItemsFound.invoke(
|
||||
onItemsFound.invoke(database,
|
||||
searchGroup.getChildEntriesInfo(database))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!searchWithoutUI) {
|
||||
onItemNotFound.invoke()
|
||||
onItemNotFound.invoke(database)
|
||||
}
|
||||
} else {
|
||||
onDatabaseClosed.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,4 +34,6 @@ class SearchParameters {
|
||||
var searchInOther = true
|
||||
var searchInUUIDs = false
|
||||
var searchInTags = true
|
||||
|
||||
var searchInTemplates = false
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
@@ -63,10 +64,18 @@ class IconDrawableFactory(private val retrieveBinaryCache : () -> BinaryCache?,
|
||||
*/
|
||||
private val standardIconMap = HashMap<CacheKey, WeakReference<Drawable>>()
|
||||
|
||||
/**
|
||||
* To load an icon pack only if current one is different
|
||||
*/
|
||||
private var mCurrentIconPack: IconPack? = null
|
||||
|
||||
/**
|
||||
* Get the [SuperDrawable] [iconDraw] (from cache, or build it and add it to the cache if not exists yet), then tint it with [tintColor] if needed
|
||||
*/
|
||||
private fun getIconSuperDrawable(context: Context, iconDraw: IconImageDraw, width: Int, tintColor: Int = Color.WHITE): SuperDrawable {
|
||||
private fun getIconSuperDrawable(context: Context,
|
||||
iconDraw: IconImageDraw,
|
||||
width: Int,
|
||||
tintColor: Int = Color.WHITE): SuperDrawable {
|
||||
val icon = iconDraw.getIconImageToDraw()
|
||||
val customIconBinary = retrieveCustomIconBinary(icon.custom.uuid)
|
||||
val binaryCache = retrieveBinaryCache()
|
||||
@@ -76,6 +85,10 @@ class IconDrawableFactory(private val retrieveBinaryCache : () -> BinaryCache?,
|
||||
}
|
||||
}
|
||||
val iconPack = IconPackChooser.getSelectedIconPack(context)
|
||||
if (mCurrentIconPack != iconPack) {
|
||||
this.mCurrentIconPack = iconPack
|
||||
this.clearCache()
|
||||
}
|
||||
iconPack?.iconToResId(icon.standard.id)?.let { iconId ->
|
||||
return SuperDrawable(getIconDrawable(context.resources, iconId, width, tintColor), iconPack.tintable())
|
||||
} ?: run {
|
||||
|
||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.icons
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
|
||||
@@ -93,7 +92,6 @@ object IconPackChooser {
|
||||
fun setSelectedIconPack(iconPackIdString: String?) {
|
||||
for (iconPack in iconPackList) {
|
||||
if (iconPack.id == iconPackIdString) {
|
||||
Database.getInstance().iconDrawableFactory.clearCache()
|
||||
iconPackSelected = iconPack
|
||||
break
|
||||
}
|
||||
@@ -108,8 +106,9 @@ object IconPackChooser {
|
||||
*/
|
||||
fun getSelectedIconPack(context: Context): IconPack? {
|
||||
build(context)
|
||||
if (iconPackSelected == null)
|
||||
if (iconPackSelected == null) {
|
||||
setSelectedIconPack(PreferencesUtil.getIconPackSelectedId(context))
|
||||
}
|
||||
return iconPackSelected
|
||||
}
|
||||
|
||||
|
||||
@@ -38,15 +38,19 @@ import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity
|
||||
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
|
||||
class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
|
||||
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
private var keyboardView: KeyboardView? = null
|
||||
private var entryText: TextView? = null
|
||||
@@ -61,6 +65,11 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||
mDatabaseTaskProvider?.registerProgressTask()
|
||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||
this.mDatabase = database
|
||||
}
|
||||
// Remove the entry and lock the keyboard when the lock signal is receive
|
||||
lockReceiver = LockReceiver {
|
||||
removeEntryInfo()
|
||||
@@ -111,13 +120,13 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
|
||||
// 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 databaseTime = mDatabase?.loadTimestamp
|
||||
val entryTime = entryInfoTimestamp
|
||||
if (!database.loaded
|
||||
|| databaseTime == null
|
||||
|| entryTime == null
|
||||
|| entryTime < databaseTime) {
|
||||
if (mDatabase == null
|
||||
|| mDatabase?.loaded != true
|
||||
|| databaseTime == null
|
||||
|| entryTime == null
|
||||
|| entryTime < databaseTime) {
|
||||
removeEntryInfo()
|
||||
}
|
||||
assignKeyboardView()
|
||||
@@ -169,6 +178,11 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
assignKeyboardView()
|
||||
}
|
||||
|
||||
override fun onEvaluateFullscreenMode(): Boolean {
|
||||
return resources.getBoolean(R.bool.magikeyboard_allow_fullscreen_mode)
|
||||
&& super.onEvaluateFullscreenMode()
|
||||
}
|
||||
|
||||
private fun playVibration(keyCode: Int) {
|
||||
when (keyCode) {
|
||||
Keyboard.KEYCODE_DELETE -> {}
|
||||
@@ -323,11 +337,12 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
override fun onDestroy() {
|
||||
dismissCustomKeys()
|
||||
unregisterLockReceiver(lockReceiver)
|
||||
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = MagikIME::class.java.name
|
||||
private val TAG = MagikeyboardService::class.java.name
|
||||
|
||||
const val KEY_BACK_KEYBOARD = 600
|
||||
const val KEY_CHANGE_KEYBOARD = 601
|
||||
39
app/src/main/java/com/kunzisoft/keepass/model/CreditCard.kt
Normal file
39
app/src/main/java/com/kunzisoft/keepass/model/CreditCard.kt
Normal file
@@ -0,0 +1,39 @@
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import org.joda.time.DateTime
|
||||
|
||||
data class CreditCard(val cardholder: String?,
|
||||
val number: String?,
|
||||
val expiration: DateTime?,
|
||||
val cvv: String?) : Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString(),
|
||||
parcel.readString(),
|
||||
parcel.readSerializable() as DateTime?,
|
||||
parcel.readString()) {
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(cardholder)
|
||||
parcel.writeString(number)
|
||||
parcel.writeSerializable(expiration)
|
||||
parcel.writeString(cvv)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<CreditCard> {
|
||||
override fun createFromParcel(parcel: Parcel): CreditCard {
|
||||
return CreditCard(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<CreditCard?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,29 +20,35 @@
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.ParcelUuid
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||
import java.util.*
|
||||
|
||||
class EntryInfo : NodeInfo {
|
||||
|
||||
var id: String = ""
|
||||
var id: UUID = UUID.randomUUID()
|
||||
var username: String = ""
|
||||
var password: String = ""
|
||||
var url: String = ""
|
||||
var notes: String = ""
|
||||
var customFields: List<Field> = ArrayList()
|
||||
var attachments: List<Attachment> = ArrayList()
|
||||
var customFields: MutableList<Field> = mutableListOf()
|
||||
var attachments: MutableList<Attachment> = mutableListOf()
|
||||
var otpModel: OtpModel? = null
|
||||
var isTemplate: Boolean = false
|
||||
|
||||
constructor(): super()
|
||||
constructor() : super()
|
||||
|
||||
constructor(parcel: Parcel): super(parcel) {
|
||||
id = parcel.readString() ?: id
|
||||
constructor(parcel: Parcel) : super(parcel) {
|
||||
id = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: id
|
||||
username = parcel.readString() ?: username
|
||||
password = parcel.readString() ?: password
|
||||
url = parcel.readString() ?: url
|
||||
@@ -50,6 +56,7 @@ class EntryInfo : NodeInfo {
|
||||
parcel.readList(customFields, Field::class.java.classLoader)
|
||||
parcel.readList(attachments, Attachment::class.java.classLoader)
|
||||
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
|
||||
isTemplate = parcel.readByte().toInt() != 0
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
@@ -58,14 +65,15 @@ class EntryInfo : NodeInfo {
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
super.writeToParcel(parcel, flags)
|
||||
parcel.writeString(id)
|
||||
parcel.writeParcelable(ParcelUuid(id), flags)
|
||||
parcel.writeString(username)
|
||||
parcel.writeString(password)
|
||||
parcel.writeString(url)
|
||||
parcel.writeString(notes)
|
||||
parcel.writeArray(customFields.toTypedArray())
|
||||
parcel.writeArray(attachments.toTypedArray())
|
||||
parcel.writeList(customFields)
|
||||
parcel.writeList(attachments)
|
||||
parcel.writeParcelable(otpModel, flags)
|
||||
parcel.writeByte((if (isTemplate) 1 else 0).toByte())
|
||||
}
|
||||
|
||||
fun containsCustomFieldsProtected(): Boolean {
|
||||
@@ -133,8 +141,7 @@ class EntryInfo : NodeInfo {
|
||||
val webDomainToStore = "$webScheme://$webDomain"
|
||||
if (database?.allowEntryCustomFields() != true || url.isEmpty()) {
|
||||
url = webDomainToStore
|
||||
}
|
||||
else if (url != webDomainToStore){
|
||||
} else if (url != webDomainToStore) {
|
||||
// Save web domain in custom field
|
||||
addUniqueField(Field(WEB_DOMAIN_FIELD_NAME,
|
||||
ProtectedString(false, webDomainToStore)),
|
||||
@@ -153,6 +160,65 @@ class EntryInfo : NodeInfo {
|
||||
}
|
||||
}
|
||||
|
||||
fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo) {
|
||||
registerInfo.username?.let {
|
||||
username = it
|
||||
}
|
||||
registerInfo.password?.let {
|
||||
password = it
|
||||
}
|
||||
|
||||
if (database?.allowEntryCustomFields() == true) {
|
||||
val creditCard: CreditCard? = registerInfo.creditCard
|
||||
creditCard?.cardholder?.let {
|
||||
addUniqueField(Field(TemplateField.LABEL_HOLDER, ProtectedString(false, it)))
|
||||
}
|
||||
creditCard?.expiration?.let {
|
||||
expires = true
|
||||
expiryTime = DateInstant(creditCard.expiration.millis)
|
||||
}
|
||||
creditCard?.number?.let {
|
||||
addUniqueField(Field(TemplateField.LABEL_NUMBER, ProtectedString(false, it)))
|
||||
}
|
||||
creditCard?.cvv?.let {
|
||||
addUniqueField(Field(TemplateField.LABEL_CVV, ProtectedString(true, it)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is EntryInfo) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (username != other.username) return false
|
||||
if (password != other.password) return false
|
||||
if (url != other.url) return false
|
||||
if (notes != other.notes) return false
|
||||
if (customFields != other.customFields) return false
|
||||
if (attachments != other.attachments) return false
|
||||
if (otpModel != other.otpModel) return false
|
||||
if (isTemplate != other.isTemplate) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + id.hashCode()
|
||||
result = 31 * result + username.hashCode()
|
||||
result = 31 * result + password.hashCode()
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + notes.hashCode()
|
||||
result = 31 * result + customFields.hashCode()
|
||||
result = 31 * result + attachments.hashCode()
|
||||
result = 31 * result + (otpModel?.hashCode() ?: 0)
|
||||
result = 31 * result + isTemplate.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
const val WEB_DOMAIN_FIELD_NAME = "URL"
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
|
||||
class FocusedEditField : Parcelable {
|
||||
|
||||
|
||||
@@ -24,6 +24,22 @@ class GroupInfo : NodeInfo {
|
||||
parcel.writeString(notes)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is GroupInfo) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
if (notes != other.notes) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + (notes?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<GroupInfo> {
|
||||
override fun createFromParcel(parcel: Parcel): GroupInfo {
|
||||
return GroupInfo(parcel)
|
||||
|
||||
@@ -12,7 +12,7 @@ open class NodeInfo() : Parcelable {
|
||||
var creationTime: DateInstant = DateInstant()
|
||||
var lastModificationTime: DateInstant = DateInstant()
|
||||
var expires: Boolean = false
|
||||
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
|
||||
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH_DATE_TIME
|
||||
|
||||
constructor(parcel: Parcel) : this() {
|
||||
title = parcel.readString() ?: title
|
||||
@@ -36,6 +36,30 @@ open class NodeInfo() : Parcelable {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is NodeInfo) return false
|
||||
|
||||
if (title != other.title) return false
|
||||
if (icon != other.icon) return false
|
||||
if (creationTime != other.creationTime) return false
|
||||
if (lastModificationTime != other.lastModificationTime) return false
|
||||
if (expires != other.expires) return false
|
||||
if (expiryTime != other.expiryTime) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = title.hashCode()
|
||||
result = 31 * result + icon.hashCode()
|
||||
result = 31 * result + creationTime.hashCode()
|
||||
result = 31 * result + lastModificationTime.hashCode()
|
||||
result = 31 * result + expires.hashCode()
|
||||
result = 31 * result + expiryTime.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<NodeInfo> {
|
||||
override fun createFromParcel(parcel: Parcel): NodeInfo {
|
||||
return NodeInfo(parcel)
|
||||
|
||||
@@ -56,7 +56,7 @@ class OtpModel() : Parcelable {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as OtpElement
|
||||
other as OtpModel
|
||||
|
||||
if (type != other.type) return false
|
||||
// Token type is important only if it's a TOTP
|
||||
|
||||
@@ -5,18 +5,21 @@ import android.os.Parcelable
|
||||
|
||||
data class RegisterInfo(val searchInfo: SearchInfo,
|
||||
val username: String?,
|
||||
val password: String?): Parcelable {
|
||||
val password: String?,
|
||||
val creditCard: CreditCard?): Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readParcelable(SearchInfo::class.java.classLoader) ?: SearchInfo(),
|
||||
parcel.readString() ?: "",
|
||||
parcel.readString() ?: "") {
|
||||
parcel.readString() ?: "",
|
||||
parcel.readParcelable(CreditCard::class.java.classLoader)) {
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeParcelable(searchInfo, flags)
|
||||
parcel.writeString(username)
|
||||
parcel.writeString(password)
|
||||
parcel.writeParcelable(creditCard, flags)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user