mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
228 Commits
2.5.0.0bet
...
2.5.0.0bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb5ca575d5 | ||
|
|
f4caaad9ee | ||
|
|
b9cfb32a20 | ||
|
|
095e5e5dd6 | ||
|
|
ffc58688d8 | ||
|
|
b4188b4712 | ||
|
|
4a22c28df4 | ||
|
|
76e9a25b1a | ||
|
|
1928d0823e | ||
|
|
c183d22412 | ||
|
|
b684353721 | ||
|
|
72f0e871c7 | ||
|
|
9a63962903 | ||
|
|
938de28b49 | ||
|
|
20fc094d71 | ||
|
|
40180d5883 | ||
|
|
59e5865318 | ||
|
|
f63d6bdc1d | ||
|
|
fe33c0ae7d | ||
|
|
ca4ad1c1fd | ||
|
|
adf5382804 | ||
|
|
7f5406ac98 | ||
|
|
23b21ea154 | ||
|
|
49d4d0421a | ||
|
|
23859a61bb | ||
|
|
221f81f51e | ||
|
|
6e7c0d5073 | ||
|
|
e8e3d53685 | ||
|
|
e6d9df2b98 | ||
|
|
477a8f2e38 | ||
|
|
5e66697b8b | ||
|
|
16320abb7d | ||
|
|
f122c2832c | ||
|
|
d84c561f44 | ||
|
|
da49c9c045 | ||
|
|
553920e37c | ||
|
|
450d2d113b | ||
|
|
744c80e34d | ||
|
|
c0f8cca7c6 | ||
|
|
b129f220f7 | ||
|
|
7a3df02e38 | ||
|
|
befd29c396 | ||
|
|
b8245621ea | ||
|
|
ecda25a743 | ||
|
|
d97a85b997 | ||
|
|
8c0d7ab9ed | ||
|
|
f3fa73ea34 | ||
|
|
788734ccad | ||
|
|
e088f4a4ad | ||
|
|
86bd018e4e | ||
|
|
283145034d | ||
|
|
163162497e | ||
|
|
56911fb58f | ||
|
|
dae6481aff | ||
|
|
6b2eb5e4f6 | ||
|
|
c563787f73 | ||
|
|
2737755b85 | ||
|
|
fd9486ca77 | ||
|
|
14020ec0b5 | ||
|
|
5a6c466ebd | ||
|
|
76fcd5fe19 | ||
|
|
3732ff1ebc | ||
|
|
22dd09954b | ||
|
|
ef7387f2f3 | ||
|
|
f774298587 | ||
|
|
f8134307f6 | ||
|
|
fe461f2e7c | ||
|
|
023c841747 | ||
|
|
af95c0903a | ||
|
|
0d756db8aa | ||
|
|
2c5dcc9b11 | ||
|
|
21c6ea73b2 | ||
|
|
51dc302bb0 | ||
|
|
87760ab4f6 | ||
|
|
88ebe58a88 | ||
|
|
fb023b81b5 | ||
|
|
2de6bbc6c0 | ||
|
|
4ef436629d | ||
|
|
9fd342f1e7 | ||
|
|
8988f17765 | ||
|
|
9d160db281 | ||
|
|
2e58c2f1b3 | ||
|
|
d1d2b99e09 | ||
|
|
def9744f75 | ||
|
|
214e2cf109 | ||
|
|
b25180c617 | ||
|
|
6a5263df77 | ||
|
|
2982f67717 | ||
|
|
b559670dff | ||
|
|
891d3142d2 | ||
|
|
2637788429 | ||
|
|
a21de3b892 | ||
|
|
e087e19120 | ||
|
|
721d61dda7 | ||
|
|
e0e7e431cf | ||
|
|
93948e7c61 | ||
|
|
b150c718a0 | ||
|
|
a71e4c3902 | ||
|
|
b7dc13d863 | ||
|
|
1e3c58e359 | ||
|
|
5f75599e9f | ||
|
|
b602f9b77d | ||
|
|
aa948c1ece | ||
|
|
e599a51152 | ||
|
|
ee6052f4d1 | ||
|
|
eba527f477 | ||
|
|
09e0d6d3cc | ||
|
|
9aefc984be | ||
|
|
2ce3b21f1b | ||
|
|
4d2f3cb4b1 | ||
|
|
e62b46c4c0 | ||
|
|
6472601170 | ||
|
|
89dd7bfefb | ||
|
|
fb2ea4c0ed | ||
|
|
8d84358d48 | ||
|
|
1d8661c633 | ||
|
|
48130eee45 | ||
|
|
2cf83962fe | ||
|
|
aecf7c0c39 | ||
|
|
39606e2676 | ||
|
|
6e00fa2d01 | ||
|
|
f79aa339e9 | ||
|
|
f412fce912 | ||
|
|
cc20b7503c | ||
|
|
2573434763 | ||
|
|
f153c26fef | ||
|
|
125f461cbe | ||
|
|
b705b4b712 | ||
|
|
c67b0bb858 | ||
|
|
ab1fc8c5d5 | ||
|
|
8477f4ba08 | ||
|
|
e6518ffdc8 | ||
|
|
99917c7f28 | ||
|
|
fcc29f67a3 | ||
|
|
7dd49f050c | ||
|
|
5f96de84b0 | ||
|
|
54c2f5a61f | ||
|
|
921c6f88aa | ||
|
|
a0cb579df4 | ||
|
|
d6a7c34ff3 | ||
|
|
bf2e61f149 | ||
|
|
eaf5dc5988 | ||
|
|
879ee013db | ||
|
|
e13d53eae4 | ||
|
|
d72c8184c9 | ||
|
|
c4d3c8cbfb | ||
|
|
02a3d85f80 | ||
|
|
19b0722f1f | ||
|
|
f14222b192 | ||
|
|
4f4f6d30d9 | ||
|
|
fdd329e982 | ||
|
|
55a4d388b3 | ||
|
|
5c6be448ec | ||
|
|
3e79ddcc21 | ||
|
|
5362758424 | ||
|
|
c10e3df2a7 | ||
|
|
166784021a | ||
|
|
5615c31e08 | ||
|
|
fb60dd5921 | ||
|
|
ff4c1b779b | ||
|
|
53a7b99567 | ||
|
|
a57103bafb | ||
|
|
2540f32dbf | ||
|
|
499ccd6b7c | ||
|
|
a4359560b9 | ||
|
|
149483cc2d | ||
|
|
a1d2022492 | ||
|
|
891036c35c | ||
|
|
4e16ba5f56 | ||
|
|
7137a2fadb | ||
|
|
9d90d0eaba | ||
|
|
94a9942db5 | ||
|
|
5f347fe106 | ||
|
|
a34a84ae16 | ||
|
|
40b0982298 | ||
|
|
4100258476 | ||
|
|
5f3f6661b7 | ||
|
|
75af97e0ae | ||
|
|
58f158c457 | ||
|
|
ce27eae1f0 | ||
|
|
0aa0b3e993 | ||
|
|
1cc5a08236 | ||
|
|
4c587eeb03 | ||
|
|
ab70c2d014 | ||
|
|
8413160ac5 | ||
|
|
5abc403171 | ||
|
|
9b891013b8 | ||
|
|
9413987355 | ||
|
|
f95b514b41 | ||
|
|
f6985c8944 | ||
|
|
4388d56c52 | ||
|
|
a70fe24c97 | ||
|
|
8e0392753c | ||
|
|
9c9980bba6 | ||
|
|
2226c15d29 | ||
|
|
82a859bd9c | ||
|
|
83873fab81 | ||
|
|
31f2be7b91 | ||
|
|
16458e6646 | ||
|
|
2b9678707d | ||
|
|
cdbb23d7f1 | ||
|
|
23fd1b83f4 | ||
|
|
40b0ebe49b | ||
|
|
7cd8682544 | ||
|
|
d0dd478ac8 | ||
|
|
ffb547c452 | ||
|
|
bd829f129f | ||
|
|
5ad3f62de5 | ||
|
|
b0ec4942bc | ||
|
|
2cbc9675f6 | ||
|
|
116643a45a | ||
|
|
2f0eb283ed | ||
|
|
6d46fccdcd | ||
|
|
f5dc94bfec | ||
|
|
94bdb0e3da | ||
|
|
65360c2a1e | ||
|
|
70d30bdbe6 | ||
|
|
66f7e6d1b1 | ||
|
|
a8ccb67a87 | ||
|
|
66051382f1 | ||
|
|
0fb3028c91 | ||
|
|
5e5baa4892 | ||
|
|
9d1257ed9d | ||
|
|
9d7546053d | ||
|
|
a1b692abe5 | ||
|
|
4e06842d0f | ||
|
|
f04c2ee1da | ||
|
|
9558fcaf21 |
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
** Keepass Database **
|
||||
- Created with: [e.g Windows KeePass 2.42]
|
||||
- Version: [e.g. 2]
|
||||
- Location: [e.g. Remote file retrieved with GDrive app]
|
||||
- Size: [e.g. 150Mo]
|
||||
- Contains attachment: [e.g. Yes]
|
||||
|
||||
**KeePass DX (please complete the following information):**
|
||||
- Version: [e.g. 2.5.0.0beta23]
|
||||
- Build: [e.g. Free]
|
||||
- Language: [e.g. French]
|
||||
|
||||
**Android (please complete the following information):**
|
||||
- Device: [e.g. GalaxyS8]
|
||||
- Version: [e.g. 8.1]
|
||||
- Browser: [e.g. Chrome]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -38,6 +38,13 @@ proguard/
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# Eclipse/VS Code
|
||||
.project
|
||||
.settings/*
|
||||
*/.project
|
||||
*/.classpath
|
||||
*/.settings/*
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea/workspace.xml
|
||||
|
||||
10
CHANGELOG
10
CHANGELOG
@@ -1,3 +1,13 @@
|
||||
KeepassDX (2.5.0.0beta24)
|
||||
* Add OTP (HOTP / TOTP)
|
||||
* Add settings (Color, Security, Master Key)
|
||||
* Show history of each entry
|
||||
* Auto repair database for nodes with same UUID
|
||||
* Management of expired nodes
|
||||
* Multi-selection for actions (Cut - Copy - Delete)
|
||||
* Open/Save database as service / Add persistent notification
|
||||
* Fix settings / edit group / small bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta23)
|
||||
* New, more secure database creation workflow
|
||||
* Recognize more database files
|
||||
|
||||
45
CONTRIBUTORS
45
CONTRIBUTORS
@@ -1,45 +0,0 @@
|
||||
Original author:
|
||||
Brian Pellin
|
||||
|
||||
Achim Weimert
|
||||
Johan Berts - search patches
|
||||
Mike Mohr - Better native code for aes and sha
|
||||
Tobias Selig - icon support
|
||||
Tolga Onbay, Dirk Bergstrom - password generator
|
||||
Space Cowboy - holo theme
|
||||
josefwells
|
||||
Nicholas FitzRoy-Dale - auto launch intents
|
||||
yulin2 - responsiveness improvements
|
||||
Tadashi Saito
|
||||
vhschlenker
|
||||
bumper314 - Samsung multiwindow support
|
||||
Hans Cappelle - fingerprint sensor integration
|
||||
Jeremy Jamet - Keepass DX Material Design - Patches
|
||||
|
||||
Translations:
|
||||
Diego Pierotto - Italian
|
||||
Laurent, Norman Obry, Nam, Bruno Parmentier, Credomo - French
|
||||
Maciej Bieniek, cod3r - Polish
|
||||
Максим Сёмочкин, i.nedoboy, filimonic, bboa - Russian
|
||||
MaWi, rvs2008, meviox, MaDill, EdlerProgrammierer, Jan Thomas - German
|
||||
yslandro - Norwegian Nynorsk
|
||||
王科峰 - Chinese
|
||||
Typhoon - Slovak
|
||||
Masahiro Inamura - Japanese
|
||||
Matsuu Takuto - Japanese
|
||||
Carlos Schlyter - Portugese (Brazil)
|
||||
YSmhXQDd6Z - Portugese (Portugal)
|
||||
andriykopanytsia - Ukranian
|
||||
intel, Zoltán Antal - Hungarian
|
||||
H Vanek - Czech
|
||||
jipanos - Spanish
|
||||
Erik Fdevriendt, Erik Jan Meijer - Dutch
|
||||
Frederik Svarre - Danish
|
||||
Oriol Garrote - Catalan
|
||||
Mika Takala - Finnish
|
||||
Niclas Burgren - Swedish
|
||||
Raimonds - Latvian
|
||||
dgarciabad - Basque
|
||||
Arthur Zamarin - Hebrew
|
||||
RaptorTFX - Greek
|
||||
zygimantus - Lithuanian
|
||||
@@ -10,7 +10,8 @@
|
||||
* Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm
|
||||
* **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePass XC...)
|
||||
* Allows **fast copy** of fields and opening of URI / URL
|
||||
* **Fingerprint** for fast unlocking
|
||||
* **Biometric recognition** for fast unlocking *(Fingerprint / Face unlock / ...)*
|
||||
* **One-Time Password** management *(HOTP / TOTP)*
|
||||
* Material design with **themes**
|
||||
* **AutoFill** and Integration
|
||||
* Field filling **keyboard**
|
||||
@@ -54,7 +55,7 @@ You can contribute in different ways to help us on our work.
|
||||
|
||||
## F.A.Q.
|
||||
|
||||
Other questions? You can read the [F.A.Q.](https://www.keepassdx.com/FAQ)
|
||||
Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassDX/wiki/F.A.Q.)
|
||||
|
||||
## Other devices
|
||||
|
||||
|
||||
1
_config.yml
Normal file
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-cayman
|
||||
@@ -6,13 +6,14 @@ apply plugin: 'kotlin-kapt'
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion '28.0.3'
|
||||
ndkVersion "20.1.5948944"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 28
|
||||
versionCode = 23
|
||||
versionName = "2.5.0.0beta23"
|
||||
versionCode = 24
|
||||
versionName = "2.5.0.0beta24"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -80,7 +81,7 @@ android {
|
||||
}
|
||||
|
||||
def spongycastleVersion = "1.58.0.0"
|
||||
def room_version = "2.1.0"
|
||||
def room_version = "2.2.1"
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
@@ -89,7 +90,7 @@ dependencies {
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.biometric:biometric:1.0.0-beta01'
|
||||
implementation 'androidx.biometric:biometric:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
@@ -97,15 +98,17 @@ dependencies {
|
||||
|
||||
implementation "com.madgag.spongycastle:core:$spongycastleVersion"
|
||||
implementation "com.madgag.spongycastle:prov:$spongycastleVersion"
|
||||
// Expandable view
|
||||
implementation 'net.cachapa.expandablelayout:expandablelayout:2.9.2'
|
||||
// Time
|
||||
implementation 'joda-time:joda-time:2.9.9'
|
||||
// Color
|
||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
|
||||
// Education
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0'
|
||||
// Apache Commons Collections
|
||||
implementation 'commons-collections:commons-collections:3.2.1'
|
||||
implementation 'org.apache.commons:commons-io:1.3.2'
|
||||
// Apache Commons Codec
|
||||
implementation 'commons-codec:commons-codec:1.11'
|
||||
// Base64
|
||||
implementation 'biz.source_code:base64coder:2010-12-19'
|
||||
// Icon pack
|
||||
|
||||
@@ -131,7 +131,8 @@
|
||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
|
||||
android:label="@string/keyboard_name">
|
||||
android:label="@string/keyboard_name"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
<activity android:name="com.kunzisoft.keepass.settings.MagikIMESettings"
|
||||
android:label="@string/keyboard_setting_label">
|
||||
@@ -140,7 +141,10 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
||||
android:enabled="true"
|
||||
|
||||
@@ -21,17 +21,19 @@ package com.kunzisoft.keepass.activities
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
||||
@@ -49,15 +51,22 @@ import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.EntryContentsView
|
||||
import java.util.*
|
||||
|
||||
class EntryActivity : LockingHideActivity() {
|
||||
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var titleIconView: ImageView? = null
|
||||
private var historyView: View? = null
|
||||
private var entryContentsView: EntryContentsView? = null
|
||||
private var entryProgress: ProgressBar? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
private var mEntry: EntryVersioned? = null
|
||||
private var mIsHistory: Boolean = false
|
||||
|
||||
private var mShowPassword: Boolean = false
|
||||
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
@@ -75,28 +84,11 @@ class EntryActivity : LockingHideActivity() {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
val currentDatabase = Database.getInstance()
|
||||
mReadOnly = currentDatabase.isReadOnly || mReadOnly
|
||||
mDatabase = Database.getInstance()
|
||||
mReadOnly = mDatabase!!.isReadOnly || mReadOnly
|
||||
|
||||
mShowPassword = !PreferencesUtil.isPasswordMask(this)
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
val keyEntry: PwNodeId<*> = intent.getParcelableExtra(KEY_ENTRY)
|
||||
mEntry = currentDatabase.getEntryById(keyEntry)
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
}
|
||||
|
||||
if (mEntry == null) {
|
||||
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Update last access time.
|
||||
mEntry?.touch(modified = false, touchParents = false)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
iconColor = taIconColor.getColor(0, Color.BLACK)
|
||||
@@ -108,8 +100,10 @@ class EntryActivity : LockingHideActivity() {
|
||||
// Get views
|
||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||
titleIconView = findViewById(R.id.entry_icon)
|
||||
historyView = findViewById(R.id.history_container)
|
||||
entryContentsView = findViewById(R.id.entry_contents)
|
||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||
entryProgress = findViewById(R.id.entry_progress)
|
||||
|
||||
// Init the clipboard helper
|
||||
clipboardHelper = ClipboardHelper(this)
|
||||
@@ -119,6 +113,29 @@ class EntryActivity : LockingHideActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
val keyEntry: PwNodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
|
||||
mEntry = mDatabase?.getEntryById(keyEntry)
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
}
|
||||
|
||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||
if (historyPosition >= 0) {
|
||||
mIsHistory = true
|
||||
mEntry = mEntry?.getHistory()?.get(historyPosition)
|
||||
}
|
||||
|
||||
if (mEntry == null) {
|
||||
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Update last access time.
|
||||
mEntry?.touch(modified = false, touchParents = false)
|
||||
|
||||
mEntry?.let { entry ->
|
||||
// Fill data in resume to update from EntryEditActivity
|
||||
fillEntryDataInContentsView(entry)
|
||||
@@ -206,6 +223,17 @@ class EntryActivity : LockingHideActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
//Assign OTP field
|
||||
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
||||
View.OnClickListener {
|
||||
entry.getOtpElement()?.let { otpElement ->
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
otpElement.token,
|
||||
getString(R.string.copy_field, getString(R.string.entry_otp))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
entryContentsView?.assignURL(entry.url)
|
||||
entryContentsView?.assignComment(entry.notes)
|
||||
|
||||
@@ -238,18 +266,12 @@ class EntryActivity : LockingHideActivity() {
|
||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
||||
|
||||
// Assign dates
|
||||
entry.creationTime.date?.let {
|
||||
entryContentsView?.assignCreationDate(it)
|
||||
}
|
||||
entry.lastModificationTime.date?.let {
|
||||
entryContentsView?.assignModificationDate(it)
|
||||
}
|
||||
entry.lastAccessTime.date?.let {
|
||||
entryContentsView?.assignLastAccessDate(it)
|
||||
}
|
||||
val expires = entry.expiryTime.date
|
||||
if (entry.isExpires && expires != null) {
|
||||
entryContentsView?.assignExpiresDate(expires)
|
||||
entryContentsView?.assignCreationDate(entry.creationTime)
|
||||
entryContentsView?.assignModificationDate(entry.lastModificationTime)
|
||||
entryContentsView?.assignLastAccessDate(entry.lastAccessTime)
|
||||
entryContentsView?.setExpires(entry.isCurrentlyExpires)
|
||||
if (entry.expires) {
|
||||
entryContentsView?.assignExpiresDate(entry.expiryTime)
|
||||
} else {
|
||||
entryContentsView?.assignExpiresDate(getString(R.string.never))
|
||||
}
|
||||
@@ -257,6 +279,24 @@ class EntryActivity : LockingHideActivity() {
|
||||
// Assign special data
|
||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||
|
||||
// Manage history
|
||||
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
||||
if (mIsHistory) {
|
||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||
taColorAccent.recycle()
|
||||
}
|
||||
val entryHistory = entry.getHistory()
|
||||
// isMainEntry = not an history
|
||||
val showHistoryView = entryHistory.isNotEmpty()
|
||||
entryContentsView?.showHistory(showHistoryView)
|
||||
if (showHistoryView) {
|
||||
entryContentsView?.assignHistory(entryHistory)
|
||||
entryContentsView?.onHistoryClick { historyItem, position ->
|
||||
launch(this, historyItem, true, position)
|
||||
}
|
||||
}
|
||||
|
||||
database.stopManageEntry(entry)
|
||||
}
|
||||
|
||||
@@ -404,7 +444,7 @@ class EntryActivity : LockingHideActivity() {
|
||||
TODO Slowdown when add entry as result
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
|
||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
|
||||
onFinish(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
|
||||
*/
|
||||
super.finish()
|
||||
}
|
||||
@@ -412,13 +452,16 @@ class EntryActivity : LockingHideActivity() {
|
||||
companion object {
|
||||
private val TAG = EntryActivity::class.java.name
|
||||
|
||||
const val KEY_ENTRY = "entry"
|
||||
const val KEY_ENTRY = "KEY_ENTRY"
|
||||
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
||||
|
||||
fun launch(activity: Activity, pw: EntryVersioned, readOnly: Boolean) {
|
||||
fun launch(activity: Activity, entry: EntryVersioned, readOnly: Boolean, historyPosition: Int? = null) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, pw.nodeId)
|
||||
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||
if (historyPosition != null)
|
||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,32 +22,36 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
|
||||
import com.kunzisoft.keepass.database.action.node.ActionNodeValues
|
||||
import com.kunzisoft.keepass.database.action.node.AddEntryRunnable
|
||||
import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable
|
||||
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.view.EntryEditContentsView
|
||||
import java.util.*
|
||||
|
||||
class EntryEditActivity : LockingHideActivity(),
|
||||
IconPickerDialogFragment.IconPickerListener,
|
||||
GeneratePasswordDialogFragment.GeneratePasswordListener {
|
||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||
SetOTPDialogFragment.CreateOtpListener {
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
@@ -60,11 +64,12 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
|
||||
// Views
|
||||
private var scrollView: ScrollView? = null
|
||||
|
||||
private var entryEditContentsView: EntryEditContentsView? = null
|
||||
|
||||
private var saveView: View? = null
|
||||
|
||||
// Dialog thread
|
||||
private var progressDialogThread: ProgressDialogThread? = null
|
||||
|
||||
// Education
|
||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||
|
||||
@@ -86,11 +91,14 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
// Focus view to reinitialize timeout
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
|
||||
|
||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||
|
||||
// Likely the app has been killed exit the activity
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
// Entry is retrieve, it's an entry to update
|
||||
intent.getParcelableExtra<PwNodeId<*>>(KEY_ENTRY)?.let {
|
||||
intent.getParcelableExtra<PwNodeId<UUID>>(KEY_ENTRY)?.let {
|
||||
mIsNew = false
|
||||
// Create an Entry copy to modify from the database entry
|
||||
mEntry = mDatabase?.getEntryById(it)
|
||||
@@ -105,16 +113,14 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the icon after an orientation change
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY) as EntryVersioned
|
||||
} else {
|
||||
// Create the new entry from the current one
|
||||
if (savedInstanceState == null
|
||||
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||
mEntry?.let { entry ->
|
||||
// Create a copy to modify
|
||||
mNewEntry = EntryVersioned(entry).also { newEntry ->
|
||||
|
||||
// WARNING Remove the parent to keep memory with parcelable
|
||||
newEntry.parent = null
|
||||
newEntry.removeParent()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,7 +129,11 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
// Parent is retrieve, it's a new entry to create
|
||||
intent.getParcelableExtra<PwNodeId<*>>(KEY_PARENT)?.let {
|
||||
mIsNew = true
|
||||
mNewEntry = mDatabase?.createEntry()
|
||||
// Create an empty new entry
|
||||
if (savedInstanceState == null
|
||||
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||
mNewEntry = mDatabase?.createEntry()
|
||||
}
|
||||
mParent = mDatabase?.getGroupById(it)
|
||||
// Add the default icon
|
||||
mDatabase?.drawFactory?.let { iconFactory ->
|
||||
@@ -131,6 +141,12 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the new entry after an orientation change
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
|
||||
}
|
||||
|
||||
// Close the activity if entry or parent can't be retrieve
|
||||
if (mNewEntry == null || mParent == null) {
|
||||
finish()
|
||||
@@ -152,10 +168,23 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
saveView = findViewById(R.id.entry_edit_save)
|
||||
saveView?.setOnClickListener { saveEntry() }
|
||||
|
||||
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) { addNewCustomField() }
|
||||
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) {
|
||||
addNewCustomField()
|
||||
}
|
||||
|
||||
// Verify the education views
|
||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||
|
||||
// Create progress dialog
|
||||
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||
if (result.isSuccess)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateViewsWithEntry(newEntry: EntryVersioned) {
|
||||
@@ -168,12 +197,14 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
// Set info in view
|
||||
entryEditContentsView?.apply {
|
||||
title = newEntry.title
|
||||
username = newEntry.username
|
||||
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
|
||||
url = newEntry.url
|
||||
password = newEntry.password
|
||||
notes = newEntry.notes
|
||||
for (entry in newEntry.customFields.entries) {
|
||||
addNewCustomField(entry.key, entry.value)
|
||||
post {
|
||||
putCustomField(entry.key, entry.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,13 +216,14 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
newEntry.apply {
|
||||
// Build info from view
|
||||
entryEditContentsView?.let { entryView ->
|
||||
removeAllFields()
|
||||
title = entryView.title
|
||||
username = entryView.username
|
||||
url = entryView.url
|
||||
password = entryView.password
|
||||
notes = entryView.notes
|
||||
entryView.customFields.forEach { customField ->
|
||||
addExtraField(customField.name, customField.protectedValue)
|
||||
putExtraField(customField.name, customField.protectedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,9 +249,7 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
* Add a new customized field view and scroll to bottom
|
||||
*/
|
||||
private fun addNewCustomField() {
|
||||
entryEditContentsView?.addNewCustomField()
|
||||
// Scroll bottom
|
||||
scrollView?.post { scrollView?.fullScroll(ScrollView.FOCUS_DOWN) }
|
||||
entryEditContentsView?.addEmptyCustomField()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,59 +260,57 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
// Launch a validation and show the error if present
|
||||
if (entryEditContentsView?.isValid() == true) {
|
||||
// Clone the entry
|
||||
mDatabase?.let { database ->
|
||||
mNewEntry?.let { newEntry ->
|
||||
mNewEntry?.let { newEntry ->
|
||||
|
||||
// WARNING Add the parent previously deleted
|
||||
newEntry.parent = mEntry?.parent
|
||||
// Build info
|
||||
newEntry.lastAccessTime = PwDate()
|
||||
newEntry.lastModificationTime = PwDate()
|
||||
// WARNING Add the parent previously deleted
|
||||
newEntry.parent = mEntry?.parent
|
||||
// Build info
|
||||
newEntry.lastAccessTime = PwDate()
|
||||
newEntry.lastModificationTime = PwDate()
|
||||
|
||||
populateEntryWithViews(newEntry)
|
||||
populateEntryWithViews(newEntry)
|
||||
|
||||
// Open a progress dialog and save entry
|
||||
var actionRunnable: ActionRunnable? = null
|
||||
val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
if (actionNodeValues.result.isSuccess)
|
||||
finish()
|
||||
}
|
||||
// Open a progress dialog and save entry
|
||||
if (mIsNew) {
|
||||
mParent?.let { parent ->
|
||||
progressDialogThread?.startDatabaseCreateEntry(
|
||||
newEntry,
|
||||
parent,
|
||||
!mReadOnly
|
||||
)
|
||||
}
|
||||
if (mIsNew) {
|
||||
mParent?.let { parent ->
|
||||
actionRunnable = AddEntryRunnable(this@EntryEditActivity,
|
||||
database,
|
||||
newEntry,
|
||||
parent,
|
||||
afterActionNodeFinishRunnable,
|
||||
!mReadOnly)
|
||||
}
|
||||
|
||||
} else {
|
||||
mEntry?.let { oldEntry ->
|
||||
actionRunnable = UpdateEntryRunnable(this@EntryEditActivity,
|
||||
database,
|
||||
oldEntry,
|
||||
newEntry,
|
||||
afterActionNodeFinishRunnable,
|
||||
!mReadOnly)
|
||||
}
|
||||
}
|
||||
actionRunnable?.let { runnable ->
|
||||
ProgressDialogSaveDatabaseThread(this@EntryEditActivity) { runnable }.start()
|
||||
} else {
|
||||
mEntry?.let { oldEntry ->
|
||||
progressDialogThread?.startDatabaseUpdateEntry(
|
||||
oldEntry,
|
||||
newEntry,
|
||||
!mReadOnly
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
progressDialogThread?.registerProgressTask()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
progressDialogThread?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
val inflater = menuInflater
|
||||
inflater.inflate(R.menu.database_lock, menu)
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
inflater.inflate(R.menu.edit_entry, menu)
|
||||
|
||||
entryEditActivityEducation?.let {
|
||||
Handler().post { performedNextEducation(it) }
|
||||
@@ -293,7 +321,7 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
|
||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
val passwordView = entryEditContentsView?.generatePasswordView
|
||||
val addNewFieldView = entryEditContentsView?.addNewFieldView
|
||||
val addNewFieldView = entryEditContentsView?.addNewFieldButton
|
||||
|
||||
val generatePasswordEducationPerformed = passwordView != null
|
||||
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||
@@ -329,12 +357,28 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_add_otp -> {
|
||||
// Retrieve the current otpElement if exists
|
||||
// and open the dialog to set up the OTP
|
||||
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
|
||||
.show(supportFragmentManager, "addOTPDialog")
|
||||
return true
|
||||
}
|
||||
|
||||
android.R.id.home -> finish()
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onOtpCreated(otpElement: OtpElement) {
|
||||
// Update the otp field with otpauth:// url
|
||||
val otpField = OtpEntryFields.buildOtpField(otpElement,
|
||||
mEntry?.title, mEntry?.username)
|
||||
entryEditContentsView?.putCustomField(otpField.name, otpField.protectedValue)
|
||||
mEntry?.putExtraField(otpField.name, otpField.protectedValue)
|
||||
}
|
||||
|
||||
override fun iconPicked(bundle: Bundle) {
|
||||
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
|
||||
temporarilySaveAndShowSelectedIcon(icon)
|
||||
@@ -342,7 +386,10 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putParcelable(KEY_NEW_ENTRY, mNewEntry)
|
||||
mNewEntry?.let {
|
||||
populateEntryWithViews(it)
|
||||
outState.putParcelable(KEY_NEW_ENTRY, it)
|
||||
}
|
||||
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
@@ -29,18 +29,17 @@ import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
||||
@@ -50,17 +49,14 @@ import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
||||
import net.cachapa.expandablelayout.ExpandableLayout
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class FileDatabaseSelectActivity : StylishActivity(),
|
||||
@@ -69,11 +65,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
// Views
|
||||
private var fileListContainer: View? = null
|
||||
private var createButtonView: View? = null
|
||||
private var browseButtonView: View? = null
|
||||
private var openButtonView: View? = null
|
||||
private var fileSelectExpandableButtonView: View? = null
|
||||
private var fileSelectExpandableLayout: ExpandableLayout? = null
|
||||
private var openFileNameView: EditText? = null
|
||||
private var openDatabaseButtonView: View? = null
|
||||
|
||||
// Adapter to manage database history list
|
||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||
@@ -84,7 +76,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
|
||||
private var mDefaultPath: String? = null
|
||||
private var progressDialogThread: ProgressDialogThread? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -98,44 +90,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
toolbar.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
openFileNameView = findViewById(R.id.file_filename)
|
||||
|
||||
// Set the initial value of the filename
|
||||
mDefaultPath = (Environment.getExternalStorageDirectory().absolutePath
|
||||
+ getString(R.string.database_file_path_default)
|
||||
+ getString(R.string.database_file_name_default)
|
||||
+ getString(R.string.database_file_extension_default))
|
||||
openFileNameView?.setHint(R.string.open_link_database)
|
||||
|
||||
// Button to expand file selection
|
||||
fileSelectExpandableButtonView = findViewById(R.id.file_select_expandable_button)
|
||||
fileSelectExpandableLayout = findViewById(R.id.file_select_expandable)
|
||||
fileSelectExpandableButtonView?.setOnClickListener { _ ->
|
||||
if (fileSelectExpandableLayout?.isExpanded == true)
|
||||
fileSelectExpandableLayout?.collapse()
|
||||
else
|
||||
fileSelectExpandableLayout?.expand()
|
||||
}
|
||||
|
||||
// Open button
|
||||
openButtonView = findViewById(R.id.open_database)
|
||||
openButtonView?.setOnClickListener { _ ->
|
||||
var fileName = openFileNameView?.text?.toString() ?: ""
|
||||
mDefaultPath?.let {
|
||||
if (fileName.isEmpty())
|
||||
fileName = it
|
||||
}
|
||||
UriUtil.parse(fileName)?.let { fileNameUri ->
|
||||
launchPasswordActivityWithPath(fileNameUri)
|
||||
} ?: run {
|
||||
Log.e(TAG, "Unable to open the database link")
|
||||
Snackbar.make(activity_file_selection_coordinator_layout, getString(R.string.error_can_not_handle_uri), Snackbar.LENGTH_LONG).asError().show()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Create button
|
||||
createButtonView = findViewById(R.id.create_database)
|
||||
createButtonView = findViewById(R.id.create_database_button)
|
||||
if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/x-keepass"
|
||||
@@ -151,10 +107,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
createButtonView?.setOnClickListener { createNewFile() }
|
||||
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
browseButtonView = findViewById(R.id.browse_button)
|
||||
browseButtonView?.setOnClickListener(mOpenFileHelper!!.getOpenFileOnClickViewListener {
|
||||
UriUtil.parse(openFileNameView?.text?.toString())
|
||||
})
|
||||
openDatabaseButtonView = findViewById(R.id.open_database_button)
|
||||
openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
||||
|
||||
// History list
|
||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||
@@ -207,6 +161,18 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
&& savedInstanceState.containsKey(EXTRA_DATABASE_URI)) {
|
||||
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
||||
}
|
||||
|
||||
// Attach the dialog thread to this activity
|
||||
progressDialogThread = ProgressDialogThread(this) { actionTask, _ ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK -> {
|
||||
// TODO Check
|
||||
// mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
// updateFileListVisibility()
|
||||
GroupActivity.launch(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -267,6 +233,23 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
})
|
||||
}
|
||||
|
||||
private fun launchGroupActivity(readOnly: Boolean) {
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity, readOnly)
|
||||
},
|
||||
{
|
||||
GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity, readOnly)
|
||||
// Do not keep history
|
||||
finish()
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity, assistStructure, readOnly)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||
launchPasswordActivity(databaseUri, null)
|
||||
// Delete flickering for kitkat <=
|
||||
@@ -294,6 +277,11 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val database = Database.getInstance()
|
||||
if (database.loaded) {
|
||||
launchGroupActivity(database.isReadOnly)
|
||||
}
|
||||
|
||||
super.onResume()
|
||||
|
||||
updateExternalStorageWarning()
|
||||
@@ -306,6 +294,16 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
// Register progress task
|
||||
progressDialogThread?.registerProgressTask()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
// Unregister progress task
|
||||
progressDialogThread?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -331,21 +329,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
|
||||
// Create the new database
|
||||
ProgressDialogThread(this@FileDatabaseSelectActivity,
|
||||
{
|
||||
CreateDatabaseRunnable(this@FileDatabaseSelectActivity,
|
||||
databaseUri,
|
||||
Database.getInstance(),
|
||||
masterPasswordChecked,
|
||||
masterPassword,
|
||||
keyFileChecked,
|
||||
keyFile,
|
||||
true, // TODO get readonly
|
||||
LaunchGroupActivityFinish(databaseUri, keyFile)
|
||||
)
|
||||
},
|
||||
R.string.progress_create)
|
||||
.start()
|
||||
progressDialogThread?.startDatabaseCreate(
|
||||
databaseUri,
|
||||
masterPasswordChecked,
|
||||
masterPassword,
|
||||
keyFileChecked,
|
||||
keyFile
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
val error = getString(R.string.error_create_database_file)
|
||||
@@ -354,28 +344,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
private inner class LaunchGroupActivityFinish(private val databaseFileUri: Uri,
|
||||
private val keyFileUri: Uri?) : ActionRunnable() {
|
||||
|
||||
override fun run() {
|
||||
finishRun(true, null)
|
||||
}
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
runOnUiThread {
|
||||
if (result.isSuccess) {
|
||||
// Add database to recent files
|
||||
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseFileUri, keyFileUri)
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
updateFileListVisibility()
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity)
|
||||
} else {
|
||||
Log.e(TAG, "Unable to open the database")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogNegativeClick(
|
||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
||||
@@ -392,12 +360,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) {
|
||||
launchPasswordActivityWithPath(uri)
|
||||
} else {
|
||||
fileSelectExpandableLayout?.expand(false)
|
||||
openFileNameView?.setText(uri.toString())
|
||||
}
|
||||
launchPasswordActivityWithPath(uri)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,7 +368,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
||||
mDatabaseFileUri = data?.data
|
||||
if (mDatabaseFileUri != null) {
|
||||
AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog")
|
||||
AssignMasterKeyDialogFragment.getInstance(true)
|
||||
.show(supportFragmentManager, "passwordDialog")
|
||||
}
|
||||
// else {
|
||||
// TODO Show error
|
||||
@@ -438,20 +402,15 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
})
|
||||
if (!createDatabaseEducationPerformed) {
|
||||
// selectDatabaseEducationPerformed
|
||||
browseButtonView != null
|
||||
openDatabaseButtonView != null
|
||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||
browseButtonView!!,
|
||||
openDatabaseButtonView!!,
|
||||
{tapTargetView ->
|
||||
tapTargetView?.let {
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
||||
}
|
||||
},
|
||||
{
|
||||
fileSelectExpandableButtonView?.let {
|
||||
fileDatabaseSelectActivityEducation
|
||||
.checkAndPerformedOpenLinkDatabaseEducation(it)
|
||||
}
|
||||
}
|
||||
{}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.SearchManager
|
||||
import android.app.assist.AssistStructure
|
||||
@@ -29,16 +28,19 @@ import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||
@@ -47,37 +49,44 @@ import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
|
||||
import com.kunzisoft.keepass.database.action.node.*
|
||||
import com.kunzisoft.keepass.database.action.node.ActionNodeDatabaseRunnable.Companion.NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.view.AddNodeButtonView
|
||||
import net.cachapa.expandablelayout.ExpandableLayout
|
||||
import com.kunzisoft.keepass.view.ToolbarAction
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
|
||||
class GroupActivity : LockingActivity(),
|
||||
GroupEditDialogFragment.EditGroupListener,
|
||||
IconPickerDialogFragment.IconPickerListener,
|
||||
NodeAdapter.NodeMenuListener,
|
||||
ListNodesFragment.NodeClickListener,
|
||||
ListNodesFragment.NodesActionMenuListener,
|
||||
ListNodesFragment.OnScrollListener,
|
||||
NodeAdapter.NodeClickCallback,
|
||||
SortDialogFragment.SortSelectionListener {
|
||||
|
||||
// Views
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
private var searchTitleView: View? = null
|
||||
private var toolbarPasteExpandableLayout: ExpandableLayout? = null
|
||||
private var toolbarPaste: Toolbar? = null
|
||||
private var toolbarAction: ToolbarAction? = null
|
||||
private var iconView: ImageView? = null
|
||||
private var numberChildrenView: TextView? = null
|
||||
private var modeTitleView: TextView? = null
|
||||
private var addNodeButtonView: AddNodeButtonView? = null
|
||||
private var groupNameView: TextView? = null
|
||||
@@ -87,12 +96,14 @@ class GroupActivity : LockingActivity(),
|
||||
private var mListNodesFragment: ListNodesFragment? = null
|
||||
private var mCurrentGroupIsASearch: Boolean = false
|
||||
|
||||
private var progressDialogThread: ProgressDialogThread? = null
|
||||
|
||||
// Nodes
|
||||
private var mRootGroup: GroupVersioned? = null
|
||||
private var mCurrentGroup: GroupVersioned? = null
|
||||
private var mOldGroupToUpdate: GroupVersioned? = null
|
||||
private var mNodeToCopy: NodeVersioned? = null
|
||||
private var mNodeToMove: NodeVersioned? = null
|
||||
// TODO private var mNodeToCopy: NodeVersioned? = null
|
||||
// TODO private var mNodeToMove: NodeVersioned? = null
|
||||
|
||||
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
|
||||
|
||||
@@ -110,15 +121,27 @@ class GroupActivity : LockingActivity(),
|
||||
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
|
||||
|
||||
// Initialize views
|
||||
iconView = findViewById(R.id.icon)
|
||||
coordinatorLayout = findViewById(R.id.group_coordinator)
|
||||
iconView = findViewById(R.id.group_icon)
|
||||
numberChildrenView = findViewById(R.id.group_numbers)
|
||||
addNodeButtonView = findViewById(R.id.add_node_button)
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
searchTitleView = findViewById(R.id.search_title)
|
||||
groupNameView = findViewById(R.id.group_name)
|
||||
toolbarPasteExpandableLayout = findViewById(R.id.expandable_toolbar_paste_layout)
|
||||
toolbarPaste = findViewById(R.id.toolbar_paste)
|
||||
toolbarAction = findViewById(R.id.toolbar_action)
|
||||
modeTitleView = findViewById(R.id.mode_title_view)
|
||||
|
||||
toolbar?.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
/*
|
||||
toolbarAction?.setNavigationOnClickListener {
|
||||
toolbarAction?.collapse()
|
||||
mNodeToCopy = null
|
||||
mNodeToMove = null
|
||||
}
|
||||
*/
|
||||
|
||||
// Focus view to reinitialize timeout
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
|
||||
|
||||
@@ -126,13 +149,6 @@ class GroupActivity : LockingActivity(),
|
||||
if (savedInstanceState != null) {
|
||||
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY))
|
||||
mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY)
|
||||
if (savedInstanceState.containsKey(NODE_TO_COPY_KEY)) {
|
||||
mNodeToCopy = savedInstanceState.getParcelable(NODE_TO_COPY_KEY)
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
|
||||
} else if (savedInstanceState.containsKey(NODE_TO_MOVE_KEY)) {
|
||||
mNodeToMove = savedInstanceState.getParcelable(NODE_TO_MOVE_KEY)
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -153,17 +169,6 @@ class GroupActivity : LockingActivity(),
|
||||
// Update last access time.
|
||||
mCurrentGroup?.touch(modified = false, touchParents = false)
|
||||
|
||||
toolbar?.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
toolbarPaste?.inflateMenu(R.menu.node_paste_menu)
|
||||
toolbarPaste?.setNavigationIcon(R.drawable.ic_arrow_left_white_24dp)
|
||||
toolbarPaste?.setNavigationOnClickListener {
|
||||
toolbarPasteExpandableLayout?.collapse()
|
||||
mNodeToCopy = null
|
||||
mNodeToMove = null
|
||||
}
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
||||
@@ -197,9 +202,74 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
})
|
||||
|
||||
// Search suggestion
|
||||
mDatabase?.let { database ->
|
||||
// Search suggestion
|
||||
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
|
||||
|
||||
// Init dialog thread
|
||||
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
|
||||
|
||||
var oldNodes: List<NodeVersioned> = ArrayList()
|
||||
result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle ->
|
||||
oldNodes = getListNodesFromBundle(database, oldNodesBundle)
|
||||
}
|
||||
var newNodes: List<NodeVersioned> = ArrayList()
|
||||
result.data?.getBundle(NEW_NODES_KEY)?.let { newNodesBundle ->
|
||||
newNodes = getListNodesFromBundle(database, newNodesBundle)
|
||||
}
|
||||
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
|
||||
if (result.isSuccess) {
|
||||
mListNodesFragment?.updateNodes(oldNodes, newNodes)
|
||||
}
|
||||
}
|
||||
ACTION_DATABASE_CREATE_GROUP_TASK,
|
||||
ACTION_DATABASE_COPY_NODES_TASK,
|
||||
ACTION_DATABASE_MOVE_NODES_TASK -> {
|
||||
if (result.isSuccess) {
|
||||
mListNodesFragment?.addNodes(newNodes)
|
||||
}
|
||||
}
|
||||
ACTION_DATABASE_DELETE_NODES_TASK -> {
|
||||
if (result.isSuccess) {
|
||||
|
||||
// Rebuild all the list the avoid bug when delete node from db sort
|
||||
if (PreferencesUtil.getListSort(this@GroupActivity) == SortNodeEnum.DB) {
|
||||
mListNodesFragment?.rebuildList()
|
||||
} else {
|
||||
// Use the old Nodes / entries unchanged with the old parent
|
||||
mListNodesFragment?.removeNodes(oldNodes)
|
||||
}
|
||||
|
||||
// Add trash in views list if it doesn't exists
|
||||
if (database.isRecycleBinEnabled) {
|
||||
val recycleBin = database.recycleBin
|
||||
if (mCurrentGroup != null && recycleBin != null
|
||||
&& mCurrentGroup!!.parent == null
|
||||
&& mCurrentGroup != recycleBin) {
|
||||
if (mListNodesFragment?.contains(recycleBin) == true)
|
||||
mListNodesFragment?.updateNode(recycleBin)
|
||||
else
|
||||
mListNodesFragment?.addNode(recycleBin)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.isSuccess) {
|
||||
result.exception?.errorId?.let { errorId ->
|
||||
coordinatorLayout?.let { coordinatorLayout ->
|
||||
Snackbar.make(coordinatorLayout, errorId, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finishNodeAction()
|
||||
|
||||
refreshNumberOfChildren()
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Finished creating tree")
|
||||
@@ -274,12 +344,6 @@ class GroupActivity : LockingActivity(),
|
||||
mOldGroupToUpdate?.let {
|
||||
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it)
|
||||
}
|
||||
mNodeToCopy?.let {
|
||||
outState.putParcelable(NODE_TO_COPY_KEY, it)
|
||||
}
|
||||
mNodeToMove?.let {
|
||||
outState.putParcelable(NODE_TO_MOVE_KEY, it)
|
||||
}
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
@@ -359,6 +423,9 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
// Assign number of children
|
||||
refreshNumberOfChildren()
|
||||
|
||||
// Show selection mode message if needed
|
||||
if (mSelectionMode) {
|
||||
modeTitleView?.visibility = View.VISIBLE
|
||||
@@ -388,6 +455,17 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshNumberOfChildren() {
|
||||
numberChildrenView?.apply {
|
||||
if (PreferencesUtil.showNumberEntries(context)) {
|
||||
text = mCurrentGroup?.getChildEntries(true)?.size?.toString() ?: ""
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolled(dy: Int) {
|
||||
addNodeButtonView?.hideButtonOnScrollListener(dy)
|
||||
}
|
||||
@@ -419,8 +497,10 @@ class GroupActivity : LockingActivity(),
|
||||
{
|
||||
// Build response with the entry selected
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
|
||||
AutofillHelper.buildResponseWhenEntrySelected(this@GroupActivity,
|
||||
entryVersioned.getEntryInfo(mDatabase!!))
|
||||
mDatabase?.let { database ->
|
||||
AutofillHelper.buildResponseWhenEntrySelected(this@GroupActivity,
|
||||
entryVersioned.getEntryInfo(database))
|
||||
}
|
||||
}
|
||||
finish()
|
||||
})
|
||||
@@ -430,12 +510,36 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
private var actionNodeMode: ActionMode? = null
|
||||
|
||||
private fun finishNodeAction() {
|
||||
actionNodeMode?.finish()
|
||||
actionNodeMode = null
|
||||
}
|
||||
|
||||
override fun onNodeSelected(nodes: List<NodeVersioned>): Boolean {
|
||||
if (nodes.isNotEmpty()) {
|
||||
if (actionNodeMode == null || toolbarAction?.getSupportActionModeCallback() == null) {
|
||||
mListNodesFragment?.actionNodesCallback(nodes, this)?.let {
|
||||
actionNodeMode = toolbarAction?.startSupportActionMode(it)
|
||||
}
|
||||
} else {
|
||||
actionNodeMode?.invalidate()
|
||||
}
|
||||
} else {
|
||||
finishNodeAction()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOpenMenuClick(node: NodeVersioned): Boolean {
|
||||
finishNodeAction()
|
||||
onNodeClick(node)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onEditMenuClick(node: NodeVersioned): Boolean {
|
||||
finishNodeAction()
|
||||
when (node.type) {
|
||||
Type.GROUP -> {
|
||||
mOldGroupToUpdate = node as GroupVersioned
|
||||
@@ -448,132 +552,56 @@ class GroupActivity : LockingActivity(),
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCopyMenuClick(node: NodeVersioned): Boolean {
|
||||
toolbarPasteExpandableLayout?.expand()
|
||||
mNodeToCopy = node
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
|
||||
return false
|
||||
}
|
||||
override fun onCopyMenuClick(nodes: List<NodeVersioned>): Boolean {
|
||||
actionNodeMode?.invalidate()
|
||||
|
||||
private inner class OnCopyMenuItemClickListener : Toolbar.OnMenuItemClickListener {
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
toolbarPasteExpandableLayout?.collapse()
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.menu_paste -> {
|
||||
when (mNodeToCopy?.type) {
|
||||
Type.GROUP -> Log.e(TAG, "Copy not allowed for group")
|
||||
Type.ENTRY -> {
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
copyEntry(mNodeToCopy as EntryVersioned, currentGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
mNodeToCopy = null
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyEntry(entryToCopy: EntryVersioned, newParent: GroupVersioned) {
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
CopyEntryRunnable(this,
|
||||
Database.getInstance(),
|
||||
entryToCopy,
|
||||
newParent,
|
||||
AfterAddNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun onMoveMenuClick(node: NodeVersioned): Boolean {
|
||||
toolbarPasteExpandableLayout?.expand()
|
||||
mNodeToMove = node
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
|
||||
return false
|
||||
}
|
||||
|
||||
private inner class OnMoveMenuItemClickListener : Toolbar.OnMenuItemClickListener {
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
toolbarPasteExpandableLayout?.collapse()
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.menu_paste -> {
|
||||
when (mNodeToMove?.type) {
|
||||
Type.GROUP -> {
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
moveGroup(mNodeToMove as GroupVersioned, currentGroup)
|
||||
}
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
moveEntry(mNodeToMove as EntryVersioned, currentGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
mNodeToMove = null
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveGroup(groupToMove: GroupVersioned, newParent: GroupVersioned) {
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
MoveGroupRunnable(
|
||||
this,
|
||||
Database.getInstance(),
|
||||
groupToMove,
|
||||
newParent,
|
||||
AfterAddNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun moveEntry(entryToMove: EntryVersioned, newParent: GroupVersioned) {
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
MoveEntryRunnable(
|
||||
this,
|
||||
Database.getInstance(),
|
||||
entryToMove,
|
||||
newParent,
|
||||
AfterAddNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun onDeleteMenuClick(node: NodeVersioned): Boolean {
|
||||
when (node.type) {
|
||||
Type.GROUP -> deleteGroup(node as GroupVersioned)
|
||||
Type.ENTRY -> deleteEntry(node as EntryVersioned)
|
||||
}
|
||||
// Nothing here fragment calls onPasteMenuClick internally
|
||||
return true
|
||||
}
|
||||
|
||||
private fun deleteGroup(group: GroupVersioned) {
|
||||
//TODO Verify trash recycle bin
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
DeleteGroupRunnable(
|
||||
this,
|
||||
Database.getInstance(),
|
||||
group,
|
||||
AfterDeleteNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
override fun onMoveMenuClick(nodes: List<NodeVersioned>): Boolean {
|
||||
actionNodeMode?.invalidate()
|
||||
|
||||
// Nothing here fragment calls onPasteMenuClick internally
|
||||
return true
|
||||
}
|
||||
|
||||
private fun deleteEntry(entry: EntryVersioned) {
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
DeleteEntryRunnable(
|
||||
this,
|
||||
Database.getInstance(),
|
||||
entry,
|
||||
AfterDeleteNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?,
|
||||
nodes: List<NodeVersioned>): Boolean {
|
||||
when (pasteMode) {
|
||||
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
|
||||
// Copy
|
||||
mCurrentGroup?.let { newParent ->
|
||||
progressDialogThread?.startDatabaseCopyNodes(
|
||||
nodes,
|
||||
newParent,
|
||||
!mReadOnly
|
||||
)
|
||||
}
|
||||
}
|
||||
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
|
||||
// Move
|
||||
mCurrentGroup?.let { newParent ->
|
||||
progressDialogThread?.startDatabaseMoveNodes(
|
||||
nodes,
|
||||
newParent,
|
||||
!mReadOnly
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
finishNodeAction()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDeleteMenuClick(nodes: List<NodeVersioned>): Boolean {
|
||||
progressDialogThread?.startDatabaseDeleteNodes(
|
||||
nodes,
|
||||
!mReadOnly
|
||||
)
|
||||
finishNodeAction()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -582,6 +610,16 @@ class GroupActivity : LockingActivity(),
|
||||
assignGroupViewElements()
|
||||
// Refresh suggestions to change preferences
|
||||
mSearchSuggestionAdapter?.reInit(this)
|
||||
|
||||
progressDialogThread?.registerProgressTask()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
progressDialogThread?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
|
||||
finishNodeAction()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@@ -713,7 +751,6 @@ class GroupActivity : LockingActivity(),
|
||||
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
|
||||
name: String?,
|
||||
icon: PwIcon?) {
|
||||
val database = Database.getInstance()
|
||||
|
||||
if (name != null && name.isNotEmpty() && icon != null) {
|
||||
when (action) {
|
||||
@@ -721,104 +758,37 @@ class GroupActivity : LockingActivity(),
|
||||
// If group creation
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
// Build the group
|
||||
database.createGroup()?.let { newGroup ->
|
||||
mDatabase?.createGroup()?.let { newGroup ->
|
||||
newGroup.title = name
|
||||
newGroup.icon = icon
|
||||
// Not really needed here because added in runnable but safe
|
||||
newGroup.parent = currentGroup
|
||||
|
||||
// If group created save it in the database
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
AddGroupRunnable(this,
|
||||
Database.getInstance(),
|
||||
newGroup,
|
||||
currentGroup,
|
||||
AfterAddNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
progressDialogThread?.startDatabaseCreateGroup(
|
||||
newGroup, currentGroup, !mReadOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
GroupEditDialogFragment.EditGroupDialogAction.UPDATE ->
|
||||
GroupEditDialogFragment.EditGroupDialogAction.UPDATE -> {
|
||||
// If update add new elements
|
||||
mOldGroupToUpdate?.let { oldGroupToUpdate ->
|
||||
GroupVersioned(oldGroupToUpdate).let { updateGroup ->
|
||||
updateGroup.title = name
|
||||
// TODO custom icon
|
||||
updateGroup.icon = icon
|
||||
updateGroup.apply {
|
||||
// WARNING remove parent and children to keep memory
|
||||
removeParent()
|
||||
removeChildren() // TODO concurrent exception
|
||||
|
||||
mListNodesFragment?.removeNode(oldGroupToUpdate)
|
||||
title = name
|
||||
this.icon = icon // TODO custom icon
|
||||
}
|
||||
|
||||
// If group updated save it in the database
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
UpdateGroupRunnable(this,
|
||||
Database.getInstance(),
|
||||
oldGroupToUpdate,
|
||||
updateGroup,
|
||||
AfterUpdateNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class AfterAddNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
runOnUiThread {
|
||||
if (actionNodeValues.result.isSuccess) {
|
||||
if (actionNodeValues.newNode != null)
|
||||
mListNodesFragment?.addNode(actionNodeValues.newNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class AfterUpdateNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
runOnUiThread {
|
||||
if (actionNodeValues.result.isSuccess) {
|
||||
if (actionNodeValues.oldNode!= null && actionNodeValues.newNode != null)
|
||||
mListNodesFragment?.updateNode(actionNodeValues.oldNode, actionNodeValues.newNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class AfterDeleteNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
runOnUiThread {
|
||||
if (actionNodeValues.result.isSuccess) {
|
||||
|
||||
// If the action register the position, use it to remove the entry view
|
||||
val positionNode = actionNodeValues.result.data?.getInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY)
|
||||
if (PreferencesUtil.getListSort(this@GroupActivity) == SortNodeEnum.DB
|
||||
&& positionNode != null) {
|
||||
mListNodesFragment?.removeNodeAt(positionNode)
|
||||
} else {
|
||||
// else use the old Node that was the entry unchanged with the old parent
|
||||
actionNodeValues.oldNode?.let { oldNode ->
|
||||
mListNodesFragment?.removeNode(oldNode)
|
||||
}
|
||||
}
|
||||
|
||||
// Add trash in views list if it doesn't exists
|
||||
val database = Database.getInstance()
|
||||
if (database.isRecycleBinEnabled) {
|
||||
val recycleBin = database.recycleBin
|
||||
if (mCurrentGroup != null && recycleBin != null
|
||||
&& mCurrentGroup!!.parent == null
|
||||
&& mCurrentGroup != recycleBin) {
|
||||
if (mListNodesFragment?.contains(recycleBin) == true)
|
||||
mListNodesFragment?.updateNode(recycleBin)
|
||||
else
|
||||
mListNodesFragment?.addNode(recycleBin)
|
||||
progressDialogThread?.startDatabaseUpdateGroup(
|
||||
oldGroupToUpdate, updateGroup, !mReadOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -902,25 +872,29 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
// Normal way when we are not in root
|
||||
if (mRootGroup != null && mRootGroup != mCurrentGroup)
|
||||
super.onBackPressed()
|
||||
// Else lock if needed
|
||||
else {
|
||||
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
||||
lockAndExit()
|
||||
if (mListNodesFragment?.nodeActionSelectionMode == true) {
|
||||
finishNodeAction()
|
||||
} else {
|
||||
// Normal way when we are not in root
|
||||
if (mRootGroup != null && mRootGroup != mCurrentGroup)
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
moveTaskToBack(true)
|
||||
// Else lock if needed
|
||||
else {
|
||||
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
||||
lockAndExit()
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment
|
||||
// to refresh fragment
|
||||
mListNodesFragment?.rebuildList()
|
||||
mCurrentGroup = mListNodesFragment?.mainGroup
|
||||
removeSearchInIntent(intent)
|
||||
assignGroupViewElements()
|
||||
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment
|
||||
// to refresh fragment
|
||||
mListNodesFragment?.rebuildList()
|
||||
mCurrentGroup = mListNodesFragment?.mainGroup
|
||||
removeSearchInIntent(intent)
|
||||
assignGroupViewElements()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -931,13 +905,15 @@ class GroupActivity : LockingActivity(),
|
||||
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
|
||||
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
|
||||
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
|
||||
private const val NODE_TO_COPY_KEY = "NODE_TO_COPY_KEY"
|
||||
private const val NODE_TO_MOVE_KEY = "NODE_TO_MOVE_KEY"
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity, group: GroupVersioned?, readOnly: Boolean,
|
||||
private fun buildAndLaunchIntent(context: Context, group: GroupVersioned?, readOnly: Boolean,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, GroupActivity::class.java)
|
||||
val checkTime = if (context is Activity)
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(context)
|
||||
else
|
||||
TimeoutHelper.checkTime(context)
|
||||
if (checkTime) {
|
||||
val intent = Intent(context, GroupActivity::class.java)
|
||||
if (group != null) {
|
||||
intent.putExtra(GROUP_ID_KEY, group.nodeId)
|
||||
}
|
||||
@@ -953,10 +929,10 @@ class GroupActivity : LockingActivity(),
|
||||
*/
|
||||
|
||||
@JvmOverloads
|
||||
fun launch(activity: Activity, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
|
||||
TimeoutHelper.recordTime(activity)
|
||||
buildAndLaunchIntent(activity, null, readOnly) { intent ->
|
||||
activity.startActivity(intent)
|
||||
fun launch(context: Context, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||
TimeoutHelper.recordTime(context)
|
||||
buildAndLaunchIntent(context, null, readOnly) { intent ->
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -967,10 +943,10 @@ class GroupActivity : LockingActivity(),
|
||||
*/
|
||||
// TODO implement pre search to directly open the direct group
|
||||
|
||||
fun launchForKeyboarSelection(activity: Activity, readOnly: Boolean) {
|
||||
TimeoutHelper.recordTime(activity)
|
||||
buildAndLaunchIntent(activity, null, readOnly) { intent ->
|
||||
EntrySelectionHelper.startActivityForEntrySelection(activity, intent)
|
||||
fun launchForKeyboardSelection(context: Context, readOnly: Boolean) {
|
||||
TimeoutHelper.recordTime(context)
|
||||
buildAndLaunchIntent(context, null, readOnly) { intent ->
|
||||
EntrySelectionHelper.startActivityForEntrySelection(context, intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ActionMode
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||
@@ -26,11 +27,12 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Type
|
||||
import java.util.*
|
||||
|
||||
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
||||
|
||||
private var nodeClickCallback: NodeAdapter.NodeClickCallback? = null
|
||||
private var nodeMenuListener: NodeAdapter.NodeMenuListener? = null
|
||||
private var nodeClickListener: NodeClickListener? = null
|
||||
private var onScrollListener: OnScrollListener? = null
|
||||
|
||||
private var listView: RecyclerView? = null
|
||||
@@ -38,6 +40,13 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
private set
|
||||
private var mAdapter: NodeAdapter? = null
|
||||
|
||||
var nodeActionSelectionMode = false
|
||||
private set
|
||||
var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED
|
||||
private set
|
||||
private val listActionNodes = LinkedList<NodeVersioned>()
|
||||
private val listPasteNodes = LinkedList<NodeVersioned>()
|
||||
|
||||
private var notFoundView: View? = null
|
||||
private var isASearchResult: Boolean = false
|
||||
|
||||
@@ -56,22 +65,13 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
nodeClickCallback = context as NodeAdapter.NodeClickCallback
|
||||
nodeClickListener = context as NodeClickListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
|
||||
}
|
||||
|
||||
try {
|
||||
nodeMenuListener = context as NodeAdapter.NodeMenuListener
|
||||
} catch (e: ClassCastException) {
|
||||
nodeMenuListener = null
|
||||
// Context menu can be omit
|
||||
Log.w(TAG, context.toString()
|
||||
+ " must implement " + NodeAdapter.NodeMenuListener::class.java.name)
|
||||
}
|
||||
|
||||
try {
|
||||
onScrollListener = context as OnScrollListener
|
||||
} catch (e: ClassCastException) {
|
||||
@@ -85,33 +85,58 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
activity?.let { currentActivity ->
|
||||
setHasOptionsMenu(true)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
|
||||
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)
|
||||
}
|
||||
arguments?.let { args ->
|
||||
// Contains all the group in element
|
||||
if (args.containsKey(GROUP_KEY)) {
|
||||
mainGroup = args.getParcelable(GROUP_KEY)
|
||||
}
|
||||
|
||||
contextThemed?.let { context ->
|
||||
mAdapter = NodeAdapter(context, currentActivity.menuInflater)
|
||||
mAdapter?.apply {
|
||||
setReadOnly(readOnly)
|
||||
setIsASearchResult(isASearchResult)
|
||||
setOnNodeClickListener(nodeClickCallback)
|
||||
setActivateContextMenu(true)
|
||||
setNodeMenuListener(nodeMenuListener)
|
||||
}
|
||||
if (args.containsKey(IS_SEARCH)) {
|
||||
isASearchResult = args.getBoolean(IS_SEARCH)
|
||||
}
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
|
||||
contextThemed?.let { context ->
|
||||
mAdapter = NodeAdapter(context)
|
||||
mAdapter?.apply {
|
||||
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
||||
override fun onNodeClick(node: NodeVersioned) {
|
||||
if (nodeActionSelectionMode) {
|
||||
if (listActionNodes.contains(node)) {
|
||||
// Remove selected item if already selected
|
||||
listActionNodes.remove(node)
|
||||
} else {
|
||||
// Add selected item if not already selected
|
||||
listActionNodes.add(node)
|
||||
}
|
||||
nodeClickListener?.onNodeSelected(listActionNodes)
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
} else {
|
||||
nodeClickListener?.onNodeClick(node)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNodeLongClick(node: NodeVersioned): Boolean {
|
||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||
// Select the first item after a long click
|
||||
if (!listActionNodes.contains(node))
|
||||
listActionNodes.add(node)
|
||||
|
||||
nodeClickListener?.onNodeSelected(listActionNodes)
|
||||
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -148,10 +173,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
activity?.intent?.let {
|
||||
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it)
|
||||
}
|
||||
// Force read only mode if selection mode
|
||||
mAdapter?.apply {
|
||||
setReadOnly(readOnly)
|
||||
}
|
||||
|
||||
// Refresh data
|
||||
mAdapter?.notifyDataSetChanged()
|
||||
@@ -207,7 +228,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
R.id.menu_sort -> {
|
||||
context?.let { context ->
|
||||
val sortDialogFragment: SortDialogFragment =
|
||||
if (Database.getInstance().isRecycleBinAvailable
|
||||
if (Database.getInstance().allowRecycleBin
|
||||
&& Database.getInstance().isRecycleBinEnabled) {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
@@ -230,6 +251,102 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
}
|
||||
|
||||
fun actionNodesCallback(nodes: List<NodeVersioned>,
|
||||
menuListener: NodesActionMenuListener?) : ActionMode.Callback {
|
||||
|
||||
return object : ActionMode.Callback {
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
nodeActionSelectionMode = false
|
||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
menu?.clear()
|
||||
|
||||
if (nodeActionPasteMode != PasteMode.UNDEFINED) {
|
||||
mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu)
|
||||
} else {
|
||||
nodeActionSelectionMode = true
|
||||
mode?.menuInflater?.inflate(R.menu.node_menu, menu)
|
||||
|
||||
val database = Database.getInstance()
|
||||
|
||||
// Open and Edit for a single item
|
||||
if (nodes.size == 1) {
|
||||
// Edition
|
||||
if (readOnly || nodes[0] == database.recycleBin) {
|
||||
menu?.removeItem(R.id.menu_edit)
|
||||
}
|
||||
} else {
|
||||
menu?.removeItem(R.id.menu_open)
|
||||
menu?.removeItem(R.id.menu_edit)
|
||||
}
|
||||
|
||||
// Copy and Move (not for groups)
|
||||
if (readOnly
|
||||
|| isASearchResult
|
||||
|| nodes.any { it == database.recycleBin }
|
||||
|| nodes.any { it.type == Type.GROUP }) {
|
||||
// TODO COPY For Group
|
||||
menu?.removeItem(R.id.menu_copy)
|
||||
menu?.removeItem(R.id.menu_move)
|
||||
}
|
||||
|
||||
// Deletion
|
||||
if (readOnly || nodes.any { it == database.recycleBin }) {
|
||||
menu?.removeItem(R.id.menu_delete)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the number of items selected in title
|
||||
mode?.title = nodes.size.toString()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
if (menuListener == null)
|
||||
return false
|
||||
return when (item?.itemId) {
|
||||
R.id.menu_open -> menuListener.onOpenMenuClick(nodes[0])
|
||||
R.id.menu_edit -> menuListener.onEditMenuClick(nodes[0])
|
||||
R.id.menu_copy -> {
|
||||
nodeActionPasteMode = PasteMode.PASTE_FROM_COPY
|
||||
mAdapter?.unselectActionNodes()
|
||||
val returnValue = menuListener.onCopyMenuClick(nodes)
|
||||
nodeActionSelectionMode = false
|
||||
returnValue
|
||||
}
|
||||
R.id.menu_move -> {
|
||||
nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE
|
||||
mAdapter?.unselectActionNodes()
|
||||
val returnValue = menuListener.onMoveMenuClick(nodes)
|
||||
nodeActionSelectionMode = false
|
||||
returnValue
|
||||
}
|
||||
R.id.menu_delete -> menuListener.onDeleteMenuClick(nodes)
|
||||
R.id.menu_paste -> {
|
||||
val returnValue = menuListener.onPasteMenuClick(nodeActionPasteMode, nodes)
|
||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||
nodeActionSelectionMode = false
|
||||
returnValue
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
listActionNodes.clear()
|
||||
listPasteNodes.clear()
|
||||
mAdapter?.unselectActionNodes()
|
||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||
nodeActionSelectionMode = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
@@ -260,18 +377,58 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
mAdapter?.addNode(newNode)
|
||||
}
|
||||
|
||||
fun addNodes(newNodes: List<NodeVersioned>) {
|
||||
mAdapter?.addNodes(newNodes)
|
||||
}
|
||||
|
||||
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned? = null) {
|
||||
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
|
||||
}
|
||||
|
||||
fun updateNodes(oldNodes: List<NodeVersioned>, newNodes: List<NodeVersioned>) {
|
||||
mAdapter?.updateNodes(oldNodes, newNodes)
|
||||
}
|
||||
|
||||
fun removeNode(pwNode: NodeVersioned) {
|
||||
mAdapter?.removeNode(pwNode)
|
||||
}
|
||||
|
||||
fun removeNodes(nodes: List<NodeVersioned>) {
|
||||
mAdapter?.removeNodes(nodes)
|
||||
}
|
||||
|
||||
fun removeNodeAt(position: Int) {
|
||||
mAdapter?.removeNodeAt(position)
|
||||
}
|
||||
|
||||
fun removeNodesAt(positions: IntArray) {
|
||||
mAdapter?.removeNodesAt(positions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
interface NodeClickListener {
|
||||
fun onNodeClick(node: NodeVersioned)
|
||||
fun onNodeSelected(nodes: List<NodeVersioned>): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu listener to redefine to do an action in menu
|
||||
*/
|
||||
interface NodesActionMenuListener {
|
||||
fun onOpenMenuClick(node: NodeVersioned): Boolean
|
||||
fun onEditMenuClick(node: NodeVersioned): Boolean
|
||||
fun onCopyMenuClick(nodes: List<NodeVersioned>): Boolean
|
||||
fun onMoveMenuClick(nodes: List<NodeVersioned>): Boolean
|
||||
fun onDeleteMenuClick(nodes: List<NodeVersioned>): Boolean
|
||||
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<NodeVersioned>): Boolean
|
||||
}
|
||||
|
||||
enum class PasteMode {
|
||||
UNDEFINED, PASTE_FROM_COPY, PASTE_FROM_MOVE
|
||||
}
|
||||
|
||||
interface OnScrollListener {
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities
|
||||
import android.app.Activity
|
||||
import android.app.assist.AssistStructure
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
@@ -30,9 +29,6 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
@@ -41,41 +37,50 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.widget.*
|
||||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.biometric.BiometricManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.FingerPrintExplanationDialog
|
||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import kotlinx.android.synthetic.main.activity_password.*
|
||||
import java.io.FileNotFoundException
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class PasswordActivity : StylishActivity() {
|
||||
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
private var containerView: View? = null
|
||||
private var filenameView: TextView? = null
|
||||
private var passwordView: EditText? = null
|
||||
private var keyFileView: EditText? = null
|
||||
@@ -87,6 +92,8 @@ class PasswordActivity : StylishActivity() {
|
||||
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
||||
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
private var mDatabaseKeyFileUri: Uri? = null
|
||||
|
||||
private var prefs: SharedPreferences? = null
|
||||
|
||||
private var mRememberKeyFile: Boolean = false
|
||||
@@ -94,6 +101,8 @@ class PasswordActivity : StylishActivity() {
|
||||
|
||||
private var readOnly: Boolean = false
|
||||
|
||||
private var progressDialogThread: ProgressDialogThread? = null
|
||||
|
||||
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -101,8 +110,7 @@ class PasswordActivity : StylishActivity() {
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
mRememberKeyFile = prefs!!.getBoolean(getString(R.string.keyfile_key),
|
||||
resources.getBoolean(R.bool.keyfile_default))
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this)
|
||||
|
||||
setContentView(R.layout.activity_password)
|
||||
|
||||
@@ -112,6 +120,7 @@ class PasswordActivity : StylishActivity() {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
containerView = findViewById(R.id.container)
|
||||
confirmButtonView = findViewById(R.id.pass_ok)
|
||||
filenameView = findViewById(R.id.filename)
|
||||
passwordView = findViewById(R.id.password)
|
||||
@@ -119,11 +128,11 @@ class PasswordActivity : StylishActivity() {
|
||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
||||
advancedUnlockInfoView = findViewById(R.id.fingerprint_info)
|
||||
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
||||
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||
|
||||
val browseView = findViewById<View>(R.id.browse_button)
|
||||
val browseView = findViewById<View>(R.id.open_database_button)
|
||||
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
||||
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
|
||||
|
||||
@@ -153,6 +162,91 @@ class PasswordActivity : StylishActivity() {
|
||||
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||
enableOrNotTheConfirmationButton()
|
||||
}
|
||||
|
||||
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
// Recheck biometric if error
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) {
|
||||
// Stay with the same mode and init it
|
||||
advancedUnlockedManager?.initBiometricMode()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the password in view in all cases
|
||||
removePassword()
|
||||
|
||||
if (result.isSuccess) {
|
||||
launchGroupActivity()
|
||||
} else {
|
||||
var resultError = ""
|
||||
val resultException = result.exception
|
||||
val resultMessage = result.message
|
||||
|
||||
if (resultException != null) {
|
||||
resultError = resultException.getLocalizedMessage(resources)
|
||||
|
||||
// Relaunch loading if we need to fix UUID
|
||||
if (resultException is LoadDatabaseDuplicateUuidException) {
|
||||
showLoadDatabaseDuplicateUuidMessage {
|
||||
|
||||
var databaseUri: Uri? = null
|
||||
var masterPassword: String? = null
|
||||
var keyFileUri: Uri? = null
|
||||
var readOnly = true
|
||||
var cipherEntity: CipherDatabaseEntity? = null
|
||||
|
||||
result.data?.let { resultData ->
|
||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
|
||||
keyFileUri = resultData.getParcelable(KEY_FILE_KEY)
|
||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||
}
|
||||
|
||||
databaseUri?.let { databaseFileUri ->
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseFileUri,
|
||||
masterPassword,
|
||||
keyFileUri,
|
||||
readOnly,
|
||||
cipherEntity,
|
||||
true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||
resultError = "$resultError $resultMessage"
|
||||
}
|
||||
Log.e(TAG, resultError, resultException)
|
||||
Snackbar.make(activity_password_coordinator_layout,
|
||||
resultError,
|
||||
Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchGroupActivity() {
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
GroupActivity.launch(this@PasswordActivity, readOnly)
|
||||
},
|
||||
{
|
||||
GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly)
|
||||
// Do not keep history
|
||||
finish()
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
||||
@@ -166,6 +260,9 @@ class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
if (Database.getInstance().loaded)
|
||||
launchGroupActivity()
|
||||
|
||||
// If the database isn't accessible make sure to clear the password field, if it
|
||||
// was saved in the instance state
|
||||
if (Database.getInstance().loaded) {
|
||||
@@ -175,6 +272,8 @@ class PasswordActivity : StylishActivity() {
|
||||
// For check shutdown
|
||||
super.onResume()
|
||||
|
||||
progressDialogThread?.registerProgressTask()
|
||||
|
||||
initUriFromIntent()
|
||||
}
|
||||
|
||||
@@ -190,17 +289,10 @@ class PasswordActivity : StylishActivity() {
|
||||
|
||||
// If is a view intent
|
||||
val action = intent.action
|
||||
if (action != null && action == VIEW_INTENT) {
|
||||
|
||||
val databaseUriRetrieve = intent.data
|
||||
// Stop activity here if we can't verify database URI
|
||||
if (!UriUtil.verifyFileUri(databaseUriRetrieve)) {
|
||||
Log.e(TAG, "File URI not validate")
|
||||
finish()
|
||||
}
|
||||
databaseUri = databaseUriRetrieve
|
||||
if (action != null
|
||||
&& action == VIEW_INTENT) {
|
||||
databaseUri = intent.data
|
||||
keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
||||
|
||||
} else {
|
||||
databaseUri = intent.getParcelableExtra(KEY_FILENAME)
|
||||
keyFileUri = intent.getParcelableExtra(KEY_KEYFILE)
|
||||
@@ -222,6 +314,7 @@ class PasswordActivity : StylishActivity() {
|
||||
|
||||
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||
mDatabaseFileUri = databaseFileUri
|
||||
mDatabaseKeyFileUri = keyFileUri
|
||||
|
||||
// Define title
|
||||
databaseFileUri?.let {
|
||||
@@ -243,11 +336,13 @@ class PasswordActivity : StylishActivity() {
|
||||
newDefaultFileName = databaseFileUri ?: newDefaultFileName
|
||||
}
|
||||
|
||||
newDefaultFileName?.let {
|
||||
prefs?.edit()?.apply {
|
||||
prefs?.edit()?.apply {
|
||||
newDefaultFileName?.let {
|
||||
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
|
||||
apply()
|
||||
} ?: kotlin.run {
|
||||
remove(KEY_DEFAULT_DATABASE_PATH)
|
||||
}
|
||||
apply()
|
||||
}
|
||||
|
||||
val backupManager = BackupManager(this@PasswordActivity)
|
||||
@@ -273,15 +368,11 @@ class PasswordActivity : StylishActivity() {
|
||||
if (launchImmediately) {
|
||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
||||
} else {
|
||||
// Init FingerPrint elements
|
||||
var fingerPrintInit = false
|
||||
// Init Biometric elements
|
||||
var biometricInitialize = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
||||
|
||||
advancedUnlockInfoView?.setOnClickListener {
|
||||
FingerPrintExplanationDialog().show(supportFragmentManager, "fingerPrintExplanationDialog")
|
||||
}
|
||||
|
||||
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
||||
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
||||
databaseFileUri,
|
||||
@@ -303,18 +394,18 @@ class PasswordActivity : StylishActivity() {
|
||||
{ passwordDecrypted ->
|
||||
// Load the database if password is retrieve from biometric
|
||||
passwordDecrypted?.let {
|
||||
// Retrieve from fingerprint
|
||||
// Retrieve from biometric
|
||||
verifyKeyFileCheckboxAndLoadDatabase(it)
|
||||
}
|
||||
})
|
||||
}
|
||||
advancedUnlockedManager?.initBiometric()
|
||||
fingerPrintInit = true
|
||||
biometricInitialize = true
|
||||
} else {
|
||||
advancedUnlockedManager?.destroy()
|
||||
}
|
||||
}
|
||||
if (!fingerPrintInit) {
|
||||
if (!biometricInitialize) {
|
||||
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||
}
|
||||
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||
@@ -368,9 +459,8 @@ class PasswordActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
advancedUnlockedManager?.pause()
|
||||
}
|
||||
progressDialogThread?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@@ -391,14 +481,18 @@ class PasswordActivity : StylishActivity() {
|
||||
keyFile: Uri?,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
|
||||
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||
loadDatabase(keyPassword, keyFileUri, cipherDatabaseEntity)
|
||||
verifyKeyFileCheckbox(keyFile)
|
||||
loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
|
||||
}
|
||||
|
||||
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
|
||||
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
|
||||
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||
loadDatabase(password, keyFileUri)
|
||||
verifyKeyFileCheckbox(keyFile)
|
||||
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
|
||||
}
|
||||
|
||||
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
|
||||
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||
}
|
||||
|
||||
private fun removePassword() {
|
||||
@@ -406,104 +500,51 @@ class PasswordActivity : StylishActivity() {
|
||||
checkboxPasswordView?.isChecked = false
|
||||
}
|
||||
|
||||
private fun loadDatabase(password: String?, keyFile: Uri?, cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
private fun loadDatabase(databaseFileUri: Uri?,
|
||||
password: String?,
|
||||
keyFileUri: Uri?,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
|
||||
runOnUiThread {
|
||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||
removePassword()
|
||||
}
|
||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||
removePassword()
|
||||
}
|
||||
|
||||
// Clear before we load
|
||||
val database = Database.getInstance()
|
||||
database.closeAndClear(applicationContext.filesDir)
|
||||
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
// Show the progress dialog and load the database
|
||||
ProgressDialogThread(this,
|
||||
{ progressTaskUpdater ->
|
||||
LoadDatabaseRunnable(
|
||||
WeakReference(this@PasswordActivity),
|
||||
database,
|
||||
databaseUri,
|
||||
password,
|
||||
keyFile,
|
||||
progressTaskUpdater,
|
||||
AfterLoadingDatabase(database, password, cipherDatabaseEntity))
|
||||
},
|
||||
R.string.loading_database).start()
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseUri,
|
||||
password,
|
||||
keyFileUri,
|
||||
readOnly,
|
||||
cipherDatabaseEntity,
|
||||
false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after verify and try to opening the database
|
||||
*/
|
||||
private inner class AfterLoadingDatabase(val database: Database, val password: String?,
|
||||
val cipherDatabaseEntity: CipherDatabaseEntity? = null)
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
runOnUiThread {
|
||||
// Recheck fingerprint if error
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) {
|
||||
// Stay with the same mode and init it
|
||||
advancedUnlockedManager?.initBiometricMode()
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Remove the password in view in all cases
|
||||
removePassword()
|
||||
|
||||
// Register the biometric
|
||||
if (cipherDatabaseEntity != null) {
|
||||
CipherDatabaseAction.getInstance(this@PasswordActivity)
|
||||
.addOrUpdateCipherDatabase(cipherDatabaseEntity) {
|
||||
checkAndLaunchGroupActivity(database, password)
|
||||
}
|
||||
} else {
|
||||
checkAndLaunchGroupActivity(database, password)
|
||||
}
|
||||
|
||||
} else {
|
||||
if (result.message != null && result.message!!.isNotEmpty()) {
|
||||
Snackbar.make(activity_password_coordinator_layout, result.message!!, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
||||
password: String?,
|
||||
keyFile: Uri?,
|
||||
readOnly: Boolean,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||
fixDuplicateUUID: Boolean) {
|
||||
progressDialogThread?.startDatabaseLoad(
|
||||
databaseUri,
|
||||
password,
|
||||
keyFile,
|
||||
readOnly,
|
||||
cipherDatabaseEntity,
|
||||
fixDuplicateUUID
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkAndLaunchGroupActivity(database: Database, password: String?) {
|
||||
if (database.validatePasswordEncoding(password)) {
|
||||
launchGroupActivity()
|
||||
} else {
|
||||
PasswordEncodingDialogFragment().apply {
|
||||
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
|
||||
launchGroupActivity()
|
||||
}
|
||||
show(supportFragmentManager, "passwordEncodingTag")
|
||||
}
|
||||
}
|
||||
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
|
||||
DuplicateUuidDialog().apply {
|
||||
positiveAction = loadDatabaseWithFix
|
||||
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
||||
}
|
||||
|
||||
private fun launchGroupActivity() {
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
GroupActivity.launch(this@PasswordActivity, readOnly)
|
||||
},
|
||||
{
|
||||
GroupActivity.launchForKeyboarSelection(this@PasswordActivity, readOnly)
|
||||
// Do not keep history
|
||||
finish()
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly)
|
||||
}
|
||||
})
|
||||
}
|
||||
// To fix multiple view education
|
||||
private var performedEductionInProgress = false
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val inflater = menuInflater
|
||||
@@ -514,23 +555,27 @@ class PasswordActivity : StylishActivity() {
|
||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||
|
||||
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// Fingerprint menu
|
||||
// biometric menu
|
||||
advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
|
||||
}
|
||||
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
// Show education views
|
||||
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||
if (!performedEductionInProgress) {
|
||||
performedEductionInProgress = true
|
||||
// Show education views
|
||||
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||
menu: Menu) {
|
||||
val unlockEducationPerformed = toolbar != null
|
||||
val educationContainerView = containerView
|
||||
val unlockEducationPerformed = educationContainerView != null
|
||||
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
||||
toolbar!!,
|
||||
educationContainerView,
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
},
|
||||
@@ -538,11 +583,11 @@ class PasswordActivity : StylishActivity() {
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
})
|
||||
if (!unlockEducationPerformed) {
|
||||
|
||||
val readOnlyEducationPerformed = toolbar != null
|
||||
&& toolbar!!.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||
val educationToolbar = toolbar
|
||||
val readOnlyEducationPerformed =
|
||||
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||
toolbar!!.findViewById(R.id.menu_open_file_read_mode_key),
|
||||
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
@@ -554,12 +599,12 @@ class PasswordActivity : StylishActivity() {
|
||||
if (!readOnlyEducationPerformed) {
|
||||
|
||||
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
|
||||
// fingerprintEducationPerformed
|
||||
// EducationPerformed
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
|
||||
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
|
||||
&& passwordActivityEducation.checkAndPerformedFingerprintEducation(advancedUnlockInfoView?.unlockIconImageView!!)
|
||||
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -583,7 +628,7 @@ class PasswordActivity : StylishActivity() {
|
||||
readOnly = !readOnly
|
||||
changeOpenFileReadIcon(item)
|
||||
}
|
||||
R.id.menu_fingerprint_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
R.id.menu_biometric_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
advancedUnlockedManager?.deleteEntryKey()
|
||||
}
|
||||
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||
|
||||
@@ -45,6 +45,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
private var rootView: View? = null
|
||||
|
||||
private var passwordCheckBox: CompoundButton? = null
|
||||
|
||||
private var passwordTextInputLayout: TextInputLayout? = null
|
||||
private var passwordView: TextView? = null
|
||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||
private var passwordRepeatView: TextView? = null
|
||||
@@ -96,6 +98,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
var allowNoMasterKey = false
|
||||
arguments?.apply {
|
||||
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
|
||||
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
val inflater = activity.layoutInflater
|
||||
|
||||
@@ -104,9 +113,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
.setTitle(R.string.assign_master_key)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
||||
@@ -116,7 +126,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
|
||||
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
rootView?.findViewById<View>(R.id.browse_button)?.setOnClickListener { view ->
|
||||
rootView?.findViewById<View>(R.id.open_database_button)?.setOnClickListener { view ->
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
|
||||
|
||||
val dialog = builder.create()
|
||||
@@ -132,7 +142,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
var error = verifyPassword() || verifyFile()
|
||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||
error = true
|
||||
showNoKeyConfirmationDialog()
|
||||
if (allowNoMasterKey)
|
||||
showNoKeyConfirmationDialog()
|
||||
else {
|
||||
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
|
||||
}
|
||||
}
|
||||
if (!error) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(
|
||||
@@ -193,6 +207,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
showEmptyPasswordConfirmationDialog()
|
||||
}
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
@@ -223,7 +238,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
}
|
||||
}
|
||||
@@ -238,7 +253,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
}
|
||||
}
|
||||
@@ -255,4 +270,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
||||
|
||||
fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment {
|
||||
val fragment = AssignMasterKeyDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class BrowserDialogFragment : DialogFragment() {
|
||||
// Get the layout inflater
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_browser_install, null)
|
||||
builder.setView(root)
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description)
|
||||
textDescription.text = getString(R.string.file_manager_install_description)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class DuplicateUuidDialog : DialogFragment() {
|
||||
|
||||
var positiveAction: (() -> Unit)? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply {
|
||||
val message = getString(R.string.contains_duplicate_uuid) +
|
||||
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
|
||||
setMessage(message)
|
||||
setPositiveButton(getString(android.R.string.ok)) { _, _ ->
|
||||
positiveAction?.invoke()
|
||||
dismiss()
|
||||
}
|
||||
setNegativeButton(getString(android.R.string.cancel)) { _, _ -> dismiss() }
|
||||
}
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
this.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.biometric.FingerPrintAnimatedVector
|
||||
import com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
class FingerPrintExplanationDialog : DialogFragment() {
|
||||
|
||||
private var fingerPrintAnimatedVector: FingerPrintAnimatedVector? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
val inflater = activity.layoutInflater
|
||||
|
||||
val rootView = inflater.inflate(R.layout.fragment_fingerprint_explanation, null)
|
||||
|
||||
rootView.findViewById<View>(R.id.fingerprint_setting_link_text).setOnClickListener {
|
||||
startActivity(Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS))
|
||||
}
|
||||
|
||||
rootView.findViewById<View>(R.id.auto_open_biometric_prompt_button).setOnClickListener {
|
||||
startActivity(Intent(activity, SettingsAdvancedUnlockActivity::class.java))
|
||||
}
|
||||
|
||||
fingerPrintAnimatedVector = FingerPrintAnimatedVector(activity,
|
||||
rootView.findViewById(R.id.biometric_image))
|
||||
|
||||
builder.setView(rootView)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
fingerPrintAnimatedVector?.startScan()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
fingerPrintAnimatedVector?.stopScan()
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
|
||||
dismiss()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
val bundle = Bundle()
|
||||
mListener?.cancelPassword(bundle)
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
editGroupListener?.cancelEditGroup(
|
||||
editGroupDialogAction,
|
||||
nameTextView?.text?.toString(),
|
||||
|
||||
@@ -77,7 +77,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
builder.setNegativeButton(R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class KeyboardExplanationDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(activity!!)
|
||||
val inflater = activity!!.layoutInflater
|
||||
|
||||
val rootView = inflater.inflate(R.layout.fragment_keyboard_explanation, null)
|
||||
|
||||
rootView.findViewById<View>(R.id.keyboards_activate_device_setting_button)
|
||||
.setOnClickListener { launchActivateKeyboardSetting() }
|
||||
|
||||
val containerKeyboardSwitcher = rootView.findViewById<View>(R.id.container_keyboard_switcher)
|
||||
if (BuildConfig.CLOSED_STORE) {
|
||||
containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
|
||||
} else {
|
||||
containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_f_droid) }
|
||||
}
|
||||
|
||||
builder.setView(rootView)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun launchActivateKeyboardSetting() {
|
||||
val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
|
||||
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
||||
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
|
||||
builder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.Spinner
|
||||
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
|
||||
import com.kunzisoft.keepass.model.OtpModel
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_HOTP_COUNTER
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_OTP_DIGITS
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
||||
import com.kunzisoft.keepass.otp.OtpTokenType
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.otp.TokenCalculator
|
||||
|
||||
class SetOTPDialogFragment : DialogFragment() {
|
||||
|
||||
private var mCreateOTPElementListener: CreateOtpListener? = null
|
||||
|
||||
private var mOtpElement: OtpElement = OtpElement()
|
||||
|
||||
private var otpTypeSpinner: Spinner? = null
|
||||
private var otpTokenTypeSpinner: Spinner? = null
|
||||
private var otpSecretContainer: TextInputLayout? = null
|
||||
private var otpSecretTextView: EditText? = null
|
||||
private var otpPeriodContainer: TextInputLayout? = null
|
||||
private var otpPeriodTextView: EditText? = null
|
||||
private var otpCounterContainer: TextInputLayout? = null
|
||||
private var otpCounterTextView: EditText? = null
|
||||
private var otpDigitsContainer: TextInputLayout? = null
|
||||
private var otpDigitsTextView: EditText? = null
|
||||
private var otpAlgorithmSpinner: Spinner? = null
|
||||
|
||||
private var otpTypeAdapter: ArrayAdapter<OtpType>? = null
|
||||
private var otpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
|
||||
|
||||
private var mManualEvent = false
|
||||
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
||||
if (!isFocus)
|
||||
mManualEvent = true
|
||||
}
|
||||
private var mOnTouchListener = View.OnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
mManualEvent = true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
private var mSecretWellFormed = false
|
||||
private var mCounterWellFormed = true
|
||||
private var mPeriodWellFormed = true
|
||||
private var mDigitsWellFormed = true
|
||||
|
||||
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
|
||||
mCreateOTPElementListener = context as CreateOtpListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + CreateOtpListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
// Retrieve OTP model from instance state
|
||||
if (savedInstanceState != null) {
|
||||
if (savedInstanceState.containsKey(KEY_OTP)) {
|
||||
savedInstanceState.getParcelable<OtpModel>(KEY_OTP)?.let { otpModel ->
|
||||
mOtpElement = OtpElement(otpModel)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_OTP)) {
|
||||
getParcelable<OtpModel?>(KEY_OTP)?.let { otpModel ->
|
||||
mOtpElement = OtpElement(otpModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
|
||||
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
|
||||
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
|
||||
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
|
||||
otpSecretTextView = root?.findViewById(R.id.setup_otp_secret)
|
||||
otpAlgorithmSpinner = root?.findViewById(R.id.setup_otp_algorithm)
|
||||
otpPeriodContainer= root?.findViewById(R.id.setup_otp_period_label)
|
||||
otpPeriodTextView = root?.findViewById(R.id.setup_otp_period)
|
||||
otpCounterContainer= root?.findViewById(R.id.setup_otp_counter_label)
|
||||
otpCounterTextView = root?.findViewById(R.id.setup_otp_counter)
|
||||
otpDigitsContainer = root?.findViewById(R.id.setup_otp_digits_label)
|
||||
otpDigitsTextView = root?.findViewById(R.id.setup_otp_digits)
|
||||
|
||||
// To fix init element
|
||||
// With tab keyboard selection
|
||||
otpSecretTextView?.onFocusChangeListener = mOnFocusChangeListener
|
||||
// With finger selection
|
||||
otpTypeSpinner?.setOnTouchListener(mOnTouchListener)
|
||||
otpTokenTypeSpinner?.setOnTouchListener(mOnTouchListener)
|
||||
otpSecretTextView?.setOnTouchListener(mOnTouchListener)
|
||||
otpAlgorithmSpinner?.setOnTouchListener(mOnTouchListener)
|
||||
otpPeriodTextView?.setOnTouchListener(mOnTouchListener)
|
||||
otpCounterTextView?.setOnTouchListener(mOnTouchListener)
|
||||
otpDigitsTextView?.setOnTouchListener(mOnTouchListener)
|
||||
|
||||
|
||||
// HOTP / TOTP Type selection
|
||||
val otpTypeArray = OtpType.values()
|
||||
otpTypeAdapter = ArrayAdapter<OtpType>(activity,
|
||||
android.R.layout.simple_spinner_item, otpTypeArray).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
otpTypeSpinner?.adapter = otpTypeAdapter
|
||||
|
||||
// Otp Token type selection
|
||||
val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
|
||||
hotpTokenTypeAdapter = ArrayAdapter(activity,
|
||||
android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
// Proprietary only on closed and full version
|
||||
val totpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
||||
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
|
||||
totpTokenTypeAdapter = ArrayAdapter(activity,
|
||||
android.R.layout.simple_spinner_item, totpTokenTypeArray).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
otpTokenTypeAdapter = hotpTokenTypeAdapter
|
||||
otpTokenTypeSpinner?.adapter = otpTokenTypeAdapter
|
||||
|
||||
// OTP Algorithm
|
||||
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
|
||||
otpAlgorithmAdapter = ArrayAdapter<TokenCalculator.HashAlgorithm>(activity,
|
||||
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
otpAlgorithmSpinner?.adapter = otpAlgorithmAdapter
|
||||
|
||||
// Set the default value of OTP element
|
||||
upgradeType()
|
||||
upgradeTokenType()
|
||||
upgradeParameters()
|
||||
|
||||
attachListeners()
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.apply {
|
||||
setTitle(R.string.entry_setup_otp)
|
||||
setView(root)
|
||||
.setPositiveButton(android.R.string.ok) {_, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
(dialog as AlertDialog).getButton(Dialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
if (mSecretWellFormed
|
||||
&& mCounterWellFormed
|
||||
&& mPeriodWellFormed
|
||||
&& mDigitsWellFormed) {
|
||||
mCreateOTPElementListener?.onOtpCreated(mOtpElement)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachListeners() {
|
||||
// Set Type listener
|
||||
otpTypeSpinner?.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
if (mManualEvent) {
|
||||
(parent?.selectedItem as OtpType?)?.let {
|
||||
mOtpElement.type = it
|
||||
upgradeTokenType()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set type token listener
|
||||
otpTokenTypeSpinner?.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
if (mManualEvent) {
|
||||
(parent?.selectedItem as OtpTokenType?)?.let {
|
||||
mOtpElement.tokenType = it
|
||||
upgradeParameters()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set algorithm spinner
|
||||
otpAlgorithmSpinner?.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
if (mManualEvent) {
|
||||
(parent?.selectedItem as TokenCalculator.HashAlgorithm?)?.let {
|
||||
mOtpElement.algorithm = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set secret in OtpElement
|
||||
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
s?.toString()?.let { userString ->
|
||||
try {
|
||||
mOtpElement.setBase32Secret(userString)
|
||||
otpSecretContainer?.error = null
|
||||
} catch (exception: Exception) {
|
||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||
}
|
||||
mSecretWellFormed = otpSecretContainer?.error == null
|
||||
}
|
||||
}
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
|
||||
// Set counter in OtpElement
|
||||
otpCounterTextView?.addTextChangedListener(object: TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
if (mManualEvent) {
|
||||
s?.toString()?.toLongOrNull()?.let {
|
||||
try {
|
||||
mOtpElement.counter = it
|
||||
otpCounterContainer?.error = null
|
||||
} catch (exception: Exception) {
|
||||
otpCounterContainer?.error = getString(R.string.error_otp_counter,
|
||||
MIN_HOTP_COUNTER, MAX_HOTP_COUNTER)
|
||||
}
|
||||
mCounterWellFormed = otpCounterContainer?.error == null
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
|
||||
// Set period in OtpElement
|
||||
otpPeriodTextView?.addTextChangedListener(object: TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
if (mManualEvent) {
|
||||
s?.toString()?.toIntOrNull()?.let {
|
||||
try {
|
||||
mOtpElement.period = it
|
||||
otpPeriodContainer?.error = null
|
||||
} catch (exception: Exception) {
|
||||
otpPeriodContainer?.error = getString(R.string.error_otp_period,
|
||||
MIN_TOTP_PERIOD, MAX_TOTP_PERIOD)
|
||||
}
|
||||
mPeriodWellFormed = otpPeriodContainer?.error == null
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
|
||||
// Set digits in OtpElement
|
||||
otpDigitsTextView?.addTextChangedListener(object: TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
if (mManualEvent) {
|
||||
s?.toString()?.toIntOrNull()?.let {
|
||||
try {
|
||||
mOtpElement.digits = it
|
||||
otpDigitsContainer?.error = null
|
||||
} catch (exception: Exception) {
|
||||
otpDigitsContainer?.error = getString(R.string.error_otp_digits,
|
||||
MIN_OTP_DIGITS, MAX_OTP_DIGITS)
|
||||
}
|
||||
mDigitsWellFormed = otpDigitsContainer?.error == null
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
}
|
||||
|
||||
private fun upgradeType() {
|
||||
otpTypeSpinner?.setSelection(OtpType.values().indexOf(mOtpElement.type))
|
||||
}
|
||||
|
||||
private fun upgradeTokenType() {
|
||||
when (mOtpElement.type) {
|
||||
OtpType.HOTP -> {
|
||||
otpPeriodContainer?.visibility = View.GONE
|
||||
otpCounterContainer?.visibility = View.VISIBLE
|
||||
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
|
||||
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
||||
.getHotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
||||
}
|
||||
OtpType.TOTP -> {
|
||||
otpPeriodContainer?.visibility = View.VISIBLE
|
||||
otpCounterContainer?.visibility = View.GONE
|
||||
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
|
||||
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
||||
.getTotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun upgradeParameters() {
|
||||
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
||||
.indexOf(mOtpElement.algorithm))
|
||||
otpSecretTextView?.apply {
|
||||
setText(mOtpElement.getBase32Secret())
|
||||
// Cursor at end
|
||||
setSelection(this.text.length)
|
||||
}
|
||||
otpCounterTextView?.setText(mOtpElement.counter.toString())
|
||||
otpPeriodTextView?.setText(mOtpElement.period.toString())
|
||||
otpDigitsTextView?.setText(mOtpElement.digits.toString())
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putParcelable(KEY_OTP, mOtpElement.otpModel)
|
||||
}
|
||||
|
||||
interface CreateOtpListener {
|
||||
fun onOtpCreated(otpElement: OtpElement)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_OTP = "KEY_OTP"
|
||||
|
||||
fun build(otpModel: OtpModel? = null): SetOTPDialogFragment {
|
||||
return SetOTPDialogFragment().apply {
|
||||
if (otpModel != null) {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(KEY_OTP, otpModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ class SortDialogFragment : DialogFragment() {
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok
|
||||
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
|
||||
// Check if is ascending or descending
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
@@ -11,10 +11,10 @@ object EntrySelectionHelper {
|
||||
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
|
||||
private const val DEFAULT_ENTRY_SELECTION_MODE = false
|
||||
|
||||
fun startActivityForEntrySelection(activity: Activity, intent: Intent) {
|
||||
fun startActivityForEntrySelection(context: Context, intent: Intent) {
|
||||
addEntrySelectionModeExtraInIntent(intent)
|
||||
// only to avoid visible flickering when redirecting
|
||||
activity.startActivity(intent)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun addEntrySelectionModeExtraInIntent(intent: Intent) {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
@@ -26,10 +27,10 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
@@ -39,7 +40,7 @@ class OpenFileHelper {
|
||||
private var fragment: Fragment? = null
|
||||
|
||||
val openFileOnClickViewListener: OpenFileOnClickViewListener
|
||||
get() = OpenFileOnClickViewListener(null)
|
||||
get() = OpenFileOnClickViewListener()
|
||||
|
||||
constructor(context: Activity) {
|
||||
this.activity = context
|
||||
@@ -51,7 +52,7 @@ class OpenFileHelper {
|
||||
this.fragment = context
|
||||
}
|
||||
|
||||
inner class OpenFileOnClickViewListener(private val dataUri: (() -> Uri?)?) : View.OnClickListener {
|
||||
inner class OpenFileOnClickViewListener : View.OnClickListener {
|
||||
|
||||
override fun onClick(v: View) {
|
||||
try {
|
||||
@@ -62,58 +63,50 @@ class OpenFileHelper {
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Enable to start the file picker activity", e)
|
||||
|
||||
// Open File picker if can't open activity
|
||||
if (lookForOpenIntentsFilePicker(dataUri?.invoke()))
|
||||
// Open browser dialog
|
||||
if (lookForOpenIntentsFilePicker())
|
||||
showBrowserDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun openActivityWithActionOpenDocument() {
|
||||
val i = Intent(ACTION_OPEN_DOCUMENT)
|
||||
i.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
i.type = "*/*"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
} else {
|
||||
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(i, OPEN_DOC)
|
||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
else
|
||||
activity?.startActivityForResult(i, OPEN_DOC)
|
||||
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun openActivityWithActionGetContent() {
|
||||
val i = Intent(Intent.ACTION_GET_CONTENT)
|
||||
i.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
i.type = "*/*"
|
||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(i, GET_CONTENT)
|
||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
else
|
||||
activity?.startActivityForResult(i, GET_CONTENT)
|
||||
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
}
|
||||
|
||||
fun getOpenFileOnClickViewListener(dataUri: () -> Uri?): OpenFileOnClickViewListener {
|
||||
return OpenFileOnClickViewListener(dataUri)
|
||||
}
|
||||
|
||||
private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean {
|
||||
private fun lookForOpenIntentsFilePicker(): Boolean {
|
||||
var showBrowser = false
|
||||
try {
|
||||
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
||||
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
|
||||
// Get file path parent if possible
|
||||
if (dataUri != null
|
||||
&& dataUri.toString().isNotEmpty()
|
||||
&& dataUri.scheme == "file") {
|
||||
intent.data = dataUri
|
||||
} else {
|
||||
Log.w(javaClass.name, "Unable to read the URI")
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intent, FILE_BROWSE)
|
||||
else
|
||||
@@ -190,22 +183,19 @@ class OpenFileHelper {
|
||||
GET_CONTENT, OPEN_DOC -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (data != null) {
|
||||
var uri = data.data
|
||||
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)
|
||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// nop
|
||||
}
|
||||
if (requestCode == GET_CONTENT) {
|
||||
uri = UriUtil.translateUri(activity!!, uri)
|
||||
}
|
||||
keyFileCallback?.invoke(uri)
|
||||
}
|
||||
}
|
||||
@@ -220,15 +210,10 @@ class OpenFileHelper {
|
||||
|
||||
private const val TAG = "OpenFileHelper"
|
||||
|
||||
private var ACTION_OPEN_DOCUMENT: String
|
||||
|
||||
init {
|
||||
ACTION_OPEN_DOCUMENT = try {
|
||||
val openDocument = Intent::class.java.getField("ACTION_OPEN_DOCUMENT")
|
||||
openDocument.get(null) as String
|
||||
} catch (e: Exception) {
|
||||
"android.intent.action.OPEN_DOCUMENT"
|
||||
}
|
||||
private var APP_ACTION_OPEN_DOCUMENT: String = try {
|
||||
Intent::class.java.getField("ACTION_OPEN_DOCUMENT").get(null) as String
|
||||
} catch (e: Exception) {
|
||||
"android.intent.action.OPEN_DOCUMENT"
|
||||
}
|
||||
|
||||
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
||||
|
||||
@@ -35,6 +35,7 @@ import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
@@ -199,6 +200,9 @@ fun Activity.lock() {
|
||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||
MagikIME.removeEntry(this)
|
||||
|
||||
// Stop the notification service
|
||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||
|
||||
Log.i(Activity::class.java.name, "Shutdown " + localClassName +
|
||||
" after inactivity or manual lock")
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
|
||||
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
var entryHistoryList: MutableList<EntryVersioned> = ArrayList()
|
||||
var onItemClickListener: ((item: EntryVersioned, position: Int)->Unit)? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder {
|
||||
return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: EntryHistoryViewHolder, position: Int) {
|
||||
val entryHistory = entryHistoryList[position]
|
||||
|
||||
holder.lastModifiedView.text = entryHistory.lastModificationTime.getDateTimeString(context.resources)
|
||||
holder.titleView.text = entryHistory.title
|
||||
holder.usernameView.text = entryHistory.username
|
||||
holder.urlView.text = entryHistory.url
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClickListener?.invoke(entryHistory, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return entryHistoryList.size
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
entryHistoryList.clear()
|
||||
}
|
||||
|
||||
inner class EntryHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var lastModifiedView: TextView = itemView.findViewById(R.id.entry_history_last_modified)
|
||||
var titleView: TextView = itemView.findViewById(R.id.entry_history_title)
|
||||
var usernameView: TextView = itemView.findViewById(R.id.entry_history_username)
|
||||
var urlView: TextView = itemView.findViewById(R.id.entry_history_url)
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.Field
|
||||
var fields: MutableList<Field> = ArrayList()
|
||||
var onItemClickListener: OnItemClickListener? = null
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldViewHolder {
|
||||
val view = inflater.inflate(R.layout.keyboard_popup_fields_item, parent, false)
|
||||
return FieldViewHolder(view)
|
||||
|
||||
@@ -21,27 +21,31 @@ package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||
import android.graphics.Paint
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
|
||||
class NodeAdapter
|
||||
/**
|
||||
* Create node list adapter with contextMenu or not
|
||||
* @param context Context to use
|
||||
*/
|
||||
(private val context: Context, private val menuInflater: MenuInflater)
|
||||
(private val context: Context)
|
||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||
|
||||
private val nodeSortedList: SortedList<NodeVersioned>
|
||||
@@ -61,11 +65,8 @@ class NodeAdapter
|
||||
private var showUserNames: Boolean = true
|
||||
private var showNumberEntries: Boolean = true
|
||||
|
||||
private var actionNodesList = LinkedList<NodeVersioned>()
|
||||
private var nodeClickCallback: NodeClickCallback? = null
|
||||
private var nodeMenuListener: NodeMenuListener? = null
|
||||
private var activateContextMenu: Boolean = false
|
||||
private var readOnly: Boolean = false
|
||||
private var isASearchResult: Boolean = false
|
||||
|
||||
private val mDatabase: Database
|
||||
|
||||
@@ -81,9 +82,6 @@ class NodeAdapter
|
||||
|
||||
init {
|
||||
assignPreferences()
|
||||
this.activateContextMenu = false
|
||||
this.readOnly = false
|
||||
this.isASearchResult = false
|
||||
|
||||
this.nodeSortedList = SortedList(NodeVersioned::class.java, object : SortedListAdapterCallback<NodeVersioned>(this) {
|
||||
override fun compare(item1: NodeVersioned, item2: NodeVersioned): Int {
|
||||
@@ -114,18 +112,6 @@ class NodeAdapter
|
||||
taTextColor.recycle()
|
||||
}
|
||||
|
||||
fun setReadOnly(readOnly: Boolean) {
|
||||
this.readOnly = readOnly
|
||||
}
|
||||
|
||||
fun setIsASearchResult(isASearchResult: Boolean) {
|
||||
this.isASearchResult = isASearchResult
|
||||
}
|
||||
|
||||
fun setActivateContextMenu(activate: Boolean) {
|
||||
this.activateContextMenu = activate
|
||||
}
|
||||
|
||||
private fun assignPreferences() {
|
||||
this.prefTextSize = PreferencesUtil.getListTextSize(context)
|
||||
this.infoTextSize = context.resources.getDimension(R.dimen.list_medium_size_default) * prefTextSize
|
||||
@@ -156,6 +142,7 @@ class NodeAdapter
|
||||
Log.e(TAG, "Can't add node elements to the list", e)
|
||||
Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun contains(node: NodeVersioned): Boolean {
|
||||
@@ -170,6 +157,14 @@ class NodeAdapter
|
||||
nodeSortedList.add(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add nodes to the list
|
||||
* @param nodes Nodes to add
|
||||
*/
|
||||
fun addNodes(nodes: List<NodeVersioned>) {
|
||||
nodeSortedList.addAll(nodes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a node in the list
|
||||
* @param node Node to delete
|
||||
@@ -178,11 +173,35 @@ class NodeAdapter
|
||||
nodeSortedList.remove(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove nodes in the list
|
||||
* @param nodes Nodes to delete
|
||||
*/
|
||||
fun removeNodes(nodes: List<NodeVersioned>) {
|
||||
nodes.forEach { node ->
|
||||
nodeSortedList.remove(node)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a node at [position] in the list
|
||||
*/
|
||||
fun removeNodeAt(position: Int) {
|
||||
nodeSortedList.removeItemAt(position)
|
||||
// Refresh all the next items
|
||||
notifyItemRangeChanged(position, nodeSortedList.size() - position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove nodes in the list by [positions]
|
||||
* Note : algorithm remove the higher position at each iteration
|
||||
*/
|
||||
fun removeNodesAt(positions: IntArray) {
|
||||
val positionsSortDescending = positions.toMutableList()
|
||||
positionsSortDescending.sortDescending()
|
||||
positionsSortDescending.forEach {
|
||||
removeNodeAt(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,6 +216,40 @@ class NodeAdapter
|
||||
nodeSortedList.endBatchedUpdates()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update nodes in the list
|
||||
* @param oldNodes Nodes before the update
|
||||
* @param newNodes Node after the update
|
||||
*/
|
||||
fun updateNodes(oldNodes: List<NodeVersioned>, newNodes: List<NodeVersioned>) {
|
||||
nodeSortedList.beginBatchedUpdates()
|
||||
oldNodes.forEach { oldNode ->
|
||||
nodeSortedList.remove(oldNode)
|
||||
}
|
||||
nodeSortedList.addAll(newNodes)
|
||||
nodeSortedList.endBatchedUpdates()
|
||||
}
|
||||
|
||||
fun notifyNodeChanged(node: NodeVersioned) {
|
||||
notifyItemChanged(nodeSortedList.indexOf(node))
|
||||
}
|
||||
|
||||
fun setActionNodes(actionNodes: List<NodeVersioned>) {
|
||||
this.actionNodesList.apply {
|
||||
clear()
|
||||
addAll(actionNodes)
|
||||
}
|
||||
}
|
||||
|
||||
fun unselectActionNodes() {
|
||||
actionNodesList.forEach {
|
||||
notifyItemChanged(nodeSortedList.indexOf(it))
|
||||
}
|
||||
this.actionNodesList.apply {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify a change sort of the list
|
||||
*/
|
||||
@@ -238,18 +291,28 @@ class NodeAdapter
|
||||
holder.text.apply {
|
||||
text = subNode.title
|
||||
setTextSize(textSizeUnit, infoTextSize)
|
||||
paintFlags = if (subNode.isCurrentlyExpires)
|
||||
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
||||
else
|
||||
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
|
||||
}
|
||||
// Assign click
|
||||
holder.container.setOnClickListener { nodeClickCallback?.onNodeClick(subNode) }
|
||||
// Context menu
|
||||
if (activateContextMenu) {
|
||||
holder.container.setOnCreateContextMenuListener(
|
||||
ContextMenuBuilder(menuInflater, subNode, readOnly, isASearchResult, nodeMenuListener))
|
||||
holder.container.setOnClickListener {
|
||||
nodeClickCallback?.onNodeClick(subNode)
|
||||
}
|
||||
holder.container.setOnLongClickListener {
|
||||
nodeClickCallback?.onNodeLongClick(subNode) ?: false
|
||||
}
|
||||
|
||||
holder.container.isSelected = actionNodesList.contains(subNode)
|
||||
|
||||
// Add subText with username
|
||||
holder.subText.apply {
|
||||
text = ""
|
||||
paintFlags = if (subNode.isCurrentlyExpires)
|
||||
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
||||
else
|
||||
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
|
||||
visibility = View.GONE
|
||||
if (subNode.type == Type.ENTRY) {
|
||||
val entry = subNode as EntryVersioned
|
||||
@@ -294,103 +357,12 @@ class NodeAdapter
|
||||
this.nodeClickCallback = nodeClickCallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a listener when an element of menu is clicked
|
||||
*/
|
||||
fun setNodeMenuListener(nodeMenuListener: NodeMenuListener?) {
|
||||
this.nodeMenuListener = nodeMenuListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
interface NodeClickCallback {
|
||||
fun onNodeClick(node: NodeVersioned)
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu listener to redefine to do an action in menu
|
||||
*/
|
||||
interface NodeMenuListener {
|
||||
fun onOpenMenuClick(node: NodeVersioned): Boolean
|
||||
fun onEditMenuClick(node: NodeVersioned): Boolean
|
||||
fun onCopyMenuClick(node: NodeVersioned): Boolean
|
||||
fun onMoveMenuClick(node: NodeVersioned): Boolean
|
||||
fun onDeleteMenuClick(node: NodeVersioned): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class for menu listener
|
||||
*/
|
||||
private class ContextMenuBuilder(val menuInflater: MenuInflater,
|
||||
val node: NodeVersioned,
|
||||
val readOnly: Boolean,
|
||||
val isASearchResult: Boolean,
|
||||
val menuListener: NodeMenuListener?)
|
||||
: View.OnCreateContextMenuListener {
|
||||
|
||||
private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item ->
|
||||
if (menuListener == null)
|
||||
return@OnMenuItemClickListener false
|
||||
when (item.itemId) {
|
||||
R.id.menu_open -> menuListener.onOpenMenuClick(node)
|
||||
R.id.menu_edit -> menuListener.onEditMenuClick(node)
|
||||
R.id.menu_copy -> menuListener.onCopyMenuClick(node)
|
||||
R.id.menu_move -> menuListener.onMoveMenuClick(node)
|
||||
R.id.menu_delete -> menuListener.onDeleteMenuClick(node)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(contextMenu: ContextMenu?,
|
||||
view: View?,
|
||||
contextMenuInfo: ContextMenu.ContextMenuInfo?) {
|
||||
menuInflater.inflate(R.menu.node_menu, contextMenu)
|
||||
|
||||
// Opening
|
||||
var menuItem = contextMenu?.findItem(R.id.menu_open)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
|
||||
val database = Database.getInstance()
|
||||
|
||||
// Edition
|
||||
if (readOnly || node == database.recycleBin) {
|
||||
contextMenu?.removeItem(R.id.menu_edit)
|
||||
} else {
|
||||
menuItem = contextMenu?.findItem(R.id.menu_edit)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
}
|
||||
|
||||
// Copy (not for group)
|
||||
if (readOnly
|
||||
|| isASearchResult
|
||||
|| node == database.recycleBin
|
||||
|| node.type == Type.GROUP) {
|
||||
// TODO COPY For Group
|
||||
contextMenu?.removeItem(R.id.menu_copy)
|
||||
} else {
|
||||
menuItem = contextMenu?.findItem(R.id.menu_copy)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
}
|
||||
|
||||
// Move
|
||||
if (readOnly
|
||||
|| isASearchResult
|
||||
|| node == database.recycleBin) {
|
||||
contextMenu?.removeItem(R.id.menu_move)
|
||||
} else {
|
||||
menuItem = contextMenu?.findItem(R.id.menu_move)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
}
|
||||
|
||||
// Deletion
|
||||
if (readOnly || node == database.recycleBin) {
|
||||
contextMenu?.removeItem(R.id.menu_delete)
|
||||
} else {
|
||||
menuItem = contextMenu?.findItem(R.id.menu_delete)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
}
|
||||
}
|
||||
fun onNodeLongClick(node: NodeVersioned): Boolean
|
||||
}
|
||||
|
||||
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.kunzisoft.keepass.app.database
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
@@ -15,7 +17,33 @@ data class CipherDatabaseEntity(
|
||||
|
||||
@ColumnInfo(name = "specs_parameters")
|
||||
var specParameters: String
|
||||
) {
|
||||
): Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString()!!,
|
||||
parcel.readString()!!,
|
||||
parcel.readString()!!)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(databaseUri)
|
||||
parcel.writeString(encryptedValue)
|
||||
parcel.writeString(specParameters)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<CipherDatabaseEntity> {
|
||||
override fun createFromParcel(parcel: Parcel): CipherDatabaseEntity {
|
||||
return CipherDatabaseEntity(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<CipherDatabaseEntity?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
@@ -19,7 +19,7 @@ interface FileDatabaseHistoryDao {
|
||||
@Delete
|
||||
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
|
||||
|
||||
@Query("REPLACE INTO file_database_history(keyfile_uri) VALUES(null)")
|
||||
@Query("UPDATE file_database_history SET keyfile_uri=null")
|
||||
fun deleteAllKeyFiles()
|
||||
|
||||
@Query("DELETE FROM file_database_history")
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
@@ -19,12 +21,12 @@ import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
var databaseFileUri: Uri,
|
||||
var advancedUnlockInfoView: AdvancedUnlockInfoView?,
|
||||
var checkboxPasswordView: CompoundButton?,
|
||||
var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null,
|
||||
private var advancedUnlockInfoView: AdvancedUnlockInfoView?,
|
||||
private var checkboxPasswordView: CompoundButton?,
|
||||
private var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null,
|
||||
var passwordView: TextView?,
|
||||
var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit,
|
||||
var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit)
|
||||
private var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit,
|
||||
private var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit)
|
||||
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
|
||||
|
||||
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
|
||||
@@ -39,11 +41,11 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
|
||||
// Check if fingerprint well init (be called the first time the fingerprint is configured
|
||||
// and the activity still active)
|
||||
if (biometricUnlockDatabaseHelper == null || !biometricUnlockDatabaseHelper!!.isFingerprintInitialized) {
|
||||
|
||||
biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context, this)
|
||||
if (biometricUnlockDatabaseHelper == null || !biometricUnlockDatabaseHelper!!.isBiometricInitialized) {
|
||||
biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context)
|
||||
// callback for fingerprint findings
|
||||
biometricUnlockDatabaseHelper?.setAuthenticationCallback(biometricCallback)
|
||||
biometricUnlockDatabaseHelper?.biometricUnlockCallback = this
|
||||
biometricUnlockDatabaseHelper?.authenticationCallback = biometricAuthenticationCallback
|
||||
}
|
||||
|
||||
// Add a check listener to change fingerprint mode
|
||||
@@ -59,7 +61,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun checkBiometricAvailability() {
|
||||
private fun checkBiometricAvailability() {
|
||||
|
||||
// fingerprint not supported (by API level or hardware) so keep option hidden
|
||||
// or manually disable
|
||||
@@ -83,10 +85,10 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
// listen for encryption
|
||||
toggleMode(Mode.STORE)
|
||||
} else {
|
||||
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
|
||||
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
|
||||
|
||||
// fingerprint available but no stored password found yet for this DB so show info don't listen
|
||||
toggleMode( if (it) {
|
||||
toggleMode( if (containsCipher) {
|
||||
// listen for decryption
|
||||
Mode.OPEN
|
||||
} else {
|
||||
@@ -106,35 +108,43 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
}
|
||||
}
|
||||
|
||||
private val biometricCallback = object : BiometricPrompt.AuthenticationCallback () {
|
||||
private val biometricAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback () {
|
||||
|
||||
override fun onAuthenticationError(
|
||||
errorCode: Int,
|
||||
errString: CharSequence) {
|
||||
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
||||
setAdvancedUnlockedMessageView(errString.toString())
|
||||
context.runOnUiThread {
|
||||
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
||||
setAdvancedUnlockedMessageView(errString.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
||||
setAdvancedUnlockedMessageView(R.string.biometric_not_recognized)
|
||||
context.runOnUiThread {
|
||||
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
||||
setAdvancedUnlockedMessageView(R.string.biometric_not_recognized)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
when (biometricMode) {
|
||||
Mode.UNAVAILABLE -> {}
|
||||
Mode.PAUSE -> {}
|
||||
Mode.NOT_CONFIGURED -> {}
|
||||
Mode.WAIT_CREDENTIAL -> {}
|
||||
Mode.STORE -> {
|
||||
// newly store the entered password in encrypted way
|
||||
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
|
||||
}
|
||||
Mode.OPEN -> {
|
||||
// retrieve the encrypted value from preferences
|
||||
cipherDatabaseAction.getCipherDatabase(databaseFileUri) {
|
||||
it?.encryptedValue?.let { value ->
|
||||
biometricUnlockDatabaseHelper?.decryptData(value)
|
||||
context.runOnUiThread {
|
||||
when (biometricMode) {
|
||||
Mode.UNAVAILABLE -> {
|
||||
}
|
||||
Mode.NOT_CONFIGURED -> {
|
||||
}
|
||||
Mode.WAIT_CREDENTIAL -> {
|
||||
}
|
||||
Mode.STORE -> {
|
||||
// newly store the entered password in encrypted way
|
||||
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
|
||||
}
|
||||
Mode.OPEN -> {
|
||||
// retrieve the encrypted value from preferences
|
||||
cipherDatabaseAction.getCipherDatabase(databaseFileUri) {
|
||||
it?.encryptedValue?.let { value ->
|
||||
biometricUnlockDatabaseHelper?.decryptData(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,16 +158,14 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
advancedUnlockInfoView?.setIconViewClickListener(null)
|
||||
}
|
||||
|
||||
private fun initPause() {
|
||||
advancedUnlockInfoView?.setIconViewClickListener(null)
|
||||
}
|
||||
|
||||
private fun initNotConfigured() {
|
||||
showFingerPrintViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
advancedUnlockInfoView?.setIconViewClickListener(null)
|
||||
advancedUnlockInfoView?.setIconViewClickListener {
|
||||
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||
}
|
||||
}
|
||||
|
||||
private fun initWaitData() {
|
||||
@@ -168,6 +176,14 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
advancedUnlockInfoView?.setIconViewClickListener(null)
|
||||
}
|
||||
|
||||
private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
|
||||
cryptoObject: BiometricPrompt.CryptoObject,
|
||||
promptInfo: BiometricPrompt.PromptInfo) {
|
||||
context.runOnUiThread {
|
||||
biometricPrompt?.authenticate(promptInfo, cryptoObject)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEncryptData() {
|
||||
showFingerPrintViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.open_biometric_prompt_store_credential)
|
||||
@@ -178,9 +194,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
cryptoObject?.let { crypto ->
|
||||
// Set listener to open the biometric dialog and save credential
|
||||
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||
context.runOnUiThread {
|
||||
biometricPrompt?.authenticate(promptInfo, crypto)
|
||||
}
|
||||
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,17 +215,13 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
cryptoObject?.let { crypto ->
|
||||
// Set listener to open the biometric dialog and check credential
|
||||
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||
context.runOnUiThread {
|
||||
biometricPrompt?.authenticate(promptInfo, crypto)
|
||||
}
|
||||
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
||||
}
|
||||
|
||||
// Auto open the biometric prompt
|
||||
if (isBiometricPromptAutoOpenEnable) {
|
||||
isBiometricPromptAutoOpenEnable = false
|
||||
context.runOnUiThread {
|
||||
biometricPrompt?.authenticate(promptInfo, crypto)
|
||||
}
|
||||
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +235,6 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
fun initBiometricMode() {
|
||||
when (biometricMode) {
|
||||
Mode.UNAVAILABLE -> initNotAvailable()
|
||||
Mode.PAUSE -> initPause()
|
||||
Mode.NOT_CONFIGURED -> initNotConfigured()
|
||||
Mode.WAIT_CREDENTIAL -> initWaitData()
|
||||
Mode.STORE -> initEncryptData()
|
||||
@@ -235,25 +244,23 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
context.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
biometricMode = Mode.PAUSE
|
||||
initBiometricMode()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
// Restore the checked listener
|
||||
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
|
||||
|
||||
biometricMode = Mode.UNAVAILABLE
|
||||
initBiometricMode()
|
||||
biometricUnlockDatabaseHelper = null
|
||||
}
|
||||
|
||||
// Only to fix multiple fingerprint menu #332
|
||||
private var addBiometricMenuInProgress = false
|
||||
fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) {
|
||||
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
|
||||
if ((biometricMode != Mode.UNAVAILABLE
|
||||
&& biometricMode != Mode.NOT_CONFIGURED) && it)
|
||||
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
||||
if (!addBiometricMenuInProgress) {
|
||||
addBiometricMenuInProgress = true
|
||||
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
|
||||
if ((biometricMode != Mode.UNAVAILABLE && biometricMode != Mode.NOT_CONFIGURED)
|
||||
&& it) {
|
||||
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
||||
addBiometricMenuInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +313,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
UNAVAILABLE, PAUSE, NOT_CONFIGURED, WAIT_CREDENTIAL, STORE, OPEN
|
||||
UNAVAILABLE, NOT_CONFIGURED, WAIT_CREDENTIAL, STORE, OPEN
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -42,8 +42,7 @@ import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
||||
private val biometricUnlockCallback: BiometricUnlockCallback?) {
|
||||
class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
|
||||
private var biometricPrompt: BiometricPrompt? = null
|
||||
|
||||
@@ -54,26 +53,37 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
||||
private var cryptoObject: BiometricPrompt.CryptoObject? = null
|
||||
|
||||
private var isBiometricInit = false
|
||||
private var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null
|
||||
var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null
|
||||
var biometricUnlockCallback: BiometricUnlockCallback? = null
|
||||
|
||||
private val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(context.getString(R.string.biometric_prompt_store_credential_title))
|
||||
.setDescription(context.getString(R.string.biometric_prompt_store_credential_message))
|
||||
//.setDeviceCredentialAllowed(true) TODO device credential
|
||||
.setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||
.build()
|
||||
private val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
||||
setTitle(context.getString(R.string.biometric_prompt_store_credential_title))
|
||||
setDescription(context.getString(R.string.biometric_prompt_store_credential_message))
|
||||
// TODO device credential
|
||||
/*
|
||||
if (keyguardManager?.isDeviceSecure == true)
|
||||
setDeviceCredentialAllowed(true)
|
||||
else
|
||||
*/
|
||||
setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||
}.build()
|
||||
|
||||
private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(context.getString(R.string.biometric_prompt_extract_credential_title))
|
||||
.setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
|
||||
//.setDeviceCredentialAllowed(true)
|
||||
.setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||
.build()
|
||||
private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
||||
setTitle(context.getString(R.string.biometric_prompt_extract_credential_title))
|
||||
setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
|
||||
// TODO device credential
|
||||
/*
|
||||
if (keyguardManager?.isDeviceSecure == true)
|
||||
setDeviceCredentialAllowed(true)
|
||||
else
|
||||
*/
|
||||
setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||
}.build()
|
||||
|
||||
val isFingerprintInitialized: Boolean
|
||||
val isBiometricInitialized: Boolean
|
||||
get() {
|
||||
if (!isBiometricInit && biometricUnlockCallback != null) {
|
||||
biometricUnlockCallback.onBiometricException(Exception("FingerPrint not initialized"))
|
||||
if (!isBiometricInit) {
|
||||
biometricUnlockCallback?.onBiometricException(Exception("Biometric not initialized"))
|
||||
}
|
||||
return isBiometricInit
|
||||
}
|
||||
@@ -103,7 +113,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
||||
}
|
||||
|
||||
private fun getSecretKey(): SecretKey? {
|
||||
if (!isFingerprintInitialized) {
|
||||
if (!isBiometricInitialized) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
@@ -145,7 +155,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
||||
: (biometricPrompt: BiometricPrompt?,
|
||||
cryptoObject: BiometricPrompt.CryptoObject?,
|
||||
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
|
||||
if (!isFingerprintInitialized) {
|
||||
if (!isBiometricInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -158,7 +168,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
||||
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
||||
deleteEntryKey()
|
||||
biometricUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
||||
biometricUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
@@ -170,7 +180,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
||||
}
|
||||
|
||||
fun encryptData(value: String) {
|
||||
if (!isFingerprintInitialized) {
|
||||
if (!isBiometricInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -194,7 +204,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
||||
: (biometricPrompt: BiometricPrompt?,
|
||||
cryptoObject: BiometricPrompt.CryptoObject?,
|
||||
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
|
||||
if (!isFingerprintInitialized) {
|
||||
if (!isBiometricInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -223,7 +233,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
||||
}
|
||||
|
||||
fun decryptData(encryptedValue: String) {
|
||||
if (!isFingerprintInitialized) {
|
||||
if (!isBiometricInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -252,10 +262,6 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
||||
}
|
||||
}
|
||||
|
||||
fun setAuthenticationCallback(authenticationCallback: BiometricPrompt.AuthenticationCallback) {
|
||||
this.authenticationCallback = authenticationCallback
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun initBiometricPrompt() {
|
||||
if (biometricPrompt == null) {
|
||||
@@ -289,22 +295,24 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
||||
* Remove entry key in keystore
|
||||
*/
|
||||
fun deleteEntryKeyInKeystoreForBiometric(context: FragmentActivity,
|
||||
biometricUnlockCallback: BiometricUnlockErrorCallback) {
|
||||
val fingerPrintHelper = BiometricUnlockDatabaseHelper(context, object : BiometricUnlockCallback {
|
||||
biometricCallback: BiometricUnlockErrorCallback) {
|
||||
BiometricUnlockDatabaseHelper(context).apply {
|
||||
biometricUnlockCallback = object : BiometricUnlockCallback {
|
||||
|
||||
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {}
|
||||
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {}
|
||||
|
||||
override fun handleDecryptedResult(decryptedValue: String) {}
|
||||
override fun handleDecryptedResult(decryptedValue: String) {}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
biometricUnlockCallback.onInvalidKeyException(e)
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
biometricCallback.onInvalidKeyException(e)
|
||||
}
|
||||
|
||||
override fun onBiometricException(e: Exception) {
|
||||
biometricCallback.onBiometricException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBiometricException(e: Exception) {
|
||||
biometricUnlockCallback.onBiometricException(e)
|
||||
}
|
||||
})
|
||||
fingerPrintHelper.deleteEntryKey()
|
||||
deleteEntryKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,12 +32,10 @@ class AesKdf internal constructor() : KdfEngine() {
|
||||
|
||||
override val defaultParameters: KdfParameters
|
||||
get() {
|
||||
val p = KdfParameters(uuid)
|
||||
|
||||
p.setParamUUID()
|
||||
p.setUInt32(ParamRounds, DEFAULT_ROUNDS.toLong())
|
||||
|
||||
return p
|
||||
return KdfParameters(uuid).apply {
|
||||
setParamUUID()
|
||||
setUInt32(PARAM_ROUNDS, DEFAULT_ROUNDS.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
override val defaultKeyRounds: Long
|
||||
@@ -54,8 +52,8 @@ class AesKdf internal constructor() : KdfEngine() {
|
||||
@Throws(IOException::class)
|
||||
override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray {
|
||||
var currentMasterKey = masterKey
|
||||
val rounds = p.getUInt64(ParamRounds)
|
||||
var seed = p.getByteArray(ParamSeed)
|
||||
val rounds = p.getUInt64(PARAM_ROUNDS)
|
||||
var seed = p.getByteArray(PARAM_SEED)
|
||||
|
||||
if (currentMasterKey.size != 32) {
|
||||
currentMasterKey = CryptoUtil.hashSha256(currentMasterKey)
|
||||
@@ -75,15 +73,15 @@ class AesKdf internal constructor() : KdfEngine() {
|
||||
val seed = ByteArray(32)
|
||||
random.nextBytes(seed)
|
||||
|
||||
p.setByteArray(ParamSeed, seed)
|
||||
p.setByteArray(PARAM_SEED, seed)
|
||||
}
|
||||
|
||||
override fun getKeyRounds(p: KdfParameters): Long {
|
||||
return p.getUInt64(ParamRounds)
|
||||
return p.getUInt64(PARAM_ROUNDS)
|
||||
}
|
||||
|
||||
override fun setKeyRounds(p: KdfParameters, keyRounds: Long) {
|
||||
p.setUInt64(ParamRounds, keyRounds)
|
||||
p.setUInt64(PARAM_ROUNDS, keyRounds)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -91,9 +89,24 @@ class AesKdf internal constructor() : KdfEngine() {
|
||||
private const val DEFAULT_ROUNDS = 6000
|
||||
|
||||
val CIPHER_UUID: UUID = Types.bytestoUUID(
|
||||
byteArrayOf(0xC9.toByte(), 0xD9.toByte(), 0xF3.toByte(), 0x9A.toByte(), 0x62.toByte(), 0x8A.toByte(), 0x44.toByte(), 0x60.toByte(), 0xBF.toByte(), 0x74.toByte(), 0x0D.toByte(), 0x08.toByte(), 0xC1.toByte(), 0x8A.toByte(), 0x4F.toByte(), 0xEA.toByte()))
|
||||
byteArrayOf(0xC9.toByte(),
|
||||
0xD9.toByte(),
|
||||
0xF3.toByte(),
|
||||
0x9A.toByte(),
|
||||
0x62.toByte(),
|
||||
0x8A.toByte(),
|
||||
0x44.toByte(),
|
||||
0x60.toByte(),
|
||||
0xBF.toByte(),
|
||||
0x74.toByte(),
|
||||
0x0D.toByte(),
|
||||
0x08.toByte(),
|
||||
0xC1.toByte(),
|
||||
0x8A.toByte(),
|
||||
0x4F.toByte(),
|
||||
0xEA.toByte()))
|
||||
|
||||
const val ParamRounds = "R"
|
||||
const val ParamSeed = "S"
|
||||
const val PARAM_ROUNDS = "R"
|
||||
const val PARAM_SEED = "S"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,16 +33,16 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
||||
val p = KdfParameters(uuid)
|
||||
|
||||
p.setParamUUID()
|
||||
p.setUInt32(ParamParallelism, DefaultParallelism)
|
||||
p.setUInt64(ParamMemory, DefaultMemory)
|
||||
p.setUInt64(ParamIterations, DefaultIterations)
|
||||
p.setUInt32(ParamVersion, MaxVersion)
|
||||
p.setUInt32(PARAM_PARALLELISM, DEFAULT_PARALLELISM)
|
||||
p.setUInt64(PARAM_MEMORY, DEFAULT_MEMORY)
|
||||
p.setUInt64(PARAM_ITERATIONS, DEFAULT_ITERATIONS)
|
||||
p.setUInt32(PARAM_VERSION, MAX_VERSION)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
override val defaultKeyRounds: Long
|
||||
get() = DefaultIterations
|
||||
get() = DEFAULT_ITERATIONS
|
||||
|
||||
init {
|
||||
uuid = CIPHER_UUID
|
||||
@@ -55,13 +55,13 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
||||
@Throws(IOException::class)
|
||||
override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray {
|
||||
|
||||
val salt = p.getByteArray(ParamSalt)
|
||||
val parallelism = p.getUInt32(ParamParallelism).toInt()
|
||||
val memory = p.getUInt64(ParamMemory)
|
||||
val iterations = p.getUInt64(ParamIterations)
|
||||
val version = p.getUInt32(ParamVersion)
|
||||
val secretKey = p.getByteArray(ParamSecretKey)
|
||||
val assocData = p.getByteArray(ParamAssocData)
|
||||
val salt = p.getByteArray(PARAM_SALT)
|
||||
val parallelism = p.getUInt32(PARAM_PARALLELISM).toInt()
|
||||
val memory = p.getUInt64(PARAM_MEMORY)
|
||||
val iterations = p.getUInt64(PARAM_ITERATIONS)
|
||||
val version = p.getUInt32(PARAM_VERSION)
|
||||
val secretKey = p.getByteArray(PARAM_SECRET_KEY)
|
||||
val assocData = p.getByteArray(PARAM_ASSOC_DATA)
|
||||
|
||||
return Argon2Native.transformKey(masterKey, salt, parallelism, memory, iterations,
|
||||
secretKey, assocData, version)
|
||||
@@ -73,71 +73,102 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
||||
val salt = ByteArray(32)
|
||||
random.nextBytes(salt)
|
||||
|
||||
p.setByteArray(ParamSalt, salt)
|
||||
p.setByteArray(PARAM_SALT, salt)
|
||||
}
|
||||
|
||||
override fun getKeyRounds(p: KdfParameters): Long {
|
||||
return p.getUInt64(ParamIterations)
|
||||
return p.getUInt64(PARAM_ITERATIONS)
|
||||
}
|
||||
|
||||
override fun setKeyRounds(p: KdfParameters, keyRounds: Long) {
|
||||
p.setUInt64(ParamIterations, keyRounds)
|
||||
p.setUInt64(PARAM_ITERATIONS, keyRounds)
|
||||
}
|
||||
|
||||
override val minKeyRounds: Long
|
||||
get() = MIN_ITERATIONS
|
||||
|
||||
override val maxKeyRounds: Long
|
||||
get() = MAX_ITERATIONS
|
||||
|
||||
override fun getMemoryUsage(p: KdfParameters): Long {
|
||||
return p.getUInt64(ParamMemory)
|
||||
return p.getUInt64(PARAM_MEMORY)
|
||||
}
|
||||
|
||||
override fun setMemoryUsage(p: KdfParameters, memory: Long) {
|
||||
p.setUInt64(ParamMemory, memory)
|
||||
p.setUInt64(PARAM_MEMORY, memory)
|
||||
}
|
||||
|
||||
override fun getDefaultMemoryUsage(): Long {
|
||||
return DefaultMemory
|
||||
}
|
||||
override val defaultMemoryUsage: Long
|
||||
get() = DEFAULT_MEMORY
|
||||
|
||||
override val minMemoryUsage: Long
|
||||
get() = MIN_MEMORY
|
||||
|
||||
override val maxMemoryUsage: Long
|
||||
get() = MAX_MEMORY
|
||||
|
||||
override fun getParallelism(p: KdfParameters): Int {
|
||||
return p.getUInt32(ParamParallelism).toInt() // TODO Verify
|
||||
return p.getUInt32(PARAM_PARALLELISM).toInt() // TODO Verify
|
||||
}
|
||||
|
||||
override fun setParallelism(p: KdfParameters, parallelism: Int) {
|
||||
p.setUInt32(ParamParallelism, parallelism.toLong())
|
||||
p.setUInt32(PARAM_PARALLELISM, parallelism.toLong())
|
||||
}
|
||||
|
||||
override fun getDefaultParallelism(): Int {
|
||||
return DefaultParallelism.toInt() // TODO Verify
|
||||
}
|
||||
override val defaultParallelism: Int
|
||||
get() = DEFAULT_PARALLELISM.toInt()
|
||||
|
||||
override val minParallelism: Int
|
||||
get() = MIN_PARALLELISM
|
||||
|
||||
override val maxParallelism: Int
|
||||
get() = MAX_PARALLELISM
|
||||
|
||||
companion object {
|
||||
|
||||
val CIPHER_UUID: UUID = Types.bytestoUUID(
|
||||
byteArrayOf(0xEF.toByte(), 0x63.toByte(), 0x6D.toByte(), 0xDF.toByte(), 0x8C.toByte(), 0x29.toByte(), 0x44.toByte(), 0x4B.toByte(), 0x91.toByte(), 0xF7.toByte(), 0xA9.toByte(), 0xA4.toByte(), 0x03.toByte(), 0xE3.toByte(), 0x0A.toByte(), 0x0C.toByte()))
|
||||
byteArrayOf(0xEF.toByte(),
|
||||
0x63.toByte(),
|
||||
0x6D.toByte(),
|
||||
0xDF.toByte(),
|
||||
0x8C.toByte(),
|
||||
0x29.toByte(),
|
||||
0x44.toByte(),
|
||||
0x4B.toByte(),
|
||||
0x91.toByte(),
|
||||
0xF7.toByte(),
|
||||
0xA9.toByte(),
|
||||
0xA4.toByte(),
|
||||
0x03.toByte(),
|
||||
0xE3.toByte(),
|
||||
0x0A.toByte(),
|
||||
0x0C.toByte()))
|
||||
|
||||
private const val ParamSalt = "S" // byte[]
|
||||
private const val ParamParallelism = "P" // UInt32
|
||||
private const val ParamMemory = "M" // UInt64
|
||||
private const val ParamIterations = "I" // UInt64
|
||||
private const val ParamVersion = "V" // UInt32
|
||||
private const val ParamSecretKey = "K" // byte[]
|
||||
private const val ParamAssocData = "A" // byte[]
|
||||
private const val PARAM_SALT = "S" // byte[]
|
||||
private const val PARAM_PARALLELISM = "P" // UInt32
|
||||
private const val PARAM_MEMORY = "M" // UInt64
|
||||
private const val PARAM_ITERATIONS = "I" // UInt64
|
||||
private const val PARAM_VERSION = "V" // UInt32
|
||||
private const val PARAM_SECRET_KEY = "K" // byte[]
|
||||
private const val PARAM_ASSOC_DATA = "A" // byte[]
|
||||
|
||||
private const val MinVersion: Long = 0x10
|
||||
private const val MaxVersion: Long = 0x13
|
||||
private const val MIN_VERSION: Long = 0x10
|
||||
private const val MAX_VERSION: Long = 0x13
|
||||
|
||||
private const val MinSalt = 8
|
||||
private const val MaxSalt = Integer.MAX_VALUE
|
||||
private const val MIN_SALT = 8
|
||||
private const val MAX_SALT = Integer.MAX_VALUE
|
||||
|
||||
private const val MinIterations: Long = 1
|
||||
private const val MaxIterations = 4294967295L
|
||||
private const val MIN_ITERATIONS: Long = 1
|
||||
private const val MAX_ITERATIONS = 4294967295L
|
||||
|
||||
private const val MinMemory = (1024 * 8).toLong()
|
||||
private const val MaxMemory = Integer.MAX_VALUE.toLong()
|
||||
private const val MIN_MEMORY = (1024 * 8).toLong()
|
||||
private const val MAX_MEMORY = Integer.MAX_VALUE.toLong()
|
||||
|
||||
private const val MinParallelism = 1
|
||||
private const val MaxParallelism = (1 shl 24) - 1
|
||||
private const val MIN_PARALLELISM = 1
|
||||
private const val MAX_PARALLELISM = (1 shl 24) - 1
|
||||
|
||||
private const val DefaultIterations: Long = 2
|
||||
private const val DefaultMemory = (1024 * 1024).toLong()
|
||||
private const val DefaultParallelism: Long = 2
|
||||
private const val DEFAULT_ITERATIONS: Long = 2
|
||||
private const val DEFAULT_MEMORY = (1024 * 1024).toLong()
|
||||
private const val DEFAULT_PARALLELISM: Long = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,28 +19,44 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
||||
|
||||
import com.kunzisoft.keepass.database.ObjectNameResource
|
||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
abstract class KdfEngine : ObjectNameResource {
|
||||
// TODO Parcelable
|
||||
abstract class KdfEngine : ObjectNameResource, Serializable {
|
||||
|
||||
var uuid: UUID? = null
|
||||
|
||||
abstract val defaultParameters: KdfParameters
|
||||
|
||||
abstract val defaultKeyRounds: Long
|
||||
|
||||
@Throws(IOException::class)
|
||||
abstract fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray
|
||||
|
||||
abstract fun randomize(p: KdfParameters)
|
||||
|
||||
/*
|
||||
* ITERATIONS
|
||||
*/
|
||||
|
||||
abstract fun getKeyRounds(p: KdfParameters): Long
|
||||
|
||||
abstract fun setKeyRounds(p: KdfParameters, keyRounds: Long)
|
||||
|
||||
abstract val defaultKeyRounds: Long
|
||||
|
||||
open val minKeyRounds: Long
|
||||
get() = 1
|
||||
|
||||
open val maxKeyRounds: Long
|
||||
get() = Int.MAX_VALUE.toLong()
|
||||
|
||||
/*
|
||||
* MEMORY
|
||||
*/
|
||||
|
||||
open fun getMemoryUsage(p: KdfParameters): Long {
|
||||
return UNKNOWN_VALUE.toLong()
|
||||
}
|
||||
@@ -49,9 +65,18 @@ abstract class KdfEngine : ObjectNameResource {
|
||||
// Do nothing by default
|
||||
}
|
||||
|
||||
open fun getDefaultMemoryUsage(): Long {
|
||||
return UNKNOWN_VALUE.toLong()
|
||||
}
|
||||
open val defaultMemoryUsage: Long
|
||||
get() = UNKNOWN_VALUE.toLong()
|
||||
|
||||
open val minMemoryUsage: Long
|
||||
get() = 1
|
||||
|
||||
open val maxMemoryUsage: Long
|
||||
get() = Int.MAX_VALUE.toLong()
|
||||
|
||||
/*
|
||||
* PARALLELISM
|
||||
*/
|
||||
|
||||
open fun getParallelism(p: KdfParameters): Int {
|
||||
return UNKNOWN_VALUE
|
||||
@@ -61,13 +86,16 @@ abstract class KdfEngine : ObjectNameResource {
|
||||
// Do nothing by default
|
||||
}
|
||||
|
||||
open fun getDefaultParallelism(): Int {
|
||||
return UNKNOWN_VALUE
|
||||
}
|
||||
open val defaultParallelism: Int
|
||||
get() = UNKNOWN_VALUE
|
||||
|
||||
open val minParallelism: Int
|
||||
get() = 1
|
||||
|
||||
open val maxParallelism: Int
|
||||
get() = Int.MAX_VALUE
|
||||
|
||||
companion object {
|
||||
|
||||
const val UNKNOWN_VALUE = -1
|
||||
const val UNKNOWN_VALUE_STRING = (-1).toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,37 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
||||
|
||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||
|
||||
import java.util.ArrayList
|
||||
|
||||
object KdfFactory {
|
||||
|
||||
var aesKdf = AesKdf()
|
||||
var argon2Kdf = Argon2Kdf()
|
||||
|
||||
var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||
var kdfListV4: MutableList<KdfEngine> = ArrayList()
|
||||
|
||||
init {
|
||||
kdfListV3.add(aesKdf)
|
||||
|
||||
kdfListV4.add(aesKdf)
|
||||
kdfListV4.add(argon2Kdf)
|
||||
}
|
||||
|
||||
@Throws(UnknownKDF::class)
|
||||
fun getEngineV4(kdfParameters: KdfParameters?): KdfEngine {
|
||||
val unknownKDFException = UnknownKDF()
|
||||
if (kdfParameters == null) {
|
||||
throw unknownKDFException
|
||||
}
|
||||
for (engine in kdfListV4) {
|
||||
if (engine.uuid == kdfParameters.uuid) {
|
||||
return engine
|
||||
}
|
||||
}
|
||||
throw unknownKDFException
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ enum class SortNodeEnum {
|
||||
|
||||
override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int {
|
||||
return object1.creationTime.date
|
||||
?.compareTo(object2.creationTime.date) ?: 0
|
||||
.compareTo(object2.creationTime.date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ enum class SortNodeEnum {
|
||||
|
||||
override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int {
|
||||
return object1.lastModificationTime.date
|
||||
?.compareTo(object2.lastModificationTime.date) ?: 0
|
||||
.compareTo(object2.lastModificationTime.date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ enum class SortNodeEnum {
|
||||
|
||||
override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int {
|
||||
return object1.lastAccessTime.date
|
||||
?.compareTo(object2.lastAccessTime.date) ?: 0
|
||||
.compareTo(object2.lastAccessTime.date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,25 +21,23 @@ package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import java.io.IOException
|
||||
|
||||
open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
||||
open class AssignPasswordInDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
protected val mDatabaseUri: Uri,
|
||||
withMasterPassword: Boolean,
|
||||
masterPassword: String?,
|
||||
withKeyFile: Boolean,
|
||||
keyFile: Uri?,
|
||||
save: Boolean,
|
||||
actionRunnable: ActionRunnable? = null)
|
||||
: SaveDatabaseRunnable(context, database, save, actionRunnable) {
|
||||
save: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, save) {
|
||||
|
||||
private var mMasterPassword: String? = null
|
||||
private var mKeyFile: Uri? = null
|
||||
protected var mKeyFile: Uri? = null
|
||||
|
||||
private var mBackupKey: ByteArray? = null
|
||||
|
||||
@@ -50,7 +48,7 @@ open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
||||
this.mKeyFile = keyFile
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
override fun onStartRun() {
|
||||
// Set key
|
||||
try {
|
||||
// TODO move master key methods
|
||||
@@ -59,20 +57,21 @@ open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
||||
|
||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFile)
|
||||
database.retrieveMasterKey(mMasterPassword, uriInputStream)
|
||||
|
||||
// To save the database
|
||||
super.run()
|
||||
finishRun(true)
|
||||
} catch (e: InvalidKeyFileException) {
|
||||
} catch (e: Exception) {
|
||||
erase(mBackupKey)
|
||||
finishRun(false, e.message)
|
||||
} catch (e: IOException) {
|
||||
erase(mBackupKey)
|
||||
finishRun(false, e.message)
|
||||
setError(e.message)
|
||||
}
|
||||
|
||||
super.onStartRun()
|
||||
}
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
override fun onFinishRun() {
|
||||
super.onFinishRun()
|
||||
|
||||
// Erase the biometric
|
||||
CipherDatabaseAction.getInstance(context)
|
||||
.deleteByDatabaseUri(mDatabaseUri)
|
||||
|
||||
if (!result.isSuccess) {
|
||||
// Erase the current master key
|
||||
erase(database.masterKey)
|
||||
@@ -80,8 +79,6 @@ open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
||||
database.masterKey = it
|
||||
}
|
||||
}
|
||||
|
||||
super.onFinishRun(result)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,38 +21,45 @@ package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class CreateDatabaseRunnable(context: Context,
|
||||
private val mDatabaseUri: Uri,
|
||||
private val mDatabase: Database,
|
||||
databaseUri: Uri,
|
||||
withMasterPassword: Boolean,
|
||||
masterPassword: String?,
|
||||
withKeyFile: Boolean,
|
||||
keyFile: Uri?,
|
||||
save: Boolean,
|
||||
actionRunnable: ActionRunnable? = null)
|
||||
: AssignPasswordInDatabaseRunnable(context, mDatabase, withMasterPassword, masterPassword, withKeyFile, keyFile, save, actionRunnable) {
|
||||
save: Boolean)
|
||||
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile, save) {
|
||||
|
||||
override fun run() {
|
||||
override fun onStartRun() {
|
||||
try {
|
||||
// Create new database record
|
||||
mDatabase.apply {
|
||||
createData(mDatabaseUri)
|
||||
// Set Database state
|
||||
loaded = true
|
||||
// Commit changes
|
||||
super.run()
|
||||
}
|
||||
|
||||
finishRun(true)
|
||||
} catch (e: Exception) {
|
||||
|
||||
mDatabase.closeAndClear()
|
||||
finishRun(false, e.message)
|
||||
setError(e.message)
|
||||
}
|
||||
|
||||
super.onStartRun()
|
||||
}
|
||||
|
||||
override fun onFinishRun(result: Result) {}
|
||||
override fun onFinishRun() {
|
||||
super.onFinishRun()
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Add database to recent files
|
||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||
.addOrUpdateDatabaseUri(mDatabaseUri, mKeyFile)
|
||||
} else {
|
||||
Log.e("CreateDatabaseRunnable", "Unable to create the database")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,116 +21,79 @@ package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.annotation.StringRes
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class LoadDatabaseRunnable(private val mWeakContext: WeakReference<Context>,
|
||||
class LoadDatabaseRunnable(private val context: Context,
|
||||
private val mDatabase: Database,
|
||||
private val mUri: Uri,
|
||||
private val mPass: String?,
|
||||
private val mKey: Uri?,
|
||||
private val mReadonly: Boolean,
|
||||
private val mCipherEntity: CipherDatabaseEntity?,
|
||||
private val mOmitBackup: Boolean,
|
||||
private val mFixDuplicateUUID: Boolean,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||
nestedAction: ActionRunnable)
|
||||
: ActionRunnable(nestedAction, executeNestedActionIfResultFalse = true) {
|
||||
private val mDuplicateUuidAction: ((Result) -> Unit)?)
|
||||
: ActionRunnable() {
|
||||
|
||||
private val mRememberKeyFile: Boolean
|
||||
get() {
|
||||
return mWeakContext.get()?.let {
|
||||
PreferenceManager.getDefaultSharedPreferences(it)
|
||||
.getBoolean(it.getString(R.string.keyfile_key),
|
||||
it.resources.getBoolean(R.bool.keyfile_default))
|
||||
} ?: true
|
||||
}
|
||||
private val cacheDirectory = context.applicationContext.filesDir
|
||||
|
||||
override fun run() {
|
||||
override fun onStartRun() {
|
||||
// Clear before we load
|
||||
mDatabase.closeAndClear(cacheDirectory)
|
||||
}
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
mWeakContext.get()?.let {
|
||||
mDatabase.loadData(it, mUri, mPass, mKey, progressTaskUpdater)
|
||||
saveFileData(mUri, mKey)
|
||||
finishRun(true)
|
||||
} ?: finishRun(false, "Context null")
|
||||
} catch (e: ArcFourException) {
|
||||
catchError(e, R.string.error_arc4)
|
||||
return
|
||||
} catch (e: InvalidPasswordException) {
|
||||
catchError(e, R.string.invalid_password)
|
||||
return
|
||||
} catch (e: ContentFileNotFoundException) {
|
||||
catchError(e, R.string.file_not_found_content)
|
||||
return
|
||||
} catch (e: FileNotFoundException) {
|
||||
catchError(e, R.string.file_not_found)
|
||||
return
|
||||
} catch (e: IOException) {
|
||||
var messageId = R.string.error_load_database
|
||||
e.message?.let {
|
||||
if (it.contains("Hash failed with code"))
|
||||
messageId = R.string.error_load_database_KDF_memory
|
||||
mDatabase.loadData(mUri, mPass, mKey,
|
||||
mReadonly,
|
||||
context.contentResolver,
|
||||
cacheDirectory,
|
||||
mOmitBackup,
|
||||
mFixDuplicateUUID,
|
||||
progressTaskUpdater)
|
||||
}
|
||||
catch (e: LoadDatabaseDuplicateUuidException) {
|
||||
mDuplicateUuidAction?.invoke(result)
|
||||
setError(e)
|
||||
}
|
||||
catch (e: LoadDatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishRun() {
|
||||
if (result.isSuccess) {
|
||||
// Save keyFile in app database
|
||||
val rememberKeyFile = PreferencesUtil.rememberKeyFiles(context)
|
||||
if (rememberKeyFile) {
|
||||
var keyUri = mKey
|
||||
if (!rememberKeyFile) {
|
||||
keyUri = null
|
||||
}
|
||||
FileDatabaseHistoryAction.getInstance(context)
|
||||
.addOrUpdateDatabaseUri(mUri, keyUri)
|
||||
}
|
||||
catchError(e, messageId, true)
|
||||
return
|
||||
} catch (e: KeyFileEmptyException) {
|
||||
catchError(e, R.string.keyfile_is_empty)
|
||||
return
|
||||
} catch (e: InvalidAlgorithmException) {
|
||||
catchError(e, R.string.invalid_algorithm)
|
||||
return
|
||||
} catch (e: InvalidKeyFileException) {
|
||||
catchError(e, R.string.keyfile_does_not_exist)
|
||||
return
|
||||
} catch (e: InvalidDBSignatureException) {
|
||||
catchError(e, R.string.invalid_db_sig)
|
||||
return
|
||||
} catch (e: InvalidDBVersionException) {
|
||||
catchError(e, R.string.unsupported_db_version)
|
||||
return
|
||||
} catch (e: InvalidDBException) {
|
||||
catchError(e, R.string.error_invalid_db)
|
||||
return
|
||||
} catch (e: OutOfMemoryError) {
|
||||
catchError(e, R.string.error_out_of_memory)
|
||||
return
|
||||
} catch (e: Exception) {
|
||||
catchError(e, R.string.error_load_database, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private fun catchError(e: Throwable, @StringRes messageId: Int, addThrowableMessage: Boolean = false) {
|
||||
var errorMessage = mWeakContext.get()?.getString(messageId)
|
||||
Log.e(TAG, errorMessage, e)
|
||||
if (addThrowableMessage)
|
||||
errorMessage = errorMessage + " " + e.localizedMessage
|
||||
finishRun(false, errorMessage)
|
||||
}
|
||||
// Register the biometric
|
||||
mCipherEntity?.let { cipherDatabaseEntity ->
|
||||
CipherDatabaseAction.getInstance(context)
|
||||
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called
|
||||
}
|
||||
|
||||
private fun saveFileData(uri: Uri, key: Uri?) {
|
||||
var keyFileUri = key
|
||||
if (!mRememberKeyFile) {
|
||||
keyFileUri = null
|
||||
// Start the opening notification
|
||||
DatabaseOpenNotificationService.startIfAllowed(context)
|
||||
} else {
|
||||
mDatabase.closeAndClear(cacheDirectory)
|
||||
}
|
||||
mWeakContext.get()?.let {
|
||||
FileDatabaseHistoryAction.getInstance(it).addOrUpdateDatabaseUri(uri, keyFileUri)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
if (!result.isSuccess) {
|
||||
mDatabase.closeAndClear(mWeakContext.get()?.filesDir)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = LoadDatabaseRunnable::class.java.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
|
||||
class ProgressDialogSaveDatabaseThread(activity: FragmentActivity,
|
||||
actionRunnable: (ProgressTaskUpdater?)-> ActionRunnable)
|
||||
: ProgressDialogThread(activity,
|
||||
actionRunnable,
|
||||
R.string.saving_database,
|
||||
null,
|
||||
R.string.do_not_kill_app)
|
||||
@@ -1,86 +1,464 @@
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.AsyncTask
|
||||
import android.content.*
|
||||
import android.content.Context.BIND_ABOVE_CLIENT
|
||||
import android.content.Context.BIND_NOT_FOREGROUND
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.annotation.StringRes
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_TASK_TITLE_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_COLOR_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_COMPRESSION_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_DEFAULT_USERNAME_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_DESCRIPTION_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_ENCRYPTION_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_ITERATIONS_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_KEY_DERIVATION_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_MAX_HISTORY_ITEMS_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_MAX_HISTORY_SIZE_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_NAME_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_PARALLELISM_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.retrieveProgressDialog
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
|
||||
open class ProgressDialogThread(private val activity: FragmentActivity,
|
||||
private val actionRunnable: (ProgressTaskUpdater?)-> ActionRunnable,
|
||||
@StringRes private val titleId: Int,
|
||||
@StringRes private val messageId: Int? = null,
|
||||
@StringRes private val warningId: Int? = null) {
|
||||
|
||||
private val progressTaskDialogFragment = ProgressTaskDialogFragment.build(
|
||||
titleId,
|
||||
messageId,
|
||||
warningId)
|
||||
private var actionRunnableAsyncTask: ActionRunnableAsyncTask? = null
|
||||
var actionFinishInUIThread: ActionRunnable? = null
|
||||
|
||||
private var intentDatabaseTask:Intent = Intent(activity, DatabaseTaskNotificationService::class.java)
|
||||
|
||||
init {
|
||||
actionRunnableAsyncTask = ActionRunnableAsyncTask(progressTaskDialogFragment,
|
||||
{
|
||||
activity.runOnUiThread {
|
||||
intentDatabaseTask.putExtra(DATABASE_TASK_TITLE_KEY, titleId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity.startForegroundService(intentDatabaseTask)
|
||||
} else {
|
||||
activity.startService(intentDatabaseTask)
|
||||
}
|
||||
TimeoutHelper.temporarilyDisableTimeout()
|
||||
// Show the dialog
|
||||
ProgressTaskDialogFragment.start(activity, progressTaskDialogFragment)
|
||||
}
|
||||
}, { result ->
|
||||
activity.runOnUiThread {
|
||||
actionFinishInUIThread?.onFinishRun(result)
|
||||
// Remove the progress task
|
||||
ProgressTaskDialogFragment.stop(activity)
|
||||
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity)
|
||||
activity.stopService(intentDatabaseTask)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun start() {
|
||||
actionRunnableAsyncTask?.execute(actionRunnable)
|
||||
}
|
||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
private class ActionRunnableAsyncTask(private val progressTaskUpdater: ProgressTaskUpdater,
|
||||
private val onPreExecute: () -> Unit,
|
||||
private val onPostExecute: (result: ActionRunnable.Result) -> Unit)
|
||||
: AsyncTask<((ProgressTaskUpdater?)-> ActionRunnable), Void, ActionRunnable.Result>() {
|
||||
class ProgressDialogThread(private val activity: FragmentActivity,
|
||||
var onActionFinish: (actionTask: String,
|
||||
result: ActionRunnable.Result) -> Unit) {
|
||||
|
||||
override fun onPreExecute() {
|
||||
super.onPreExecute()
|
||||
onPreExecute.invoke()
|
||||
private var intentDatabaseTask = Intent(activity, DatabaseTaskNotificationService::class.java)
|
||||
|
||||
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
||||
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
||||
|
||||
private var serviceConnection: ServiceConnection? = null
|
||||
|
||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
TimeoutHelper.temporarilyDisableTimeout(activity)
|
||||
startOrUpdateDialog(titleId, messageId, warningId)
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg actionRunnables: ((ProgressTaskUpdater?)-> ActionRunnable)?): ActionRunnable.Result {
|
||||
var resultTask = ActionRunnable.Result(false)
|
||||
actionRunnables.forEach {
|
||||
it?.invoke(progressTaskUpdater)?.apply {
|
||||
run()
|
||||
resultTask = result
|
||||
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
TimeoutHelper.temporarilyDisableTimeout(activity)
|
||||
startOrUpdateDialog(titleId, messageId, warningId)
|
||||
}
|
||||
|
||||
override fun onStopAction(actionTask: String, result: ActionRunnable.Result) {
|
||||
onActionFinish.invoke(actionTask, result)
|
||||
// Remove the progress task
|
||||
ProgressTaskDialogFragment.stop(activity)
|
||||
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startOrUpdateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
var progressTaskDialogFragment = retrieveProgressDialog(activity)
|
||||
if (progressTaskDialogFragment == null) {
|
||||
progressTaskDialogFragment = ProgressTaskDialogFragment.build()
|
||||
ProgressTaskDialogFragment.start(activity, progressTaskDialogFragment)
|
||||
}
|
||||
progressTaskDialogFragment.apply {
|
||||
titleId?.let {
|
||||
updateTitle(it)
|
||||
}
|
||||
messageId?.let {
|
||||
updateMessage(it)
|
||||
}
|
||||
warningId?.let {
|
||||
updateWarning(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun initServiceConnection() {
|
||||
if (serviceConnection == null) {
|
||||
serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder).apply {
|
||||
addActionTaskListener(actionTaskListener)
|
||||
getService().checkAction()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||
mBinder = null
|
||||
}
|
||||
}
|
||||
return resultTask
|
||||
}
|
||||
|
||||
override fun onPostExecute(result: ActionRunnable.Result) {
|
||||
super.onPostExecute(result)
|
||||
onPostExecute.invoke(result)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun bindService() {
|
||||
initServiceConnection()
|
||||
serviceConnection?.let {
|
||||
activity.bindService(intentDatabaseTask, it, BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbind the service and assign null to the service connection to check if already unbind or not
|
||||
*/
|
||||
@Synchronized
|
||||
private fun unBindService() {
|
||||
serviceConnection?.let {
|
||||
activity.unbindService(it)
|
||||
}
|
||||
serviceConnection = null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun registerProgressTask() {
|
||||
ProgressTaskDialogFragment.stop(activity)
|
||||
|
||||
// Register a database task receiver to stop loading dialog when service finish the task
|
||||
databaseTaskBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
activity.runOnUiThread {
|
||||
when (intent?.action) {
|
||||
DATABASE_START_TASK_ACTION -> {
|
||||
// Bind to the service when is starting
|
||||
bindService()
|
||||
}
|
||||
DATABASE_STOP_TASK_ACTION -> {
|
||||
unBindService()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
activity.registerReceiver(databaseTaskBroadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(DATABASE_START_TASK_ACTION)
|
||||
addAction(DATABASE_STOP_TASK_ACTION)
|
||||
}
|
||||
)
|
||||
|
||||
// Check if a service is currently running else do nothing
|
||||
bindService()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun unregisterProgressTask() {
|
||||
ProgressTaskDialogFragment.stop(activity)
|
||||
|
||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||
mBinder = null
|
||||
|
||||
unBindService()
|
||||
|
||||
activity.unregisterReceiver(databaseTaskBroadcastReceiver)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun start(bundle: Bundle? = null, actionTask: String) {
|
||||
activity.stopService(intentDatabaseTask)
|
||||
if (bundle != null)
|
||||
intentDatabaseTask.putExtras(bundle)
|
||||
activity.runOnUiThread {
|
||||
intentDatabaseTask.action = actionTask
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity.startForegroundService(intentDatabaseTask)
|
||||
} else {
|
||||
activity.startService(intentDatabaseTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
----
|
||||
Main methods
|
||||
----
|
||||
*/
|
||||
|
||||
fun startDatabaseCreate(databaseUri: Uri,
|
||||
masterPasswordChecked: Boolean,
|
||||
masterPassword: String?,
|
||||
keyFileChecked: Boolean,
|
||||
keyFile: Uri?) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
||||
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
|
||||
}
|
||||
, ACTION_DATABASE_CREATE_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseLoad(databaseUri: Uri,
|
||||
masterPassword: String?,
|
||||
keyFile: Uri?,
|
||||
readOnly: Boolean,
|
||||
cipherEntity: CipherDatabaseEntity?,
|
||||
fixDuplicateUuid: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
|
||||
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
||||
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
|
||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||
}
|
||||
, ACTION_DATABASE_LOAD_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseAssignPassword(databaseUri: Uri,
|
||||
masterPasswordChecked: Boolean,
|
||||
masterPassword: String?,
|
||||
keyFileChecked: Boolean,
|
||||
keyFile: Uri?) {
|
||||
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
||||
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
|
||||
}
|
||||
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
|
||||
}
|
||||
|
||||
/*
|
||||
----
|
||||
Nodes Actions
|
||||
----
|
||||
*/
|
||||
|
||||
fun startDatabaseCreateGroup(newGroup: GroupVersioned,
|
||||
parent: GroupVersioned,
|
||||
save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
|
||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_CREATE_GROUP_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseUpdateGroup(oldGroup: GroupVersioned,
|
||||
groupToUpdate: GroupVersioned,
|
||||
save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
|
||||
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_GROUP_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseCreateEntry(newEntry: EntryVersioned,
|
||||
parent: GroupVersioned,
|
||||
save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
|
||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_CREATE_ENTRY_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseUpdateEntry(oldEntry: EntryVersioned,
|
||||
entryToUpdate: EntryVersioned,
|
||||
save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_ENTRY_TASK)
|
||||
}
|
||||
|
||||
private fun startDatabaseActionListNodes(actionTask: String,
|
||||
nodesPaste: List<NodeVersioned>,
|
||||
newParent: GroupVersioned?,
|
||||
save: Boolean) {
|
||||
val groupsIdToCopy = ArrayList<PwNodeId<*>>()
|
||||
val entriesIdToCopy = ArrayList<PwNodeId<UUID>>()
|
||||
nodesPaste.forEach { nodeVersioned ->
|
||||
when (nodeVersioned.type) {
|
||||
Type.GROUP -> {
|
||||
(nodeVersioned as GroupVersioned).nodeId?.let { groupId ->
|
||||
groupsIdToCopy.add(groupId)
|
||||
}
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
entriesIdToCopy.add((nodeVersioned as EntryVersioned).nodeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
val newParentId = newParent?.nodeId
|
||||
|
||||
start(Bundle().apply {
|
||||
putAll(getBundleFromListNodes(nodesPaste))
|
||||
putParcelableArrayList(DatabaseTaskNotificationService.GROUPS_ID_KEY, groupsIdToCopy)
|
||||
putParcelableArrayList(DatabaseTaskNotificationService.ENTRIES_ID_KEY, entriesIdToCopy)
|
||||
if (newParentId != null)
|
||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, actionTask)
|
||||
}
|
||||
|
||||
fun startDatabaseCopyNodes(nodesToCopy: List<NodeVersioned>,
|
||||
newParent: GroupVersioned,
|
||||
save: Boolean) {
|
||||
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
|
||||
}
|
||||
|
||||
fun startDatabaseMoveNodes(nodesToMove: List<NodeVersioned>,
|
||||
newParent: GroupVersioned,
|
||||
save: Boolean) {
|
||||
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
|
||||
}
|
||||
|
||||
fun startDatabaseDeleteNodes(nodesToDelete: List<NodeVersioned>,
|
||||
save: Boolean) {
|
||||
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
|
||||
}
|
||||
|
||||
/*
|
||||
-----------------
|
||||
Main Settings
|
||||
-----------------
|
||||
*/
|
||||
|
||||
fun startDatabaseSaveName(oldName: String,
|
||||
newName: String) {
|
||||
start(Bundle().apply {
|
||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
|
||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE_NAME_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveDescription(oldDescription: String,
|
||||
newDescription: String) {
|
||||
start(Bundle().apply {
|
||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
|
||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE_DESCRIPTION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String,
|
||||
newDefaultUsername: String) {
|
||||
start(Bundle().apply {
|
||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
|
||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE_DEFAULT_USERNAME_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveColor(oldColor: String,
|
||||
newColor: String) {
|
||||
start(Bundle().apply {
|
||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
|
||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE_COLOR_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveCompression(oldCompression: PwCompressionAlgorithm,
|
||||
newCompression: PwCompressionAlgorithm) {
|
||||
start(Bundle().apply {
|
||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
|
||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE_COMPRESSION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
||||
newMaxHistoryItems: Int) {
|
||||
start(Bundle().apply {
|
||||
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
|
||||
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE_MAX_HISTORY_ITEMS_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long,
|
||||
newMaxHistorySize: Long) {
|
||||
start(Bundle().apply {
|
||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
|
||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE_MAX_HISTORY_SIZE_TASK)
|
||||
}
|
||||
|
||||
/*
|
||||
-------------------
|
||||
Security Settings
|
||||
-------------------
|
||||
*/
|
||||
|
||||
fun startDatabaseSaveEncryption(oldEncryption: PwEncryptionAlgorithm,
|
||||
newEncryption: PwEncryptionAlgorithm) {
|
||||
start(Bundle().apply {
|
||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
|
||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE_ENCRYPTION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine,
|
||||
newKeyDerivation: KdfEngine) {
|
||||
start(Bundle().apply {
|
||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
|
||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE_KEY_DERIVATION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveIterations(oldIterations: Long,
|
||||
newIterations: Long) {
|
||||
start(Bundle().apply {
|
||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
|
||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE_ITERATIONS_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long,
|
||||
newMemoryUsage: Long) {
|
||||
start(Bundle().apply {
|
||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
|
||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveParallelism(oldParallelism: Int,
|
||||
newParallelism: Int) {
|
||||
start(Bundle().apply {
|
||||
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
|
||||
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE_PARALLELISM_TASK)
|
||||
}
|
||||
}
|
||||
@@ -21,43 +21,33 @@ package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.PwDbOutputException
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import java.io.IOException
|
||||
|
||||
abstract class SaveDatabaseRunnable(protected var context: Context,
|
||||
open class SaveDatabaseRunnable(protected var context: Context,
|
||||
protected var database: Database,
|
||||
private val save: Boolean,
|
||||
nestedAction: ActionRunnable? = null) : ActionRunnable(nestedAction) {
|
||||
private var saveDatabase: Boolean)
|
||||
: ActionRunnable() {
|
||||
|
||||
// TODO Service to prevent background thread kill
|
||||
override fun run() {
|
||||
if (save) {
|
||||
var mAfterSaveDatabase: ((Result) -> Unit)? = null
|
||||
|
||||
override fun onStartRun() {}
|
||||
|
||||
override fun onActionRun() {
|
||||
if (saveDatabase && result.isSuccess) {
|
||||
try {
|
||||
database.saveData(context.contentResolver)
|
||||
} catch (e: IOException) {
|
||||
finishRun(false, e.message)
|
||||
} catch (e: PwDbOutputException) {
|
||||
finishRun(false, e.message)
|
||||
setError(e.message)
|
||||
} catch (e: DatabaseOutputException) {
|
||||
setError(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Need to call super.run() in child class
|
||||
}
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
// Need to call super.onFinishRun(result) in child class
|
||||
}
|
||||
}
|
||||
|
||||
class SaveDatabaseActionRunnable(context: Context,
|
||||
database: Database,
|
||||
save: Boolean,
|
||||
nestedAction: ActionRunnable? = null)
|
||||
: SaveDatabaseRunnable(context, database, save, nestedAction) {
|
||||
|
||||
override fun run() {
|
||||
super.run()
|
||||
finishRun(true)
|
||||
override fun onFinishRun() {
|
||||
// Need to call super.onFinishRun() in child class
|
||||
mAfterSaveDatabase?.invoke(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,35 @@
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import android.util.Log
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
|
||||
abstract class ActionNodeDatabaseRunnable(
|
||||
context: FragmentActivity,
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val callbackRunnable: AfterActionNodeFinishRunnable?,
|
||||
private val afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
save: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, save) {
|
||||
|
||||
/**
|
||||
* Function do to a node action, don't implements run() if used this
|
||||
* Function do to a node action
|
||||
*/
|
||||
abstract fun nodeAction()
|
||||
|
||||
override fun run() {
|
||||
try {
|
||||
nodeAction()
|
||||
// To save the database
|
||||
super.run()
|
||||
finishRun(true)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ActionNodeDBRunnable", e.message)
|
||||
finishRun(false, e.message)
|
||||
}
|
||||
override fun onStartRun() {
|
||||
nodeAction()
|
||||
super.onStartRun()
|
||||
}
|
||||
|
||||
/**
|
||||
* Function do get the finish node action, don't implements onFinishRun() if used this
|
||||
* Function do get the finish node action
|
||||
*/
|
||||
abstract fun nodeFinish(result: Result): ActionNodeValues
|
||||
abstract fun nodeFinish(): ActionNodesValues
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
callbackRunnable?.apply {
|
||||
onActionNodeFinish(nodeFinish(result))
|
||||
override fun onFinishRun() {
|
||||
super.onFinishRun()
|
||||
afterActionNodesFinish?.apply {
|
||||
onActionNodesFinish(result, nodeFinish())
|
||||
}
|
||||
|
||||
if (!result.isSuccess) {
|
||||
displayMessage(context)
|
||||
}
|
||||
|
||||
super.onFinishRun(result)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY = "NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,19 +19,20 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||
import com.kunzisoft.keepass.database.element.NodeVersioned
|
||||
|
||||
class AddEntryRunnable constructor(
|
||||
context: FragmentActivity,
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val mNewEntry: EntryVersioned,
|
||||
private val mParent: GroupVersioned,
|
||||
finishRunnable: AfterActionNodeFinishRunnable?,
|
||||
save: Boolean)
|
||||
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
|
||||
override fun nodeAction() {
|
||||
mNewEntry.touch(modified = true, touchParents = true)
|
||||
@@ -39,12 +40,16 @@ class AddEntryRunnable constructor(
|
||||
database.addEntryTo(mNewEntry, mParent)
|
||||
}
|
||||
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
override fun nodeFinish(): ActionNodesValues {
|
||||
if (!result.isSuccess) {
|
||||
mNewEntry.parent?.let {
|
||||
database.removeEntryFrom(mNewEntry, it)
|
||||
}
|
||||
}
|
||||
return ActionNodeValues(result, null, mNewEntry)
|
||||
|
||||
val oldNodesReturn = ArrayList<NodeVersioned>()
|
||||
val newNodesReturn = ArrayList<NodeVersioned>()
|
||||
newNodesReturn.add(mNewEntry)
|
||||
return ActionNodesValues(oldNodesReturn, newNodesReturn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,18 +19,19 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||
import com.kunzisoft.keepass.database.element.NodeVersioned
|
||||
|
||||
class AddGroupRunnable constructor(
|
||||
context: FragmentActivity,
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val mNewGroup: GroupVersioned,
|
||||
private val mParent: GroupVersioned,
|
||||
afterAddNodeRunnable: AfterActionNodeFinishRunnable?,
|
||||
save: Boolean)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterAddNodeRunnable, save) {
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
|
||||
override fun nodeAction() {
|
||||
mNewGroup.touch(modified = true, touchParents = true)
|
||||
@@ -38,10 +39,14 @@ class AddGroupRunnable constructor(
|
||||
database.addGroupTo(mNewGroup, mParent)
|
||||
}
|
||||
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
override fun nodeFinish(): ActionNodesValues {
|
||||
if (!result.isSuccess) {
|
||||
database.removeGroupFrom(mNewGroup, mParent)
|
||||
}
|
||||
return ActionNodeValues(result, null, mNewGroup)
|
||||
|
||||
val oldNodesReturn = ArrayList<NodeVersioned>()
|
||||
val newNodesReturn = ArrayList<NodeVersioned>()
|
||||
newNodesReturn.add(mNewGroup)
|
||||
return ActionNodesValues(oldNodesReturn, newNodesReturn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
/**
|
||||
* Callback method who return the node(s) modified after an action
|
||||
* - Add : @param oldNode NULL, @param newNode CreatedNode
|
||||
* - Copy : @param oldNode NodeToCopy, @param newNode NodeCopied
|
||||
* - Delete : @param oldNode NodeToDelete, @param NULL
|
||||
* - Move : @param oldNode NULL, @param NodeToMove
|
||||
* - Update : @param oldNode NodeToUpdate, @param NodeUpdated
|
||||
* - Add : @param oldNodes empty, @param newNodes CreatedNodes
|
||||
* - Copy : @param oldNodes NodesToCopy, @param newNodes NodesCopied
|
||||
* - Delete : @param oldNodes NodesToDelete, @param newNodes empty
|
||||
* - Move : @param oldNodes empty, @param newNodes NodesToMove
|
||||
* - Update : @param oldNodes NodesToUpdate, @param newNodes NodesUpdated
|
||||
*/
|
||||
data class ActionNodeValues(val result: ActionRunnable.Result, val oldNode: NodeVersioned?, val newNode: NodeVersioned?)
|
||||
class ActionNodesValues(val oldNodes: List<NodeVersioned>, val newNodes: List<NodeVersioned>)
|
||||
|
||||
abstract class AfterActionNodeFinishRunnable {
|
||||
abstract fun onActionNodeFinish(actionNodeValues: ActionNodeValues)
|
||||
abstract class AfterActionNodesFinish {
|
||||
abstract fun onActionNodesFinish(result: ActionRunnable.Result, actionNodesValues: ActionNodesValues)
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||
|
||||
class CopyEntryRunnable constructor(
|
||||
context: FragmentActivity,
|
||||
database: Database,
|
||||
private val mEntryToCopy: EntryVersioned,
|
||||
private val mNewParent: GroupVersioned,
|
||||
afterAddNodeRunnable: AfterActionNodeFinishRunnable?,
|
||||
save: Boolean)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterAddNodeRunnable, save) {
|
||||
|
||||
private var mEntryCopied: EntryVersioned? = null
|
||||
|
||||
override fun nodeAction() {
|
||||
// Condition
|
||||
var conditionAccepted = true
|
||||
if(mNewParent == database.rootGroup && !database.rootCanContainsEntry())
|
||||
conditionAccepted = false
|
||||
if (conditionAccepted) {
|
||||
// Update entry with new values
|
||||
mNewParent.touch(modified = false, touchParents = true)
|
||||
mEntryCopied = database.copyEntryTo(mEntryToCopy, mNewParent)
|
||||
} else {
|
||||
// Only finish thread
|
||||
throw Exception(context.getString(R.string.error_copy_entry_here))
|
||||
}
|
||||
|
||||
mEntryCopied?.apply {
|
||||
touch(modified = true, touchParents = true)
|
||||
} ?: Log.e(TAG, "Unable to create a copy of the entry")
|
||||
}
|
||||
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, try to delete the copy
|
||||
try {
|
||||
mEntryCopied?.let {
|
||||
database.deleteEntry(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.i(TAG, "Unable to delete the copied entry")
|
||||
}
|
||||
}
|
||||
return ActionNodeValues(result, mEntryToCopy, mEntryCopied)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = CopyEntryRunnable::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.exception.CopyDatabaseEntryException
|
||||
import com.kunzisoft.keepass.database.exception.CopyDatabaseGroupException
|
||||
|
||||
class CopyNodesRunnable constructor(
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val mNodesToCopy: List<NodeVersioned>,
|
||||
private val mNewParent: GroupVersioned,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
|
||||
private var mEntriesCopied = ArrayList<EntryVersioned>()
|
||||
|
||||
override fun nodeAction() {
|
||||
|
||||
foreachNode@ for(currentNode in mNodesToCopy) {
|
||||
when (currentNode.type) {
|
||||
Type.GROUP -> {
|
||||
Log.e(TAG, "Copy not allowed for group")// Only finish thread
|
||||
setError(CopyDatabaseGroupException())
|
||||
break@foreachNode
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
// Root can contains entry
|
||||
if (mNewParent != database.rootGroup || database.rootCanContainsEntry()) {
|
||||
// Update entry with new values
|
||||
mNewParent.touch(modified = false, touchParents = true)
|
||||
|
||||
val entryCopied = database.copyEntryTo(currentNode as EntryVersioned, mNewParent)
|
||||
if (entryCopied != null) {
|
||||
entryCopied.touch(modified = true, touchParents = true)
|
||||
mEntriesCopied.add(entryCopied)
|
||||
} else {
|
||||
Log.e(TAG, "Unable to create a copy of the entry")
|
||||
setError(CopyDatabaseEntryException())
|
||||
break@foreachNode
|
||||
}
|
||||
} else {
|
||||
// Only finish thread
|
||||
setError(CopyDatabaseEntryException())
|
||||
break@foreachNode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFinish(): ActionNodesValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, try to delete the copy
|
||||
mEntriesCopied.forEach {
|
||||
try {
|
||||
database.deleteEntry(it)
|
||||
} catch (e: Exception) {
|
||||
Log.i(TAG, "Unable to delete the copied entry")
|
||||
}
|
||||
}
|
||||
}
|
||||
return ActionNodesValues(mNodesToCopy, mEntriesCopied)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = CopyNodesRunnable::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||
|
||||
class DeleteEntryRunnable constructor(
|
||||
context: FragmentActivity,
|
||||
database: Database,
|
||||
private val mEntryToDelete: EntryVersioned,
|
||||
finishRunnable: AfterActionNodeFinishRunnable?,
|
||||
save: Boolean)
|
||||
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
|
||||
|
||||
private var mParent: GroupVersioned? = null
|
||||
private var mCanRecycle: Boolean = false
|
||||
|
||||
private var mEntryToDeleteBackup: EntryVersioned? = null
|
||||
private var mNodePosition: Int? = null
|
||||
|
||||
override fun nodeAction() {
|
||||
mParent = mEntryToDelete.parent
|
||||
mParent?.touch(modified = false, touchParents = true)
|
||||
|
||||
// Get the node position
|
||||
mNodePosition = mEntryToDelete.nodePositionInParent
|
||||
|
||||
// Create a copy to keep the old ref and remove it visually
|
||||
mEntryToDeleteBackup = EntryVersioned(mEntryToDelete)
|
||||
|
||||
// Remove Entry from parent
|
||||
mCanRecycle = database.canRecycle(mEntryToDelete)
|
||||
if (mCanRecycle) {
|
||||
database.recycle(mEntryToDelete, context.resources)
|
||||
} else {
|
||||
database.deleteEntry(mEntryToDelete)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
mParent?.let {
|
||||
if (mCanRecycle) {
|
||||
database.undoRecycle(mEntryToDelete, it)
|
||||
} else {
|
||||
database.undoDeleteEntry(mEntryToDelete, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add position in bundle to delete the node in view
|
||||
mNodePosition?.let { position ->
|
||||
result.data = Bundle().apply {
|
||||
putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position )
|
||||
}
|
||||
}
|
||||
|
||||
// Return a copy of unchanged entry as old param
|
||||
// and entry deleted or moved in recycle bin as new param
|
||||
return ActionNodeValues(result, mEntryToDeleteBackup, mEntryToDelete)
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||
|
||||
class DeleteGroupRunnable(context: FragmentActivity,
|
||||
database: Database,
|
||||
private val mGroupToDelete: GroupVersioned,
|
||||
finish: AfterActionNodeFinishRunnable,
|
||||
save: Boolean) : ActionNodeDatabaseRunnable(context, database, finish, save) {
|
||||
private var mParent: GroupVersioned? = null
|
||||
private var mRecycle: Boolean = false
|
||||
|
||||
private var mGroupToDeleteBackup: GroupVersioned? = null
|
||||
private var mNodePosition: Int? = null
|
||||
|
||||
override fun nodeAction() {
|
||||
mParent = mGroupToDelete.parent
|
||||
mParent?.touch(modified = false, touchParents = true)
|
||||
|
||||
// Get the node position
|
||||
mNodePosition = mGroupToDelete.nodePositionInParent
|
||||
|
||||
// Create a copy to keep the old ref and remove it visually
|
||||
mGroupToDeleteBackup = GroupVersioned(mGroupToDelete)
|
||||
|
||||
// Remove Group from parent
|
||||
mRecycle = database.canRecycle(mGroupToDelete)
|
||||
if (mRecycle) {
|
||||
database.recycle(mGroupToDelete, context.resources)
|
||||
} else {
|
||||
database.deleteGroup(mGroupToDelete)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
if (mRecycle) {
|
||||
mParent?.let {
|
||||
database.undoRecycle(mGroupToDelete, it)
|
||||
}
|
||||
}
|
||||
// else {
|
||||
// Let's not bother recovering from a failure to save a deleted tree. It is too much work.
|
||||
// TODO database.undoDeleteGroupFrom(mGroup, mParent);
|
||||
// }
|
||||
}
|
||||
|
||||
// Add position in bundle to delete the node in view
|
||||
mNodePosition?.let { position ->
|
||||
result.data = Bundle().apply {
|
||||
putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position )
|
||||
}
|
||||
}
|
||||
|
||||
// Return a copy of unchanged group as old param
|
||||
// and group deleted or moved in recycle bin as new param
|
||||
return ActionNodeValues(result, mGroupToDeleteBackup, mGroupToDelete)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
|
||||
class DeleteNodesRunnable(context: Context,
|
||||
database: Database,
|
||||
private val mNodesToDelete: List<NodeVersioned>,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
|
||||
private var mParent: GroupVersioned? = null
|
||||
private var mCanRecycle: Boolean = false
|
||||
|
||||
private var mNodesToDeleteBackup = ArrayList<NodeVersioned>()
|
||||
|
||||
override fun nodeAction() {
|
||||
|
||||
foreachNode@ for(currentNode in mNodesToDelete) {
|
||||
mParent = currentNode.parent
|
||||
mParent?.touch(modified = false, touchParents = true)
|
||||
|
||||
when (currentNode.type) {
|
||||
Type.GROUP -> {
|
||||
// Create a copy to keep the old ref and remove it visually
|
||||
mNodesToDeleteBackup.add(GroupVersioned(currentNode as GroupVersioned))
|
||||
// Remove Node from parent
|
||||
mCanRecycle = database.canRecycle(currentNode)
|
||||
if (mCanRecycle) {
|
||||
database.recycle(currentNode, context.resources)
|
||||
} else {
|
||||
database.deleteGroup(currentNode)
|
||||
}
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
// Create a copy to keep the old ref and remove it visually
|
||||
mNodesToDeleteBackup.add(EntryVersioned(currentNode as EntryVersioned))
|
||||
// Remove Node from parent
|
||||
mCanRecycle = database.canRecycle(currentNode)
|
||||
if (mCanRecycle) {
|
||||
database.recycle(currentNode, context.resources)
|
||||
} else {
|
||||
database.deleteEntry(currentNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFinish(): ActionNodesValues {
|
||||
if (!result.isSuccess) {
|
||||
if (mCanRecycle) {
|
||||
mParent?.let {
|
||||
mNodesToDeleteBackup.forEach { backupNode ->
|
||||
when (backupNode.type) {
|
||||
Type.GROUP -> {
|
||||
database.undoRecycle(backupNode as GroupVersioned, it)
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
database.undoRecycle(backupNode as EntryVersioned, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// else {
|
||||
// Let's not bother recovering from a failure to save a deleted tree. It is too much work.
|
||||
// TODO database.undoDeleteGroupFrom(mGroup, mParent);
|
||||
// }
|
||||
}
|
||||
|
||||
// Return a copy of unchanged nodes as old param
|
||||
// and nodes deleted or moved in recycle bin as new param
|
||||
return ActionNodesValues(mNodesToDeleteBackup, mNodesToDelete)
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||
|
||||
class MoveEntryRunnable constructor(
|
||||
context: FragmentActivity,
|
||||
database: Database,
|
||||
private val mEntryToMove: EntryVersioned?,
|
||||
private val mNewParent: GroupVersioned,
|
||||
afterAddNodeRunnable: AfterActionNodeFinishRunnable?,
|
||||
save: Boolean)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterAddNodeRunnable, save) {
|
||||
|
||||
private var mOldParent: GroupVersioned? = null
|
||||
|
||||
override fun nodeAction() {
|
||||
// Move entry in new parent
|
||||
mEntryToMove?.let {
|
||||
mOldParent = it.parent
|
||||
|
||||
// Condition
|
||||
var conditionAccepted = true
|
||||
if(mNewParent == database.rootGroup && !database.rootCanContainsEntry())
|
||||
conditionAccepted = false
|
||||
// Move only if the parent change
|
||||
if (mOldParent != mNewParent && conditionAccepted) {
|
||||
database.moveEntryTo(it, mNewParent)
|
||||
} else {
|
||||
// Only finish thread
|
||||
throw Exception(context.getString(R.string.error_move_entry_here))
|
||||
}
|
||||
it.touch(modified = true, touchParents = true)
|
||||
} ?: Log.e(TAG, "Unable to create a copy of the entry")
|
||||
}
|
||||
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, try to remove in the first place
|
||||
try {
|
||||
if (mEntryToMove != null && mOldParent != null)
|
||||
database.moveEntryTo(mEntryToMove, mOldParent!!)
|
||||
} catch (e: Exception) {
|
||||
Log.i(TAG, "Unable to replace the entry")
|
||||
}
|
||||
|
||||
}
|
||||
return ActionNodeValues(result, null, mEntryToMove)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = MoveEntryRunnable::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||
|
||||
class MoveGroupRunnable constructor(
|
||||
context: FragmentActivity,
|
||||
database: Database,
|
||||
private val mGroupToMove: GroupVersioned?,
|
||||
private val mNewParent: GroupVersioned,
|
||||
afterAddNodeRunnable: AfterActionNodeFinishRunnable?,
|
||||
save: Boolean)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterAddNodeRunnable, save) {
|
||||
|
||||
private var mOldParent: GroupVersioned? = null
|
||||
|
||||
override fun nodeAction() {
|
||||
mGroupToMove?.let {
|
||||
mOldParent = it.parent
|
||||
// Move group in new parent if not in the current group
|
||||
if (mGroupToMove != mNewParent && !mNewParent.isContainedIn(mGroupToMove)) {
|
||||
database.moveGroupTo(mGroupToMove, mNewParent)
|
||||
mGroupToMove.touch(modified = true, touchParents = true)
|
||||
finishRun(true)
|
||||
} else {
|
||||
// Only finish thread
|
||||
throw Exception(context.getString(R.string.error_move_folder_in_itself))
|
||||
}
|
||||
} ?: Log.e(TAG, "Unable to create a copy of the group")
|
||||
}
|
||||
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, try to move in the first place
|
||||
try {
|
||||
if (mGroupToMove != null && mOldParent != null)
|
||||
database.moveGroupTo(mGroupToMove, mOldParent!!)
|
||||
} catch (e: Exception) {
|
||||
Log.i(TAG, "Unable to replace the group")
|
||||
}
|
||||
|
||||
}
|
||||
return ActionNodeValues(result, null, mGroupToMove)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = MoveGroupRunnable::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.MoveDatabaseEntryException
|
||||
import com.kunzisoft.keepass.database.exception.MoveDatabaseGroupException
|
||||
|
||||
class MoveNodesRunnable constructor(
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val mNodesToMove: List<NodeVersioned>,
|
||||
private val mNewParent: GroupVersioned,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
|
||||
private var mOldParent: GroupVersioned? = null
|
||||
|
||||
override fun nodeAction() {
|
||||
|
||||
foreachNode@ for(nodeToMove in mNodesToMove) {
|
||||
// Move node in new parent
|
||||
mOldParent = nodeToMove.parent
|
||||
|
||||
when (nodeToMove.type) {
|
||||
Type.GROUP -> {
|
||||
val groupToMove = nodeToMove as GroupVersioned
|
||||
// Move group in new parent if not in the current group
|
||||
if (groupToMove != mNewParent
|
||||
&& !mNewParent.isContainedIn(groupToMove)) {
|
||||
nodeToMove.touch(modified = true, touchParents = true)
|
||||
database.moveGroupTo(groupToMove, mNewParent)
|
||||
} else {
|
||||
// Only finish thread
|
||||
setError(MoveDatabaseGroupException())
|
||||
break@foreachNode
|
||||
}
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
val entryToMove = nodeToMove as EntryVersioned
|
||||
// Move only if the parent change
|
||||
if (mOldParent != mNewParent
|
||||
// and root can contains entry
|
||||
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
|
||||
nodeToMove.touch(modified = true, touchParents = true)
|
||||
database.moveEntryTo(entryToMove, mNewParent)
|
||||
} else {
|
||||
// Only finish thread
|
||||
setError(MoveDatabaseEntryException())
|
||||
break@foreachNode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFinish(): ActionNodesValues {
|
||||
if (!result.isSuccess) {
|
||||
try {
|
||||
mNodesToMove.forEach { nodeToMove ->
|
||||
// If we fail to save, try to move in the first place
|
||||
if (mOldParent != null &&
|
||||
mOldParent != nodeToMove.parent) {
|
||||
when (nodeToMove.type) {
|
||||
Type.GROUP -> database.moveGroupTo(nodeToMove as GroupVersioned, mOldParent!!)
|
||||
Type.ENTRY -> database.moveEntryTo(nodeToMove as EntryVersioned, mOldParent!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.i(TAG, "Unable to replace the node")
|
||||
}
|
||||
}
|
||||
return ActionNodesValues(ArrayList(), mNodesToMove)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = MoveNodesRunnable::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -19,36 +19,50 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.NodeVersioned
|
||||
|
||||
class UpdateEntryRunnable constructor(
|
||||
context: FragmentActivity,
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val mOldEntry: EntryVersioned,
|
||||
private val mNewEntry: EntryVersioned,
|
||||
finishRunnable: AfterActionNodeFinishRunnable?,
|
||||
save: Boolean)
|
||||
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
|
||||
// Keep backup of original values in case save fails
|
||||
private var mBackupEntry: EntryVersioned? = null
|
||||
private var mBackupEntryHistory: EntryVersioned = EntryVersioned(mOldEntry)
|
||||
|
||||
override fun nodeAction() {
|
||||
mBackupEntry = database.addHistoryBackupTo(mOldEntry)
|
||||
mOldEntry.touch(modified = true, touchParents = true)
|
||||
// WARNING : Re attribute parent removed in entry edit activity to save memory
|
||||
mNewEntry.addParentFrom(mOldEntry)
|
||||
|
||||
// Update entry with new values
|
||||
mOldEntry.updateWith(mNewEntry)
|
||||
mNewEntry.touch(modified = true, touchParents = true)
|
||||
|
||||
// Create an entry history (an entry history don't have history)
|
||||
mOldEntry.addEntryToHistory(EntryVersioned(mBackupEntryHistory, copyHistory = false))
|
||||
database.removeOldestHistory(mOldEntry)
|
||||
|
||||
// Only change data in index
|
||||
database.updateEntry(mOldEntry)
|
||||
}
|
||||
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
override fun nodeFinish(): ActionNodesValues {
|
||||
if (!result.isSuccess) {
|
||||
mOldEntry.updateWith(mBackupEntryHistory)
|
||||
// If we fail to save, back out changes to global structure
|
||||
mBackupEntry?.let {
|
||||
mOldEntry.updateWith(it)
|
||||
}
|
||||
database.updateEntry(mOldEntry)
|
||||
}
|
||||
return ActionNodeValues(result, mOldEntry, mNewEntry)
|
||||
|
||||
val oldNodesReturn = ArrayList<NodeVersioned>()
|
||||
oldNodesReturn.add(mBackupEntryHistory)
|
||||
val newNodesReturn = ArrayList<NodeVersioned>()
|
||||
newNodesReturn.add(mOldEntry)
|
||||
return ActionNodesValues(oldNodesReturn, newNodesReturn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,33 +19,47 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||
import com.kunzisoft.keepass.database.element.NodeVersioned
|
||||
|
||||
class UpdateGroupRunnable constructor(
|
||||
context: FragmentActivity,
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val mOldGroup: GroupVersioned,
|
||||
private val mNewGroup: GroupVersioned,
|
||||
finishRunnable: AfterActionNodeFinishRunnable?,
|
||||
save: Boolean)
|
||||
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
|
||||
// Keep backup of original values in case save fails
|
||||
private val mBackupGroup: GroupVersioned = GroupVersioned(mOldGroup)
|
||||
|
||||
override fun nodeAction() {
|
||||
// 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.touch(modified = true, touchParents = true)
|
||||
mOldGroup.updateWith(mNewGroup)
|
||||
mOldGroup.touch(modified = true, touchParents = true)
|
||||
|
||||
// Only change data in index
|
||||
database.updateGroup(mOldGroup)
|
||||
}
|
||||
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
override fun nodeFinish(): ActionNodesValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, back out changes to global structure
|
||||
mOldGroup.updateWith(mBackupGroup)
|
||||
database.updateGroup(mOldGroup)
|
||||
}
|
||||
return ActionNodeValues(result, mOldGroup, mNewGroup)
|
||||
|
||||
val oldNodesReturn = ArrayList<NodeVersioned>()
|
||||
oldNodesReturn.add(mBackupGroup)
|
||||
val newNodesReturn = ArrayList<NodeVersioned>()
|
||||
newNodesReturn.add(mOldGroup)
|
||||
return ActionNodesValues(oldNodesReturn, newNodesReturn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@ package com.kunzisoft.keepass.database.cursor
|
||||
|
||||
import android.database.MatrixCursor
|
||||
import android.provider.BaseColumns
|
||||
import com.kunzisoft.keepass.database.element.PwEntry
|
||||
import com.kunzisoft.keepass.database.element.PwIconFactory
|
||||
import com.kunzisoft.keepass.database.element.PwNodeId
|
||||
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
abstract class EntryCursor<PwEntryV : PwEntry<*, *>> : MatrixCursor(arrayOf(
|
||||
abstract class EntryCursor<EntryId, PwEntryV : PwEntry<*, EntryId, *, *>> : MatrixCursor(arrayOf(
|
||||
_ID,
|
||||
COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS,
|
||||
COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS,
|
||||
@@ -25,10 +24,10 @@ abstract class EntryCursor<PwEntryV : PwEntry<*, *>> : MatrixCursor(arrayOf(
|
||||
|
||||
abstract fun addEntry(entry: PwEntryV)
|
||||
|
||||
abstract fun getPwNodeId(): PwNodeId<EntryId>
|
||||
|
||||
open fun populateEntry(pwEntry: PwEntryV, iconFactory: PwIconFactory) {
|
||||
pwEntry.nodeId = PwNodeIdUUID(
|
||||
UUID(getLong(getColumnIndex(COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
|
||||
getLong(getColumnIndex(COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS))))
|
||||
pwEntry.nodeId = getPwNodeId()
|
||||
pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE))
|
||||
|
||||
val iconStandard = iconFactory.getIcon(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD)))
|
||||
@@ -53,5 +52,4 @@ abstract class EntryCursor<PwEntryV : PwEntry<*, *>> : MatrixCursor(arrayOf(
|
||||
const val COLUMN_INDEX_URL = "URL"
|
||||
const val COLUMN_INDEX_NOTES = "notes"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.kunzisoft.keepass.database.cursor
|
||||
|
||||
import com.kunzisoft.keepass.database.element.PwEntry
|
||||
import com.kunzisoft.keepass.database.element.PwNodeId
|
||||
import com.kunzisoft.keepass.database.element.PwNodeIdUUID
|
||||
import java.util.*
|
||||
|
||||
abstract class EntryCursorUUID<EntryV: PwEntry<*, UUID, *, *>>: EntryCursor<UUID, EntryV>() {
|
||||
|
||||
override fun getPwNodeId(): PwNodeId<UUID> {
|
||||
return PwNodeIdUUID(
|
||||
UUID(getLong(getColumnIndex(COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
|
||||
getLong(getColumnIndex(COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS))))
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package com.kunzisoft.keepass.database.cursor
|
||||
import com.kunzisoft.keepass.database.element.PwDatabase
|
||||
import com.kunzisoft.keepass.database.element.PwEntryV3
|
||||
|
||||
class EntryCursorV3 : EntryCursor<PwEntryV3>() {
|
||||
class EntryCursorV3 : EntryCursorUUID<PwEntryV3>() {
|
||||
|
||||
override fun addEntry(entry: PwEntryV3) {
|
||||
addRow(arrayOf(
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.kunzisoft.keepass.database.element.PwIconFactory
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
class EntryCursorV4 : EntryCursor<PwEntryV4>() {
|
||||
class EntryCursorV4 : EntryCursorUUID<PwEntryV4>() {
|
||||
|
||||
private val extraFieldCursor: ExtraFieldCursor = ExtraFieldCursor()
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class ExtraFieldCursor : MatrixCursor(arrayOf(
|
||||
}
|
||||
|
||||
fun populateExtraFieldInEntry(pwEntry: PwEntryV4) {
|
||||
pwEntry.addExtraField(getString(getColumnIndex(COLUMN_LABEL)),
|
||||
pwEntry.putExtraField(getString(getColumnIndex(COLUMN_LABEL)),
|
||||
ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0,
|
||||
getString(getColumnIndex(COLUMN_VALUE))))
|
||||
}
|
||||
|
||||
@@ -20,14 +20,12 @@
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.webkit.URLUtil
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||
import com.kunzisoft.keepass.database.NodeHandler
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursorV3
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursorV4
|
||||
@@ -40,7 +38,6 @@ import com.kunzisoft.keepass.database.file.save.PwDbV3Output
|
||||
import com.kunzisoft.keepass.database.file.save.PwDbV4Output
|
||||
import com.kunzisoft.keepass.database.search.SearchDbHelper
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||
@@ -56,8 +53,11 @@ class Database {
|
||||
private var pwDatabaseV3: PwDatabaseV3? = null
|
||||
private var pwDatabaseV4: PwDatabaseV4? = null
|
||||
|
||||
private var mUri: Uri? = null
|
||||
private var searchHelper: SearchDbHelper? = null
|
||||
var fileUri: Uri? = null
|
||||
private set
|
||||
|
||||
private var mSearchHelper: SearchDbHelper? = null
|
||||
|
||||
var isReadOnly = false
|
||||
|
||||
val drawFactory = IconDrawableFactory()
|
||||
@@ -69,51 +69,120 @@ class Database {
|
||||
return pwDatabaseV3?.iconFactory ?: pwDatabaseV4?.iconFactory ?: PwIconFactory()
|
||||
}
|
||||
|
||||
val name: String
|
||||
val allowName: Boolean
|
||||
get() = pwDatabaseV4 != null
|
||||
|
||||
var name: String
|
||||
get() {
|
||||
return pwDatabaseV4?.name ?: ""
|
||||
}
|
||||
set(name) {
|
||||
pwDatabaseV4?.name = name
|
||||
pwDatabaseV4?.nameChanged = PwDate()
|
||||
}
|
||||
|
||||
val description: String
|
||||
val allowDescription: Boolean
|
||||
get() = pwDatabaseV4 != null
|
||||
|
||||
var description: String
|
||||
get() {
|
||||
return pwDatabaseV4?.description ?: ""
|
||||
}
|
||||
set(description) {
|
||||
pwDatabaseV4?.description = description
|
||||
pwDatabaseV4?.descriptionChanged = PwDate()
|
||||
}
|
||||
|
||||
val allowDefaultUsername: Boolean
|
||||
get() = pwDatabaseV4 != null
|
||||
// TODO get() = pwDatabaseV3 != null || pwDatabaseV4 != null
|
||||
|
||||
var defaultUsername: String
|
||||
get() {
|
||||
return pwDatabaseV4?.defaultUserName ?: ""
|
||||
return pwDatabaseV4?.defaultUserName ?: "" // TODO pwDatabaseV3 default username
|
||||
}
|
||||
set(username) {
|
||||
pwDatabaseV4?.defaultUserName = username
|
||||
pwDatabaseV4?.defaultUserNameChanged = PwDate()
|
||||
}
|
||||
|
||||
val encryptionAlgorithm: PwEncryptionAlgorithm?
|
||||
val allowCustomColor: Boolean
|
||||
get() = pwDatabaseV4 != null
|
||||
// TODO get() = pwDatabaseV3 != null || pwDatabaseV4 != null
|
||||
|
||||
// with format "#000000"
|
||||
var customColor: String
|
||||
get() {
|
||||
return pwDatabaseV4?.encryptionAlgorithm
|
||||
return pwDatabaseV4?.color ?: "" // TODO pwDatabaseV3 color
|
||||
}
|
||||
set(value) {
|
||||
// TODO Check color string
|
||||
pwDatabaseV4?.color = value
|
||||
}
|
||||
|
||||
val version: String
|
||||
get() = pwDatabaseV3?.version ?: pwDatabaseV4?.version ?: "-"
|
||||
|
||||
val allowDataCompression: Boolean
|
||||
get() = pwDatabaseV4 != null
|
||||
|
||||
val availableCompressionAlgorithms: List<PwCompressionAlgorithm>
|
||||
get() = pwDatabaseV4?.availableCompressionAlgorithms ?: ArrayList()
|
||||
|
||||
var compressionAlgorithm: PwCompressionAlgorithm?
|
||||
get() = pwDatabaseV4?.compressionAlgorithm
|
||||
set(value) {
|
||||
value?.let {
|
||||
pwDatabaseV4?.compressionAlgorithm = it
|
||||
}
|
||||
}
|
||||
|
||||
val allowNoMasterKey: Boolean
|
||||
get() = pwDatabaseV4 != null
|
||||
|
||||
val allowEncryptionAlgorithmModification: Boolean
|
||||
get() = availableEncryptionAlgorithms.size > 1
|
||||
|
||||
fun getEncryptionAlgorithmName(resources: Resources): String {
|
||||
return pwDatabaseV3?.encryptionAlgorithm?.getName(resources)
|
||||
?: pwDatabaseV4?.encryptionAlgorithm?.getName(resources)
|
||||
?: ""
|
||||
}
|
||||
|
||||
val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm>
|
||||
get() = pwDatabaseV3?.availableEncryptionAlgorithms ?: pwDatabaseV4?.availableEncryptionAlgorithms ?: ArrayList()
|
||||
|
||||
var encryptionAlgorithm: PwEncryptionAlgorithm?
|
||||
get() = pwDatabaseV3?.encryptionAlgorithm ?: pwDatabaseV4?.encryptionAlgorithm
|
||||
set(algorithm) {
|
||||
algorithm?.let {
|
||||
pwDatabaseV4?.encryptionAlgorithm = algorithm
|
||||
pwDatabaseV4?.setDataEngine(algorithm.cipherEngine)
|
||||
pwDatabaseV4?.dataCipher = algorithm.dataCipher
|
||||
}
|
||||
}
|
||||
|
||||
val availableKdfEngines: List<KdfEngine>
|
||||
get() {
|
||||
if (pwDatabaseV3 != null) {
|
||||
return KdfFactory.kdfListV3
|
||||
get() = pwDatabaseV3?.kdfAvailableList ?: pwDatabaseV4?.kdfAvailableList ?: ArrayList()
|
||||
|
||||
val allowKdfModification: Boolean
|
||||
get() = availableKdfEngines.size > 1
|
||||
|
||||
var kdfEngine: KdfEngine?
|
||||
get() = pwDatabaseV3?.kdfEngine ?: pwDatabaseV4?.kdfEngine
|
||||
set(kdfEngine) {
|
||||
kdfEngine?.let {
|
||||
if (pwDatabaseV4?.kdfParameters?.uuid != kdfEngine.defaultParameters.uuid)
|
||||
pwDatabaseV4?.kdfParameters = kdfEngine.defaultParameters
|
||||
numberKeyEncryptionRounds = kdfEngine.defaultKeyRounds
|
||||
memoryUsage = kdfEngine.defaultMemoryUsage
|
||||
parallelism = kdfEngine.defaultParallelism
|
||||
}
|
||||
if (pwDatabaseV4 != null) {
|
||||
return KdfFactory.kdfListV4
|
||||
}
|
||||
return ArrayList()
|
||||
}
|
||||
|
||||
val kdfEngine: KdfEngine
|
||||
get() {
|
||||
return pwDatabaseV4?.kdfEngine ?: return KdfFactory.aesKdf
|
||||
}
|
||||
|
||||
val numberKeyEncryptionRoundsAsString: String
|
||||
get() = numberKeyEncryptionRounds.toString()
|
||||
fun getKeyDerivationName(resources: Resources): String {
|
||||
return kdfEngine?.getName(resources) ?: ""
|
||||
}
|
||||
|
||||
var numberKeyEncryptionRounds: Long
|
||||
get() = pwDatabaseV3?.numberKeyEncryptionRounds ?: pwDatabaseV4?.numberKeyEncryptionRounds ?: 0
|
||||
@@ -123,9 +192,6 @@ class Database {
|
||||
pwDatabaseV4?.numberKeyEncryptionRounds = numberRounds
|
||||
}
|
||||
|
||||
val memoryUsageAsString: String
|
||||
get() = memoryUsage.toString()
|
||||
|
||||
var memoryUsage: Long
|
||||
get() {
|
||||
return pwDatabaseV4?.memoryUsage ?: return KdfEngine.UNKNOWN_VALUE.toLong()
|
||||
@@ -134,9 +200,6 @@ class Database {
|
||||
pwDatabaseV4?.memoryUsage = memory
|
||||
}
|
||||
|
||||
val parallelismAsString: String
|
||||
get() = parallelism.toString()
|
||||
|
||||
var parallelism: Int
|
||||
get() = pwDatabaseV4?.parallelism ?: KdfEngine.UNKNOWN_VALUE
|
||||
set(parallelism) {
|
||||
@@ -161,11 +224,30 @@ class Database {
|
||||
return null
|
||||
}
|
||||
|
||||
val manageHistory: Boolean
|
||||
get() = pwDatabaseV4 != null
|
||||
|
||||
var historyMaxItems: Int
|
||||
get() {
|
||||
return pwDatabaseV4?.historyMaxItems ?: 0
|
||||
}
|
||||
set(value) {
|
||||
pwDatabaseV4?.historyMaxItems = value
|
||||
}
|
||||
|
||||
var historyMaxSize: Long
|
||||
get() {
|
||||
return pwDatabaseV4?.historyMaxSize ?: 0
|
||||
}
|
||||
set(value) {
|
||||
pwDatabaseV4?.historyMaxSize = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if RecycleBin is available or not for this version of database
|
||||
* @return true if RecycleBin available
|
||||
*/
|
||||
val isRecycleBinAvailable: Boolean
|
||||
val allowRecycleBin: Boolean
|
||||
get() = pwDatabaseV4 != null
|
||||
|
||||
val isRecycleBinEnabled: Boolean
|
||||
@@ -203,14 +285,20 @@ class Database {
|
||||
fun createData(databaseUri: Uri) {
|
||||
// Always create a new database with the last version
|
||||
setDatabaseV4(PwDatabaseV4(dbNameFromUri(databaseUri)))
|
||||
this.mUri = databaseUri
|
||||
this.fileUri = databaseUri
|
||||
}
|
||||
|
||||
@Throws(IOException::class, InvalidDBException::class)
|
||||
fun loadData(ctx: Context, uri: Uri, password: String?, keyfile: Uri?, progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
@Throws(LoadDatabaseException::class)
|
||||
fun loadData(uri: Uri, password: String?, keyfile: Uri?,
|
||||
readOnly: Boolean,
|
||||
contentResolver: ContentResolver,
|
||||
cacheDirectory: File,
|
||||
omitBackup: Boolean,
|
||||
fixDuplicateUUID: Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
|
||||
mUri = uri
|
||||
isReadOnly = false
|
||||
this.fileUri = uri
|
||||
isReadOnly = readOnly
|
||||
if (uri.scheme == "file") {
|
||||
val file = File(uri.path!!)
|
||||
isReadOnly = !file.canWrite()
|
||||
@@ -219,20 +307,20 @@ class Database {
|
||||
// Pass Uris as InputStreams
|
||||
val inputStream: InputStream?
|
||||
try {
|
||||
inputStream = UriUtil.getUriInputStream(ctx.contentResolver, uri)
|
||||
inputStream = UriUtil.getUriInputStream(contentResolver, uri)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KPD", "Database::loadData", e)
|
||||
throw ContentFileNotFoundException.getInstance(uri)
|
||||
throw LoadDatabaseFileNotFoundException()
|
||||
}
|
||||
|
||||
// Pass KeyFile Uri as InputStreams
|
||||
var keyFileInputStream: InputStream? = null
|
||||
keyfile?.let {
|
||||
try {
|
||||
keyFileInputStream = UriUtil.getUriInputStream(ctx.contentResolver, keyfile)
|
||||
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KPD", "Database::loadData", e)
|
||||
throw ContentFileNotFoundException.getInstance(keyfile)
|
||||
throw LoadDatabaseFileNotFoundException()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,28 +345,25 @@ class Database {
|
||||
// Header of database V3
|
||||
PwDbHeaderV3.matchesHeader(sig1, sig2) -> setDatabaseV3(ImporterV3()
|
||||
.openDatabase(bufferedInputStream,
|
||||
password,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater))
|
||||
password,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater))
|
||||
|
||||
// Header of database V4
|
||||
PwDbHeaderV4.matchesHeader(sig1, sig2) -> setDatabaseV4(ImporterV4(ctx.filesDir)
|
||||
PwDbHeaderV4.matchesHeader(sig1, sig2) -> setDatabaseV4(ImporterV4(
|
||||
cacheDirectory,
|
||||
fixDuplicateUUID)
|
||||
.openDatabase(bufferedInputStream,
|
||||
password,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater))
|
||||
password,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater))
|
||||
|
||||
// Header not recognized
|
||||
else -> throw InvalidDBSignatureException()
|
||||
else -> throw LoadDatabaseSignatureException()
|
||||
}
|
||||
|
||||
try {
|
||||
searchHelper = SearchDbHelper(PreferencesUtil.omitBackup(ctx))
|
||||
loaded = true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Load can't be performed with this Database version", e)
|
||||
loaded = false
|
||||
}
|
||||
this.mSearchHelper = SearchDbHelper(omitBackup)
|
||||
loaded = true
|
||||
}
|
||||
|
||||
fun isGroupSearchable(group: GroupVersioned, isOmitBackup: Boolean): Boolean {
|
||||
@@ -289,7 +374,7 @@ class Database {
|
||||
|
||||
@JvmOverloads
|
||||
fun search(str: String, max: Int = Integer.MAX_VALUE): GroupVersioned? {
|
||||
return searchHelper?.search(this, str, max)
|
||||
return mSearchHelper?.search(this, str, max)
|
||||
}
|
||||
|
||||
fun searchEntries(query: String): Cursor? {
|
||||
@@ -340,14 +425,14 @@ class Database {
|
||||
return entry
|
||||
}
|
||||
|
||||
@Throws(IOException::class, PwDbOutputException::class)
|
||||
@Throws(IOException::class, DatabaseOutputException::class)
|
||||
fun saveData(contentResolver: ContentResolver) {
|
||||
mUri?.let {
|
||||
this.fileUri?.let {
|
||||
saveData(contentResolver, it)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, PwDbOutputException::class)
|
||||
@Throws(IOException::class, DatabaseOutputException::class)
|
||||
private fun saveData(contentResolver: ContentResolver, uri: Uri) {
|
||||
val errorMessage = "Failed to store database."
|
||||
|
||||
@@ -394,7 +479,7 @@ class Database {
|
||||
outputStream?.close()
|
||||
}
|
||||
}
|
||||
mUri = uri
|
||||
this.fileUri = uri
|
||||
}
|
||||
|
||||
// TODO Clear database when lock broadcast is receive in backstage
|
||||
@@ -406,77 +491,23 @@ class Database {
|
||||
// In all cases, delete all the files in the temp dir
|
||||
try {
|
||||
FileUtils.cleanDirectory(filesDirectory)
|
||||
} catch (e: IOException) {
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to clear the directory cache.", e)
|
||||
}
|
||||
|
||||
pwDatabaseV3 = null
|
||||
pwDatabaseV4 = null
|
||||
mUri = null
|
||||
loaded = false
|
||||
this.pwDatabaseV3 = null
|
||||
this.pwDatabaseV4 = null
|
||||
this.fileUri = null
|
||||
this.loaded = false
|
||||
}
|
||||
|
||||
fun getVersion(): String {
|
||||
return pwDatabaseV3?.version ?: pwDatabaseV4?.version ?: "unknown"
|
||||
}
|
||||
|
||||
fun containsName(): Boolean {
|
||||
pwDatabaseV4?.let { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fun assignName(name: String) {
|
||||
pwDatabaseV4?.name = name
|
||||
pwDatabaseV4?.nameChanged = PwDate()
|
||||
}
|
||||
|
||||
fun containsDescription(): Boolean {
|
||||
pwDatabaseV4?.let { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fun assignDescription(description: String) {
|
||||
pwDatabaseV4?.description = description
|
||||
pwDatabaseV4?.descriptionChanged = PwDate()
|
||||
}
|
||||
|
||||
fun allowEncryptionAlgorithmModification(): Boolean {
|
||||
return availableEncryptionAlgorithms.size > 1
|
||||
}
|
||||
|
||||
fun assignEncryptionAlgorithm(algorithm: PwEncryptionAlgorithm) {
|
||||
pwDatabaseV4?.encryptionAlgorithm = algorithm
|
||||
pwDatabaseV4?.setDataEngine(algorithm.cipherEngine)
|
||||
pwDatabaseV4?.dataCipher = algorithm.dataCipher
|
||||
}
|
||||
|
||||
fun getEncryptionAlgorithmName(resources: Resources): String {
|
||||
return pwDatabaseV3?.encryptionAlgorithm?.getName(resources) ?: pwDatabaseV4?.encryptionAlgorithm?.getName(resources) ?: ""
|
||||
}
|
||||
|
||||
fun allowKdfModification(): Boolean {
|
||||
return availableKdfEngines.size > 1
|
||||
}
|
||||
|
||||
fun assignKdfEngine(kdfEngine: KdfEngine) {
|
||||
if (pwDatabaseV4?.kdfParameters?.uuid != kdfEngine.defaultParameters.uuid)
|
||||
pwDatabaseV4?.kdfParameters = kdfEngine.defaultParameters
|
||||
numberKeyEncryptionRounds = kdfEngine.defaultKeyRounds
|
||||
memoryUsage = kdfEngine.getDefaultMemoryUsage()
|
||||
parallelism = kdfEngine.getDefaultParallelism()
|
||||
}
|
||||
|
||||
fun getKeyDerivationName(resources: Resources): String {
|
||||
return kdfEngine.getName(resources)
|
||||
}
|
||||
|
||||
fun validatePasswordEncoding(key: String?): Boolean {
|
||||
return pwDatabaseV3?.validatePasswordEncoding(key)
|
||||
?: pwDatabaseV4?.validatePasswordEncoding(key)
|
||||
fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||
return pwDatabaseV3?.validatePasswordEncoding(password, containsKeyFile)
|
||||
?: pwDatabaseV4?.validatePasswordEncoding(password, containsKeyFile)
|
||||
?: false
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(IOException::class)
|
||||
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
|
||||
pwDatabaseV3?.retrieveMasterKey(key, keyInputStream)
|
||||
pwDatabaseV4?.retrieveMasterKey(key, keyInputStream)
|
||||
@@ -516,7 +547,7 @@ class Database {
|
||||
return null
|
||||
}
|
||||
|
||||
fun getEntryById(id: PwNodeId<*>): EntryVersioned? {
|
||||
fun getEntryById(id: PwNodeId<UUID>): EntryVersioned? {
|
||||
pwDatabaseV3?.getEntryById(id)?.let {
|
||||
return EntryVersioned(it)
|
||||
}
|
||||
@@ -527,12 +558,14 @@ class Database {
|
||||
}
|
||||
|
||||
fun getGroupById(id: PwNodeId<*>): GroupVersioned? {
|
||||
pwDatabaseV3?.getGroupById(id)?.let {
|
||||
return GroupVersioned(it)
|
||||
}
|
||||
pwDatabaseV4?.getGroupById(id)?.let {
|
||||
return GroupVersioned(it)
|
||||
}
|
||||
if (id is PwNodeIdInt)
|
||||
pwDatabaseV3?.getGroupById(id)?.let {
|
||||
return GroupVersioned(it)
|
||||
}
|
||||
else if (id is PwNodeIdUUID)
|
||||
pwDatabaseV4?.getGroupById(id)?.let {
|
||||
return GroupVersioned(it)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -546,6 +579,15 @@ class Database {
|
||||
entry.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun updateEntry(entry: EntryVersioned) {
|
||||
entry.pwEntryV3?.let { entryV3 ->
|
||||
pwDatabaseV3?.updateEntry(entryV3)
|
||||
}
|
||||
entry.pwEntryV4?.let { entryV4 ->
|
||||
pwDatabaseV4?.updateEntry(entryV4)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEntryFrom(entry: EntryVersioned, parent: GroupVersioned) {
|
||||
entry.pwEntryV3?.let { entryV3 ->
|
||||
pwDatabaseV3?.removeEntryFrom(entryV3, parent.pwGroupV3)
|
||||
@@ -566,6 +608,15 @@ class Database {
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun updateGroup(group: GroupVersioned) {
|
||||
group.pwGroupV3?.let { groupV3 ->
|
||||
pwDatabaseV3?.updateGroup(groupV3)
|
||||
}
|
||||
group.pwGroupV4?.let { groupV4 ->
|
||||
pwDatabaseV4?.updateGroup(groupV4)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeGroupFrom(group: GroupVersioned, parent: GroupVersioned) {
|
||||
group.pwGroupV3?.let { groupV3 ->
|
||||
pwDatabaseV3?.removeGroupFrom(groupV3, parent.pwGroupV3)
|
||||
@@ -582,7 +633,7 @@ class Database {
|
||||
* @param newParent
|
||||
*/
|
||||
fun copyEntryTo(entryToCopy: EntryVersioned, newParent: GroupVersioned): EntryVersioned? {
|
||||
val entryCopied = EntryVersioned(entryToCopy)
|
||||
val entryCopied = EntryVersioned(entryToCopy, false)
|
||||
entryCopied.nodeId = pwDatabaseV3?.newEntryId() ?: pwDatabaseV4?.newEntryId() ?: PwNodeIdUUID()
|
||||
entryCopied.parent = newParent
|
||||
entryCopied.title += " (~)"
|
||||
@@ -702,31 +753,28 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
fun addHistoryBackupTo(entry: EntryVersioned): EntryVersioned {
|
||||
val backupEntry = EntryVersioned(entry)
|
||||
fun removeOldestHistory(entry: EntryVersioned) {
|
||||
|
||||
entry.addBackupToHistory()
|
||||
|
||||
// Remove oldest backup if more than max items or max memory
|
||||
// Remove oldest history if more than max items or max memory
|
||||
pwDatabaseV4?.let {
|
||||
val history = entry.getHistory()
|
||||
|
||||
val maxItems = it.historyMaxItems
|
||||
val maxItems = historyMaxItems
|
||||
if (maxItems >= 0) {
|
||||
while (history.size > maxItems) {
|
||||
entry.removeOldestEntryFromHistory()
|
||||
}
|
||||
}
|
||||
|
||||
val maxSize = it.historyMaxSize
|
||||
val maxSize = historyMaxSize
|
||||
if (maxSize >= 0) {
|
||||
while (true) {
|
||||
var histSize: Long = 0
|
||||
for (backup in history) {
|
||||
histSize += backup.size
|
||||
var historySize: Long = 0
|
||||
for (entryHistory in history) {
|
||||
historySize += entryHistory.getSize()
|
||||
}
|
||||
|
||||
if (histSize > maxSize) {
|
||||
if (historySize > maxSize) {
|
||||
entry.removeOldestEntryFromHistory()
|
||||
} else {
|
||||
break
|
||||
@@ -734,8 +782,6 @@ class Database {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return backupEntry
|
||||
}
|
||||
|
||||
companion object : SingletonHolder<Database>(::Database) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
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.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@@ -15,26 +17,26 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
|
||||
var pwEntryV4: PwEntryV4? = null
|
||||
private set
|
||||
|
||||
fun updateWith(entry: EntryVersioned) {
|
||||
fun updateWith(entry: EntryVersioned, copyHistory: Boolean = true) {
|
||||
entry.pwEntryV3?.let {
|
||||
this.pwEntryV3?.updateWith(it)
|
||||
}
|
||||
entry.pwEntryV4?.let {
|
||||
this.pwEntryV4?.updateWith(it)
|
||||
this.pwEntryV4?.updateWith(it, copyHistory)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this constructor to copy an Entry with exact same values
|
||||
*/
|
||||
constructor(entry: EntryVersioned) {
|
||||
constructor(entry: EntryVersioned, copyHistory: Boolean = true) {
|
||||
if (entry.pwEntryV3 != null) {
|
||||
this.pwEntryV3 = PwEntryV3()
|
||||
}
|
||||
if (entry.pwEntryV4 != null) {
|
||||
this.pwEntryV4 = PwEntryV4()
|
||||
}
|
||||
updateWith(entry)
|
||||
updateWith(entry, copyHistory)
|
||||
}
|
||||
|
||||
constructor(entry: PwEntryV3) {
|
||||
@@ -61,7 +63,7 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
|
||||
dest.writeParcelable(pwEntryV4, flags)
|
||||
}
|
||||
|
||||
var nodeId: PwNodeId<UUID>
|
||||
override var nodeId: PwNodeId<UUID>
|
||||
get() = pwEntryV4?.nodeId ?: pwEntryV3?.nodeId ?: PwNodeIdUUID()
|
||||
set(value) {
|
||||
pwEntryV3?.nodeId = value
|
||||
@@ -154,13 +156,16 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
|
||||
pwEntryV4?.expiryTime = value
|
||||
}
|
||||
|
||||
override var isExpires: Boolean
|
||||
get() =pwEntryV3?.isExpires ?: pwEntryV4?.isExpires ?: false
|
||||
override var expires: Boolean
|
||||
get() = pwEntryV3?.expires ?: pwEntryV4?.expires ?: false
|
||||
set(value) {
|
||||
pwEntryV3?.isExpires = value
|
||||
pwEntryV4?.isExpires = value
|
||||
pwEntryV3?.expires = value
|
||||
pwEntryV4?.expires = value
|
||||
}
|
||||
|
||||
override val isCurrentlyExpires: Boolean
|
||||
get() = pwEntryV3?.isCurrentlyExpires ?: pwEntryV4?.isCurrentlyExpires ?: false
|
||||
|
||||
override var username: String
|
||||
get() = pwEntryV3?.username ?: pwEntryV4?.username ?: ""
|
||||
set(value) {
|
||||
@@ -241,13 +246,23 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
|
||||
return pwEntryV4?.allowCustomFields() ?: false
|
||||
}
|
||||
|
||||
fun removeAllFields() {
|
||||
pwEntryV4?.removeAllFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an extra field to the list (standard or custom)
|
||||
* Update or add an extra field to the list (standard or custom)
|
||||
* @param label Label of field, must be unique
|
||||
* @param value Value of field
|
||||
*/
|
||||
fun addExtraField(label: String, value: ProtectedString) {
|
||||
pwEntryV4?.addExtraField(label, value)
|
||||
fun putExtraField(label: String, value: ProtectedString) {
|
||||
pwEntryV4?.putExtraField(label, value)
|
||||
}
|
||||
|
||||
fun getOtpElement(): OtpElement? {
|
||||
return OtpEntryFields.parseFields { key ->
|
||||
customFields[key]?.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun startToManageFieldReferences(db: PwDatabaseV4) {
|
||||
@@ -258,20 +273,31 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
|
||||
pwEntryV4?.stopToManageFieldReferences()
|
||||
}
|
||||
|
||||
fun addBackupToHistory() {
|
||||
pwEntryV4?.let {
|
||||
val entryHistory = PwEntryV4()
|
||||
entryHistory.updateWith(it)
|
||||
it.addEntryToHistory(entryHistory)
|
||||
fun getHistory(): ArrayList<EntryVersioned> {
|
||||
val history = ArrayList<EntryVersioned>()
|
||||
val entryV4History = pwEntryV4?.history ?: ArrayList()
|
||||
for (entryHistory in entryV4History) {
|
||||
history.add(EntryVersioned(entryHistory))
|
||||
}
|
||||
return history
|
||||
}
|
||||
|
||||
fun addEntryToHistory(entry: EntryVersioned) {
|
||||
entry.pwEntryV4?.let {
|
||||
pwEntryV4?.addEntryToHistory(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllHistory() {
|
||||
pwEntryV4?.removeAllHistory()
|
||||
}
|
||||
|
||||
fun removeOldestEntryFromHistory() {
|
||||
pwEntryV4?.removeOldestEntryFromHistory()
|
||||
}
|
||||
|
||||
fun getHistory(): ArrayList<PwEntryV4> {
|
||||
return pwEntryV4?.history ?: ArrayList()
|
||||
fun getSize(): Long {
|
||||
return pwEntryV4?.size ?: 0L
|
||||
}
|
||||
|
||||
fun containsCustomData(): Boolean {
|
||||
@@ -284,6 +310,10 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
|
||||
------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieve generated entry info,
|
||||
* Remove parameter fields and add auto generated elements in auto custom fields
|
||||
*/
|
||||
fun getEntryInfo(database: Database?, raw: Boolean = false): EntryInfo {
|
||||
val entryInfo = EntryInfo()
|
||||
if (raw)
|
||||
@@ -300,6 +330,10 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
|
||||
entryInfo.customFields.add(
|
||||
Field(entry.key, entry.value))
|
||||
}
|
||||
// Add otpElement to generate token
|
||||
entryInfo.otpModel = getOtpElement()?.otpModel
|
||||
// Replace parameter fields by generated OTP fields
|
||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||
if (!raw)
|
||||
database?.stopManageEntry(this)
|
||||
return entryInfo
|
||||
|
||||
@@ -70,7 +70,7 @@ class GroupVersioned : NodeVersioned, PwGroupInterface<GroupVersioned, EntryVers
|
||||
dest.writeParcelable(pwGroupV4, flags)
|
||||
}
|
||||
|
||||
val nodeId: PwNodeId<*>?
|
||||
override val nodeId: PwNodeId<*>?
|
||||
get() = pwGroupV4?.nodeId ?: pwGroupV3?.nodeId
|
||||
|
||||
override var title: String
|
||||
@@ -114,6 +114,38 @@ class GroupVersioned : NodeVersioned, PwGroupInterface<GroupVersioned, EntryVers
|
||||
pwGroupV4?.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun addChildrenFrom(group: GroupVersioned) {
|
||||
group.pwGroupV3?.getChildEntries()?.forEach { entryToAdd ->
|
||||
pwGroupV3?.addChildEntry(entryToAdd)
|
||||
}
|
||||
group.pwGroupV3?.getChildGroups()?.forEach { groupToAdd ->
|
||||
pwGroupV3?.addChildGroup(groupToAdd)
|
||||
}
|
||||
|
||||
group.pwGroupV4?.getChildEntries()?.forEach { entryToAdd ->
|
||||
pwGroupV4?.addChildEntry(entryToAdd)
|
||||
}
|
||||
group.pwGroupV4?.getChildGroups()?.forEach { groupToAdd ->
|
||||
pwGroupV4?.addChildGroup(groupToAdd)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChildren() {
|
||||
pwGroupV3?.getChildEntries()?.forEach { entryToRemove ->
|
||||
pwGroupV3?.removeChildEntry(entryToRemove)
|
||||
}
|
||||
pwGroupV3?.getChildGroups()?.forEach { groupToRemove ->
|
||||
pwGroupV3?.removeChildGroup(groupToRemove)
|
||||
}
|
||||
|
||||
pwGroupV4?.getChildEntries()?.forEach { entryToRemove ->
|
||||
pwGroupV4?.removeChildEntry(entryToRemove)
|
||||
}
|
||||
pwGroupV4?.getChildGroups()?.forEach { groupToRemove ->
|
||||
pwGroupV4?.removeChildGroup(groupToRemove)
|
||||
}
|
||||
}
|
||||
|
||||
override fun touch(modified: Boolean, touchParents: Boolean) {
|
||||
pwGroupV3?.touch(modified, touchParents)
|
||||
pwGroupV4?.touch(modified, touchParents)
|
||||
@@ -158,13 +190,16 @@ class GroupVersioned : NodeVersioned, PwGroupInterface<GroupVersioned, EntryVers
|
||||
pwGroupV4?.expiryTime = value
|
||||
}
|
||||
|
||||
override var isExpires: Boolean
|
||||
get() = pwGroupV3?.isExpires ?: pwGroupV4?.isExpires ?: false
|
||||
override var expires: Boolean
|
||||
get() = pwGroupV3?.expires ?: pwGroupV4?.expires ?: false
|
||||
set(value) {
|
||||
pwGroupV3?.isExpires = value
|
||||
pwGroupV4?.isExpires = value
|
||||
pwGroupV3?.expires = value
|
||||
pwGroupV4?.expires = value
|
||||
}
|
||||
|
||||
override val isCurrentlyExpires: Boolean
|
||||
get() = pwGroupV3?.isCurrentlyExpires ?: pwGroupV4?.isCurrentlyExpires ?: false
|
||||
|
||||
override fun getChildGroups(): MutableList<GroupVersioned> {
|
||||
val children = ArrayList<GroupVersioned>()
|
||||
|
||||
|
||||
@@ -29,5 +29,7 @@ interface NodeTimeInterface {
|
||||
|
||||
var expiryTime: PwDate
|
||||
|
||||
var isExpires: Boolean
|
||||
var expires: Boolean
|
||||
|
||||
val isCurrentlyExpires: Boolean
|
||||
}
|
||||
|
||||
@@ -2,16 +2,26 @@ package com.kunzisoft.keepass.database.element
|
||||
|
||||
interface NodeVersioned: PwNodeInterface<GroupVersioned> {
|
||||
|
||||
val nodeId: PwNodeId<*>?
|
||||
|
||||
val nodePositionInParent: Int
|
||||
get() {
|
||||
parent?.getChildren(true)?.let { children ->
|
||||
for ((i, child) in children.withIndex()) {
|
||||
if (child == this)
|
||||
return i
|
||||
children.forEachIndexed { index, nodeVersioned ->
|
||||
if (nodeVersioned.nodeId == this.nodeId)
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun addParentFrom(node: NodeVersioned) {
|
||||
parent = node.parent
|
||||
}
|
||||
|
||||
fun removeParent() {
|
||||
parent = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,25 +17,24 @@
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.file
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.content.res.Resources
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||
|
||||
// Note: We can get away with using int's to store unsigned 32-bit ints
|
||||
// since we won't do arithmetic on these values (also unlikely to
|
||||
// reach negative ids).
|
||||
enum class PwCompressionAlgorithm constructor(val id: Int) {
|
||||
enum class PwCompressionAlgorithm : ObjectNameResource {
|
||||
|
||||
None(0),
|
||||
Gzip(1);
|
||||
None,
|
||||
GZip;
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromId(num: Int): PwCompressionAlgorithm? {
|
||||
for (e in values()) {
|
||||
if (e.id == num) {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return null
|
||||
override fun getName(resources: Resources): String {
|
||||
return when (this) {
|
||||
None -> resources.getString(R.string.compression_none)
|
||||
GZip -> resources.getString(R.string.compression_gzip)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,26 +19,29 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
|
||||
import com.kunzisoft.keepass.database.exception.KeyFileEmptyException
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseKeyFileEmptyException
|
||||
import com.kunzisoft.keepass.utils.MemoryUtil
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.io.*
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.LinkedHashMap
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
|
||||
abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Group, Entry>> {
|
||||
abstract class PwDatabase<
|
||||
GroupId,
|
||||
EntryId,
|
||||
Group : PwGroup<GroupId, EntryId, Group, Entry>,
|
||||
Entry : PwEntry<GroupId, EntryId, Group, Entry>
|
||||
> {
|
||||
|
||||
// Algorithm used to encrypt the database
|
||||
protected var algorithm: PwEncryptionAlgorithm? = null
|
||||
|
||||
abstract val kdfEngine: KdfEngine?
|
||||
|
||||
abstract val kdfAvailableList: List<KdfEngine>
|
||||
|
||||
var masterKey = ByteArray(32)
|
||||
var finalKey: ByteArray? = null
|
||||
protected set
|
||||
@@ -46,8 +49,10 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
var iconFactory = PwIconFactory()
|
||||
protected set
|
||||
|
||||
private var groupIndexes = LinkedHashMap<PwNodeId<*>, Group>()
|
||||
private var entryIndexes = LinkedHashMap<PwNodeId<*>, Entry>()
|
||||
var changeDuplicateId = false
|
||||
|
||||
private var groupIndexes = LinkedHashMap<PwNodeId<GroupId>, Group>()
|
||||
private var entryIndexes = LinkedHashMap<PwNodeId<EntryId>, Entry>()
|
||||
|
||||
abstract val version: String
|
||||
|
||||
@@ -67,15 +72,15 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
|
||||
var rootGroup: Group? = null
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(IOException::class)
|
||||
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(IOException::class)
|
||||
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
|
||||
masterKey = getMasterKey(key, keyInputStream)
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(IOException::class)
|
||||
protected fun getCompositeKey(key: String, keyInputStream: InputStream): ByteArray {
|
||||
val fileKey = getFileKey(keyInputStream)
|
||||
val passwordKey = getPasswordKey(key)
|
||||
@@ -115,7 +120,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
return messageDigest.digest()
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(IOException::class)
|
||||
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
||||
|
||||
val keyByteArrayOutputStream = ByteArrayOutputStream()
|
||||
@@ -129,7 +134,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
}
|
||||
|
||||
when (keyData.size.toLong()) {
|
||||
0L -> throw KeyFileEmptyException()
|
||||
0L -> throw LoadDatabaseKeyFileEmptyException()
|
||||
32L -> return keyData
|
||||
64L -> try {
|
||||
return hexStringToByteArray(String(keyData))
|
||||
@@ -156,15 +161,18 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
|
||||
protected abstract fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray?
|
||||
|
||||
open fun validatePasswordEncoding(key: String?): Boolean {
|
||||
if (key == null)
|
||||
open fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||
if (password == null && !containsKeyFile)
|
||||
return false
|
||||
|
||||
if (password == null)
|
||||
return true
|
||||
|
||||
val encoding = passwordEncoding
|
||||
|
||||
val bKey: ByteArray
|
||||
try {
|
||||
bKey = key.toByteArray(charset(encoding))
|
||||
bKey = password.toByteArray(charset(encoding))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
return false
|
||||
}
|
||||
@@ -175,7 +183,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
return false
|
||||
}
|
||||
return key == reEncoded
|
||||
return password == reEncoded
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -184,9 +192,9 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
* -------------------------------------
|
||||
*/
|
||||
|
||||
abstract fun newGroupId(): PwNodeId<*>
|
||||
abstract fun newGroupId(): PwNodeId<GroupId>
|
||||
|
||||
abstract fun newEntryId(): PwNodeId<*>
|
||||
abstract fun newEntryId(): PwNodeId<EntryId>
|
||||
|
||||
abstract fun createGroup(): Group
|
||||
|
||||
@@ -211,7 +219,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
* ID number to check for
|
||||
* @return True if the ID is used, false otherwise
|
||||
*/
|
||||
fun isGroupIdUsed(id: PwNodeId<*>): Boolean {
|
||||
fun isGroupIdUsed(id: PwNodeId<GroupId>): Boolean {
|
||||
return groupIndexes.containsKey(id)
|
||||
}
|
||||
|
||||
@@ -226,19 +234,33 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupById(id: PwNodeId<*>): Group? {
|
||||
fun getGroupById(id: PwNodeId<GroupId>): Group? {
|
||||
return this.groupIndexes[id]
|
||||
}
|
||||
|
||||
fun addGroupIndex(group: Group) {
|
||||
val groupId = group.nodeId
|
||||
if (groupIndexes.containsKey(groupId)) {
|
||||
Log.e(TAG, "Error, a group with the same UUID $groupId already exists")
|
||||
if (changeDuplicateId) {
|
||||
val newGroupId = newGroupId()
|
||||
group.nodeId = newGroupId
|
||||
group.parent?.addChildGroup(group)
|
||||
this.groupIndexes[newGroupId] = group
|
||||
} else {
|
||||
throw LoadDatabaseDuplicateUuidException(Type.GROUP, groupId)
|
||||
}
|
||||
} else {
|
||||
this.groupIndexes[groupId] = group
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGroupIndex(group: Group) {
|
||||
val groupId = group.nodeId
|
||||
if (groupIndexes.containsKey(groupId)) {
|
||||
groupIndexes[groupId] = group
|
||||
}
|
||||
}
|
||||
|
||||
fun removeGroupIndex(group: Group) {
|
||||
this.groupIndexes.remove(group.nodeId)
|
||||
}
|
||||
@@ -253,7 +275,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
}
|
||||
}
|
||||
|
||||
fun isEntryIdUsed(id: PwNodeId<*>): Boolean {
|
||||
fun isEntryIdUsed(id: PwNodeId<EntryId>): Boolean {
|
||||
return entryIndexes.containsKey(id)
|
||||
}
|
||||
|
||||
@@ -261,20 +283,33 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
return entryIndexes.values
|
||||
}
|
||||
|
||||
fun getEntryById(id: PwNodeId<*>): Entry? {
|
||||
fun getEntryById(id: PwNodeId<EntryId>): Entry? {
|
||||
return this.entryIndexes[id]
|
||||
}
|
||||
|
||||
fun addEntryIndex(entry: Entry) {
|
||||
val entryId = entry.nodeId
|
||||
if (entryIndexes.containsKey(entryId)) {
|
||||
// TODO History
|
||||
Log.e(TAG, "Error, a group with the same UUID $entryId already exists, change the UUID")
|
||||
if (changeDuplicateId) {
|
||||
val newEntryId = newEntryId()
|
||||
entry.nodeId = newEntryId
|
||||
entry.parent?.addChildEntry(entry)
|
||||
this.entryIndexes[newEntryId] = entry
|
||||
} else {
|
||||
throw LoadDatabaseDuplicateUuidException(Type.ENTRY, entryId)
|
||||
}
|
||||
} else {
|
||||
this.entryIndexes[entryId] = entry
|
||||
}
|
||||
}
|
||||
|
||||
fun updateEntryIndex(entry: Entry) {
|
||||
val entryId = entry.nodeId
|
||||
if (entryIndexes.containsKey(entryId)) {
|
||||
entryIndexes[entryId] = entry
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEntryIndex(entry: Entry) {
|
||||
this.entryIndexes.remove(entry.nodeId)
|
||||
}
|
||||
@@ -305,6 +340,10 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
addGroupIndex(newGroup)
|
||||
}
|
||||
|
||||
fun updateGroup(group: Group) {
|
||||
updateGroupIndex(group)
|
||||
}
|
||||
|
||||
fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
|
||||
// Remove tree from parent tree
|
||||
parent?.removeChildGroup(groupToRemove)
|
||||
@@ -318,6 +357,10 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
addEntryIndex(newEntry)
|
||||
}
|
||||
|
||||
fun updateEntry(entry: Entry) {
|
||||
updateEntryIndex(entry)
|
||||
}
|
||||
|
||||
open fun removeEntryFrom(entryToRemove: Entry, parent: Group?) {
|
||||
// Remove entry from parent
|
||||
parent?.removeChildEntry(entryToRemove)
|
||||
|
||||
@@ -20,26 +20,36 @@
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory
|
||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.DigestOutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
/**
|
||||
* @author Naomaru Itoi <nao></nao>@phoneid.org>
|
||||
* @author Bill Zwicky <wrzwicky></wrzwicky>@pobox.com>
|
||||
* @author Dominik Reichl <dominik.reichl></dominik.reichl>@t-online.de>
|
||||
*/
|
||||
class PwDatabaseV3 : PwDatabase<PwGroupV3, PwEntryV3>() {
|
||||
class PwDatabaseV3 : PwDatabase<Int, UUID, PwGroupV3, PwEntryV3>() {
|
||||
|
||||
private var numKeyEncRounds: Int = 0
|
||||
|
||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||
|
||||
override val version: String
|
||||
get() = "KeePass 1"
|
||||
|
||||
init {
|
||||
kdfListV3.add(KdfFactory.aesKdf)
|
||||
}
|
||||
|
||||
override val kdfEngine: KdfEngine?
|
||||
get() = kdfListV3[0]
|
||||
|
||||
override val kdfAvailableList: List<KdfEngine>
|
||||
get() = kdfListV3
|
||||
|
||||
override val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm>
|
||||
get() {
|
||||
val list = ArrayList<PwEncryptionAlgorithm>()
|
||||
@@ -103,7 +113,7 @@ class PwDatabaseV3 : PwDatabase<PwGroupV3, PwEntryV3>() {
|
||||
return newId
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(IOException::class)
|
||||
override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
|
||||
|
||||
return if (key != null && keyInputStream != null) {
|
||||
|
||||
@@ -26,12 +26,8 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.crypto.CryptoUtil
|
||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.*
|
||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||
import com.kunzisoft.keepass.database.file.PwCompressionAlgorithm
|
||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||
import org.w3c.dom.Node
|
||||
import org.w3c.dom.Text
|
||||
@@ -40,17 +36,20 @@ import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
import javax.xml.XMLConstants
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.parsers.ParserConfigurationException
|
||||
|
||||
|
||||
class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
class PwDatabaseV4 : PwDatabase<UUID, UUID, PwGroupV4, PwEntryV4> {
|
||||
|
||||
var hmacKey: ByteArray? = null
|
||||
private set
|
||||
var dataCipher = AesEngine.CIPHER_UUID
|
||||
private var dataEngine: CipherEngine = AesEngine()
|
||||
var compressionAlgorithm = PwCompressionAlgorithm.Gzip
|
||||
var compressionAlgorithm = PwCompressionAlgorithm.GZip
|
||||
var kdfParameters: KdfParameters? = null
|
||||
private var kdfV4List: MutableList<KdfEngine> = ArrayList()
|
||||
private var numKeyEncRounds: Long = 0
|
||||
var publicCustomData = VariantDictionary()
|
||||
|
||||
@@ -93,6 +92,11 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
|
||||
var localizedAppName = "KeePassDX" // TODO resource
|
||||
|
||||
init {
|
||||
kdfV4List.add(KdfFactory.aesKdf)
|
||||
kdfV4List.add(KdfFactory.argon2Kdf)
|
||||
}
|
||||
|
||||
constructor()
|
||||
|
||||
constructor(databaseName: String) {
|
||||
@@ -107,6 +111,39 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
override val version: String
|
||||
get() = "KeePass 2"
|
||||
|
||||
override val kdfEngine: KdfEngine?
|
||||
get() = try {
|
||||
getEngineV4(kdfParameters)
|
||||
} catch (unknownKDF: UnknownKDF) {
|
||||
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
|
||||
null
|
||||
}
|
||||
|
||||
override val kdfAvailableList: List<KdfEngine>
|
||||
get() = kdfV4List
|
||||
|
||||
@Throws(UnknownKDF::class)
|
||||
fun getEngineV4(kdfParameters: KdfParameters?): KdfEngine {
|
||||
val unknownKDFException = UnknownKDF()
|
||||
if (kdfParameters == null) {
|
||||
throw unknownKDFException
|
||||
}
|
||||
for (engine in kdfV4List) {
|
||||
if (engine.uuid == kdfParameters.uuid) {
|
||||
return engine
|
||||
}
|
||||
}
|
||||
throw unknownKDFException
|
||||
}
|
||||
|
||||
val availableCompressionAlgorithms: List<PwCompressionAlgorithm>
|
||||
get() {
|
||||
val list = ArrayList<PwCompressionAlgorithm>()
|
||||
list.add(PwCompressionAlgorithm.None)
|
||||
list.add(PwCompressionAlgorithm.GZip)
|
||||
return list
|
||||
}
|
||||
|
||||
override val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm>
|
||||
get() {
|
||||
val list = ArrayList<PwEncryptionAlgorithm>()
|
||||
@@ -116,45 +153,45 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
return list
|
||||
}
|
||||
|
||||
val kdfEngine: KdfEngine?
|
||||
get() {
|
||||
return try {
|
||||
KdfFactory.getEngineV4(kdfParameters)
|
||||
} catch (unknownKDF: UnknownKDF) {
|
||||
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override var numberKeyEncryptionRounds: Long
|
||||
get() {
|
||||
val kdfEngine = kdfEngine
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
numKeyEncRounds = kdfEngine!!.getKeyRounds(kdfParameters!!)
|
||||
numKeyEncRounds = kdfEngine.getKeyRounds(kdfParameters!!)
|
||||
return numKeyEncRounds
|
||||
}
|
||||
@Throws(NumberFormatException::class)
|
||||
set(rounds) {
|
||||
val kdfEngine = kdfEngine
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
kdfEngine!!.setKeyRounds(kdfParameters!!, rounds)
|
||||
kdfEngine.setKeyRounds(kdfParameters!!, rounds)
|
||||
numKeyEncRounds = rounds
|
||||
}
|
||||
|
||||
var memoryUsage: Long
|
||||
get() = if (kdfEngine != null && kdfParameters != null) {
|
||||
kdfEngine!!.getMemoryUsage(kdfParameters!!)
|
||||
} else KdfEngine.UNKNOWN_VALUE.toLong()
|
||||
get() {
|
||||
val kdfEngine = kdfEngine
|
||||
return if (kdfEngine != null && kdfParameters != null) {
|
||||
kdfEngine.getMemoryUsage(kdfParameters!!)
|
||||
} else KdfEngine.UNKNOWN_VALUE.toLong()
|
||||
}
|
||||
set(memory) {
|
||||
val kdfEngine = kdfEngine
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
kdfEngine!!.setMemoryUsage(kdfParameters!!, memory)
|
||||
kdfEngine.setMemoryUsage(kdfParameters!!, memory)
|
||||
}
|
||||
|
||||
var parallelism: Int
|
||||
get() = if (kdfEngine != null && kdfParameters != null) {
|
||||
kdfEngine!!.getParallelism(kdfParameters!!)
|
||||
} else KdfEngine.UNKNOWN_VALUE
|
||||
get() {
|
||||
val kdfEngine = kdfEngine
|
||||
return if (kdfEngine != null && kdfParameters != null) {
|
||||
kdfEngine.getParallelism(kdfParameters!!)
|
||||
} else KdfEngine.UNKNOWN_VALUE
|
||||
}
|
||||
set(parallelism) {
|
||||
val kdfEngine = kdfEngine
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
kdfEngine!!.setParallelism(kdfParameters!!, parallelism)
|
||||
kdfEngine.setParallelism(kdfParameters!!, parallelism)
|
||||
}
|
||||
|
||||
override val passwordEncoding: String
|
||||
@@ -200,7 +237,7 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
return getCustomData().isNotEmpty()
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(IOException::class)
|
||||
public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
|
||||
|
||||
var masterKey = byteArrayOf()
|
||||
@@ -227,7 +264,7 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
fun makeFinalKey(masterSeed: ByteArray) {
|
||||
|
||||
kdfParameters?.let { keyDerivationFunctionParameters ->
|
||||
val kdfEngine = KdfFactory.getEngineV4(keyDerivationFunctionParameters)
|
||||
val kdfEngine = getEngineV4(keyDerivationFunctionParameters)
|
||||
|
||||
var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters)
|
||||
if (transformedMasterKey.size != 32) {
|
||||
@@ -254,16 +291,24 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
|
||||
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||
try {
|
||||
val dbf = DocumentBuilderFactory.newInstance()
|
||||
val db = dbf.newDocumentBuilder()
|
||||
val doc = db.parse(keyInputStream)
|
||||
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
|
||||
|
||||
val el = doc.documentElement
|
||||
if (el == null || !el.nodeName.equals(RootElementName, ignoreCase = true)) {
|
||||
// Disable certain unsecure XML-Parsing DocumentBuilderFactory features
|
||||
try {
|
||||
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
|
||||
} catch (e : ParserConfigurationException) {
|
||||
Log.e(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)", e)
|
||||
}
|
||||
|
||||
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
|
||||
val doc = documentBuilder.parse(keyInputStream)
|
||||
|
||||
val docElement = doc.documentElement
|
||||
if (docElement == null || !docElement.nodeName.equals(RootElementName, ignoreCase = true)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val children = el.childNodes
|
||||
val children = docElement.childNodes
|
||||
if (children.length < 2) {
|
||||
return null
|
||||
}
|
||||
@@ -360,9 +405,7 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
}
|
||||
addGroupTo(recycleBinGroup, rootGroup)
|
||||
recycleBinUUID = recycleBinGroup.id
|
||||
recycleBinGroup.lastModificationTime.date?.let {
|
||||
recycleBinChanged = it
|
||||
}
|
||||
recycleBinChanged = recycleBinGroup.lastModificationTime.date
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,10 +470,10 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
return publicCustomData.size() > 0
|
||||
}
|
||||
|
||||
override fun validatePasswordEncoding(key: String?): Boolean {
|
||||
if (key == null)
|
||||
override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||
if (password == null)
|
||||
return true
|
||||
return super.validatePasswordEncoding(key)
|
||||
return super.validatePasswordEncoding(password, containsKeyFile)
|
||||
}
|
||||
|
||||
override fun clearCache() {
|
||||
|
||||
@@ -19,14 +19,12 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import com.kunzisoft.keepass.utils.Types
|
||||
|
||||
import java.util.Arrays
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Converting from the C Date format to the Java data format is
|
||||
@@ -34,14 +32,14 @@ import java.util.Date
|
||||
*/
|
||||
class PwDate : Parcelable {
|
||||
|
||||
private var jDate: Date? = null
|
||||
private var jDate: Date = Date()
|
||||
private var jDateBuilt = false
|
||||
@Transient
|
||||
private var cDate: ByteArray? = null
|
||||
@Transient
|
||||
private var cDateBuilt = false
|
||||
|
||||
val date: Date?
|
||||
val date: Date
|
||||
get() {
|
||||
if (!jDateBuilt) {
|
||||
jDate = readTime(cDate, 0, calendar)
|
||||
@@ -68,9 +66,7 @@ class PwDate : Parcelable {
|
||||
}
|
||||
|
||||
constructor(source: PwDate) {
|
||||
if (source.jDate != null) {
|
||||
this.jDate = Date(source.jDate!!.time)
|
||||
}
|
||||
this.jDate = Date(source.jDate.time)
|
||||
this.jDateBuilt = source.jDateBuilt
|
||||
|
||||
if (source.cDate != null) {
|
||||
@@ -106,6 +102,10 @@ class PwDate : Parcelable {
|
||||
return 0
|
||||
}
|
||||
|
||||
fun getDateTimeString(resources: Resources): String {
|
||||
return Companion.getDateTimeString(resources, this.date)
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeSerializable(date)
|
||||
dest.writeByte((if (jDateBuilt) 1 else 0).toByte())
|
||||
@@ -135,7 +135,7 @@ class PwDate : Parcelable {
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = jDate?.hashCode() ?: 0
|
||||
var result = jDate.hashCode()
|
||||
result = 31 * result + jDateBuilt.hashCode()
|
||||
result = 31 * result + (cDate?.contentHashCode() ?: 0)
|
||||
result = 31 * result + cDateBuilt.hashCode()
|
||||
@@ -149,10 +149,6 @@ class PwDate : Parcelable {
|
||||
private var mCalendar: Calendar? = null
|
||||
|
||||
val NEVER_EXPIRE = neverExpire
|
||||
val DEFAULT_DATE = defaultDate
|
||||
|
||||
val PW_NEVER_EXPIRE = PwDate(NEVER_EXPIRE)
|
||||
val DEFAULT_PWDATE = PwDate(DEFAULT_DATE)
|
||||
|
||||
private val calendar: Calendar?
|
||||
get() {
|
||||
@@ -162,20 +158,7 @@ class PwDate : Parcelable {
|
||||
return mCalendar
|
||||
}
|
||||
|
||||
private val defaultDate: Date
|
||||
get() {
|
||||
val cal = Calendar.getInstance()
|
||||
cal.set(Calendar.YEAR, 2004)
|
||||
cal.set(Calendar.MONTH, Calendar.JANUARY)
|
||||
cal.set(Calendar.DAY_OF_MONTH, 1)
|
||||
cal.set(Calendar.HOUR, 0)
|
||||
cal.set(Calendar.MINUTE, 0)
|
||||
cal.set(Calendar.SECOND, 0)
|
||||
|
||||
return cal.time
|
||||
}
|
||||
|
||||
private val neverExpire: Date
|
||||
private val neverExpire: PwDate
|
||||
get() {
|
||||
val cal = Calendar.getInstance()
|
||||
cal.set(Calendar.YEAR, 2999)
|
||||
@@ -185,7 +168,7 @@ class PwDate : Parcelable {
|
||||
cal.set(Calendar.MINUTE, 59)
|
||||
cal.set(Calendar.SECOND, 59)
|
||||
|
||||
return cal.time
|
||||
return PwDate(cal.time)
|
||||
}
|
||||
|
||||
@JvmField
|
||||
@@ -280,5 +263,13 @@ class PwDate : Parcelable {
|
||||
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.MEDIUM,
|
||||
ConfigurationCompat.getLocales(resources.configuration)[0])
|
||||
.format(date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.crypto.engine.AesEngine
|
||||
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.crypto.engine.TwofishEngine
|
||||
import com.kunzisoft.keepass.database.ObjectNameResource
|
||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ import java.util.*
|
||||
|
||||
abstract class PwEntry
|
||||
<
|
||||
ParentGroup: PwGroupInterface<ParentGroup, Entry>,
|
||||
Entry: PwEntryInterface<ParentGroup>
|
||||
GroupId,
|
||||
EntryId,
|
||||
ParentGroup: PwGroup<GroupId, EntryId, ParentGroup, Entry>,
|
||||
Entry: PwEntry<GroupId, EntryId, ParentGroup, Entry>
|
||||
>
|
||||
: PwNode<UUID, ParentGroup, Entry>, PwEntryInterface<ParentGroup> {
|
||||
: PwNode<EntryId, ParentGroup, Entry>, PwEntryInterface<ParentGroup> {
|
||||
|
||||
constructor() : super()
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ import java.util.UUID
|
||||
* @author Dominik Reichl <dominik.reichl></dominik.reichl>@t-online.de>
|
||||
* @author Jeremy Jamet <jeremy.jamet></jeremy.jamet>@kunzisoft.com>
|
||||
*/
|
||||
class PwEntryV3 : PwEntry<PwGroupV3, PwEntryV3> {
|
||||
class PwEntryV3 : PwEntry<Int, UUID, PwGroupV3, PwEntryV3>, PwNodeV3Interface {
|
||||
|
||||
/** A string describing what is in pBinaryData */
|
||||
var binaryDesc = ""
|
||||
|
||||
@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.utils.MemoryUtil
|
||||
import java.util.*
|
||||
|
||||
class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
class PwEntryV4 : PwEntry<UUID, UUID, PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
|
||||
// To decode each field not parcelable
|
||||
@Transient
|
||||
@@ -88,6 +88,8 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
return size
|
||||
}
|
||||
|
||||
override var expires: Boolean = false
|
||||
|
||||
constructor() : super()
|
||||
|
||||
constructor(parcel: Parcel) : super(parcel) {
|
||||
@@ -129,7 +131,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
* Update with deep copy of each entry element
|
||||
* @param source
|
||||
*/
|
||||
fun updateWith(source: PwEntryV4) {
|
||||
fun updateWith(source: PwEntryV4, copyHistory: Boolean = true) {
|
||||
super.updateWith(source)
|
||||
iconCustom = PwIconCustom(source.iconCustom)
|
||||
usageCount = source.usageCount
|
||||
@@ -146,7 +148,8 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
overrideURL = source.overrideURL
|
||||
autoType = AutoType(source.autoType)
|
||||
history.clear()
|
||||
history.addAll(source.history)
|
||||
if (copyHistory)
|
||||
history.addAll(source.history)
|
||||
url = source.url
|
||||
additional = source.additional
|
||||
tags = source.tags
|
||||
@@ -263,7 +266,11 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
return true
|
||||
}
|
||||
|
||||
fun addExtraField(label: String, value: ProtectedString) {
|
||||
fun removeAllFields() {
|
||||
fields.clear()
|
||||
}
|
||||
|
||||
fun putExtraField(label: String, value: ProtectedString) {
|
||||
fields[label] = value
|
||||
}
|
||||
|
||||
@@ -287,6 +294,10 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
history.add(entry)
|
||||
}
|
||||
|
||||
fun removeAllHistory() {
|
||||
history.clear()
|
||||
}
|
||||
|
||||
fun removeOldestEntryFromHistory() {
|
||||
var min: Date? = null
|
||||
var index = -1
|
||||
@@ -294,7 +305,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
for (i in history.indices) {
|
||||
val entry = history[i]
|
||||
val lastMod = entry.lastModificationTime.date
|
||||
if (min == null || lastMod == null || lastMod.before(min)) {
|
||||
if (min == null || lastMod.before(min)) {
|
||||
index = i
|
||||
min = lastMod
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ import android.os.Parcel
|
||||
|
||||
abstract class PwGroup
|
||||
<
|
||||
Id,
|
||||
Group: PwGroupInterface<Group, Entry>,
|
||||
Entry: PwEntryInterface<Group>
|
||||
GroupId,
|
||||
EntryId,
|
||||
Group: PwGroup<GroupId, EntryId, Group, Entry>,
|
||||
Entry: PwEntry<GroupId, EntryId, Group, Entry>
|
||||
>
|
||||
: PwNode<Id, Group, Entry>, PwGroupInterface<Group, Entry> {
|
||||
: PwNode<GroupId, Group, Entry>, PwGroupInterface<Group, Entry> {
|
||||
|
||||
private var titleGroup = ""
|
||||
@Transient
|
||||
@@ -27,10 +28,12 @@ abstract class PwGroup
|
||||
dest.writeString(titleGroup)
|
||||
}
|
||||
|
||||
protected fun updateWith(source: PwGroup<Id, Group, Entry>) {
|
||||
protected fun updateWith(source: PwGroup<GroupId, EntryId, Group, Entry>) {
|
||||
super.updateWith(source)
|
||||
titleGroup = source.titleGroup
|
||||
childGroups.clear()
|
||||
childGroups.addAll(source.childGroups)
|
||||
childEntries.clear()
|
||||
childEntries.addAll(source.childEntries)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,9 @@ package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import java.util.*
|
||||
|
||||
class PwGroupV3 : PwGroup<Int, PwGroupV3, PwEntryV3> {
|
||||
class PwGroupV3 : PwGroup<Int, UUID, PwGroupV3, PwEntryV3>, PwNodeV3Interface {
|
||||
|
||||
var level = 0 // short
|
||||
/** Used by KeePass internally, don't use */
|
||||
|
||||
@@ -25,7 +25,7 @@ import android.os.Parcelable
|
||||
import java.util.HashMap
|
||||
import java.util.UUID
|
||||
|
||||
class PwGroupV4 : PwGroup<UUID, PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
class PwGroupV4 : PwGroup<UUID, UUID, PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
|
||||
// TODO Encapsulate
|
||||
override var icon: PwIcon
|
||||
@@ -43,12 +43,15 @@ class PwGroupV4 : PwGroup<UUID, PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
var iconCustom = PwIconCustom.UNKNOWN_ICON
|
||||
private val customData = HashMap<String, String>()
|
||||
var notes = ""
|
||||
|
||||
var isExpanded = true
|
||||
var defaultAutoTypeSequence = ""
|
||||
var enableAutoType: Boolean? = null
|
||||
var enableSearching: Boolean? = null
|
||||
var lastTopVisibleEntry: UUID = PwDatabase.UUID_ZERO
|
||||
|
||||
override var expires: Boolean = false
|
||||
|
||||
override val type: Type
|
||||
get() = Type.GROUP
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import org.joda.time.LocalDate
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Abstract class who manage Groups and Entries
|
||||
@@ -44,6 +44,7 @@ abstract class PwNode<IdType, Parent : PwGroupInterface<Parent, Entry>, Entry :
|
||||
this.lastModificationTime = parcel.readParcelable(PwDate::class.java.classLoader) ?: lastModificationTime
|
||||
this.lastAccessTime = parcel.readParcelable(PwDate::class.java.classLoader) ?: lastAccessTime
|
||||
this.expiryTime = parcel.readParcelable(PwDate::class.java.classLoader) ?: expiryTime
|
||||
this.expires = parcel.readByte().toInt() != 0
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
@@ -54,6 +55,7 @@ abstract class PwNode<IdType, Parent : PwGroupInterface<Parent, Entry>, Entry :
|
||||
dest.writeParcelable(lastModificationTime, flags)
|
||||
dest.writeParcelable(lastAccessTime, flags)
|
||||
dest.writeParcelable(expiryTime, flags)
|
||||
dest.writeByte((if (expires) 1 else 0).toByte())
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
@@ -68,6 +70,7 @@ abstract class PwNode<IdType, Parent : PwGroupInterface<Parent, Entry>, Entry :
|
||||
this.lastModificationTime = PwDate(source.lastModificationTime)
|
||||
this.lastAccessTime = PwDate(source.lastAccessTime)
|
||||
this.expiryTime = PwDate(source.expiryTime)
|
||||
this.expires = source.expires
|
||||
}
|
||||
|
||||
protected abstract fun initNodeId(): PwNodeId<IdType>
|
||||
@@ -85,17 +88,11 @@ abstract class PwNode<IdType, Parent : PwGroupInterface<Parent, Entry>, Entry :
|
||||
|
||||
final override var lastAccessTime: PwDate = PwDate()
|
||||
|
||||
final override var expiryTime: PwDate = PwDate.PW_NEVER_EXPIRE
|
||||
final override var expiryTime: PwDate = PwDate()
|
||||
|
||||
final override var isExpires: Boolean
|
||||
// If expireDate is before NEVER_EXPIRE date less 1 month (to be sure)
|
||||
get() = expiryTime.date
|
||||
?.before(LocalDate.fromDateFields(PwDate.NEVER_EXPIRE).minusMonths(1).toDate()) ?: true
|
||||
set(value) {
|
||||
if (!value) {
|
||||
expiryTime = PwDate.PW_NEVER_EXPIRE
|
||||
}
|
||||
}
|
||||
final override val isCurrentlyExpires: Boolean
|
||||
get() = expires
|
||||
&& LocalDateTime.fromDateFields(expiryTime.date).isBefore(LocalDateTime.now())
|
||||
|
||||
/**
|
||||
* @return true if parent is present (false if not present, can be a root or a detach element)
|
||||
|
||||
@@ -31,4 +31,17 @@ abstract class PwNodeId<Id> : Parcelable {
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is PwNodeId<*>) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id?.hashCode() ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
interface PwNodeV3Interface : 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(PwDate.NEVER_EXPIRE.date)
|
||||
.minusMonths(1))
|
||||
set(value) {
|
||||
if (!value)
|
||||
expiryTime = PwDate.NEVER_EXPIRE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class ArcFourException : InvalidDBException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = 2103983626687861237L
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
import android.net.Uri
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class ContentFileNotFoundException : FileNotFoundException() {
|
||||
companion object {
|
||||
fun getInstance(uri: Uri?): FileNotFoundException {
|
||||
if (uri == null) {
|
||||
return FileNotFoundException()
|
||||
}
|
||||
|
||||
val scheme = uri.scheme
|
||||
return if (scheme != null
|
||||
&& scheme.isNotEmpty()
|
||||
&& scheme.equals("content", ignoreCase = true)) {
|
||||
ContentFileNotFoundException()
|
||||
} else FileNotFoundException()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.StringRes
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.PwNodeId
|
||||
import com.kunzisoft.keepass.database.element.Type
|
||||
|
||||
abstract class DatabaseException : Exception {
|
||||
|
||||
abstract var errorId: Int
|
||||
var parameters: (Array<out String>)? = null
|
||||
|
||||
constructor() : super()
|
||||
|
||||
constructor(throwable: Throwable) : super(throwable)
|
||||
|
||||
fun getLocalizedMessage(resources: Resources): String {
|
||||
parameters?.let {
|
||||
return resources.getString(errorId, *it)
|
||||
} ?: return resources.getString(errorId)
|
||||
}
|
||||
}
|
||||
|
||||
open class LoadDatabaseException : DatabaseException {
|
||||
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_load_database
|
||||
|
||||
constructor() : super()
|
||||
|
||||
constructor(vararg params: String) : super() {
|
||||
parameters = params
|
||||
}
|
||||
|
||||
constructor(throwable: Throwable) : super(throwable)
|
||||
}
|
||||
|
||||
class LoadDatabaseArcFourException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_arc4
|
||||
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class LoadDatabaseFileNotFoundException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.file_not_found_content
|
||||
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class LoadDatabaseInvalidAlgorithmException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.invalid_algorithm
|
||||
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class LoadDatabaseDuplicateUuidException: LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.invalid_db_same_uuid
|
||||
|
||||
constructor(type: Type, uuid: PwNodeId<*>) : super() {
|
||||
parameters = arrayOf(type.name, uuid.toString())
|
||||
}
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class LoadDatabaseIOException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_load_database
|
||||
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class LoadDatabaseKDFMemoryException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_load_database_KDF_memory
|
||||
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class LoadDatabaseSignatureException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.invalid_db_sig
|
||||
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class LoadDatabaseVersionException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.unsupported_db_version
|
||||
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class LoadDatabaseInvalidCredentialsException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.invalid_credentials
|
||||
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class LoadDatabaseKeyFileEmptyException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.keyfile_is_empty
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class LoadDatabaseNoMemoryException: LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_out_of_memory
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class MoveDatabaseEntryException: LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_move_entry_here
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class MoveDatabaseGroupException: LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_move_folder_in_itself
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class CopyDatabaseEntryException: LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_copy_entry_here
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class CopyDatabaseGroupException: LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_copy_group_here
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class DatabaseOutputException : Exception {
|
||||
constructor(string: String) : super(string)
|
||||
|
||||
constructor(string: String, e: Exception) : super(string, e)
|
||||
|
||||
constructor(e: Exception) : super(e)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class InvalidAlgorithmException : InvalidDBException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = 3062682891863487208L
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
open class InvalidDBException : Exception {
|
||||
|
||||
constructor(str: String) : super(str)
|
||||
|
||||
constructor() : super()
|
||||
|
||||
companion object {
|
||||
private const val serialVersionUID = 5191964825154190923L
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class InvalidDBSignatureException : InvalidDBException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = -5358923878743513758L
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class InvalidDBVersionException : InvalidDBException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = -4260650987856400586L
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/package com.kunzisoft.keepass.database.exception
|
||||
|
||||
open class InvalidKeyFileException : InvalidDBException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = 5540694419562294464L
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class InvalidPasswordException : InvalidDBException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = -8729476180242058319L
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class KeyFileEmptyException : InvalidKeyFileException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = -1630780661204212325L
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class PwDbOutputException : Exception {
|
||||
constructor(string: String) : super(string)
|
||||
|
||||
constructor(string: String, e: Exception) : super(string, e)
|
||||
|
||||
constructor(e: Exception) : super(e)
|
||||
|
||||
companion object {
|
||||
private const val serialVersionUID = 3321212743159473368L
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,4 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class SamsungClipboardException(e: Exception) : Exception(e) {
|
||||
companion object {
|
||||
private const val serialVersionUID = -3168837280393843509L
|
||||
}
|
||||
|
||||
}
|
||||
class SamsungClipboardException(e: Exception) : Exception(e)
|
||||
|
||||
@@ -2,8 +2,4 @@ package com.kunzisoft.keepass.database.exception
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
class UnknownKDF : IOException(message) {
|
||||
companion object {
|
||||
private const val message = "Unknown key derivation function"
|
||||
}
|
||||
}
|
||||
class UnknownKDF : IOException("Unknown key derivation function")
|
||||
|
||||
@@ -24,11 +24,8 @@ import com.kunzisoft.keepass.crypto.keyDerivation.AesKdf
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
||||
import com.kunzisoft.keepass.database.NodeHandler
|
||||
import com.kunzisoft.keepass.database.element.PwNodeV4Interface
|
||||
import com.kunzisoft.keepass.database.element.PwDatabaseV4
|
||||
import com.kunzisoft.keepass.database.element.PwEntryV4
|
||||
import com.kunzisoft.keepass.database.element.PwGroupV4
|
||||
import com.kunzisoft.keepass.database.exception.InvalidDBVersionException
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseVersionException
|
||||
import com.kunzisoft.keepass.stream.CopyInputStream
|
||||
import com.kunzisoft.keepass.stream.HmacBlockStream
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
@@ -51,10 +48,10 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
|
||||
// version < FILE_VERSION_32_4)
|
||||
var transformSeed: ByteArray?
|
||||
get() = databaseV4.kdfParameters?.getByteArray(AesKdf.ParamSeed)
|
||||
get() = databaseV4.kdfParameters?.getByteArray(AesKdf.PARAM_SEED)
|
||||
private set(seed) {
|
||||
assignAesKdfEngineIfNotExists()
|
||||
databaseV4.kdfParameters?.setByteArray(AesKdf.ParamSeed, seed)
|
||||
databaseV4.kdfParameters?.setByteArray(AesKdf.PARAM_SEED, seed)
|
||||
}
|
||||
|
||||
object PwDbHeaderV4Fields {
|
||||
@@ -133,9 +130,9 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
/** Assumes the input stream is at the beginning of the .kdbx file
|
||||
* @param inputStream
|
||||
* @throws IOException
|
||||
* @throws InvalidDBVersionException
|
||||
* @throws LoadDatabaseVersionException
|
||||
*/
|
||||
@Throws(IOException::class, InvalidDBVersionException::class)
|
||||
@Throws(IOException::class, LoadDatabaseVersionException::class)
|
||||
fun loadFromFile(inputStream: InputStream): HeaderAndHash {
|
||||
val messageDigest: MessageDigest
|
||||
try {
|
||||
@@ -153,12 +150,12 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
val sig2 = littleEndianDataInputStream.readInt()
|
||||
|
||||
if (!matchesHeader(sig1, sig2)) {
|
||||
throw InvalidDBVersionException()
|
||||
throw LoadDatabaseVersionException()
|
||||
}
|
||||
|
||||
version = littleEndianDataInputStream.readUInt() // Erase previous value
|
||||
if (!validVersion(version)) {
|
||||
throw InvalidDBVersionException()
|
||||
throw LoadDatabaseVersionException()
|
||||
}
|
||||
|
||||
var done = false
|
||||
@@ -229,7 +226,9 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
}
|
||||
|
||||
private fun assignAesKdfEngineIfNotExists() {
|
||||
if (databaseV4.kdfParameters == null || databaseV4.kdfParameters!!.uuid != KdfFactory.aesKdf.uuid) {
|
||||
val kdfParams = databaseV4.kdfParameters
|
||||
if (kdfParams == null
|
||||
|| kdfParams.uuid != KdfFactory.aesKdf.uuid) {
|
||||
databaseV4.kdfParameters = KdfFactory.aesKdf.defaultParameters
|
||||
}
|
||||
}
|
||||
@@ -246,7 +245,7 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
private fun setTransformRound(roundsByte: ByteArray?) {
|
||||
assignAesKdfEngineIfNotExists()
|
||||
val rounds = LEDataInputStream.readLong(roundsByte!!, 0)
|
||||
databaseV4.kdfParameters?.setUInt64(AesKdf.ParamRounds, rounds)
|
||||
databaseV4.kdfParameters?.setUInt64(AesKdf.PARAM_ROUNDS, rounds)
|
||||
databaseV4.numberKeyEncryptionRounds = rounds
|
||||
}
|
||||
|
||||
@@ -261,7 +260,7 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
throw IOException("Unrecognized compression flag.")
|
||||
}
|
||||
|
||||
PwCompressionAlgorithm.fromId(flag)?.let { compression ->
|
||||
getCompressionFromFlag(flag)?.let { compression ->
|
||||
databaseV4.compressionAlgorithm = compression
|
||||
}
|
||||
}
|
||||
@@ -299,6 +298,21 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
const val FILE_VERSION_32_3: Long = 0x00030001
|
||||
const val FILE_VERSION_32_4: Long = 0x00040000
|
||||
|
||||
fun getCompressionFromFlag(flag: Int): PwCompressionAlgorithm? {
|
||||
return when (flag) {
|
||||
0 -> PwCompressionAlgorithm.None
|
||||
1 -> PwCompressionAlgorithm.GZip
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun getFlagFromCompression(compression: PwCompressionAlgorithm): Int {
|
||||
return when (compression) {
|
||||
PwCompressionAlgorithm.GZip -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
fun matchesHeader(sig1: Int, sig2: Int): Boolean {
|
||||
return sig1 == PWM_DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user