diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..80a899f6a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -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.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..4fe86d5ec
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -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.
diff --git a/.gitignore b/.gitignore
index be230663e..b6de83e58 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,6 +38,13 @@ proguard/
# Android Studio captures folder
captures/
+# Eclipse/VS Code
+.project
+.settings/*
+*/.project
+*/.classpath
+*/.settings/*
+
# Intellij
*.iml
.idea/workspace.xml
diff --git a/CHANGELOG b/CHANGELOG
index 2612ef808..f796ec313 100644
--- a/CHANGELOG
+++ b/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
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
deleted file mode 100644
index 52ccfe1a4..000000000
--- a/CONTRIBUTORS
+++ /dev/null
@@ -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
diff --git a/ReadMe.md b/ReadMe.md
index 5ac23fc0f..1fbedeedd 100644
--- a/ReadMe.md
+++ b/ReadMe.md
@@ -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**
diff --git a/app/build.gradle b/app/build.gradle
index 4e3978343..e86c1b10d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b19a964e5..0613db3eb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -131,7 +131,8 @@
+ android:label="@string/keyboard_name"
+ android:exported="true">
@@ -140,7 +141,10 @@
-
+
= 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 = 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)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
index 234cef43b..0ad978870 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
@@ -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>(KEY_ENTRY)?.let {
+ intent.getParcelableExtra>(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>(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 ->
-
- // WARNING Add the parent previously deleted
- newEntry.parent = mEntry?.parent
- // Build info
- newEntry.lastAccessTime = PwDate()
- newEntry.lastModificationTime = PwDate()
-
- populateEntryWithViews(newEntry)
-
- // Open a progress dialog and save entry
- var actionRunnable: ActionRunnable? = null
- val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() {
- override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
- if (actionNodeValues.result.isSuccess)
- finish()
- }
- }
- if (mIsNew) {
- mParent?.let { parent ->
- actionRunnable = AddEntryRunnable(this@EntryEditActivity,
- database,
- newEntry,
- parent,
- afterActionNodeFinishRunnable,
- !mReadOnly)
- }
-
- } else {
- mEntry?.let { oldEntry ->
- actionRunnable = UpdateEntryRunnable(this@EntryEditActivity,
- database,
- oldEntry,
- newEntry,
- afterActionNodeFinishRunnable,
- !mReadOnly)
- }
+ mNewEntry?.let { newEntry ->
+
+ // WARNING Add the parent previously deleted
+ newEntry.parent = mEntry?.parent
+ // Build info
+ newEntry.lastAccessTime = PwDate()
+ newEntry.lastModificationTime = PwDate()
+
+ populateEntryWithViews(newEntry)
+
+ // Open a progress dialog and save entry
+ if (mIsNew) {
+ mParent?.let { parent ->
+ progressDialogThread?.startDatabaseCreateEntry(
+ newEntry,
+ parent,
+ !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)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
index 70ef64046..78ac5f64a 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
@@ -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(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)
- }
- }
+ {}
)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
index 1b5a29882..1cf9d7771 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
@@ -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 = ArrayList()
+ result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle ->
+ oldNodes = getListNodesFromBundle(database, oldNodesBundle)
+ }
+ var newNodes: List = 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): 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
- }
-
- private inner class OnCopyMenuItemClickListener : Toolbar.OnMenuItemClickListener {
- override fun onMenuItemClick(item: MenuItem): Boolean {
- toolbarPasteExpandableLayout?.collapse()
+ override fun onCopyMenuClick(nodes: List): Boolean {
+ actionNodeMode?.invalidate()
- 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
- }
+ // Nothing here fragment calls onPasteMenuClick internally
+ return true
}
- private fun copyEntry(entryToCopy: EntryVersioned, newParent: GroupVersioned) {
- ProgressDialogSaveDatabaseThread(this) {
- CopyEntryRunnable(this,
- Database.getInstance(),
- entryToCopy,
- newParent,
- AfterAddNodeRunnable(),
- !mReadOnly)
- }.start()
- }
+ override fun onMoveMenuClick(nodes: List): Boolean {
+ actionNodeMode?.invalidate()
- override fun onMoveMenuClick(node: NodeVersioned): Boolean {
- toolbarPasteExpandableLayout?.expand()
- mNodeToMove = node
- toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
- return false
+ // Nothing here fragment calls onPasteMenuClick internally
+ return true
}
- 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
+ override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?,
+ nodes: List): Boolean {
+ when (pasteMode) {
+ ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
+ // Copy
+ mCurrentGroup?.let { newParent ->
+ progressDialogThread?.startDatabaseCopyNodes(
+ nodes,
+ newParent,
+ !mReadOnly
+ )
}
}
- 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)
+ ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
+ // Move
+ mCurrentGroup?.let { newParent ->
+ progressDialogThread?.startDatabaseMoveNodes(
+ nodes,
+ newParent,
+ !mReadOnly
+ )
+ }
+ }
+ else -> {}
}
+ finishNodeAction()
return true
}
- private fun deleteGroup(group: GroupVersioned) {
- //TODO Verify trash recycle bin
- ProgressDialogSaveDatabaseThread(this) {
- DeleteGroupRunnable(
- this,
- Database.getInstance(),
- group,
- AfterDeleteNodeRunnable(),
- !mReadOnly)
- }.start()
- }
-
- private fun deleteEntry(entry: EntryVersioned) {
- ProgressDialogSaveDatabaseThread(this) {
- DeleteEntryRunnable(
- this,
- Database.getInstance(),
- entry,
- AfterDeleteNodeRunnable(),
- !mReadOnly)
- }.start()
+ override fun onDeleteMenuClick(nodes: List): 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)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt
index 3346deb1f..470034783 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt
@@ -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()
+ private val listPasteNodes = LinkedList()
+
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)
+ }
+ if (args.containsKey(IS_SEARCH)) {
+ isASearchResult = args.getBoolean(IS_SEARCH)
}
+ }
- contextThemed?.let { context ->
- mAdapter = NodeAdapter(context, currentActivity.menuInflater)
- mAdapter?.apply {
- setReadOnly(readOnly)
- setIsASearchResult(isASearchResult)
- setOnNodeClickListener(nodeClickCallback)
- setActivateContextMenu(true)
- setNodeMenuListener(nodeMenuListener)
- }
+ 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)
}
+ 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,
+ 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) {
+ mAdapter?.addNodes(newNodes)
+ }
+
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned? = null) {
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
}
+ fun updateNodes(oldNodes: List, newNodes: List) {
+ mAdapter?.updateNodes(oldNodes, newNodes)
+ }
+
fun removeNode(pwNode: NodeVersioned) {
mAdapter?.removeNode(pwNode)
}
+ fun removeNodes(nodes: List) {
+ 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): 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): Boolean
+ fun onMoveMenuClick(nodes: List): Boolean
+ fun onDeleteMenuClick(nodes: List): Boolean
+ fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List): Boolean
+ }
+
+ enum class PasteMode {
+ UNDEFINED, PASTE_FROM_COPY, PASTE_FROM_MOVE
+ }
+
interface OnScrollListener {
/**
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt
index 1df82893d..8340b7eff 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt
@@ -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(R.id.browse_button)
+ val browseView = findViewById(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(R.id.menu_open_file_read_mode_key) != null
+ val educationToolbar = toolbar
+ val readOnlyEducationPerformed =
+ educationToolbar?.findViewById(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)
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt
index a71a8f09b..f78a239ea 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt
@@ -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(R.id.browse_button)?.setOnClickListener { view ->
+ rootView?.findViewById(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
+ }
+ }
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt
index a981e04d0..3dd5514ac 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt
@@ -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(R.id.file_manager_install_description)
textDescription.text = getString(R.string.file_manager_install_description)
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DuplicateUuidDialog.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DuplicateUuidDialog.kt
new file mode 100644
index 000000000..219dd9c01
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DuplicateUuidDialog.kt
@@ -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 .
+ *
+ */
+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()
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/FingerPrintExplanationDialog.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/FingerPrintExplanationDialog.kt
deleted file mode 100644
index d78a51162..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/FingerPrintExplanationDialog.kt
+++ /dev/null
@@ -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 .
- *
- */
-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(R.id.fingerprint_setting_link_text).setOnClickListener {
- startActivity(Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS))
- }
-
- rootView.findViewById(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()
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt
index 350049fba..156a0be88 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt
@@ -114,7 +114,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
dismiss()
}
- .setNegativeButton(R.string.cancel) { _, _ ->
+ .setNegativeButton(android.R.string.cancel) { _, _ ->
val bundle = Bundle()
mListener?.cancelPassword(bundle)
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt
index 032f80cb8..8f126d083 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt
@@ -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(),
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt
index 1e218ccff..7b0691ba9 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt
@@ -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()
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/KeyboardExplanationDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/KeyboardExplanationDialogFragment.kt
deleted file mode 100644
index c00c80b7e..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/KeyboardExplanationDialogFragment.kt
+++ /dev/null
@@ -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 .
- *
- */
-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(R.id.keyboards_activate_device_setting_button)
- .setOnClickListener { launchActivateKeyboardSetting() }
-
- val containerKeyboardSwitcher = rootView.findViewById(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)
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt
index 87b1baa87..4cb8f1234 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt
@@ -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()
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt
new file mode 100644
index 000000000..5cb840453
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt
@@ -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? = null
+ private var otpTokenTypeAdapter: ArrayAdapter? = null
+ private var totpTokenTypeAdapter: ArrayAdapter? = null
+ private var hotpTokenTypeAdapter: ArrayAdapter? = null
+ private var otpAlgorithmAdapter: ArrayAdapter? = 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(KEY_OTP)?.let { otpModel ->
+ mOtpElement = OtpElement(otpModel)
+ }
+ }
+ } else {
+ arguments?.apply {
+ if (containsKey(KEY_OTP)) {
+ getParcelable(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(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(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)
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt
index 7f67091ab..9c17defbb 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt
@@ -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(R.id.sort_selection_ascending)
// Check if is ascending or descending
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt
index e046223f6..dc04e3291 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt
@@ -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) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/OpenFileHelper.kt b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/OpenFileHelper.kt
index 52ebf4bb5..c9f09f0a4 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/OpenFileHelper.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/OpenFileHelper.kt
@@ -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)
- }
-
- fun getOpenFileOnClickViewListener(dataUri: () -> Uri?): OpenFileOnClickViewListener {
- return OpenFileOnClickViewListener(dataUri)
+ activity?.startActivityForResult(intentGetContent, GET_CONTENT)
}
- 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"
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt
index 46b760641..574ad9f95 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt
@@ -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 {
diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryHistoryAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryHistoryAdapter.kt
new file mode 100644
index 000000000..bd6744cf2
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryHistoryAdapter.kt
@@ -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() {
+
+ private val inflater: LayoutInflater = LayoutInflater.from(context)
+ var entryHistoryList: MutableList = 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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/FieldsAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/FieldsAdapter.kt
index 2354675d6..bdd16ae11 100644
--- a/app/src/main/java/com/kunzisoft/keepass/adapters/FieldsAdapter.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/adapters/FieldsAdapter.kt
@@ -18,7 +18,6 @@ class FieldsAdapter(context: Context) : RecyclerView.Adapter = 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)
diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt
index 32e280e27..f22e4cb88 100644
--- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt
@@ -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() {
private val nodeSortedList: SortedList
@@ -61,11 +65,8 @@ class NodeAdapter
private var showUserNames: Boolean = true
private var showNumberEntries: Boolean = true
+ private var actionNodesList = LinkedList()
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(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) {
+ 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) {
+ 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, newNodes: List) {
+ 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) {
+ 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) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseEntity.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseEntity.kt
index 140b2bf2c..304517552 100644
--- a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseEntity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseEntity.kt
@@ -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 {
+ override fun createFromParcel(parcel: Parcel): CipherDatabaseEntity {
+ return CipherDatabaseEntity(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryDao.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryDao.kt
index 62dc2642c..1268238da 100644
--- a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryDao.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryDao.kt
@@ -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")
diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt
index 2698d32cd..6d8e14ae0 100644
--- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt
@@ -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 {
diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt
index 63a4e53ad..ad869885b 100644
--- a/app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt
@@ -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
-
- 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 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()
-
- val isFingerprintInitialized: Boolean
+ var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null
+ var biometricUnlockCallback: BiometricUnlockCallback? = null
+
+ 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().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 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) {
- biometricUnlockCallback.onBiometricException(e)
+ override fun onBiometricException(e: Exception) {
+ biometricCallback.onBiometricException(e)
+ }
}
- })
- fingerPrintHelper.deleteEntryKey()
+ deleteEntryKey()
+ }
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/AesKdf.kt b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/AesKdf.kt
index a56827df1..7d7060272 100644
--- a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/AesKdf.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/AesKdf.kt
@@ -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()))
-
- const val ParamRounds = "R"
- const val ParamSeed = "S"
+ 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 PARAM_ROUNDS = "R"
+ const val PARAM_SEED = "S"
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Kdf.kt b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Kdf.kt
index f246f627a..a2d485ccb 100644
--- a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Kdf.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Kdf.kt
@@ -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()
- companion object {
+ override val minParallelism: Int
+ get() = MIN_PARALLELISM
- 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()))
+ override val maxParallelism: Int
+ get() = MAX_PARALLELISM
- 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 MinVersion: Long = 0x10
- private const val MaxVersion: Long = 0x13
-
- private const val MinSalt = 8
- private const val MaxSalt = Integer.MAX_VALUE
-
- private const val MinIterations: Long = 1
- private const val MaxIterations = 4294967295L
-
- private const val MinMemory = (1024 * 8).toLong()
- private const val MaxMemory = Integer.MAX_VALUE.toLong()
-
- private const val MinParallelism = 1
- private const val MaxParallelism = (1 shl 24) - 1
+ companion object {
- private const val DefaultIterations: Long = 2
- private const val DefaultMemory = (1024 * 1024).toLong()
- private const val DefaultParallelism: Long = 2
+ 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()))
+
+ 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 MIN_VERSION: Long = 0x10
+ private const val MAX_VERSION: Long = 0x13
+
+ private const val MIN_SALT = 8
+ private const val MAX_SALT = Integer.MAX_VALUE
+
+ private const val MIN_ITERATIONS: Long = 1
+ private const val MAX_ITERATIONS = 4294967295L
+
+ private const val MIN_MEMORY = (1024 * 8).toLong()
+ private const val MAX_MEMORY = Integer.MAX_VALUE.toLong()
+
+ private const val MIN_PARALLELISM = 1
+ private const val MAX_PARALLELISM = (1 shl 24) - 1
+
+ private const val DEFAULT_ITERATIONS: Long = 2
+ private const val DEFAULT_MEMORY = (1024 * 1024).toLong()
+ private const val DEFAULT_PARALLELISM: Long = 2
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfEngine.kt b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfEngine.kt
index ce123626e..fbd22c191 100644
--- a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfEngine.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfEngine.kt
@@ -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
- companion object {
+ 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()
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfFactory.kt b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfFactory.kt
index 5db965de2..d2cfa0994 100644
--- a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfFactory.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfFactory.kt
@@ -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 = ArrayList()
- var kdfListV4: MutableList = 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
- }
-
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt b/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt
index 87d2832c2..7caddbe8e 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt
@@ -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)
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt
index a72f1317c..e2499b24a 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt
@@ -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) {
- erase(mBackupKey)
- finishRun(false, e.message)
- } catch (e: IOException) {
+ } catch (e: Exception) {
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)
}
/**
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt
index a9b6b565f..188ba639c 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt
@@ -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")
+ }
+ }
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt
index 1b31b3274..8404fd650 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt
@@ -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,
+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 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 mDuplicateUuidAction: ((Result) -> Unit)?)
+ : ActionRunnable() {
- override fun run() {
- 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
- }
- 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 val cacheDirectory = context.applicationContext.filesDir
- 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)
+ override fun onStartRun() {
+ // Clear before we load
+ mDatabase.closeAndClear(cacheDirectory)
}
- private fun saveFileData(uri: Uri, key: Uri?) {
- var keyFileUri = key
- if (!mRememberKeyFile) {
- keyFileUri = null
+ override fun onActionRun() {
+ try {
+ mDatabase.loadData(mUri, mPass, mKey,
+ mReadonly,
+ context.contentResolver,
+ cacheDirectory,
+ mOmitBackup,
+ mFixDuplicateUUID,
+ progressTaskUpdater)
}
- mWeakContext.get()?.let {
- FileDatabaseHistoryAction.getInstance(it).addOrUpdateDatabaseUri(uri, keyFileUri)
+ catch (e: LoadDatabaseDuplicateUuidException) {
+ mDuplicateUuidAction?.invoke(result)
+ setError(e)
}
- }
-
- override fun onFinishRun(result: Result) {
- if (!result.isSuccess) {
- mDatabase.closeAndClear(mWeakContext.get()?.filesDir)
+ catch (e: LoadDatabaseException) {
+ setError(e)
}
}
- companion object {
- private val TAG = LoadDatabaseRunnable::class.java.name
+ 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)
+ }
+
+ // Register the biometric
+ mCipherEntity?.let { cipherDatabaseEntity ->
+ CipherDatabaseAction.getInstance(context)
+ .addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called
+ }
+
+ // Start the opening notification
+ DatabaseOpenNotificationService.startIfAllowed(context)
+ } else {
+ mDatabase.closeAndClear(cacheDirectory)
+ }
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogSaveDatabaseThread.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogSaveDatabaseThread.kt
deleted file mode 100644
index 33f97d9dd..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogSaveDatabaseThread.kt
+++ /dev/null
@@ -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)
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogThread.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogThread.kt
index 38ff4ef09..8e4beb7e5 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogThread.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogThread.kt
@@ -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
+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
-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)
+
+class ProgressDialogThread(private val activity: FragmentActivity,
+ var onActionFinish: (actionTask: String,
+ result: ActionRunnable.Result) -> Unit) {
+
+ 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 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()
}
- }, { result ->
- activity.runOnUiThread {
- actionFinishInUIThread?.onFinishRun(result)
- // Remove the progress task
- ProgressTaskDialogFragment.stop(activity)
- TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity)
- activity.stopService(intentDatabaseTask)
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ mBinder?.removeActionTaskListener(actionTaskListener)
+ mBinder = null
+ }
+ }
+ }
+ }
+
+ @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 start() {
- actionRunnableAsyncTask?.execute(actionRunnable)
+ 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)
+ }
- private class ActionRunnableAsyncTask(private val progressTaskUpdater: ProgressTaskUpdater,
- private val onPreExecute: () -> Unit,
- private val onPostExecute: (result: ActionRunnable.Result) -> Unit)
- : AsyncTask<((ProgressTaskUpdater?)-> ActionRunnable), Void, ActionRunnable.Result>() {
+ 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)
+ }
- override fun onPreExecute() {
- super.onPreExecute()
- onPreExecute.invoke()
+ 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)
+ }
- override fun doInBackground(vararg actionRunnables: ((ProgressTaskUpdater?)-> ActionRunnable)?): ActionRunnable.Result {
- var resultTask = ActionRunnable.Result(false)
- actionRunnables.forEach {
- it?.invoke(progressTaskUpdater)?.apply {
- run()
- resultTask = result
+ 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,
+ newParent: GroupVersioned?,
+ save: Boolean) {
+ val groupsIdToCopy = ArrayList>()
+ val entriesIdToCopy = ArrayList>()
+ 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)
}
}
- return resultTask
}
+ 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,
+ newParent: GroupVersioned,
+ save: Boolean) {
+ startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
+ }
+
+ fun startDatabaseMoveNodes(nodesToMove: List,
+ newParent: GroupVersioned,
+ save: Boolean) {
+ startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
+ }
+
+ fun startDatabaseDeleteNodes(nodesToDelete: List,
+ 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)
+ }
- override fun onPostExecute(result: ActionRunnable.Result) {
- super.onPostExecute(result)
- onPostExecute.invoke(result)
+ 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)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt
index bba0a529f..092744fb8 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt
@@ -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)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt
index cce1386f7..e8c8c2fee 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt
@@ -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
-
- override fun onFinishRun(result: Result) {
- callbackRunnable?.apply {
- onActionNodeFinish(nodeFinish(result))
- }
+ abstract fun nodeFinish(): ActionNodesValues
- if (!result.isSuccess) {
- displayMessage(context)
+ override fun onFinishRun() {
+ super.onFinishRun()
+ afterActionNodesFinish?.apply {
+ onActionNodesFinish(result, nodeFinish())
}
-
- super.onFinishRun(result)
- }
-
- companion object {
- const val NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY = "NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY"
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddEntryRunnable.kt
index 63a555c66..991b91be2 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddEntryRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddEntryRunnable.kt
@@ -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()
+ val newNodesReturn = ArrayList()
+ newNodesReturn.add(mNewEntry)
+ return ActionNodesValues(oldNodesReturn, newNodesReturn)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddGroupRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddGroupRunnable.kt
index bf3c7fd0c..ebbce2abf 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddGroupRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddGroupRunnable.kt
@@ -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()
+ val newNodesReturn = ArrayList()
+ newNodesReturn.add(mNewGroup)
+ return ActionNodesValues(oldNodesReturn, newNodesReturn)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AfterActionNodeFinishRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AfterActionNodesFinish.kt
similarity index 63%
rename from app/src/main/java/com/kunzisoft/keepass/database/action/node/AfterActionNodeFinishRunnable.kt
rename to app/src/main/java/com/kunzisoft/keepass/database/action/node/AfterActionNodesFinish.kt
index f3d9e1bdb..c509a3223 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AfterActionNodeFinishRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AfterActionNodesFinish.kt
@@ -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, val newNodes: List)
-abstract class AfterActionNodeFinishRunnable {
- abstract fun onActionNodeFinish(actionNodeValues: ActionNodeValues)
+abstract class AfterActionNodesFinish {
+ abstract fun onActionNodesFinish(result: ActionRunnable.Result, actionNodesValues: ActionNodesValues)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyEntryRunnable.kt
deleted file mode 100644
index aae0a530c..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyEntryRunnable.kt
+++ /dev/null
@@ -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 .
- *
- */
-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
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt
new file mode 100644
index 000000000..e76479992
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt
@@ -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 .
+ *
+ */
+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,
+ private val mNewParent: GroupVersioned,
+ save: Boolean,
+ afterActionNodesFinish: AfterActionNodesFinish?)
+ : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
+
+ private var mEntriesCopied = ArrayList()
+
+ 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
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteEntryRunnable.kt
deleted file mode 100644
index 7111bb00a..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteEntryRunnable.kt
+++ /dev/null
@@ -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 .
- *
- */
-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)
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteGroupRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteGroupRunnable.kt
deleted file mode 100644
index 023988453..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteGroupRunnable.kt
+++ /dev/null
@@ -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 .
- *
- */
-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)
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt
new file mode 100644
index 000000000..e24619d46
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt
@@ -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 .
+ *
+ */
+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,
+ save: Boolean,
+ afterActionNodesFinish: AfterActionNodesFinish)
+ : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
+
+ private var mParent: GroupVersioned? = null
+ private var mCanRecycle: Boolean = false
+
+ private var mNodesToDeleteBackup = ArrayList()
+
+ 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)
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveEntryRunnable.kt
deleted file mode 100644
index 0ca58c81e..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveEntryRunnable.kt
+++ /dev/null
@@ -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 .
- *
- */
-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
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveGroupRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveGroupRunnable.kt
deleted file mode 100644
index 23dc80fdd..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveGroupRunnable.kt
+++ /dev/null
@@ -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 .
- *
- */
-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
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt
new file mode 100644
index 000000000..ae8335f6b
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt
@@ -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 .
+ *
+ */
+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,
+ 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
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt
index 4c5be2905..14e1689f5 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt
@@ -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()
+ oldNodesReturn.add(mBackupEntryHistory)
+ val newNodesReturn = ArrayList()
+ newNodesReturn.add(mOldEntry)
+ return ActionNodesValues(oldNodesReturn, newNodesReturn)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateGroupRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateGroupRunnable.kt
index 40d3104c0..273bb7227 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateGroupRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateGroupRunnable.kt
@@ -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()
+ oldNodesReturn.add(mBackupGroup)
+ val newNodesReturn = ArrayList()
+ newNodesReturn.add(mOldGroup)
+ return ActionNodesValues(oldNodesReturn, newNodesReturn)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursor.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursor.kt
index 67f89fd51..e9f59624b 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursor.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursor.kt
@@ -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> : MatrixCursor(arrayOf(
+abstract class EntryCursor> : MatrixCursor(arrayOf(
_ID,
COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS,
COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS,
@@ -25,10 +24,10 @@ abstract class EntryCursor> : MatrixCursor(arrayOf(
abstract fun addEntry(entry: PwEntryV)
+ abstract fun getPwNodeId(): PwNodeId
+
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> : MatrixCursor(arrayOf(
const val COLUMN_INDEX_URL = "URL"
const val COLUMN_INDEX_NOTES = "notes"
}
-
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorUUID.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorUUID.kt
new file mode 100644
index 000000000..95be574a9
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorUUID.kt
@@ -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>: EntryCursor() {
+
+ override fun getPwNodeId(): PwNodeId {
+ return PwNodeIdUUID(
+ UUID(getLong(getColumnIndex(COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
+ getLong(getColumnIndex(COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS))))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV3.kt
index 9ef52fbe5..f50b65388 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV3.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV3.kt
@@ -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() {
+class EntryCursorV3 : EntryCursorUUID() {
override fun addEntry(entry: PwEntryV3) {
addRow(arrayOf(
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV4.kt
index d4b1c02dd..f4fcbb3a1 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV4.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV4.kt
@@ -5,7 +5,7 @@ import com.kunzisoft.keepass.database.element.PwIconFactory
import java.util.UUID
-class EntryCursorV4 : EntryCursor() {
+class EntryCursorV4 : EntryCursorUUID() {
private val extraFieldCursor: ExtraFieldCursor = ExtraFieldCursor()
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/ExtraFieldCursor.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/ExtraFieldCursor.kt
index 33913ab08..f86a27b82 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/cursor/ExtraFieldCursor.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/ExtraFieldCursor.kt
@@ -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))))
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt
index 8954abf88..1cb8c505f 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt
@@ -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 allowDescription: Boolean
+ get() = pwDatabaseV4 != null
- val description: String
+ 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
+ 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
get() = pwDatabaseV3?.availableEncryptionAlgorithms ?: pwDatabaseV4?.availableEncryptionAlgorithms ?: ArrayList()
- val availableKdfEngines: List
- get() {
- if (pwDatabaseV3 != null) {
- return KdfFactory.kdfListV3
- }
- if (pwDatabaseV4 != null) {
- return KdfFactory.kdfListV4
+ var encryptionAlgorithm: PwEncryptionAlgorithm?
+ get() = pwDatabaseV3?.encryptionAlgorithm ?: pwDatabaseV4?.encryptionAlgorithm
+ set(algorithm) {
+ algorithm?.let {
+ pwDatabaseV4?.encryptionAlgorithm = algorithm
+ pwDatabaseV4?.setDataEngine(algorithm.cipherEngine)
+ pwDatabaseV4?.dataCipher = algorithm.dataCipher
}
- return ArrayList()
}
- val kdfEngine: KdfEngine
- get() {
- return pwDatabaseV4?.kdfEngine ?: return KdfFactory.aesKdf
+ val availableKdfEngines: List
+ 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
+ }
}
- 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
- }
-
- 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)
+ this.pwDatabaseV3 = null
+ this.pwDatabaseV4 = null
+ this.fileUri = null
+ this.loaded = false
}
- 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): 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)
-
- entry.addBackupToHistory()
+ fun removeOldestHistory(entry: EntryVersioned) {
- // 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) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/EntryVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/EntryVersioned.kt
index 676f2ceb0..900778505 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/EntryVersioned.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/EntryVersioned.kt
@@ -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 {
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 {
dest.writeParcelable(pwEntryV4, flags)
}
- var nodeId: PwNodeId
+ override var nodeId: PwNodeId
get() = pwEntryV4?.nodeId ?: pwEntryV3?.nodeId ?: PwNodeIdUUID()
set(value) {
pwEntryV3?.nodeId = value
@@ -154,13 +156,16 @@ class EntryVersioned : NodeVersioned, PwEntryInterface {
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 {
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 {
pwEntryV4?.stopToManageFieldReferences()
}
- fun addBackupToHistory() {
- pwEntryV4?.let {
- val entryHistory = PwEntryV4()
- entryHistory.updateWith(it)
- it.addEntryToHistory(entryHistory)
+ fun getHistory(): ArrayList {
+ val history = ArrayList()
+ 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 {
- return pwEntryV4?.history ?: ArrayList()
+ fun getSize(): Long {
+ return pwEntryV4?.size ?: 0L
}
fun containsCustomData(): Boolean {
@@ -284,6 +310,10 @@ class EntryVersioned : NodeVersioned, PwEntryInterface {
------------
*/
+ /**
+ * 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 {
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
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/GroupVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/GroupVersioned.kt
index 3673e9cfb..5737738a4 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/GroupVersioned.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/GroupVersioned.kt
@@ -70,7 +70,7 @@ class GroupVersioned : NodeVersioned, PwGroupInterface?
+ override val nodeId: PwNodeId<*>?
get() = pwGroupV4?.nodeId ?: pwGroupV3?.nodeId
override var title: String
@@ -114,6 +114,38 @@ class GroupVersioned : NodeVersioned, PwGroupInterface
+ 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 {
val children = ArrayList()
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/NodeTimeInterface.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/NodeTimeInterface.kt
index 44223b6fa..7decbd1c0 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/NodeTimeInterface.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/NodeTimeInterface.kt
@@ -29,5 +29,7 @@ interface NodeTimeInterface {
var expiryTime: PwDate
- var isExpires: Boolean
+ var expires: Boolean
+
+ val isCurrentlyExpires: Boolean
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/NodeVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/NodeVersioned.kt
index 8d6759f15..f231245f8 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/NodeVersioned.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/NodeVersioned.kt
@@ -2,16 +2,26 @@ package com.kunzisoft.keepass.database.element
interface NodeVersioned: PwNodeInterface {
+ 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
+ }
}
/**
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/PwCompressionAlgorithm.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwCompressionAlgorithm.kt
similarity index 67%
rename from app/src/main/java/com/kunzisoft/keepass/database/file/PwCompressionAlgorithm.kt
rename to app/src/main/java/com/kunzisoft/keepass/database/element/PwCompressionAlgorithm.kt
index 32d7295e9..c4bd02208 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/PwCompressionAlgorithm.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwCompressionAlgorithm.kt
@@ -17,25 +17,24 @@
* along with KeePass DX. If not, see .
*
*/
-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) {
-
- None(0),
- Gzip(1);
+enum class PwCompressionAlgorithm : ObjectNameResource {
- companion object {
+ None,
+ GZip;
- 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)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt
index 61e7b0a50..d15b1fe41 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt
@@ -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, Entry : PwEntry> {
+abstract class PwDatabase<
+ GroupId,
+ EntryId,
+ Group : PwGroup,
+ Entry : PwEntry
+ > {
// Algorithm used to encrypt the database
protected var algorithm: PwEncryptionAlgorithm? = null
+ abstract val kdfEngine: KdfEngine?
+
+ abstract val kdfAvailableList: List
+
var masterKey = ByteArray(32)
var finalKey: ByteArray? = null
protected set
@@ -46,8 +49,10 @@ abstract class PwDatabase, Entry : PwEntry, Group>()
- private var entryIndexes = LinkedHashMap, Entry>()
+ var changeDuplicateId = false
+
+ private var groupIndexes = LinkedHashMap, Group>()
+ private var entryIndexes = LinkedHashMap, Entry>()
abstract val version: String
@@ -67,15 +72,15 @@ abstract class PwDatabase, Entry : PwEntry, Entry : PwEntry, Entry : PwEntry throw KeyFileEmptyException()
+ 0L -> throw LoadDatabaseKeyFileEmptyException()
32L -> return keyData
64L -> try {
return hexStringToByteArray(String(keyData))
@@ -156,15 +161,18 @@ abstract class PwDatabase, Entry : PwEntry, Entry : PwEntry, Entry : PwEntry
+ abstract fun newGroupId(): PwNodeId
- abstract fun newEntryId(): PwNodeId<*>
+ abstract fun newEntryId(): PwNodeId
abstract fun createGroup(): Group
@@ -211,7 +219,7 @@ abstract class PwDatabase, Entry : PwEntry): Boolean {
+ fun isGroupIdUsed(id: PwNodeId): Boolean {
return groupIndexes.containsKey(id)
}
@@ -226,19 +234,33 @@ abstract class PwDatabase, Entry : PwEntry): Group? {
+ fun getGroupById(id: PwNodeId): 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, Entry : PwEntry): Boolean {
+ fun isEntryIdUsed(id: PwNodeId): Boolean {
return entryIndexes.containsKey(id)
}
@@ -261,20 +283,33 @@ abstract class PwDatabase, Entry : PwEntry): Entry? {
+ fun getEntryById(id: PwNodeId): 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, Entry : PwEntry, Entry : PwEntry@phoneid.org>
- * @author Bill Zwicky @pobox.com>
- * @author Dominik Reichl @t-online.de>
- */
-class PwDatabaseV3 : PwDatabase() {
+class PwDatabaseV3 : PwDatabase() {
private var numKeyEncRounds: Int = 0
+ private var kdfListV3: MutableList = ArrayList()
+
override val version: String
get() = "KeePass 1"
+ init {
+ kdfListV3.add(KdfFactory.aesKdf)
+ }
+
+ override val kdfEngine: KdfEngine?
+ get() = kdfListV3[0]
+
+ override val kdfAvailableList: List
+ get() = kdfListV3
+
override val availableEncryptionAlgorithms: List
get() {
val list = ArrayList()
@@ -103,7 +113,7 @@ class PwDatabaseV3 : PwDatabase() {
return newId
}
- @Throws(InvalidKeyFileException::class, IOException::class)
+ @Throws(IOException::class)
override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
return if (key != null && keyInputStream != null) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt
index d4580bae2..31b11f8e1 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt
@@ -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 {
+class PwDatabaseV4 : PwDatabase {
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 = ArrayList()
private var numKeyEncRounds: Long = 0
var publicCustomData = VariantDictionary()
@@ -93,6 +92,11 @@ class PwDatabaseV4 : PwDatabase {
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 {
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
+ 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
+ get() {
+ val list = ArrayList()
+ list.add(PwCompressionAlgorithm.None)
+ list.add(PwCompressionAlgorithm.GZip)
+ return list
+ }
+
override val availableEncryptionAlgorithms: List
get() {
val list = ArrayList()
@@ -116,45 +153,45 @@ class PwDatabaseV4 : PwDatabase {
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 {
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 {
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 {
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 {
}
addGroupTo(recycleBinGroup, rootGroup)
recycleBinUUID = recycleBinGroup.id
- recycleBinGroup.lastModificationTime.date?.let {
- recycleBinChanged = it
- }
+ recycleBinChanged = recycleBinGroup.lastModificationTime.date
}
}
@@ -427,10 +470,10 @@ class PwDatabaseV4 : PwDatabase {
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() {
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.kt
index 1c2ec3c47..1ce08df7c 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.kt
@@ -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)
+ }
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEncryptionAlgorithm.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEncryptionAlgorithm.kt
index 72ce3cffb..d214bd9cd 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEncryptionAlgorithm.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEncryptionAlgorithm.kt
@@ -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
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntry.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntry.kt
index 1b27ce15d..e5a1067ae 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntry.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntry.kt
@@ -5,10 +5,12 @@ import java.util.*
abstract class PwEntry
<
- ParentGroup: PwGroupInterface,
- Entry: PwEntryInterface
+ GroupId,
+ EntryId,
+ ParentGroup: PwGroup,
+ Entry: PwEntry
>
- : PwNode, PwEntryInterface {
+ : PwNode, PwEntryInterface {
constructor() : super()
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV3.kt
index 41e723047..9c69dfd6c 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV3.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV3.kt
@@ -48,7 +48,7 @@ import java.util.UUID
* @author Dominik Reichl @t-online.de>
* @author Jeremy Jamet @kunzisoft.com>
*/
-class PwEntryV3 : PwEntry {
+class PwEntryV3 : PwEntry, PwNodeV3Interface {
/** A string describing what is in pBinaryData */
var binaryDesc = ""
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt
index cb355f064..465e72b16 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt
@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.utils.MemoryUtil
import java.util.*
-class PwEntryV4 : PwEntry, PwNodeV4Interface {
+class PwEntryV4 : PwEntry, PwNodeV4Interface {
// To decode each field not parcelable
@Transient
@@ -88,6 +88,8 @@ class PwEntryV4 : PwEntry, PwNodeV4Interface {
return size
}
+ override var expires: Boolean = false
+
constructor() : super()
constructor(parcel: Parcel) : super(parcel) {
@@ -129,7 +131,7 @@ class PwEntryV4 : PwEntry, 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, 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, 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, PwNodeV4Interface {
history.add(entry)
}
+ fun removeAllHistory() {
+ history.clear()
+ }
+
fun removeOldestEntryFromHistory() {
var min: Date? = null
var index = -1
@@ -294,7 +305,7 @@ class PwEntryV4 : PwEntry, 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
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroup.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroup.kt
index b696a145e..50d94dca0 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroup.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroup.kt
@@ -4,11 +4,12 @@ import android.os.Parcel
abstract class PwGroup
<
- Id,
- Group: PwGroupInterface,
- Entry: PwEntryInterface
+ GroupId,
+ EntryId,
+ Group: PwGroup,
+ Entry: PwEntry
>
- : PwNode, PwGroupInterface {
+ : PwNode, PwGroupInterface {
private var titleGroup = ""
@Transient
@@ -27,10 +28,12 @@ abstract class PwGroup
dest.writeString(titleGroup)
}
- protected fun updateWith(source: PwGroup) {
+ protected fun updateWith(source: PwGroup) {
super.updateWith(source)
titleGroup = source.titleGroup
+ childGroups.clear()
childGroups.addAll(source.childGroups)
+ childEntries.clear()
childEntries.addAll(source.childEntries)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV3.kt
index cad871a94..ed2aaec3d 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV3.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV3.kt
@@ -22,8 +22,9 @@ package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.Parcelable
+import java.util.*
-class PwGroupV3 : PwGroup {
+class PwGroupV3 : PwGroup, PwNodeV3Interface {
var level = 0 // short
/** Used by KeePass internally, don't use */
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV4.kt
index ed0ef76f7..e7eee8abc 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV4.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV4.kt
@@ -25,7 +25,7 @@ import android.os.Parcelable
import java.util.HashMap
import java.util.UUID
-class PwGroupV4 : PwGroup, PwNodeV4Interface {
+class PwGroupV4 : PwGroup, PwNodeV4Interface {
// TODO Encapsulate
override var icon: PwIcon
@@ -43,12 +43,15 @@ class PwGroupV4 : PwGroup, PwNodeV4Interface {
var iconCustom = PwIconCustom.UNKNOWN_ICON
private val customData = HashMap()
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
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwNode.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNode.kt
index 3cf796319..60e1a6028 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwNode.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNode.kt
@@ -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, 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, 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, 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
@@ -85,17 +88,11 @@ abstract class PwNode, 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)
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeId.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeId.kt
index 9cfa914ec..38bc119b5 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeId.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeId.kt
@@ -31,4 +31,17 @@ abstract class PwNodeId : 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
+ }
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeV3Interface.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeV3Interface.kt
new file mode 100644
index 000000000..ee39cc0e6
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeV3Interface.kt
@@ -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
+ }
+}
+
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/ArcFourException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/ArcFourException.kt
deleted file mode 100644
index 9ad91f6ca..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/exception/ArcFourException.kt
+++ /dev/null
@@ -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 .
- *
- */
-package com.kunzisoft.keepass.database.exception
-
-class ArcFourException : InvalidDBException() {
- companion object {
- private const val serialVersionUID = 2103983626687861237L
- }
-
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/ContentFileNotFoundException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/ContentFileNotFoundException.kt
deleted file mode 100644
index 2c9f4c3c9..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/exception/ContentFileNotFoundException.kt
+++ /dev/null
@@ -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 .
- *
- */
-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()
- }
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt
new file mode 100644
index 000000000..e16dadb92
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt
@@ -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)? = 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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidAlgorithmException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidAlgorithmException.kt
deleted file mode 100644
index 99cc71f2c..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidAlgorithmException.kt
+++ /dev/null
@@ -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 .
- *
- */
-package com.kunzisoft.keepass.database.exception
-
-class InvalidAlgorithmException : InvalidDBException() {
- companion object {
- private const val serialVersionUID = 3062682891863487208L
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBException.kt
deleted file mode 100644
index d50721f39..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBException.kt
+++ /dev/null
@@ -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 .
- *
- */
-package com.kunzisoft.keepass.database.exception
-
-open class InvalidDBException : Exception {
-
- constructor(str: String) : super(str)
-
- constructor() : super()
-
- companion object {
- private const val serialVersionUID = 5191964825154190923L
- }
-
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBSignatureException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBSignatureException.kt
deleted file mode 100644
index 9445755b0..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBSignatureException.kt
+++ /dev/null
@@ -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 .
- *
- */
-package com.kunzisoft.keepass.database.exception
-
-class InvalidDBSignatureException : InvalidDBException() {
- companion object {
- private const val serialVersionUID = -5358923878743513758L
- }
-
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBVersionException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBVersionException.kt
deleted file mode 100644
index 577bc0052..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBVersionException.kt
+++ /dev/null
@@ -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 .
- *
- */
-package com.kunzisoft.keepass.database.exception
-
-class InvalidDBVersionException : InvalidDBException() {
- companion object {
- private const val serialVersionUID = -4260650987856400586L
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidKeyFileException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidKeyFileException.kt
deleted file mode 100644
index d7f54fcec..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidKeyFileException.kt
+++ /dev/null
@@ -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 .
- *
- */package com.kunzisoft.keepass.database.exception
-
-open class InvalidKeyFileException : InvalidDBException() {
- companion object {
- private const val serialVersionUID = 5540694419562294464L
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidPasswordException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidPasswordException.kt
deleted file mode 100644
index dcd51b94d..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidPasswordException.kt
+++ /dev/null
@@ -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 .
- *
- */
-package com.kunzisoft.keepass.database.exception
-
-class InvalidPasswordException : InvalidDBException() {
- companion object {
- private const val serialVersionUID = -8729476180242058319L
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/KeyFileEmptyException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/KeyFileEmptyException.kt
deleted file mode 100644
index 474d2dfea..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/exception/KeyFileEmptyException.kt
+++ /dev/null
@@ -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 .
- *
- */
-package com.kunzisoft.keepass.database.exception
-
-class KeyFileEmptyException : InvalidKeyFileException() {
- companion object {
- private const val serialVersionUID = -1630780661204212325L
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/PwDbOutputException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/PwDbOutputException.kt
deleted file mode 100644
index d892cdee2..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/exception/PwDbOutputException.kt
+++ /dev/null
@@ -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 .
- *
- */
-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
- }
-}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/SamsungClipboardException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/SamsungClipboardException.kt
index 8db01ab9c..0c3f13a8d 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/exception/SamsungClipboardException.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/exception/SamsungClipboardException.kt
@@ -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)
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt
index 2897148c5..435bf4b48 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt
@@ -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")
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt
index a6d089c1e..9cff24a2a 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt
@@ -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)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/load/Importer.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/load/Importer.kt
index 1eec9700e..8743eb855 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/load/Importer.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/load/Importer.kt
@@ -20,13 +20,11 @@
package com.kunzisoft.keepass.database.file.load
import com.kunzisoft.keepass.database.element.PwDatabase
-import com.kunzisoft.keepass.database.exception.InvalidDBException
+import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
-
-import java.io.IOException
import java.io.InputStream
-abstract class Importer> {
+abstract class Importer> {
/**
* Load a versioned database file, return contents in a new PwDatabase.
@@ -35,10 +33,9 @@ abstract class Importer> {
* @param password Pass phrase for infile.
* @return new PwDatabase container.
*
- * @throws IOException on any file error.
- * @throws InvalidDBException on database error.
+ * @throws LoadDatabaseException on database error (contains IO exceptions)
*/
- @Throws(IOException::class, InvalidDBException::class)
+ @Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyInputStream: InputStream?,
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt
index 3771f2d09..be42a451a 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt
@@ -73,155 +73,163 @@ class ImporterV3 : Importer() {
private lateinit var mDatabaseToOpen: PwDatabaseV3
- @Throws(IOException::class, InvalidDBException::class)
+ @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyInputStream: InputStream?,
progressTaskUpdater: ProgressTaskUpdater?): PwDatabaseV3 {
- // Load entire file, most of it's encrypted.
- val fileSize = databaseInputStream.available()
- val filebuf = ByteArray(fileSize + 16) // Pad with a blocksize (Twofish uses 128 bits), since Android 4.3 tries to write more to the buffer
- databaseInputStream.read(filebuf, 0, fileSize) // TODO remove
- databaseInputStream.close()
+ try {
+ // Load entire file, most of it's encrypted.
+ val fileSize = databaseInputStream.available()
+ val filebuf = ByteArray(fileSize + 16) // Pad with a blocksize (Twofish uses 128 bits), since Android 4.3 tries to write more to the buffer
+ databaseInputStream.read(filebuf, 0, fileSize) // TODO remove
+ databaseInputStream.close()
+
+ // Parse header (unencrypted)
+ if (fileSize < PwDbHeaderV3.BUF_SIZE)
+ throw IOException("File too short for header")
+ val hdr = PwDbHeaderV3()
+ hdr.loadFromFile(filebuf, 0)
+
+ if (hdr.signature1 != PwDbHeader.PWM_DBSIG_1 || hdr.signature2 != PwDbHeaderV3.DBSIG_2) {
+ throw LoadDatabaseSignatureException()
+ }
- // Parse header (unencrypted)
- if (fileSize < PwDbHeaderV3.BUF_SIZE)
- throw IOException("File too short for header")
- val hdr = PwDbHeaderV3()
- hdr.loadFromFile(filebuf, 0)
+ if (!hdr.matchesVersion()) {
+ throw LoadDatabaseVersionException()
+ }
- if (hdr.signature1 != PwDbHeader.PWM_DBSIG_1 || hdr.signature2 != PwDbHeaderV3.DBSIG_2) {
- throw InvalidDBSignatureException()
- }
+ progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
+ mDatabaseToOpen = PwDatabaseV3()
+ mDatabaseToOpen.retrieveMasterKey(password, keyInputStream)
- if (!hdr.matchesVersion()) {
- throw InvalidDBVersionException()
- }
+ // Select algorithm
+ when {
+ hdr.flags and PwDbHeaderV3.FLAG_RIJNDAEL != 0 -> mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.AESRijndael
+ hdr.flags and PwDbHeaderV3.FLAG_TWOFISH != 0 -> mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.Twofish
+ else -> throw LoadDatabaseInvalidAlgorithmException()
+ }
- progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
- mDatabaseToOpen = PwDatabaseV3()
- mDatabaseToOpen.retrieveMasterKey(password, keyInputStream)
-
- // Select algorithm
- if (hdr.flags and PwDbHeaderV3.FLAG_RIJNDAEL != 0) {
- mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.AESRijndael
- } else if (hdr.flags and PwDbHeaderV3.FLAG_TWOFISH != 0) {
- mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.Twofish
- } else {
- throw InvalidAlgorithmException()
- }
+ mDatabaseToOpen.numberKeyEncryptionRounds = hdr.numKeyEncRounds.toLong()
- mDatabaseToOpen.numberKeyEncryptionRounds = hdr.numKeyEncRounds.toLong()
+ // Generate transformedMasterKey from masterKey
+ mDatabaseToOpen.makeFinalKey(hdr.masterSeed, hdr.transformSeed, mDatabaseToOpen.numberKeyEncryptionRounds)
- // Generate transformedMasterKey from masterKey
- mDatabaseToOpen.makeFinalKey(hdr.masterSeed, hdr.transformSeed, mDatabaseToOpen.numberKeyEncryptionRounds)
+ progressTaskUpdater?.updateMessage(R.string.decrypting_db)
+ // Initialize Rijndael algorithm
+ val cipher: Cipher
+ try {
+ if (mDatabaseToOpen.encryptionAlgorithm === PwEncryptionAlgorithm.AESRijndael) {
+ cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
+ } else if (mDatabaseToOpen.encryptionAlgorithm === PwEncryptionAlgorithm.Twofish) {
+ cipher = CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
+ } else {
+ throw IOException("Encryption algorithm is not supported")
+ }
- progressTaskUpdater?.updateMessage(R.string.decrypting_db)
- // Initialize Rijndael algorithm
- val cipher: Cipher
- try {
- if (mDatabaseToOpen.encryptionAlgorithm === PwEncryptionAlgorithm.AESRijndael) {
- cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
- } else if (mDatabaseToOpen.encryptionAlgorithm === PwEncryptionAlgorithm.Twofish) {
- cipher = CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING")
- } else {
- throw IOException("Encryption algorithm is not supported")
+ } catch (e1: NoSuchAlgorithmException) {
+ throw IOException("No such algorithm")
+ } catch (e1: NoSuchPaddingException) {
+ throw IOException("No such pdading")
}
- } catch (e1: NoSuchAlgorithmException) {
- throw IOException("No such algorithm")
- } catch (e1: NoSuchPaddingException) {
- throw IOException("No such pdading")
- }
-
- try {
- cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(mDatabaseToOpen.finalKey, "AES"), IvParameterSpec(hdr.encryptionIV))
- } catch (e1: InvalidKeyException) {
- throw IOException("Invalid key")
- } catch (e1: InvalidAlgorithmParameterException) {
- throw IOException("Invalid algorithm parameter.")
- }
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(mDatabaseToOpen.finalKey, "AES"), IvParameterSpec(hdr.encryptionIV))
+ } catch (e1: InvalidKeyException) {
+ throw IOException("Invalid key")
+ } catch (e1: InvalidAlgorithmParameterException) {
+ throw IOException("Invalid algorithm parameter.")
+ }
- // Decrypt! The first bytes aren't encrypted (that's the header)
- val encryptedPartSize: Int
- try {
- encryptedPartSize = cipher.doFinal(filebuf, PwDbHeaderV3.BUF_SIZE, fileSize - PwDbHeaderV3.BUF_SIZE, filebuf, PwDbHeaderV3.BUF_SIZE)
- } catch (e1: ShortBufferException) {
- throw IOException("Buffer too short")
- } catch (e1: IllegalBlockSizeException) {
- throw IOException("Invalid block size")
- } catch (e1: BadPaddingException) {
- throw InvalidPasswordException()
- }
+ // Decrypt! The first bytes aren't encrypted (that's the header)
+ val encryptedPartSize: Int
+ try {
+ encryptedPartSize = cipher.doFinal(filebuf, PwDbHeaderV3.BUF_SIZE, fileSize - PwDbHeaderV3.BUF_SIZE, filebuf, PwDbHeaderV3.BUF_SIZE)
+ } catch (e1: ShortBufferException) {
+ throw IOException("Buffer too short")
+ } catch (e1: IllegalBlockSizeException) {
+ throw IOException("Invalid block size")
+ } catch (e1: BadPaddingException) {
+ throw LoadDatabaseInvalidCredentialsException()
+ }
- val md: MessageDigest
- try {
- md = MessageDigest.getInstance("SHA-256")
- } catch (e: NoSuchAlgorithmException) {
- throw IOException("No SHA-256 algorithm")
- }
+ val md: MessageDigest
+ try {
+ md = MessageDigest.getInstance("SHA-256")
+ } catch (e: NoSuchAlgorithmException) {
+ throw IOException("No SHA-256 algorithm")
+ }
- val nos = NullOutputStream()
- val dos = DigestOutputStream(nos, md)
- dos.write(filebuf, PwDbHeaderV3.BUF_SIZE, encryptedPartSize)
- dos.close()
- val hash = md.digest()
+ val nos = NullOutputStream()
+ val dos = DigestOutputStream(nos, md)
+ dos.write(filebuf, PwDbHeaderV3.BUF_SIZE, encryptedPartSize)
+ dos.close()
+ val hash = md.digest()
- if (!Arrays.equals(hash, hdr.contentsHash)) {
+ if (!Arrays.equals(hash, hdr.contentsHash)) {
- Log.w(TAG, "Database file did not decrypt correctly. (checksum code is broken)")
- throw InvalidPasswordException()
- }
+ Log.w(TAG, "Database file did not decrypt correctly. (checksum code is broken)")
+ throw LoadDatabaseInvalidCredentialsException()
+ }
- // New manual root because V3 contains multiple root groups (here available with getRootGroups())
- val newRoot = mDatabaseToOpen.createGroup()
- newRoot.level = -1
- mDatabaseToOpen.rootGroup = newRoot
+ // New manual root because V3 contains multiple root groups (here available with getRootGroups())
+ val newRoot = mDatabaseToOpen.createGroup()
+ newRoot.level = -1
+ mDatabaseToOpen.rootGroup = newRoot
+
+ // Import all groups
+ var pos = PwDbHeaderV3.BUF_SIZE
+ var newGrp = mDatabaseToOpen.createGroup()
+ run {
+ var i = 0
+ while (i < hdr.numGroups) {
+ val fieldType = LEDataInputStream.readUShort(filebuf, pos)
+ pos += 2
+ val fieldSize = LEDataInputStream.readInt(filebuf, pos)
+ pos += 4
+
+ if (fieldType == 0xFFFF) {
+ // End-Group record. Save group and count it.
+ mDatabaseToOpen.addGroupIndex(newGrp)
+ newGrp = mDatabaseToOpen.createGroup()
+ i++
+ } else {
+ readGroupField(mDatabaseToOpen, newGrp, fieldType, filebuf, pos)
+ }
+ pos += fieldSize
+ }
+ }
- // Import all groups
- var pos = PwDbHeaderV3.BUF_SIZE
- var newGrp = mDatabaseToOpen.createGroup()
- run {
+ // Import all entries
+ var newEnt = mDatabaseToOpen.createEntry()
var i = 0
- while (i < hdr.numGroups) {
+ while (i < hdr.numEntries) {
val fieldType = LEDataInputStream.readUShort(filebuf, pos)
- pos += 2
- val fieldSize = LEDataInputStream.readInt(filebuf, pos)
- pos += 4
+ val fieldSize = LEDataInputStream.readInt(filebuf, pos + 2)
if (fieldType == 0xFFFF) {
// End-Group record. Save group and count it.
- mDatabaseToOpen.addGroupIndex(newGrp)
- newGrp = mDatabaseToOpen.createGroup()
+ mDatabaseToOpen.addEntryIndex(newEnt)
+ newEnt = mDatabaseToOpen.createEntry()
i++
} else {
- readGroupField(mDatabaseToOpen, newGrp, fieldType, filebuf, pos)
+ readEntryField(mDatabaseToOpen, newEnt, filebuf, pos)
}
- pos += fieldSize
+ pos += 2 + 4 + fieldSize
}
- }
- // Import all entries
- var newEnt = mDatabaseToOpen.createEntry()
- var i = 0
- while (i < hdr.numEntries) {
- val fieldType = LEDataInputStream.readUShort(filebuf, pos)
- val fieldSize = LEDataInputStream.readInt(filebuf, pos + 2)
-
- if (fieldType == 0xFFFF) {
- // End-Group record. Save group and count it.
- mDatabaseToOpen.addEntryIndex(newEnt)
- newEnt = mDatabaseToOpen.createEntry()
- i++
- } else {
- readEntryField(mDatabaseToOpen, newEnt, filebuf, pos)
- }
- pos += 2 + 4 + fieldSize
+ constructTreeFromIndex()
+ } catch (e: LoadDatabaseException) {
+ throw e
+ } catch (e: IOException) {
+ throw LoadDatabaseIOException(e)
+ } catch (e: OutOfMemoryError) {
+ throw LoadDatabaseNoMemoryException(e)
+ } catch (e: Exception) {
+ throw LoadDatabaseException(e)
}
- constructTreeFromIndex()
-
return mDatabaseToOpen
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt
index 546456b80..09adaba71 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt
@@ -24,20 +24,17 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.crypto.StreamCipherFactory
import com.kunzisoft.keepass.crypto.engine.CipherEngine
-import com.kunzisoft.keepass.database.file.PwCompressionAlgorithm
import com.kunzisoft.keepass.database.element.*
-import com.kunzisoft.keepass.database.exception.ArcFourException
-import com.kunzisoft.keepass.database.exception.InvalidDBException
-import com.kunzisoft.keepass.database.exception.InvalidPasswordException
-import com.kunzisoft.keepass.database.file.PwDbHeaderV4
import com.kunzisoft.keepass.database.element.security.ProtectedBinary
import com.kunzisoft.keepass.database.element.security.ProtectedString
+import com.kunzisoft.keepass.database.exception.*
+import com.kunzisoft.keepass.database.file.KDBX4DateUtil
+import com.kunzisoft.keepass.database.file.PwDbHeaderV4
import com.kunzisoft.keepass.stream.BetterCipherInputStream
import com.kunzisoft.keepass.stream.HashedBlockInputStream
import com.kunzisoft.keepass.stream.HmacBlockInputStream
import com.kunzisoft.keepass.stream.LEDataInputStream
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
-import com.kunzisoft.keepass.database.file.KDBX4DateUtil
import com.kunzisoft.keepass.utils.MemoryUtil
import com.kunzisoft.keepass.utils.Types
import org.spongycastle.crypto.StreamCipher
@@ -46,17 +43,14 @@ import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
import java.io.*
import java.nio.charset.Charset
-import java.security.InvalidAlgorithmParameterException
-import java.security.InvalidKeyException
-import java.security.NoSuchAlgorithmException
import java.text.ParseException
import java.util.*
import java.util.zip.GZIPInputStream
import javax.crypto.Cipher
-import javax.crypto.NoSuchPaddingException
import kotlin.math.min
-class ImporterV4(private val streamDir: File) : Importer() {
+class ImporterV4(private val streamDir: File,
+ private val fixDuplicateUUID: Boolean = false) : Importer() {
private var randomStream: StreamCipher? = null
private lateinit var mDatabase: PwDatabaseV4
@@ -89,108 +83,120 @@ class ImporterV4(private val streamDir: File) : Importer() {
private var entryCustomDataKey: String? = null
private var entryCustomDataValue: String? = null
- @Throws(IOException::class, InvalidDBException::class)
+ @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyInputStream: InputStream?,
progressTaskUpdater: ProgressTaskUpdater?): PwDatabaseV4 {
- // TODO performance
- progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
+ try {
+ // TODO performance
+ progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
- mDatabase = PwDatabaseV4()
- val header = PwDbHeaderV4(mDatabase)
+ mDatabase = PwDatabaseV4()
- val headerAndHash = header.loadFromFile(databaseInputStream)
- version = header.version
+ mDatabase.changeDuplicateId = fixDuplicateUUID
- hashOfHeader = headerAndHash.hash
- val pbHeader = headerAndHash.header
+ val header = PwDbHeaderV4(mDatabase)
- mDatabase.retrieveMasterKey(password, keyInputStream)
- mDatabase.makeFinalKey(header.masterSeed)
- // TODO performance
+ val headerAndHash = header.loadFromFile(databaseInputStream)
+ version = header.version
- progressTaskUpdater?.updateMessage(R.string.decrypting_db)
- val engine: CipherEngine
- val cipher: Cipher
- try {
- engine = CipherFactory.getInstance(mDatabase.dataCipher)
- mDatabase.setDataEngine(engine)
- mDatabase.encryptionAlgorithm = engine.getPwEncryptionAlgorithm()
- cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
- } catch (e: NoSuchAlgorithmException) {
- throw IOException("Invalid algorithm.", e)
- } catch (e: NoSuchPaddingException) {
- throw IOException("Invalid algorithm.", e)
- } catch (e: InvalidKeyException) {
- throw IOException("Invalid algorithm.", e)
- } catch (e: InvalidAlgorithmParameterException) {
- throw IOException("Invalid algorithm.", e)
- }
+ hashOfHeader = headerAndHash.hash
+ val pbHeader = headerAndHash.header
- val isPlain: InputStream
- if (version < PwDbHeaderV4.FILE_VERSION_32_4) {
+ mDatabase.retrieveMasterKey(password, keyInputStream)
+ mDatabase.makeFinalKey(header.masterSeed)
+ // TODO performance
- val decrypted = attachCipherStream(databaseInputStream, cipher)
- val dataDecrypted = LEDataInputStream(decrypted)
- val storedStartBytes: ByteArray?
+ progressTaskUpdater?.updateMessage(R.string.decrypting_db)
+ val engine: CipherEngine
+ val cipher: Cipher
try {
- storedStartBytes = dataDecrypted.readBytes(32)
- if (storedStartBytes == null || storedStartBytes.size != 32) {
- throw InvalidPasswordException()
- }
- } catch (e: IOException) {
- throw InvalidPasswordException()
+ engine = CipherFactory.getInstance(mDatabase.dataCipher)
+ mDatabase.setDataEngine(engine)
+ mDatabase.encryptionAlgorithm = engine.getPwEncryptionAlgorithm()
+ cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
+ } catch (e: Exception) {
+ throw LoadDatabaseInvalidAlgorithmException(e)
}
- if (!Arrays.equals(storedStartBytes, header.streamStartBytes)) {
- throw InvalidPasswordException()
- }
+ val isPlain: InputStream
+ if (version < PwDbHeaderV4.FILE_VERSION_32_4) {
- isPlain = HashedBlockInputStream(dataDecrypted)
- } else { // KDBX 4
- val isData = LEDataInputStream(databaseInputStream)
- val storedHash = isData.readBytes(32)
- if (!Arrays.equals(storedHash, hashOfHeader)) {
- throw InvalidDBException()
- }
+ val decrypted = attachCipherStream(databaseInputStream, cipher)
+ val dataDecrypted = LEDataInputStream(decrypted)
+ val storedStartBytes: ByteArray?
+ try {
+ storedStartBytes = dataDecrypted.readBytes(32)
+ if (storedStartBytes == null || storedStartBytes.size != 32) {
+ throw LoadDatabaseInvalidCredentialsException()
+ }
+ } catch (e: IOException) {
+ throw LoadDatabaseInvalidCredentialsException()
+ }
- val hmacKey = mDatabase.hmacKey ?: throw InvalidDBException()
- val headerHmac = PwDbHeaderV4.computeHeaderHmac(pbHeader, hmacKey)
- val storedHmac = isData.readBytes(32)
- if (storedHmac == null || storedHmac.size != 32) {
- throw InvalidDBException()
- }
- // Mac doesn't match
- if (!Arrays.equals(headerHmac, storedHmac)) {
- throw InvalidDBException()
+ if (!Arrays.equals(storedStartBytes, header.streamStartBytes)) {
+ throw LoadDatabaseInvalidCredentialsException()
+ }
+
+ isPlain = HashedBlockInputStream(dataDecrypted)
+ } else { // KDBX 4
+ val isData = LEDataInputStream(databaseInputStream)
+ val storedHash = isData.readBytes(32)
+ if (!Arrays.equals(storedHash, hashOfHeader)) {
+ throw LoadDatabaseInvalidCredentialsException()
+ }
+
+ val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException()
+ val headerHmac = PwDbHeaderV4.computeHeaderHmac(pbHeader, hmacKey)
+ val storedHmac = isData.readBytes(32)
+ if (storedHmac == null || storedHmac.size != 32) {
+ throw LoadDatabaseInvalidCredentialsException()
+ }
+ // Mac doesn't match
+ if (!Arrays.equals(headerHmac, storedHmac)) {
+ throw LoadDatabaseInvalidCredentialsException()
+ }
+
+ val hmIs = HmacBlockInputStream(isData, true, hmacKey)
+
+ isPlain = attachCipherStream(hmIs, cipher)
}
- val hmIs = HmacBlockInputStream(isData, true, hmacKey)
+ val inputStreamXml: InputStream
+ inputStreamXml = when (mDatabase.compressionAlgorithm) {
+ PwCompressionAlgorithm.GZip -> GZIPInputStream(isPlain)
+ else -> isPlain
+ }
- isPlain = attachCipherStream(hmIs, cipher)
- }
+ if (version >= PwDbHeaderV4.FILE_VERSION_32_4) {
+ loadInnerHeader(inputStreamXml, header)
+ }
- val isXml: InputStream
- if (mDatabase.compressionAlgorithm === PwCompressionAlgorithm.Gzip) {
- isXml = GZIPInputStream(isPlain)
- } else {
- isXml = isPlain
- }
+ randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
- if (version >= PwDbHeaderV4.FILE_VERSION_32_4) {
- loadInnerHeader(isXml, header)
- }
+ if (randomStream == null) {
+ throw LoadDatabaseArcFourException()
+ }
- randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
+ readDocumentStreamed(createPullParser(inputStreamXml))
- if (randomStream == null) {
- throw ArcFourException()
+ } catch (e: LoadDatabaseException) {
+ throw e
+ } catch (e: XmlPullParserException) {
+ throw LoadDatabaseIOException(e)
+ } catch (e: IOException) {
+ if (e.message?.contains("Hash failed with code") == true)
+ throw LoadDatabaseKDFMemoryException(e)
+ else
+ throw LoadDatabaseIOException(e)
+ } catch (e: OutOfMemoryError) {
+ throw LoadDatabaseNoMemoryException(e)
+ } catch (e: Exception) {
+ throw LoadDatabaseException(e)
}
- readXmlStreamed(isXml)
-
return mDatabase
}
@@ -271,18 +277,7 @@ class ImporterV4(private val streamDir: File) : Importer() {
Binaries
}
- @Throws(IOException::class, InvalidDBException::class)
- private fun readXmlStreamed(readerStream: InputStream) {
- try {
- readDocumentStreamed(createPullParser(readerStream))
- } catch (e: XmlPullParserException) {
- e.printStackTrace()
- throw IOException(e.localizedMessage)
- }
-
- }
-
- @Throws(XmlPullParserException::class, IOException::class, InvalidDBException::class)
+ @Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class)
private fun readDocumentStreamed(xpp: XmlPullParser) {
ctxGroups.clear()
@@ -313,7 +308,7 @@ class ImporterV4(private val streamDir: File) : Importer() {
if (ctxGroups.size != 0) throw IOException("Malformed")
}
- @Throws(XmlPullParserException::class, IOException::class, InvalidDBException::class)
+ @Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class)
private fun readXmlElement(ctx: KdbContext, xpp: XmlPullParser): KdbContext {
val name = xpp.name
when (ctx) {
@@ -337,7 +332,7 @@ class ImporterV4(private val streamDir: File) : Importer() {
if (encodedHash.isNotEmpty() && hashOfHeader != null) {
val hash = Base64Coder.decode(encodedHash)
if (!Arrays.equals(hash, hashOfHeader)) {
- throw InvalidDBException()
+ throw LoadDatabaseException()
}
}
} else if (name.equals(PwDatabaseV4XML.ElemSettingsChanged, ignoreCase = true)) {
@@ -355,7 +350,6 @@ class ImporterV4(private val streamDir: File) : Importer() {
} else if (name.equals(PwDatabaseV4XML.ElemDbDefaultUserChanged, ignoreCase = true)) {
mDatabase.defaultUserNameChanged = readPwTime(xpp)
} else if (name.equals(PwDatabaseV4XML.ElemDbColor, ignoreCase = true)) {
- // TODO: Add support to interpret the color if we want to allow changing the database color
mDatabase.color = readString(xpp)
} else if (name.equals(PwDatabaseV4XML.ElemDbMntncHistoryDays, ignoreCase = true)) {
mDatabase.maintenanceHistoryDays = readUInt(xpp, DEFAULT_HISTORY_DAYS)
@@ -589,7 +583,7 @@ class ImporterV4(private val streamDir: File) : Importer() {
name.equals(PwDatabaseV4XML.ElemCreationTime, ignoreCase = true) -> tl?.creationTime = readPwTime(xpp)
name.equals(PwDatabaseV4XML.ElemLastAccessTime, ignoreCase = true) -> tl?.lastAccessTime = readPwTime(xpp)
name.equals(PwDatabaseV4XML.ElemExpiryTime, ignoreCase = true) -> tl?.expiryTime = readPwTime(xpp)
- name.equals(PwDatabaseV4XML.ElemExpires, ignoreCase = true) -> tl?.isExpires = readBool(xpp, false)
+ name.equals(PwDatabaseV4XML.ElemExpires, ignoreCase = true) -> tl?.expires = readBool(xpp, false)
name.equals(PwDatabaseV4XML.ElemUsageCount, ignoreCase = true) -> tl?.usageCount = readULong(xpp, 0)
name.equals(PwDatabaseV4XML.ElemLocationChanged, ignoreCase = true) -> tl?.locationChanged = readPwTime(xpp)
else -> readUnknown(xpp)
@@ -748,7 +742,7 @@ class ImporterV4(private val streamDir: File) : Importer() {
return KdbContext.Entry
} else if (ctx == KdbContext.EntryString && name.equals(PwDatabaseV4XML.ElemString, ignoreCase = true)) {
if (ctxStringName != null && ctxStringValue != null)
- ctxEntry?.addExtraField(ctxStringName!!, ctxStringValue!!)
+ ctxEntry?.putExtraField(ctxStringName!!, ctxStringValue!!)
ctxStringName = null
ctxStringValue = null
@@ -1001,13 +995,12 @@ class ImporterV4(private val streamDir: File) : Importer() {
try {
return String(buf, Charset.forName("UTF-8"))
} catch (e: UnsupportedEncodingException) {
- throw IOException(e.localizedMessage)
+ throw IOException(e)
}
}
- //readNextNode = false;
- return xpp.nextText()
+ return xpp.safeNextText()
}
@@ -1015,7 +1008,7 @@ class ImporterV4(private val streamDir: File) : Importer() {
private fun readBase64String(xpp: XmlPullParser): ByteArray {
//readNextNode = false;
- Base64Coder.decode(xpp.nextText())?.let { buffer ->
+ Base64Coder.decode(xpp.safeNextText())?.let { buffer ->
val plainText = ByteArray(buffer.size)
randomStream?.processBytes(buffer, 0, buffer.size, plainText, 0)
return plainText
@@ -1066,3 +1059,12 @@ class ImporterV4(private val streamDir: File) : Importer() {
private const val MAX_UINT = 4294967296L // 2^32
}
}
+
+@Throws(IOException::class, XmlPullParserException::class)
+fun XmlPullParser.safeNextText(): String {
+ val result = nextText()
+ if (eventType != XmlPullParser.END_TAG) {
+ nextTag()
+ }
+ return result
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt
index 8d81f5dbc..6a5720825 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt
@@ -24,7 +24,7 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
import com.kunzisoft.keepass.database.element.PwDatabaseV4
import com.kunzisoft.keepass.database.file.PwDbHeader
import com.kunzisoft.keepass.database.file.PwDbHeaderV4
-import com.kunzisoft.keepass.database.exception.PwDbOutputException
+import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.stream.HmacBlockStream
import com.kunzisoft.keepass.stream.LEDataOutputStream
import com.kunzisoft.keepass.stream.MacOutputStream
@@ -41,7 +41,7 @@ import java.security.NoSuchAlgorithmException
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
-class PwDbHeaderOutputV4 @Throws(PwDbOutputException::class)
+class PwDbHeaderOutputV4 @Throws(DatabaseOutputException::class)
constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os: OutputStream) : PwDbHeaderOutput() {
private val los: LEDataOutputStream
private val mos: MacOutputStream
@@ -54,13 +54,13 @@ constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os:
try {
md = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
- throw PwDbOutputException("SHA-256 not implemented here.")
+ throw DatabaseOutputException("SHA-256 not implemented here.")
}
try {
db.makeFinalKey(header.masterSeed)
} catch (e: IOException) {
- throw PwDbOutputException(e)
+ throw DatabaseOutputException(e)
}
val hmac: Mac
@@ -69,9 +69,9 @@ constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os:
val signingKey = SecretKeySpec(HmacBlockStream.GetHmacKey64(db.hmacKey, Types.ULONG_MAX_VALUE), "HmacSHA256")
hmac.init(signingKey)
} catch (e: NoSuchAlgorithmException) {
- throw PwDbOutputException(e)
+ throw DatabaseOutputException(e)
} catch (e: InvalidKeyException) {
- throw PwDbOutputException(e)
+ throw DatabaseOutputException(e)
}
dos = DigestOutputStream(os, md)
@@ -86,9 +86,8 @@ constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os:
los.writeUInt(PwDbHeaderV4.DBSIG_2.toLong())
los.writeUInt(header.version)
-
writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.CipherID, Types.UUIDtoBytes(db.dataCipher))
- writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.CompressionFlags, LEDataOutputStream.writeIntBuf(db.compressionAlgorithm.id))
+ writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.CompressionFlags, LEDataOutputStream.writeIntBuf(PwDbHeaderV4.getFlagFromCompression(db.compressionAlgorithm)))
writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt
index e9a486af6..a670182ca 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt
@@ -20,7 +20,7 @@
package com.kunzisoft.keepass.database.file.save
import com.kunzisoft.keepass.database.file.PwDbHeader
-import com.kunzisoft.keepass.database.exception.PwDbOutputException
+import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import java.io.OutputStream
import java.security.NoSuchAlgorithmException
@@ -28,13 +28,13 @@ import java.security.SecureRandom
abstract class PwDbOutput protected constructor(protected var mOS: OutputStream) {
- @Throws(PwDbOutputException::class)
+ @Throws(DatabaseOutputException::class)
protected open fun setIVs(header: Header): SecureRandom {
val random: SecureRandom
try {
random = SecureRandom.getInstance("SHA1PRNG")
} catch (e: NoSuchAlgorithmException) {
- throw PwDbOutputException("Does not support secure random number generation.")
+ throw DatabaseOutputException("Does not support secure random number generation.")
}
random.nextBytes(header.encryptionIV)
@@ -43,10 +43,10 @@ abstract class PwDbOutput protected constructor(protected v
return random
}
- @Throws(PwDbOutputException::class)
+ @Throws(DatabaseOutputException::class)
abstract fun output()
- @Throws(PwDbOutputException::class)
+ @Throws(DatabaseOutputException::class)
abstract fun outputHeader(outputStream: OutputStream): Header
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV3Output.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV3Output.kt
index 8a546c298..5a2710cbe 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV3Output.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV3Output.kt
@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.database.file.save
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.database.element.*
-import com.kunzisoft.keepass.database.exception.PwDbOutputException
+import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.PwDbHeader
import com.kunzisoft.keepass.database.file.PwDbHeaderV3
import com.kunzisoft.keepass.stream.LEDataOutputStream
@@ -42,18 +42,18 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
private var headerHashBlock: ByteArray? = null
- @Throws(PwDbOutputException::class)
+ @Throws(DatabaseOutputException::class)
fun getFinalKey(header: PwDbHeader): ByteArray? {
try {
val h3 = header as PwDbHeaderV3
mDatabaseV3.makeFinalKey(h3.masterSeed, h3.transformSeed, mDatabaseV3.numberKeyEncryptionRounds)
return mDatabaseV3.finalKey
} catch (e: IOException) {
- throw PwDbOutputException("Key creation failed.", e)
+ throw DatabaseOutputException("Key creation failed.", e)
}
}
- @Throws(PwDbOutputException::class)
+ @Throws(DatabaseOutputException::class)
override fun output() {
// Before we output the header, we should sort our list of groups
// and remove any orphaned nodes that are no longer part of the tree hierarchy
@@ -74,7 +74,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
throw Exception()
}
} catch (e: Exception) {
- throw PwDbOutputException("Algorithm not supported.", e)
+ throw DatabaseOutputException("Algorithm not supported.", e)
}
try {
@@ -86,23 +86,23 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
bos.close()
} catch (e: InvalidKeyException) {
- throw PwDbOutputException("Invalid key", e)
+ throw DatabaseOutputException("Invalid key", e)
} catch (e: InvalidAlgorithmParameterException) {
- throw PwDbOutputException("Invalid algorithm parameter.", e)
+ throw DatabaseOutputException("Invalid algorithm parameter.", e)
} catch (e: IOException) {
- throw PwDbOutputException("Failed to output final encrypted part.", e)
+ throw DatabaseOutputException("Failed to output final encrypted part.", e)
}
}
- @Throws(PwDbOutputException::class)
+ @Throws(DatabaseOutputException::class)
override fun setIVs(header: PwDbHeaderV3): SecureRandom {
val random = super.setIVs(header)
random.nextBytes(header.transformSeed)
return random
}
- @Throws(PwDbOutputException::class)
+ @Throws(DatabaseOutputException::class)
override fun outputHeader(outputStream: OutputStream): PwDbHeaderV3 {
// Build header
val header = PwDbHeaderV3()
@@ -115,7 +115,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
} else if (mDatabaseV3.encryptionAlgorithm === PwEncryptionAlgorithm.Twofish) {
header.flags = header.flags or PwDbHeaderV3.FLAG_TWOFISH
} else {
- throw PwDbOutputException("Unsupported algorithm.")
+ throw DatabaseOutputException("Unsupported algorithm.")
}
header.version = PwDbHeaderV3.DBVER_DW
@@ -130,7 +130,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
try {
messageDigest = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
- throw PwDbOutputException("SHA-256 not implemented here.", e)
+ throw DatabaseOutputException("SHA-256 not implemented here.", e)
}
// Header checksum
@@ -138,7 +138,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
try {
headerDigest = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
- throw PwDbOutputException("SHA-256 not implemented here.", e)
+ throw DatabaseOutputException("SHA-256 not implemented here.", e)
}
var nos = NullOutputStream()
@@ -151,7 +151,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
pho.outputEnd()
headerDos.flush()
} catch (e: IOException) {
- throw PwDbOutputException(e)
+ throw DatabaseOutputException(e)
}
val headerHash = headerDigest.digest()
@@ -166,7 +166,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
bos.flush()
bos.close()
} catch (e: IOException) {
- throw PwDbOutputException("Failed to generate checksum.", e)
+ throw DatabaseOutputException("Failed to generate checksum.", e)
}
header.contentsHash = messageDigest!!.digest()
@@ -181,14 +181,14 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
pho.outputEnd()
dos.flush()
} catch (e: IOException) {
- throw PwDbOutputException(e)
+ throw DatabaseOutputException(e)
}
return header
}
@Suppress("CAST_NEVER_SUCCEEDS")
- @Throws(PwDbOutputException::class)
+ @Throws(DatabaseOutputException::class)
fun outputPlanGroupAndEntries(os: OutputStream) {
val los = LEDataOutputStream(os)
@@ -199,7 +199,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
los.writeInt(headerHashBlock!!.size)
los.write(headerHashBlock!!)
} catch (e: IOException) {
- throw PwDbOutputException("Failed to output header hash.", e)
+ throw DatabaseOutputException("Failed to output header hash.", e)
}
}
@@ -209,7 +209,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
try {
pgo.output()
} catch (e: IOException) {
- throw PwDbOutputException("Failed to output a tree", e)
+ throw DatabaseOutputException("Failed to output a tree", e)
}
}
mDatabaseV3.doForEachEntryInIndex { entry ->
@@ -217,7 +217,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
try {
peo.output()
} catch (e: IOException) {
- throw PwDbOutputException("Failed to output an entry.", e)
+ throw DatabaseOutputException("Failed to output an entry.", e)
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt
index 5ec421bce..20227dded 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt
@@ -29,9 +29,9 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
import com.kunzisoft.keepass.database.*
import com.kunzisoft.keepass.database.element.*
-import com.kunzisoft.keepass.database.exception.PwDbOutputException
+import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.exception.UnknownKDF
-import com.kunzisoft.keepass.database.file.PwCompressionAlgorithm
+import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm
import com.kunzisoft.keepass.database.file.PwDbHeaderV4
import com.kunzisoft.keepass.database.element.security.ProtectedBinary
import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -63,14 +63,14 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
private var headerHmac: ByteArray? = null
private var engine: CipherEngine? = null
- @Throws(PwDbOutputException::class)
+ @Throws(DatabaseOutputException::class)
override fun output() {
try {
try {
engine = CipherFactory.getInstance(mDatabaseV4.dataCipher)
} catch (e: NoSuchAlgorithmException) {
- throw PwDbOutputException("No such cipher", e)
+ throw DatabaseOutputException("No such cipher", e)
}
header = outputHeader(mOS)
@@ -91,10 +91,9 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
val osXml: OutputStream
try {
- if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.Gzip) {
- osXml = GZIPOutputStream(osPlain)
- } else {
- osXml = osPlain
+ osXml = when(mDatabaseV4.compressionAlgorithm) {
+ PwCompressionAlgorithm.GZip -> GZIPOutputStream(osPlain)
+ else -> osPlain
}
if (header!!.version >= PwDbHeaderV4.FILE_VERSION_32_4) {
@@ -105,13 +104,13 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
outputDatabase(osXml)
osXml.close()
} catch (e: IllegalArgumentException) {
- throw PwDbOutputException(e)
+ throw DatabaseOutputException(e)
} catch (e: IllegalStateException) {
- throw PwDbOutputException(e)
+ throw DatabaseOutputException(e)
}
} catch (e: IOException) {
- throw PwDbOutputException(e)
+ throw DatabaseOutputException(e)
}
}
@@ -229,7 +228,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
xml.endTag(null, PwDatabaseV4XML.ElemMeta)
}
- @Throws(PwDbOutputException::class)
+ @Throws(DatabaseOutputException::class)
private fun attachStreamEncryptor(header: PwDbHeaderV4, os: OutputStream): CipherOutputStream {
val cipher: Cipher
try {
@@ -237,13 +236,13 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
cipher = engine!!.getCipher(Cipher.ENCRYPT_MODE, mDatabaseV4.finalKey!!, header.encryptionIV)
} catch (e: Exception) {
- throw PwDbOutputException("Invalid algorithm.", e)
+ throw DatabaseOutputException("Invalid algorithm.", e)
}
return CipherOutputStream(os, cipher)
}
- @Throws(PwDbOutputException::class)
+ @Throws(DatabaseOutputException::class)
override fun setIVs(header: PwDbHeaderV4): SecureRandom {
val random = super.setIVs(header)
random.nextBytes(header.masterSeed)
@@ -259,7 +258,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
}
try {
- val kdf = KdfFactory.getEngineV4(mDatabaseV4.kdfParameters)
+ val kdf = mDatabaseV4.getEngineV4(mDatabaseV4.kdfParameters)
kdf.randomize(mDatabaseV4.kdfParameters!!)
} catch (unknownKDF: UnknownKDF) {
Log.e(TAG, "Unable to retrieve header", unknownKDF)
@@ -276,7 +275,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
if (randomStream == null) {
- throw PwDbOutputException("Invalid random cipher")
+ throw DatabaseOutputException("Invalid random cipher")
}
if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) {
@@ -286,7 +285,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
return random
}
- @Throws(PwDbOutputException::class)
+ @Throws(DatabaseOutputException::class)
override fun outputHeader(outputStream: OutputStream): PwDbHeaderV4 {
val header = PwDbHeaderV4(mDatabaseV4)
@@ -296,7 +295,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
try {
pho.output()
} catch (e: IOException) {
- throw PwDbOutputException("Failed to output the header.", e)
+ throw DatabaseOutputException("Failed to output the header.", e)
}
hashOfHeader = pho.hashOfHeader
@@ -403,7 +402,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
}
} else {
- if (mDatabaseV4.getCompressionAlgorithm() == PwCompressionAlgorithm.Gzip) {
+ if (mDatabaseV4.getCompressionAlgorithm() == PwCompressionAlgorithm.GZip) {
xml.attribute(null, PwDatabaseV4XML.AttrCompressed, PwDatabaseV4XML.ValTrue);
@@ -445,7 +444,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
xml.text(String(Base64Coder.encode(encoded)))
} else {
- if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.Gzip) {
+ if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.GZip) {
xml.attribute(null, PwDatabaseV4XML.AttrCompressed, PwDatabaseV4XML.ValTrue)
val compressData = MemoryUtil.compress(buffer)
@@ -662,7 +661,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
writeObject(PwDatabaseV4XML.ElemCreationTime, it.creationTime.date)
writeObject(PwDatabaseV4XML.ElemLastAccessTime, it.lastAccessTime.date)
writeObject(PwDatabaseV4XML.ElemExpiryTime, it.expiryTime.date)
- writeObject(PwDatabaseV4XML.ElemExpires, it.isExpires)
+ writeObject(PwDatabaseV4XML.ElemExpires, it.expires)
writeObject(PwDatabaseV4XML.ElemUsageCount, it.usageCount)
writeObject(PwDatabaseV4XML.ElemLocationChanged, it.locationChanged.date)
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/search/EntrySearchHandlerV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/search/EntrySearchHandlerV4.kt
index a62a28dd4..a14670220 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/search/EntrySearchHandlerV4.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/search/EntrySearchHandlerV4.kt
@@ -24,16 +24,14 @@ import com.kunzisoft.keepass.database.element.PwEntryV4
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorV4
import com.kunzisoft.keepass.utils.StringUtil
-import java.util.Date
import java.util.Locale
class EntrySearchHandlerV4(private val mSearchParametersV4: SearchParametersV4, private val mListStorage: MutableList) : NodeHandler() {
- private var now: Date = Date()
-
override fun operate(node: PwEntryV4): Boolean {
- if (mSearchParametersV4.excludeExpired && node.isExpires && now.after(node.expiryTime.date)) {
+ if (mSearchParametersV4.excludeExpired
+ && node.isCurrentlyExpires) {
return true
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/education/Education.kt b/app/src/main/java/com/kunzisoft/keepass/education/Education.kt
index d39783e2b..ba4f3d35b 100644
--- a/app/src/main/java/com/kunzisoft/keepass/education/Education.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/education/Education.kt
@@ -33,11 +33,10 @@ open class Education(val activity: Activity) {
return doEducation
}
-
/**
* Define if educations screens are enabled
*/
- fun isEducationScreensEnabled(): Boolean {
+ private fun isEducationScreensEnabled(): Boolean {
return isEducationScreensEnabled(activity)
}
@@ -47,7 +46,7 @@ open class Education(val activity: Activity) {
* @param context The context to retrieve the key string in XML
* @param educationKeys Keys to save as boolean 'true'
*/
- fun saveEducationPreference(context: Context, vararg educationKeys: Int) {
+ private fun saveEducationPreference(context: Context, vararg educationKeys: Int) {
val sharedPreferences = getEducationSharedPreferences(context)
val editor = sharedPreferences.edit()
for (key in educationKeys) {
@@ -66,10 +65,9 @@ open class Education(val activity: Activity) {
val educationResourcesKeys = intArrayOf(
R.string.education_create_db_key,
R.string.education_select_db_key,
- R.string.education_open_link_db_key,
R.string.education_unlock_key,
R.string.education_read_only_key,
- R.string.education_fingerprint_key,
+ R.string.education_biometric_key,
R.string.education_search_key,
R.string.education_new_node_key,
R.string.education_sort_key,
@@ -122,18 +120,6 @@ open class Education(val activity: Activity) {
context.resources.getBoolean(R.bool.education_select_db_default))
}
- /**
- * Determines whether the explanatory view of the database selection has already been displayed.
- *
- * @param context The context to open the SharedPreferences
- * @return boolean value of education_select_db_key key
- */
- fun isEducationOpenLinkDatabasePerformed(context: Context): Boolean {
- val prefs = getEducationSharedPreferences(context)
- return prefs.getBoolean(context.getString(R.string.education_open_link_db_key),
- context.resources.getBoolean(R.bool.education_open_link_db_default))
- }
-
/**
* Determines whether the explanatory view of the database unlock has already been displayed.
*
@@ -159,15 +145,15 @@ open class Education(val activity: Activity) {
}
/**
- * Determines whether the explanatory view of the fingerprint unlock has already been displayed.
+ * Determines whether the explanatory view of the biometric unlock has already been displayed.
*
* @param context The context to open the SharedPreferences
- * @return boolean value of education_fingerprint_key key
+ * @return boolean value of education_biometric_key key
*/
- fun isEducationFingerprintPerformed(context: Context): Boolean {
+ fun isEducationBiometricPerformed(context: Context): Boolean {
val prefs = getEducationSharedPreferences(context)
- return prefs.getBoolean(context.getString(R.string.education_fingerprint_key),
- context.resources.getBoolean(R.bool.education_fingerprint_default))
+ return prefs.getBoolean(context.getString(R.string.education_biometric_key),
+ context.resources.getBoolean(R.bool.education_biometric_default))
}
/**
diff --git a/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt
index 12f61384f..00d759d72 100644
--- a/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt
@@ -72,31 +72,4 @@ class FileDatabaseSelectActivityEducation(activity: Activity)
},
R.string.education_select_db_key)
}
-
-
- fun checkAndPerformedOpenLinkDatabaseEducation(educationView: View,
- onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
- onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
- return checkAndPerformedEducation(!isEducationOpenLinkDatabasePerformed(activity),
- TapTarget.forView(educationView,
- activity.getString(R.string.education_open_link_database_title),
- activity.getString(R.string.education_open_link_database_summary))
- .icon(ContextCompat.getDrawable(activity, R.drawable.ic_link_white_24dp))
- .textColorInt(Color.WHITE)
- .tintTarget(true)
- .cancelable(true),
- object : TapTargetView.Listener() {
- override fun onTargetClick(view: TapTargetView) {
- super.onTargetClick(view)
- onEducationViewClick?.invoke(view)
- }
-
- override fun onOuterCircleClick(view: TapTargetView?) {
- super.onOuterCircleClick(view)
- view?.dismiss(false)
- onOuterViewClick?.invoke(view)
- }
- },
- R.string.education_open_link_db_key)
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt
index ffd404c43..2b1fdcd6f 100644
--- a/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt
@@ -18,7 +18,6 @@ class PasswordActivityEducation(activity: Activity)
TapTarget.forView(educationView,
activity.getString(R.string.education_unlock_title),
activity.getString(R.string.education_unlock_summary))
- .dimColor(R.color.green)
.icon(ContextCompat.getDrawable(activity, R.mipmap.ic_launcher_round))
.textColorInt(Color.WHITE)
.tintTarget(false)
@@ -63,13 +62,13 @@ class PasswordActivityEducation(activity: Activity)
R.string.education_read_only_key)
}
- fun checkAndPerformedFingerprintEducation(educationView: View,
- onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
- onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
- return checkAndPerformedEducation(!isEducationFingerprintPerformed(activity),
+ fun checkAndPerformedBiometricEducation(educationView: View,
+ onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
+ onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
+ return checkAndPerformedEducation(!isEducationBiometricPerformed(activity),
TapTarget.forView(educationView,
- activity.getString(R.string.education_fingerprint_title),
- activity.getString(R.string.education_fingerprint_summary))
+ activity.getString(R.string.education_biometric_title),
+ activity.getString(R.string.education_biometric_summary))
.textColorInt(Color.WHITE)
.tintTarget(false)
.cancelable(true),
@@ -85,6 +84,6 @@ class PasswordActivityEducation(activity: Activity)
onOuterViewClick?.invoke(view)
}
},
- R.string.education_fingerprint_key)
+ R.string.education_biometric_key)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt
index efe0269ee..2828960a2 100644
--- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt
@@ -12,7 +12,7 @@ class KeyboardLauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
- GroupActivity.launchForKeyboarSelection(this, PreferencesUtil.enableReadOnlyDatabase(this))
+ GroupActivity.launchForKeyboardSelection(this, PreferencesUtil.enableReadOnlyDatabase(this))
else {
// Pass extra to get entry
FileDatabaseSelectActivity.launchForKeyboardSelection(this)
diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt
index b740c987d..83690fdf9 100644
--- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt
@@ -100,20 +100,20 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
val popupFieldsView = LayoutInflater.from(context)
.inflate(R.layout.keyboard_popup_fields, FrameLayout(context))
- popupCustomKeys?.dismiss()
-
- popupCustomKeys = PopupWindow(context)
- popupCustomKeys?.width = WindowManager.LayoutParams.WRAP_CONTENT
- popupCustomKeys?.height = WindowManager.LayoutParams.WRAP_CONTENT
- popupCustomKeys?.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
- popupCustomKeys?.inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED
- popupCustomKeys?.contentView = popupFieldsView
+ dismissCustomKeys()
+ popupCustomKeys = PopupWindow(context).apply {
+ width = WindowManager.LayoutParams.WRAP_CONTENT
+ height = WindowManager.LayoutParams.WRAP_CONTENT
+ softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+ inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED
+ contentView = popupFieldsView
+ }
val recyclerView = popupFieldsView.findViewById(R.id.keyboard_popup_fields_list)
fieldsAdapter = FieldsAdapter(this)
fieldsAdapter?.onItemClickListener = object : FieldsAdapter.OnItemClickListener {
override fun onItemClick(item: Field) {
- currentInputConnection.commitText(item.protectedValue.toString(), 1)
+ currentInputConnection.commitText(entryInfoKey?.getGeneratedFieldValue(item.name) , 1)
}
}
recyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this, androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL, true)
@@ -129,6 +129,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
}
private fun assignKeyboardView() {
+ dismissCustomKeys()
if (keyboardView != null) {
if (entryInfoKey != null) {
if (keyboardEntry != null) {
@@ -138,7 +139,6 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
} else {
if (keyboard != null) {
hideEntryInfo()
- dismissCustomKeys()
keyboardView?.keyboard = keyboard
}
}
@@ -251,6 +251,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
KEY_FIELDS -> {
if (entryInfoKey != null) {
fieldsAdapter?.fields = entryInfoKey!!.customFields
+ fieldsAdapter?.notifyDataSetChanged()
}
popupCustomKeys?.showAtLocation(keyboardView, Gravity.END or Gravity.TOP, 0, 0)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt
index 142dcdf16..26866973e 100644
--- a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt
@@ -2,8 +2,9 @@ package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.Parcelable
-
-import java.util.ArrayList
+import com.kunzisoft.keepass.otp.OtpElement
+import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
+import java.util.*
class EntryInfo : Parcelable {
@@ -14,6 +15,7 @@ class EntryInfo : Parcelable {
var url: String = ""
var notes: String = ""
var customFields: MutableList = ArrayList()
+ var otpModel: OtpModel? = null
constructor()
@@ -25,6 +27,7 @@ class EntryInfo : Parcelable {
url = parcel.readString() ?: url
notes = parcel.readString() ?: notes
parcel.readList(customFields, Field::class.java.classLoader)
+ otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
}
override fun describeContents(): Int {
@@ -39,6 +42,7 @@ class EntryInfo : Parcelable {
parcel.writeString(url)
parcel.writeString(notes)
parcel.writeArray(customFields.toTypedArray())
+ parcel.writeParcelable(otpModel, flags)
}
fun containsCustomFieldsProtected(): Boolean {
@@ -49,6 +53,19 @@ class EntryInfo : Parcelable {
return customFields.any { !it.protectedValue.isProtected }
}
+ fun isAutoGeneratedField(field: Field): Boolean {
+ return field.name == OTP_TOKEN_FIELD
+ }
+
+ fun getGeneratedFieldValue(label: String): String {
+ otpModel?.let {
+ if (label == OTP_TOKEN_FIELD) {
+ return OtpElement(it).token
+ }
+ }
+ return customFields.lastOrNull { it.name == label }?.protectedValue?.toString() ?: ""
+ }
+
companion object {
@JvmField
diff --git a/app/src/main/java/com/kunzisoft/keepass/model/Field.kt b/app/src/main/java/com/kunzisoft/keepass/model/Field.kt
index d689684b5..9695db421 100644
--- a/app/src/main/java/com/kunzisoft/keepass/model/Field.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/model/Field.kt
@@ -9,7 +9,7 @@ class Field : Parcelable {
var name: String = ""
var protectedValue: ProtectedString = ProtectedString()
- constructor(name: String, value: ProtectedString) {
+ constructor(name: String, value: ProtectedString = ProtectedString()) {
this.name = name
this.protectedValue = value
}
@@ -28,6 +28,21 @@ class Field : Parcelable {
dest.writeParcelable(protectedValue, flags)
}
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Field
+
+ if (name != other.name) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return name.hashCode()
+ }
+
companion object {
@JvmField
diff --git a/app/src/main/java/com/kunzisoft/keepass/model/OtpModel.kt b/app/src/main/java/com/kunzisoft/keepass/model/OtpModel.kt
new file mode 100644
index 000000000..b5cf65274
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/model/OtpModel.kt
@@ -0,0 +1,95 @@
+package com.kunzisoft.keepass.model
+
+import android.os.Parcel
+import android.os.Parcelable
+import com.kunzisoft.keepass.otp.OtpElement
+import com.kunzisoft.keepass.otp.OtpTokenType
+import com.kunzisoft.keepass.otp.OtpType
+import com.kunzisoft.keepass.otp.TokenCalculator
+import com.kunzisoft.keepass.otp.TokenCalculator.OTP_DEFAULT_ALGORITHM
+
+class OtpModel() : Parcelable {
+
+ var type: OtpType = OtpType.TOTP // ie : HOTP or TOTP
+ var tokenType: OtpTokenType = OtpTokenType.RFC6238
+ var name: String = "OTP" // ie : user@email.com
+ var issuer: String = "None" // ie : Gitlab
+ var secret: ByteArray? = null // Seed
+ var counter: Long = TokenCalculator.HOTP_INITIAL_COUNTER // ie : 5 - only for HOTP
+ var period: Int = TokenCalculator.TOTP_DEFAULT_PERIOD // ie : 30 seconds - only for TOTP
+ var digits: Int = TokenCalculator.OTP_DEFAULT_DIGITS
+ var algorithm: TokenCalculator.HashAlgorithm = OTP_DEFAULT_ALGORITHM
+
+ constructor(parcel: Parcel) : this() {
+ val typeRead = parcel.readInt()
+ type = OtpType.values()[typeRead]
+ tokenType = OtpTokenType.values()[parcel.readInt()]
+ name = parcel.readString() ?: name
+ issuer = parcel.readString() ?: issuer
+ secret = parcel.createByteArray() ?: secret
+ counter = parcel.readLong()
+ period = parcel.readInt()
+ digits = parcel.readInt()
+ algorithm = TokenCalculator.HashAlgorithm.values()[parcel.readInt()]
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as OtpElement
+
+ if (type != other.type) return false
+ // Token type is important only if it's a TOTP
+ if (type == OtpType.TOTP && tokenType != other.tokenType) return false
+ if (secret == null || other.secret == null) return false
+ if (!secret!!.contentEquals(other.secret!!)) return false
+ // Counter only for HOTP
+ if (type == OtpType.HOTP && counter != other.counter) return false
+ // Step only for TOTP
+ if (type == OtpType.TOTP && period != other.period) return false
+ if (digits != other.digits) return false
+ if (algorithm != other.algorithm) return false
+
+ return true
+ }
+
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ override fun hashCode(): Int {
+ var result = type.hashCode()
+ result = 31 * result + tokenType.hashCode()
+ result = 31 * result + name.hashCode()
+ result = 31 * result + issuer.hashCode()
+ result = 31 * result + (secret?.contentHashCode() ?: 0)
+ result = 31 * result + counter.hashCode()
+ result = 31 * result + period
+ result = 31 * result + digits
+ result = 31 * result + algorithm.hashCode()
+ return result
+ }
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeInt(type.ordinal)
+ parcel.writeInt(tokenType.ordinal)
+ parcel.writeString(name)
+ parcel.writeString(issuer)
+ parcel.writeByteArray(secret)
+ parcel.writeLong(counter)
+ parcel.writeInt(period)
+ parcel.writeInt(digits)
+ parcel.writeInt(algorithm.ordinal)
+ }
+
+ companion object CREATOR : Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel): OtpModel {
+ return OtpModel(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt
index 5f16bc089..064c8b4a0 100644
--- a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt
@@ -19,24 +19,19 @@
*/
package com.kunzisoft.keepass.notifications
-import android.content.res.Resources
import android.os.Parcel
import android.os.Parcelable
import android.util.Log
-
-import com.kunzisoft.keepass.R
-
-import java.util.ArrayList
+import com.kunzisoft.keepass.model.EntryInfo
+import java.util.*
/**
* Utility class to manage fields in Notifications
*/
-open class ClipboardEntryNotificationField : Parcelable {
+class ClipboardEntryNotificationField : Parcelable {
private var id: NotificationFieldId = NotificationFieldId.UNKNOWN
- var value: String = ""
var label: String = ""
- var copyText: String = ""
val actionKey: String
get() = getActionKey(id)
@@ -44,32 +39,30 @@ open class ClipboardEntryNotificationField : Parcelable {
val extraKey: String
get() = getExtraKey(id)
- constructor(id: NotificationFieldId, value: String, resources: Resources) {
- this.id = id
- this.value = value
- this.label = getLabel(resources)
- this.copyText = getCopyText(resources)
- }
-
- constructor(id: NotificationFieldId, value: String, label: String, resources: Resources) {
+ constructor(id: NotificationFieldId, label: String) {
this.id = id
- this.value = value
this.label = label
- this.copyText = getCopyText(resources)
}
- protected constructor(parcel: Parcel) {
+ constructor(parcel: Parcel) {
id = NotificationFieldId.values()[parcel.readInt()]
- value = parcel.readString() ?: value
label = parcel.readString() ?: label
- copyText = parcel.readString() ?: copyText
+ }
+
+ fun getGeneratedValue(entryInfo: EntryInfo?): String {
+ return when (id) {
+ NotificationFieldId.UNKNOWN -> ""
+ NotificationFieldId.USERNAME -> entryInfo?.username ?: ""
+ NotificationFieldId.PASSWORD -> entryInfo?.password ?: ""
+ NotificationFieldId.FIELD_A,
+ NotificationFieldId.FIELD_B,
+ NotificationFieldId.FIELD_C -> entryInfo?.getGeneratedFieldValue(label) ?: ""
+ }
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeInt(id.ordinal)
- dest.writeString(value)
dest.writeString(label)
- dest.writeString(copyText)
}
override fun describeContents(): Int {
@@ -91,24 +84,11 @@ open class ClipboardEntryNotificationField : Parcelable {
UNKNOWN, USERNAME, PASSWORD, FIELD_A, FIELD_B, FIELD_C;
companion object {
-
val anonymousFieldId: Array
get() = arrayOf(FIELD_A, FIELD_B, FIELD_C)
}
}
- private fun getLabel(resources: Resources): String {
- return when (id) {
- NotificationFieldId.USERNAME -> resources.getString(R.string.entry_user_name)
- NotificationFieldId.PASSWORD -> resources.getString(R.string.entry_password)
- else -> id.name
- }
- }
-
- private fun getCopyText(resources: Resources): String {
- return resources.getString(R.string.select_to_copy, label)
- }
-
companion object {
private val TAG = ClipboardEntryNotificationField::class.java.name
diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt
index 2831176b8..620819f9f 100644
--- a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt
@@ -29,17 +29,17 @@ import com.kunzisoft.keepass.database.exception.SamsungClipboardException
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
+import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER
import com.kunzisoft.keepass.utils.LOCK_ACTION
import java.util.*
-class ClipboardEntryNotificationService : NotificationService() {
-
- private var notificationId = 485
- private var cleanNotificationTimerTask: Thread? = null
- private var notificationTimeoutMilliSecs: Long = 0
+class ClipboardEntryNotificationService : LockNotificationService() {
+ override val notificationId = 485
+ private var mEntryInfo: EntryInfo? = null
private var clipboardHelper: ClipboardHelper? = null
+ private var notificationTimeoutMilliSecs: Long = 0
private var cleanCopyNotificationTimerTask: Thread? = null
override fun onCreate() {
@@ -53,28 +53,27 @@ class ClipboardEntryNotificationService : NotificationService() {
if (PreferencesUtil.isClearClipboardNotificationEnable(this)) {
sendBroadcast(Intent(LOCK_ACTION))
}
+ // Stop the service
+ stopSelf()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ // Get entry info from intent
+ mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO)
+
//Get settings
- val prefs = PreferenceManager.getDefaultSharedPreferences(this)
- val timeoutClipboardClear = prefs.getString(getString(R.string.clipboard_timeout_key),
- getString(R.string.clipboard_timeout_default)) ?: "6000"
- notificationTimeoutMilliSecs = java.lang.Long.parseLong(timeoutClipboardClear)
+ notificationTimeoutMilliSecs = PreferenceManager.getDefaultSharedPreferences(this)
+ .getString(getString(R.string.clipboard_timeout_key),
+ getString(R.string.clipboard_timeout_default))?.toLong() ?: TimeoutHelper.DEFAULT_TIMEOUT
when {
intent == null -> Log.w(TAG, "null intent")
ACTION_NEW_NOTIFICATION == intent.action -> {
- val title = intent.getStringExtra(EXTRA_ENTRY_TITLE)
- newNotification(title, constructListOfField(intent))
+ newNotification(mEntryInfo?.title, constructListOfField(intent))
}
ACTION_CLEAN_CLIPBOARD == intent.action -> {
stopTask(cleanCopyNotificationTimerTask)
- try {
- clipboardHelper?.cleanClipboard()
- } catch (e: SamsungClipboardException) {
- Log.e(TAG, "Clipboard can't be cleaned", e)
- }
+ cleanClipboard()
stopNotificationAndSendLockIfNeeded()
}
else -> for (actionKey in ClipboardEntryNotificationField.allActionKeys) {
@@ -94,18 +93,19 @@ class ClipboardEntryNotificationService : NotificationService() {
private fun constructListOfField(intent: Intent?): ArrayList {
var fieldList = ArrayList()
if (intent != null && intent.extras != null) {
- if (intent.extras!!.containsKey(EXTRA_FIELDS))
- fieldList = intent.getParcelableArrayListExtra(EXTRA_FIELDS)
+ if (intent.extras!!.containsKey(EXTRA_CLIPBOARD_FIELDS))
+ fieldList = intent.getParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS)
}
return fieldList
}
private fun getCopyPendingIntent(fieldToCopy: ClipboardEntryNotificationField, fieldsToAdd: ArrayList): PendingIntent {
- val copyIntent = Intent(this, ClipboardEntryNotificationService::class.java)
- copyIntent.action = fieldToCopy.actionKey
- copyIntent.putExtra(fieldToCopy.extraKey, fieldToCopy)
- copyIntent.putParcelableArrayListExtra(EXTRA_FIELDS, fieldsToAdd)
-
+ val copyIntent = Intent(this, ClipboardEntryNotificationService::class.java).apply {
+ action = fieldToCopy.actionKey
+ putExtra(EXTRA_ENTRY_INFO, mEntryInfo)
+ putExtra(fieldToCopy.extraKey, fieldToCopy)
+ putParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS, fieldsToAdd)
+ }
return PendingIntent.getService(
this, 0, copyIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}
@@ -114,14 +114,14 @@ class ClipboardEntryNotificationService : NotificationService() {
stopTask(cleanCopyNotificationTimerTask)
val builder = buildNewNotification()
- .setSmallIcon(R.drawable.ic_clipboard_key_white_24dp)
+ .setSmallIcon(R.drawable.notification_ic_clipboard_key_24dp)
if (title != null)
builder.setContentTitle(title)
if (fieldsToAdd.size > 0) {
val field = fieldsToAdd[0]
- builder.setContentText(field.copyText)
+ builder.setContentText(getString(R.string.select_to_copy, field.label))
builder.setContentIntent(getCopyPendingIntent(field, fieldsToAdd))
// Add extra actions without 1st field
@@ -129,45 +129,28 @@ class ClipboardEntryNotificationService : NotificationService() {
fieldsWithoutFirstField.remove(field)
// Add extra actions
for (fieldToAdd in fieldsWithoutFirstField) {
- builder.addAction(R.drawable.ic_clipboard_key_white_24dp, fieldToAdd.label,
+ builder.addAction(R.drawable.notification_ic_clipboard_key_24dp, fieldToAdd.label,
getCopyPendingIntent(fieldToAdd, fieldsToAdd))
}
}
-
- notificationManager?.cancel(notificationId)
- notificationManager?.notify(++notificationId, builder.build())
-
- val myNotificationId = notificationId
- stopTask(cleanNotificationTimerTask)
- // If timer
- if (notificationTimeoutMilliSecs != NEVER) {
- cleanNotificationTimerTask = Thread {
- try {
- Thread.sleep(notificationTimeoutMilliSecs)
- } catch (e: InterruptedException) {
- cleanNotificationTimerTask = null
- }
- notificationManager?.cancel(myNotificationId)
- }
- cleanNotificationTimerTask?.start()
- }
+ notificationManager?.notify(notificationId, builder.build())
}
private fun copyField(fieldToCopy: ClipboardEntryNotificationField, nextFields: ArrayList) {
stopTask(cleanCopyNotificationTimerTask)
- stopTask(cleanNotificationTimerTask)
try {
- clipboardHelper?.copyToClipboard(fieldToCopy.label, fieldToCopy.value)
+ var generatedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
+ clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue)
val builder = buildNewNotification()
- .setSmallIcon(R.drawable.ic_clipboard_key_white_24dp)
+ .setSmallIcon(R.drawable.notification_ic_clipboard_key_24dp)
.setContentTitle(fieldToCopy.label)
// New action with next field if click
if (nextFields.size > 0) {
val nextField = nextFields[0]
- builder.setContentText(nextField.copyText)
+ builder.setContentText(getString(R.string.select_to_copy, nextField.label))
builder.setContentIntent(getCopyPendingIntent(nextField, nextFields))
// Else tell to swipe for a clean
} else {
@@ -187,6 +170,12 @@ class ClipboardEntryNotificationService : NotificationService() {
val maxPos = 100
val posDurationMills = notificationTimeoutMilliSecs / maxPos
for (pos in maxPos downTo 0) {
+ val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
+ // New auto generated value
+ if (generatedValue != newGeneratedValue) {
+ generatedValue = newGeneratedValue
+ clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue)
+ }
builder.setProgress(maxPos, pos, false)
notificationManager?.notify(myNotificationId, builder.build())
try {
@@ -202,11 +191,7 @@ class ClipboardEntryNotificationService : NotificationService() {
notificationManager?.cancel(myNotificationId)
// Clean password only if no next field
if (nextFields.size <= 0)
- try {
- clipboardHelper?.cleanClipboard()
- } catch (e: SamsungClipboardException) {
- Log.e(TAG, "Clipboard can't be cleaned", e)
- }
+ cleanClipboard()
}
cleanCopyNotificationTimerTask?.start()
} else {
@@ -220,9 +205,27 @@ class ClipboardEntryNotificationService : NotificationService() {
}
- private fun stopTask(task: Thread?) {
- if (task != null && task.isAlive)
- task.interrupt()
+ private fun cleanClipboard() {
+ try {
+ clipboardHelper?.cleanClipboard()
+ } catch (e: SamsungClipboardException) {
+ Log.e(TAG, "Clipboard can't be cleaned", e)
+ }
+ }
+
+ override fun onTaskRemoved(rootIntent: Intent?) {
+ cleanClipboard()
+
+ super.onTaskRemoved(rootIntent)
+ }
+
+ override fun onDestroy() {
+ cleanClipboard()
+
+ stopTask(cleanCopyNotificationTimerTask)
+ cleanCopyNotificationTimerTask = null
+
+ super.onDestroy()
}
companion object {
@@ -230,8 +233,8 @@ class ClipboardEntryNotificationService : NotificationService() {
private val TAG = ClipboardEntryNotificationService::class.java.name
const val ACTION_NEW_NOTIFICATION = "ACTION_NEW_NOTIFICATION"
- const val EXTRA_ENTRY_TITLE = "EXTRA_ENTRY_TITLE"
- const val EXTRA_FIELDS = "EXTRA_FIELDS"
+ const val EXTRA_ENTRY_INFO = "EXTRA_ENTRY_INFO"
+ const val EXTRA_CLIPBOARD_FIELDS = "EXTRA_CLIPBOARD_FIELDS"
const val ACTION_CLEAN_CLIPBOARD = "ACTION_CLEAN_CLIPBOARD"
fun launchNotificationIfAllowed(context: Context, entry: EntryInfo) {
@@ -245,15 +248,17 @@ class ClipboardEntryNotificationService : NotificationService() {
(entry.containsCustomFieldsProtected() && PreferencesUtil.allowCopyPasswordAndProtectedFields(context))
)
+ var startService = false
+ val intent = Intent(context, ClipboardEntryNotificationService::class.java)
+
// If notifications enabled in settings
// Don't if application timeout
if (PreferencesUtil.isClipboardNotificationsEnable(context)) {
if (containsUsernameToCopy || containsPasswordToCopy || containsExtraFieldToCopy) {
// username already copied, waiting for user's action before copy password.
- val intent = Intent(context, ClipboardEntryNotificationService::class.java)
intent.action = ACTION_NEW_NOTIFICATION
- intent.putExtra(EXTRA_ENTRY_TITLE, entry.title)
+ intent.putExtra(EXTRA_ENTRY_INFO, entry)
// Construct notification fields
val notificationFields = ArrayList()
// Add username if exists to notifications
@@ -261,15 +266,13 @@ class ClipboardEntryNotificationService : NotificationService() {
notificationFields.add(
ClipboardEntryNotificationField(
ClipboardEntryNotificationField.NotificationFieldId.USERNAME,
- entry.username,
- context.resources))
+ context.getString(R.string.entry_user_name)))
// Add password to notifications
if (containsPasswordToCopy) {
notificationFields.add(
ClipboardEntryNotificationField(
ClipboardEntryNotificationField.NotificationFieldId.PASSWORD,
- entry.password,
- context.resources))
+ context.getString(R.string.entry_password)))
}
// Add extra fields
if (containsExtraFieldToCopy) {
@@ -282,9 +285,7 @@ class ClipboardEntryNotificationService : NotificationService() {
notificationFields.add(
ClipboardEntryNotificationField(
ClipboardEntryNotificationField.NotificationFieldId.anonymousFieldId[anonymousFieldNumber],
- field.protectedValue.toString(),
- field.name,
- context.resources))
+ field.name))
anonymousFieldNumber++
}
}
@@ -295,10 +296,14 @@ class ClipboardEntryNotificationService : NotificationService() {
}
// Add notifications
- intent.putParcelableArrayListExtra(EXTRA_FIELDS, notificationFields)
+ startService = true
+ intent.putParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS, notificationFields)
context.startService(intent)
}
}
+
+ if (!startService)
+ context.stopService(intent)
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseOpenNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseOpenNotificationService.kt
new file mode 100644
index 000000000..13fd168d4
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseOpenNotificationService.kt
@@ -0,0 +1,77 @@
+package com.kunzisoft.keepass.notifications
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.activities.GroupActivity
+import com.kunzisoft.keepass.database.element.Database
+import com.kunzisoft.keepass.settings.PreferencesUtil
+import com.kunzisoft.keepass.utils.LOCK_ACTION
+
+class DatabaseOpenNotificationService: LockNotificationService() {
+
+ override val notificationId: Int = 340
+
+ private fun stopNotificationAndSendLock() {
+ // Send lock action
+ sendBroadcast(Intent(LOCK_ACTION))
+ // Stop the service
+ stopSelf()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+
+ when(intent?.action) {
+ ACTION_CLOSE_DATABASE -> {
+ stopNotificationAndSendLock()
+ }
+ else -> {
+ val databaseIntent = Intent(this, GroupActivity::class.java)
+ var pendingDatabaseFlag = 0
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ pendingDatabaseFlag = PendingIntent.FLAG_IMMUTABLE
+ }
+ val pendingDatabaseIntent = PendingIntent.getActivity(this, 0, databaseIntent, pendingDatabaseFlag)
+ val deleteIntent = Intent(this, DatabaseOpenNotificationService::class.java).apply {
+ action = ACTION_CLOSE_DATABASE
+ }
+ val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
+
+ val database = Database.getInstance()
+ if (database.loaded) {
+ notificationManager?.notify(notificationId, buildNewNotification().apply {
+ setSmallIcon(R.drawable.notification_ic_database_open)
+ setContentTitle(getString(R.string.database_opened))
+ setContentText(database.name + " (" + database.version + ")")
+ setAutoCancel(false)
+ setContentIntent(pendingDatabaseIntent)
+ setDeleteIntent(pendingDeleteIntent)
+ }.build())
+ } else {
+ stopSelf()
+ }
+ }
+ }
+
+ return START_STICKY
+ }
+
+ companion object {
+ const val ACTION_CLOSE_DATABASE = "ACTION_CLOSE_DATABASE"
+
+ fun startIfAllowed(context: Context) {
+ if (PreferencesUtil.isPersistentNotificationEnable(context)) {
+ // Start the opening notification
+ context.startService(Intent(context, DatabaseOpenNotificationService::class.java))
+ }
+ }
+
+ fun stop(context: Context) {
+ // Stop the opening notification
+ context.stopService(Intent(context, DatabaseOpenNotificationService::class.java))
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt
index bf2e37132..8ca0dafb1 100644
--- a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt
@@ -1,44 +1,531 @@
package com.kunzisoft.keepass.notifications
import android.content.Intent
-import android.util.Log
+import android.net.Uri
+import android.os.AsyncTask
+import android.os.Binder
+import android.os.Bundle
+import android.os.IBinder
import com.kunzisoft.keepass.R
+import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
+import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable
+import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable
+import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable
+import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
+import com.kunzisoft.keepass.database.action.node.*
+import com.kunzisoft.keepass.database.element.*
+import com.kunzisoft.keepass.settings.PreferencesUtil
+import com.kunzisoft.keepass.tasks.ActionRunnable
+import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
+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
-class DatabaseTaskNotificationService : NotificationService() {
+class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdater {
- private val notificationId = 532
+ override val notificationId: Int = 575
+
+ private var actionRunnableAsyncTask: ActionRunnableAsyncTask? = null
+
+ private var mActionTaskBinder = ActionTaskBinder()
+ private var mActionTaskListeners = LinkedList()
+
+ private var mTitleId: Int? = null
+ private var mMessageId: Int? = null
+ private var mWarningId: Int? = null
+
+ inner class ActionTaskBinder: Binder() {
+
+ fun getService(): DatabaseTaskNotificationService = this@DatabaseTaskNotificationService
+
+ fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
+ mActionTaskListeners.add(actionTaskListener)
+ }
+
+ fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
+ mActionTaskListeners.remove(actionTaskListener)
+ }
+ }
+
+ interface ActionTaskListener {
+ fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?)
+ fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?)
+ fun onStopAction(actionTask: String, result: ActionRunnable.Result)
+ }
+
+ fun checkAction() {
+ mActionTaskListeners.forEach { actionTaskListener ->
+ actionTaskListener.onUpdateAction(mTitleId, mMessageId, mWarningId)
+ }
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ return mActionTaskBinder
+ }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- if (intent == null) {
- Log.w(TAG, "null intent")
- } else {
- newNotification(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, R.string.saving_database))
+
+ if (intent == null) return START_REDELIVER_INTENT
+
+ val intentAction = intent.action
+
+ val titleId: Int = when (intentAction) {
+ ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
+ ACTION_DATABASE_LOAD_TASK -> R.string.loading_database
+ else -> R.string.saving_database
+ }
+ val messageId: Int? = when (intentAction) {
+ ACTION_DATABASE_LOAD_TASK -> null
+ else -> null
}
- return START_NOT_STICKY
+ val warningId: Int? =
+ if (intentAction == ACTION_DATABASE_LOAD_TASK)
+ null
+ else
+ R.string.do_not_kill_app
+
+ val actionRunnable: ActionRunnable? = when (intentAction) {
+ ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
+ ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent)
+ ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent)
+ ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent)
+ ACTION_DATABASE_UPDATE_GROUP_TASK -> buildDatabaseUpdateGroupActionTask(intent)
+ ACTION_DATABASE_CREATE_ENTRY_TASK -> buildDatabaseCreateEntryActionTask(intent)
+ ACTION_DATABASE_UPDATE_ENTRY_TASK -> buildDatabaseUpdateEntryActionTask(intent)
+ ACTION_DATABASE_COPY_NODES_TASK -> buildDatabaseCopyNodesActionTask(intent)
+ ACTION_DATABASE_MOVE_NODES_TASK -> buildDatabaseMoveNodesActionTask(intent)
+ ACTION_DATABASE_DELETE_NODES_TASK -> buildDatabaseDeleteNodesActionTask(intent)
+ ACTION_DATABASE_SAVE_NAME_TASK,
+ ACTION_DATABASE_SAVE_DESCRIPTION_TASK,
+ ACTION_DATABASE_SAVE_DEFAULT_USERNAME_TASK,
+ ACTION_DATABASE_SAVE_COLOR_TASK,
+ ACTION_DATABASE_SAVE_COMPRESSION_TASK,
+ ACTION_DATABASE_SAVE_MAX_HISTORY_ITEMS_TASK,
+ ACTION_DATABASE_SAVE_MAX_HISTORY_SIZE_TASK,
+ ACTION_DATABASE_SAVE_ENCRYPTION_TASK,
+ ACTION_DATABASE_SAVE_KEY_DERIVATION_TASK,
+ ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK,
+ ACTION_DATABASE_SAVE_PARALLELISM_TASK,
+ ACTION_DATABASE_SAVE_ITERATIONS_TASK -> buildDatabaseSaveElementActionTask(intent)
+ else -> null
+ }
+
+ actionRunnable?.let { actionRunnableNotNull ->
+ // Assign elements for updates
+ mTitleId = titleId
+ mMessageId = messageId
+ mWarningId = warningId
+
+ // Create the notification
+ newNotification(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, titleId))
+
+ // Build and launch the action
+ actionRunnableAsyncTask = ActionRunnableAsyncTask(this,
+ {
+ sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply {
+ putExtra(DATABASE_TASK_TITLE_KEY, titleId)
+ putExtra(DATABASE_TASK_MESSAGE_KEY, messageId)
+ putExtra(DATABASE_TASK_WARNING_KEY, warningId)
+ })
+
+ mActionTaskListeners.forEach { actionTaskListener ->
+ actionTaskListener.onStartAction(titleId, messageId, warningId)
+ }
+
+ }, { result ->
+ mActionTaskListeners.forEach { actionTaskListener ->
+ actionTaskListener.onStopAction(intentAction!!, result)
+ }
+
+ sendBroadcast(Intent(DATABASE_STOP_TASK_ACTION))
+
+ stopSelf()
+ }
+ )
+ actionRunnableAsyncTask?.execute({ actionRunnableNotNull })
+ }
+
+ return START_REDELIVER_INTENT
}
private fun newNotification(title: Int) {
val builder = buildNewNotification()
- .setSmallIcon(R.drawable.ic_data_usage_white_24dp)
+ .setSmallIcon(R.drawable.notification_ic_database_load)
.setContentTitle(getString(title))
.setAutoCancel(false)
.setContentIntent(null)
startForeground(notificationId, builder.build())
}
- override fun onDestroy() {
+ override fun updateMessage(resId: Int) {
+ mMessageId = resId
+ mActionTaskListeners.forEach { actionTaskListener ->
+ actionTaskListener.onUpdateAction(mTitleId, mMessageId, mWarningId)
+ }
+ }
+
+ private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? {
+
+ if (intent.hasExtra(DATABASE_URI_KEY)
+ && intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
+ && intent.hasExtra(MASTER_PASSWORD_KEY)
+ && intent.hasExtra(KEY_FILE_CHECKED_KEY)
+ && intent.hasExtra(KEY_FILE_KEY)
+ ) {
+ val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY)
+ val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
+ return CreateDatabaseRunnable(this,
+ Database.getInstance(),
+ databaseUri,
+ intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
+ intent.getStringExtra(MASTER_PASSWORD_KEY),
+ intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
+ keyFileUri,
+ true // TODO get readonly
+ )
+ } else {
+ return null
+ }
+ }
+
+ private fun buildDatabaseLoadActionTask(intent: Intent): ActionRunnable? {
+
+ if (intent.hasExtra(DATABASE_URI_KEY)
+ && intent.hasExtra(MASTER_PASSWORD_KEY)
+ && intent.hasExtra(KEY_FILE_KEY)
+ && intent.hasExtra(READ_ONLY_KEY)
+ && intent.hasExtra(CIPHER_ENTITY_KEY)
+ && intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
+ ) {
+ val database = Database.getInstance()
+ val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY)
+ val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY)
+ val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY)
+ val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
+ val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
+
+ return LoadDatabaseRunnable(
+ this,
+ database,
+ databaseUri,
+ masterPassword,
+ keyFileUri,
+ readOnly,
+ cipherEntity,
+ PreferencesUtil.omitBackup(this),
+ intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
+ this
+ ) { result ->
+ // Add each info to reload database after thrown duplicate UUID exception
+ result.data = Bundle().apply {
+ putParcelable(DATABASE_URI_KEY, databaseUri)
+ putString(MASTER_PASSWORD_KEY, masterPassword)
+ putParcelable(KEY_FILE_KEY, keyFileUri)
+ putBoolean(READ_ONLY_KEY, readOnly)
+ putParcelable(CIPHER_ENTITY_KEY, cipherEntity)
+ }
+ }
+ } else {
+ return null
+ }
+ }
+
+ private fun buildDatabaseAssignPasswordActionTask(intent: Intent): ActionRunnable? {
+ return if (intent.hasExtra(DATABASE_URI_KEY)
+ && intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
+ && intent.hasExtra(MASTER_PASSWORD_KEY)
+ && intent.hasExtra(KEY_FILE_CHECKED_KEY)
+ && intent.hasExtra(KEY_FILE_KEY)
+ ) {
+ AssignPasswordInDatabaseRunnable(this,
+ Database.getInstance(),
+ intent.getParcelableExtra(DATABASE_URI_KEY),
+ intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
+ intent.getStringExtra(MASTER_PASSWORD_KEY),
+ intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
+ intent.getParcelableExtra(KEY_FILE_KEY),
+ true)
+ } else {
+ null
+ }
+ }
+
+ private inner class AfterActionNodesRunnable : AfterActionNodesFinish() {
+ override fun onActionNodesFinish(result: ActionRunnable.Result,
+ actionNodesValues: ActionNodesValues) {
+ val bundle = result.data ?: Bundle()
+ bundle.putBundle(OLD_NODES_KEY, getBundleFromListNodes(actionNodesValues.oldNodes))
+ bundle.putBundle(NEW_NODES_KEY, getBundleFromListNodes(actionNodesValues.newNodes))
+ result.data = bundle
+ }
+ }
- notificationManager?.cancel(notificationId)
+ private fun buildDatabaseCreateGroupActionTask(intent: Intent): ActionRunnable? {
+ return if (intent.hasExtra(GROUP_KEY)
+ && intent.hasExtra(PARENT_ID_KEY)
+ && intent.hasExtra(SAVE_DATABASE_KEY)
+ ) {
+ val database = Database.getInstance()
+ database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { parent ->
+ AddGroupRunnable(this,
+ database,
+ intent.getParcelableExtra(GROUP_KEY),
+ parent,
+ intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
+ AfterActionNodesRunnable())
+ }
+ } else {
+ null
+ }
+ }
- super.onDestroy()
+ private fun buildDatabaseUpdateGroupActionTask(intent: Intent): ActionRunnable? {
+ return if (intent.hasExtra(GROUP_ID_KEY)
+ && intent.hasExtra(GROUP_KEY)
+ && intent.hasExtra(SAVE_DATABASE_KEY)
+ ) {
+ val database = Database.getInstance()
+ database.getGroupById(intent.getParcelableExtra(GROUP_ID_KEY))?.let { oldGroup ->
+ val newGroup: GroupVersioned = intent.getParcelableExtra(GROUP_KEY)
+ UpdateGroupRunnable(this,
+ database,
+ oldGroup,
+ newGroup,
+ intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
+ AfterActionNodesRunnable())
+ }
+ } else {
+ null
+ }
+ }
+
+ private fun buildDatabaseCreateEntryActionTask(intent: Intent): ActionRunnable? {
+ return if (intent.hasExtra(ENTRY_KEY)
+ && intent.hasExtra(PARENT_ID_KEY)
+ && intent.hasExtra(SAVE_DATABASE_KEY)
+ ) {
+ val database = Database.getInstance()
+ database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { parent ->
+ AddEntryRunnable(this,
+ database,
+ intent.getParcelableExtra(ENTRY_KEY),
+ parent,
+ intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
+ AfterActionNodesRunnable())
+ }
+ } else {
+ null
+ }
+ }
+
+ private fun buildDatabaseUpdateEntryActionTask(intent: Intent): ActionRunnable? {
+ return if (intent.hasExtra(ENTRY_ID_KEY)
+ && intent.hasExtra(ENTRY_KEY)
+ && intent.hasExtra(SAVE_DATABASE_KEY)
+ ) {
+ val database = Database.getInstance()
+ database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { oldEntry ->
+ val newEntry: EntryVersioned = intent.getParcelableExtra(ENTRY_KEY)
+ UpdateEntryRunnable(this,
+ database,
+ oldEntry,
+ newEntry,
+ intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
+ AfterActionNodesRunnable())
+ }
+ } else {
+ null
+ }
+ }
+
+ private fun buildDatabaseCopyNodesActionTask(intent: Intent): ActionRunnable? {
+ return if (intent.hasExtra(GROUPS_ID_KEY)
+ && intent.hasExtra(ENTRIES_ID_KEY)
+ && intent.hasExtra(PARENT_ID_KEY)
+ && intent.hasExtra(SAVE_DATABASE_KEY)
+ ) {
+ val database = Database.getInstance()
+ database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { newParent ->
+ CopyNodesRunnable(this,
+ database,
+ getListNodesFromBundle(database, intent.extras!!),
+ newParent,
+ intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
+ AfterActionNodesRunnable())
+ }
+
+ } else {
+ null
+ }
+ }
+
+ private fun buildDatabaseMoveNodesActionTask(intent: Intent): ActionRunnable? {
+ return if (intent.hasExtra(GROUPS_ID_KEY)
+ && intent.hasExtra(ENTRIES_ID_KEY)
+ && intent.hasExtra(PARENT_ID_KEY)
+ && intent.hasExtra(SAVE_DATABASE_KEY)
+ ) {
+ val database = Database.getInstance()
+ database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { newParent ->
+ MoveNodesRunnable(this,
+ database,
+ getListNodesFromBundle(database, intent.extras!!),
+ newParent,
+ intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
+ AfterActionNodesRunnable())
+ }
+
+ } else {
+ null
+ }
+ }
+
+ private fun buildDatabaseDeleteNodesActionTask(intent: Intent): ActionRunnable? {
+ return if (intent.hasExtra(GROUPS_ID_KEY)
+ && intent.hasExtra(ENTRIES_ID_KEY)
+ && intent.hasExtra(SAVE_DATABASE_KEY)
+ ) {
+ val database = Database.getInstance()
+ DeleteNodesRunnable(this,
+ database,
+ getListNodesFromBundle(database, intent.extras!!),
+ intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
+ AfterActionNodesRunnable())
+
+ } else {
+ null
+ }
+ }
+
+ private fun buildDatabaseSaveElementActionTask(intent: Intent): ActionRunnable? {
+ return SaveDatabaseRunnable(this,
+ Database.getInstance(),
+ true
+ ).apply {
+ mAfterSaveDatabase = { result ->
+ result.data = intent.extras
+ }
+ }
+ }
+
+ private class ActionRunnableAsyncTask(private val progressTaskUpdater: ProgressTaskUpdater,
+ private val onPreExecute: () -> Unit,
+ private val onPostExecute: (result: ActionRunnable.Result) -> Unit)
+ : AsyncTask<((ProgressTaskUpdater?) -> ActionRunnable), Void, ActionRunnable.Result>() {
+
+ override fun onPreExecute() {
+ super.onPreExecute()
+ onPreExecute.invoke()
+ }
+
+ override fun doInBackground(vararg actionRunnables: ((ProgressTaskUpdater?)-> ActionRunnable)?): ActionRunnable.Result {
+ var resultTask = ActionRunnable.Result(false)
+ // Without that, bind listeners don't work properly (I don't know why?)
+ Thread.sleep(500)
+ actionRunnables.forEach {
+ it?.invoke(progressTaskUpdater)?.apply {
+ run()
+ resultTask = result
+ }
+ }
+ return resultTask
+ }
+
+ override fun onPostExecute(result: ActionRunnable.Result) {
+ super.onPostExecute(result)
+ onPostExecute.invoke(result)
+ }
}
companion object {
private val TAG = DatabaseTaskNotificationService::class.java.name
- const val DATABASE_TASK_TITLE_KEY = "DatabaseTaskTitle"
+ const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
+ const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"
+ const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY"
+
+ const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
+ const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
+ const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"
+ const val ACTION_DATABASE_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK"
+ const val ACTION_DATABASE_UPDATE_GROUP_TASK = "ACTION_DATABASE_UPDATE_GROUP_TASK"
+ const val ACTION_DATABASE_CREATE_ENTRY_TASK = "ACTION_DATABASE_CREATE_ENTRY_TASK"
+ const val ACTION_DATABASE_UPDATE_ENTRY_TASK = "ACTION_DATABASE_UPDATE_ENTRY_TASK"
+ const val ACTION_DATABASE_COPY_NODES_TASK = "ACTION_DATABASE_COPY_NODES_TASK"
+ const val ACTION_DATABASE_MOVE_NODES_TASK = "ACTION_DATABASE_MOVE_NODES_TASK"
+ const val ACTION_DATABASE_DELETE_NODES_TASK = "ACTION_DATABASE_DELETE_NODES_TASK"
+ const val ACTION_DATABASE_SAVE_NAME_TASK = "ACTION_DATABASE_SAVE_NAME_TASK"
+ const val ACTION_DATABASE_SAVE_DESCRIPTION_TASK = "ACTION_DATABASE_SAVE_DESCRIPTION_TASK"
+ const val ACTION_DATABASE_SAVE_DEFAULT_USERNAME_TASK = "ACTION_DATABASE_SAVE_DEFAULT_USERNAME_TASK"
+ const val ACTION_DATABASE_SAVE_COLOR_TASK = "ACTION_DATABASE_SAVE_COLOR_TASK"
+ const val ACTION_DATABASE_SAVE_COMPRESSION_TASK = "ACTION_DATABASE_SAVE_COMPRESSION_TASK"
+ const val ACTION_DATABASE_SAVE_MAX_HISTORY_ITEMS_TASK = "ACTION_DATABASE_SAVE_MAX_HISTORY_ITEMS_TASK"
+ const val ACTION_DATABASE_SAVE_MAX_HISTORY_SIZE_TASK = "ACTION_DATABASE_SAVE_MAX_HISTORY_SIZE_TASK"
+ const val ACTION_DATABASE_SAVE_ENCRYPTION_TASK = "ACTION_DATABASE_SAVE_ENCRYPTION_TASK"
+ const val ACTION_DATABASE_SAVE_KEY_DERIVATION_TASK = "ACTION_DATABASE_SAVE_KEY_DERIVATION_TASK"
+ const val ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK = "ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK"
+ const val ACTION_DATABASE_SAVE_PARALLELISM_TASK = "ACTION_DATABASE_SAVE_PARALLELISM_TASK"
+ const val ACTION_DATABASE_SAVE_ITERATIONS_TASK = "ACTION_DATABASE_SAVE_ITERATIONS_TASK"
+
+ const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
+ const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
+ const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
+ const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
+ const val KEY_FILE_KEY = "KEY_FILE_KEY"
+ const val READ_ONLY_KEY = "READ_ONLY_KEY"
+ const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY"
+ const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY"
+ const val GROUP_KEY = "GROUP_KEY"
+ const val ENTRY_KEY = "ENTRY_KEY"
+ const val GROUP_ID_KEY = "GROUP_ID_KEY"
+ const val ENTRY_ID_KEY = "ENTRY_ID_KEY"
+ const val GROUPS_ID_KEY = "GROUPS_ID_KEY"
+ const val ENTRIES_ID_KEY = "ENTRIES_ID_KEY"
+ const val PARENT_ID_KEY = "PARENT_ID_KEY"
+ const val SAVE_DATABASE_KEY = "SAVE_DATABASE_KEY"
+ const val OLD_NODES_KEY = "OLD_NODES_KEY"
+ const val NEW_NODES_KEY = "NEW_NODES_KEY"
+ const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time
+ const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time
+
+ fun getListNodesFromBundle(database: Database, bundle: Bundle): List {
+ val nodesAction = ArrayList()
+ bundle.getParcelableArrayList>(GROUPS_ID_KEY)?.forEach {
+ database.getGroupById(it)?.let { groupRetrieve ->
+ nodesAction.add(groupRetrieve)
+ }
+ }
+ bundle.getParcelableArrayList>(ENTRIES_ID_KEY)?.forEach {
+ database.getEntryById(it)?.let { entryRetrieve ->
+ nodesAction.add(entryRetrieve)
+ }
+ }
+ return nodesAction
+ }
+
+ fun getBundleFromListNodes(nodes: List