mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'develop' into Bug_Feature_Template
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
KeepassDX (2.5.0.0beta24)
|
||||
* Add settings (Color, Security, Master Key)
|
||||
* Show history of each entry
|
||||
* Auto repair database for nodes with same UUID
|
||||
* Fix settings
|
||||
* Fix edit group
|
||||
* Fix small bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta23)
|
||||
* New, more secure database creation workflow
|
||||
* Recognize more database files
|
||||
|
||||
@@ -6,13 +6,14 @@ apply plugin: 'kotlin-kapt'
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion '28.0.3'
|
||||
ndkVersion "20.0.5594570"
|
||||
|
||||
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"
|
||||
@@ -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-beta02'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
@@ -97,10 +98,10 @@ 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
|
||||
|
||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.activities
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
@@ -49,15 +50,19 @@ import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.EntryContentsView
|
||||
import java.util.*
|
||||
|
||||
class EntryActivity : LockingHideActivity() {
|
||||
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var titleIconView: ImageView? = null
|
||||
private var historyView: View? = null
|
||||
private var entryContentsView: EntryContentsView? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
private var mEntry: EntryVersioned? = null
|
||||
private var mIsHistory: Boolean = false
|
||||
|
||||
private var mShowPassword: Boolean = false
|
||||
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
@@ -82,12 +87,18 @@ class EntryActivity : LockingHideActivity() {
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
val keyEntry: PwNodeId<*> = intent.getParcelableExtra(KEY_ENTRY)
|
||||
val keyEntry: PwNodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
|
||||
mEntry = currentDatabase.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()
|
||||
@@ -108,6 +119,7 @@ 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))
|
||||
|
||||
@@ -238,18 +250,11 @@ 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)
|
||||
if (entry.isExpires) {
|
||||
entryContentsView?.assignExpiresDate(entry.expiryTime)
|
||||
} else {
|
||||
entryContentsView?.assignExpiresDate(getString(R.string.never))
|
||||
}
|
||||
@@ -257,6 +262,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)
|
||||
}
|
||||
|
||||
@@ -412,13 +435,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ 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,
|
||||
@@ -90,7 +91,7 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
// Entry is retrieve, it's an entry to update
|
||||
intent.getParcelableExtra<PwNodeId<*>>(KEY_ENTRY)?.let {
|
||||
intent.getParcelableExtra<PwNodeId<UUID>>(KEY_ENTRY)?.let {
|
||||
mIsNew = false
|
||||
// Create an Entry copy to modify from the database entry
|
||||
mEntry = mDatabase?.getEntryById(it)
|
||||
@@ -105,14 +106,12 @@ 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
|
||||
}
|
||||
@@ -123,7 +122,11 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
// Parent is retrieve, it's a new entry to create
|
||||
intent.getParcelableExtra<PwNodeId<*>>(KEY_PARENT)?.let {
|
||||
mIsNew = true
|
||||
mNewEntry = mDatabase?.createEntry()
|
||||
// Create an empty new entry
|
||||
if (savedInstanceState == null
|
||||
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||
mNewEntry = mDatabase?.createEntry()
|
||||
}
|
||||
mParent = mDatabase?.getGroupById(it)
|
||||
// Add the default icon
|
||||
mDatabase?.drawFactory?.let { iconFactory ->
|
||||
@@ -131,6 +134,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()
|
||||
@@ -168,12 +177,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 {
|
||||
addNewCustomField(entry.key, entry.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,6 +196,7 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
newEntry.apply {
|
||||
// Build info from view
|
||||
entryEditContentsView?.let { entryView ->
|
||||
removeAllFields()
|
||||
title = entryView.title
|
||||
username = entryView.username
|
||||
url = entryView.url
|
||||
@@ -218,8 +230,6 @@ class EntryEditActivity : LockingHideActivity(),
|
||||
*/
|
||||
private fun addNewCustomField() {
|
||||
entryEditContentsView?.addNewCustomField()
|
||||
// Scroll bottom
|
||||
scrollView?.post { scrollView?.fullScroll(ScrollView.FOCUS_DOWN) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -342,7 +352,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)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ 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 com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||
@@ -54,13 +53,11 @@ 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.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 +66,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,8 +77,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
|
||||
private var mDefaultPath: String? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -98,44 +89,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 +106,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
createButtonView?.setOnClickListener { createNewFile() }
|
||||
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
browseButtonView = findViewById(R.id.browse_button)
|
||||
browseButtonView?.setOnClickListener(mOpenFileHelper!!.getOpenFileOnClickViewListener {
|
||||
UriUtil.parse(openFileNameView?.text?.toString())
|
||||
})
|
||||
openDatabaseButtonView = findViewById(R.id.open_database_button)
|
||||
openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
||||
|
||||
// History list
|
||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||
@@ -358,7 +311,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
private val keyFileUri: Uri?) : ActionRunnable() {
|
||||
|
||||
override fun run() {
|
||||
finishRun(true, null)
|
||||
finishRun(true)
|
||||
}
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
@@ -392,12 +345,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 +353,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 +387,15 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
})
|
||||
if (!createDatabaseEducationPerformed) {
|
||||
// selectDatabaseEducationPerformed
|
||||
browseButtonView != null
|
||||
openDatabaseButtonView != null
|
||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||
browseButtonView!!,
|
||||
openDatabaseButtonView!!,
|
||||
{tapTargetView ->
|
||||
tapTargetView?.let {
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
||||
}
|
||||
},
|
||||
{
|
||||
fileSelectExpandableButtonView?.let {
|
||||
fileDatabaseSelectActivityEducation
|
||||
.checkAndPerformedOpenLinkDatabaseEducation(it)
|
||||
}
|
||||
}
|
||||
{}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.SearchManager
|
||||
import android.app.assist.AssistStructure
|
||||
@@ -29,16 +28,16 @@ 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.widget.SearchView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||
@@ -62,7 +61,8 @@ 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.collapse
|
||||
import com.kunzisoft.keepass.view.expand
|
||||
|
||||
class GroupActivity : LockingActivity(),
|
||||
GroupEditDialogFragment.EditGroupListener,
|
||||
@@ -75,7 +75,6 @@ class GroupActivity : LockingActivity(),
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
private var searchTitleView: View? = null
|
||||
private var toolbarPasteExpandableLayout: ExpandableLayout? = null
|
||||
private var toolbarPaste: Toolbar? = null
|
||||
private var iconView: ImageView? = null
|
||||
private var modeTitleView: TextView? = null
|
||||
@@ -115,10 +114,21 @@ class GroupActivity : LockingActivity(),
|
||||
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)
|
||||
modeTitleView = findViewById(R.id.mode_title_view)
|
||||
|
||||
toolbar?.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
toolbarPaste?.inflateMenu(R.menu.node_paste_menu)
|
||||
toolbarPaste?.setNavigationIcon(R.drawable.ic_arrow_left_white_24dp)
|
||||
toolbarPaste?.collapse(false)
|
||||
toolbarPaste?.setNavigationOnClickListener {
|
||||
toolbarPaste?.collapse()
|
||||
mNodeToCopy = null
|
||||
mNodeToMove = null
|
||||
}
|
||||
|
||||
// Focus view to reinitialize timeout
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
|
||||
|
||||
@@ -129,9 +139,11 @@ class GroupActivity : LockingActivity(),
|
||||
if (savedInstanceState.containsKey(NODE_TO_COPY_KEY)) {
|
||||
mNodeToCopy = savedInstanceState.getParcelable(NODE_TO_COPY_KEY)
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
|
||||
toolbarPaste?.expand(false)
|
||||
} else if (savedInstanceState.containsKey(NODE_TO_MOVE_KEY)) {
|
||||
mNodeToMove = savedInstanceState.getParcelable(NODE_TO_MOVE_KEY)
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
|
||||
toolbarPaste?.expand(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,17 +165,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)
|
||||
@@ -449,7 +450,7 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun onCopyMenuClick(node: NodeVersioned): Boolean {
|
||||
toolbarPasteExpandableLayout?.expand()
|
||||
toolbarPaste?.expand()
|
||||
mNodeToCopy = node
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
|
||||
return false
|
||||
@@ -457,7 +458,7 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
private inner class OnCopyMenuItemClickListener : Toolbar.OnMenuItemClickListener {
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
toolbarPasteExpandableLayout?.collapse()
|
||||
toolbarPaste?.collapse()
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.menu_paste -> {
|
||||
@@ -489,7 +490,7 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun onMoveMenuClick(node: NodeVersioned): Boolean {
|
||||
toolbarPasteExpandableLayout?.expand()
|
||||
toolbarPaste?.expand()
|
||||
mNodeToMove = node
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
|
||||
return false
|
||||
@@ -497,7 +498,7 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
private inner class OnMoveMenuItemClickListener : Toolbar.OnMenuItemClickListener {
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
toolbarPasteExpandableLayout?.collapse()
|
||||
toolbarPaste?.collapse()
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.menu_paste -> {
|
||||
|
||||
@@ -44,6 +44,7 @@ import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.widget.*
|
||||
import androidx.biometric.BiometricManager
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||
import com.kunzisoft.keepass.activities.dialogs.FingerPrintExplanationDialog
|
||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
@@ -61,6 +62,8 @@ import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException
|
||||
import com.kunzisoft.keepass.database.search.SearchDbHelper
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
@@ -69,7 +72,6 @@ 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() {
|
||||
|
||||
@@ -87,6 +89,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
|
||||
@@ -101,8 +105,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)
|
||||
|
||||
@@ -123,7 +126,7 @@ class PasswordActivity : StylishActivity() {
|
||||
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||
|
||||
val browseView = findViewById<View>(R.id.browse_button)
|
||||
val browseView = findViewById<View>(R.id.open_database_button)
|
||||
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
||||
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
|
||||
|
||||
@@ -222,6 +225,7 @@ class PasswordActivity : StylishActivity() {
|
||||
|
||||
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||
mDatabaseFileUri = databaseFileUri
|
||||
mDatabaseKeyFileUri = keyFileUri
|
||||
|
||||
// Define title
|
||||
databaseFileUri?.let {
|
||||
@@ -243,11 +247,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)
|
||||
@@ -391,14 +397,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,7 +416,10 @@ 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)) {
|
||||
@@ -418,65 +431,119 @@ class PasswordActivity : StylishActivity() {
|
||||
val database = Database.getInstance()
|
||||
database.closeAndClear(applicationContext.filesDir)
|
||||
|
||||
mDatabaseFileUri?.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()
|
||||
}
|
||||
}
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
|
||||
/**
|
||||
* Called after verify and try to opening the database
|
||||
*/
|
||||
private inner class AfterLoadingDatabase(val database: Database, val password: String?,
|
||||
val cipherDatabaseEntity: CipherDatabaseEntity? = null)
|
||||
: ActionRunnable() {
|
||||
val onFinishLoadDatabase = object: 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()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if (result.isSuccess) {
|
||||
// Save keyFile in app database
|
||||
if (mRememberKeyFile) {
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
saveKeyFileData(databaseUri, mDatabaseKeyFileUri)
|
||||
}
|
||||
} 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()
|
||||
// Remove the password in view in all cases
|
||||
removePassword()
|
||||
|
||||
// Register the biometric
|
||||
if (cipherDatabaseEntity != null) {
|
||||
CipherDatabaseAction.getInstance(this@PasswordActivity)
|
||||
.addOrUpdateCipherDatabase(cipherDatabaseEntity) {
|
||||
checkAndLaunchGroupActivity(database, password, keyFileUri)
|
||||
}
|
||||
} else {
|
||||
checkAndLaunchGroupActivity(database, password, keyFileUri)
|
||||
}
|
||||
|
||||
} else {
|
||||
var resultError = ""
|
||||
val resultException = result.exception
|
||||
val resultMessage = result.message
|
||||
|
||||
if (resultException != null) {
|
||||
resultError = resultException.getLocalizedMessage(resources)
|
||||
if (resultException is LoadDatabaseDuplicateUuidException)
|
||||
showLoadDatabaseDuplicateUuidMessage {
|
||||
showProgressDialogAndLoadDatabase(database,
|
||||
databaseUri,
|
||||
password,
|
||||
keyFileUri,
|
||||
true,
|
||||
this)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show the progress dialog and load the database
|
||||
showProgressDialogAndLoadDatabase(database,
|
||||
databaseUri,
|
||||
password,
|
||||
keyFileUri,
|
||||
false,
|
||||
onFinishLoadDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAndLaunchGroupActivity(database: Database, password: String?) {
|
||||
if (database.validatePasswordEncoding(password)) {
|
||||
private fun showProgressDialogAndLoadDatabase(database: Database,
|
||||
databaseUri: Uri,
|
||||
password: String?,
|
||||
keyFile: Uri?,
|
||||
fixDuplicateUUID: Boolean,
|
||||
onFinishLoadDatabase: ActionRunnable) {
|
||||
ProgressDialogThread(this,
|
||||
{ progressTaskUpdater ->
|
||||
LoadDatabaseRunnable(
|
||||
database,
|
||||
databaseUri,
|
||||
password,
|
||||
keyFile,
|
||||
this@PasswordActivity.contentResolver,
|
||||
this@PasswordActivity.filesDir,
|
||||
SearchDbHelper(PreferencesUtil.omitBackup(this@PasswordActivity)),
|
||||
fixDuplicateUUID,
|
||||
progressTaskUpdater,
|
||||
onFinishLoadDatabase)
|
||||
},
|
||||
R.string.loading_database).start()
|
||||
}
|
||||
|
||||
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
|
||||
DuplicateUuidDialog().apply {
|
||||
positiveAction = loadDatabaseWithFix
|
||||
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
||||
}
|
||||
|
||||
private fun saveKeyFileData(databaseUri: Uri, keyUri: Uri?) {
|
||||
var keyFileUri = keyUri
|
||||
if (!mRememberKeyFile) {
|
||||
keyFileUri = null
|
||||
}
|
||||
FileDatabaseHistoryAction.getInstance(this).addOrUpdateDatabaseUri(databaseUri, keyFileUri)
|
||||
}
|
||||
|
||||
private fun checkAndLaunchGroupActivity(database: Database, password: String?, keyFileUri: Uri?) {
|
||||
if (database.validatePasswordEncoding(password, keyFileUri != null)) {
|
||||
launchGroupActivity()
|
||||
} else {
|
||||
PasswordEncodingDialogFragment().apply {
|
||||
|
||||
@@ -45,6 +45,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
private var rootView: View? = null
|
||||
|
||||
private var passwordCheckBox: CompoundButton? = null
|
||||
|
||||
private var passwordTextInputLayout: TextInputLayout? = null
|
||||
private var passwordView: TextView? = null
|
||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||
private var passwordRepeatView: TextView? = null
|
||||
@@ -96,6 +98,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
var allowNoMasterKey = false
|
||||
arguments?.apply {
|
||||
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
|
||||
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
val inflater = activity.layoutInflater
|
||||
|
||||
@@ -104,9 +113,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
.setTitle(R.string.assign_master_key)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
||||
@@ -116,7 +126,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
|
||||
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
rootView?.findViewById<View>(R.id.browse_button)?.setOnClickListener { view ->
|
||||
rootView?.findViewById<View>(R.id.open_database_button)?.setOnClickListener { view ->
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
|
||||
|
||||
val dialog = builder.create()
|
||||
@@ -132,7 +142,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
var error = verifyPassword() || verifyFile()
|
||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||
error = true
|
||||
showNoKeyConfirmationDialog()
|
||||
if (allowNoMasterKey)
|
||||
showNoKeyConfirmationDialog()
|
||||
else {
|
||||
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
|
||||
}
|
||||
}
|
||||
if (!error) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(
|
||||
@@ -193,6 +207,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
showEmptyPasswordConfirmationDialog()
|
||||
}
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
@@ -223,7 +238,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
}
|
||||
}
|
||||
@@ -238,7 +253,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
}
|
||||
}
|
||||
@@ -255,4 +270,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
||||
|
||||
fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment {
|
||||
val fragment = AssignMasterKeyDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class BrowserDialogFragment : DialogFragment() {
|
||||
// Get the layout inflater
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_browser_install, null)
|
||||
builder.setView(root)
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description)
|
||||
textDescription.text = getString(R.string.file_manager_install_description)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class DuplicateUuidDialog : DialogFragment() {
|
||||
|
||||
var positiveAction: (() -> Unit)? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply {
|
||||
val message = getString(R.string.contains_duplicate_uuid) +
|
||||
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
|
||||
setMessage(message)
|
||||
setPositiveButton(getString(android.R.string.ok)) { _, _ ->
|
||||
positiveAction?.invoke()
|
||||
dismiss()
|
||||
}
|
||||
setNegativeButton(getString(android.R.string.cancel)) { _, _ -> dismiss() }
|
||||
}
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
this.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
|
||||
dismiss()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
val bundle = Bundle()
|
||||
mListener?.cancelPassword(bundle)
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
editGroupListener?.cancelEditGroup(
|
||||
editGroupDialogAction,
|
||||
nameTextView?.text?.toString(),
|
||||
|
||||
@@ -77,7 +77,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
builder.setNegativeButton(R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class SortDialogFragment : DialogFragment() {
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok
|
||||
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
|
||||
// Check if is ascending or descending
|
||||
|
||||
@@ -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,44 @@ 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
|
||||
val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
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
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(i, OPEN_DOC)
|
||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
else
|
||||
activity?.startActivityForResult(i, OPEN_DOC)
|
||||
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
}
|
||||
|
||||
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 = "*/*"
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(i, GET_CONTENT)
|
||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
else
|
||||
activity?.startActivityForResult(i, GET_CONTENT)
|
||||
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
}
|
||||
|
||||
fun getOpenFileOnClickViewListener(dataUri: () -> Uri?): OpenFileOnClickViewListener {
|
||||
return OpenFileOnClickViewListener(dataUri)
|
||||
}
|
||||
|
||||
private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean {
|
||||
private fun lookForOpenIntentsFilePicker(): Boolean {
|
||||
var showBrowser = false
|
||||
try {
|
||||
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
||||
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
|
||||
// Get file path parent if possible
|
||||
if (dataUri != null
|
||||
&& dataUri.toString().isNotEmpty()
|
||||
&& dataUri.scheme == "file") {
|
||||
intent.data = dataUri
|
||||
} else {
|
||||
Log.w(javaClass.name, "Unable to read the URI")
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intent, FILE_BROWSE)
|
||||
else
|
||||
@@ -220,15 +207,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"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
|
||||
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
var entryHistoryList: MutableList<EntryVersioned> = ArrayList()
|
||||
var onItemClickListener: ((item: EntryVersioned, position: Int)->Unit)? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder {
|
||||
return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: EntryHistoryViewHolder, position: Int) {
|
||||
val entryHistory = entryHistoryList[position]
|
||||
|
||||
holder.lastModifiedView.text = entryHistory.lastModificationTime.getDateTimeString(context.resources)
|
||||
holder.titleView.text = entryHistory.title
|
||||
holder.usernameView.text = entryHistory.username
|
||||
holder.urlView.text = entryHistory.url
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClickListener?.invoke(entryHistory, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return entryHistoryList.size
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
entryHistoryList.clear()
|
||||
}
|
||||
|
||||
inner class EntryHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var lastModifiedView: TextView = itemView.findViewById(R.id.entry_history_last_modified)
|
||||
var titleView: TextView = itemView.findViewById(R.id.entry_history_title)
|
||||
var usernameView: TextView = itemView.findViewById(R.id.entry_history_username)
|
||||
var urlView: TextView = itemView.findViewById(R.id.entry_history_url)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -32,12 +32,10 @@ class AesKdf internal constructor() : KdfEngine() {
|
||||
|
||||
override val defaultParameters: KdfParameters
|
||||
get() {
|
||||
val p = KdfParameters(uuid)
|
||||
|
||||
p.setParamUUID()
|
||||
p.setUInt32(ParamRounds, DEFAULT_ROUNDS.toLong())
|
||||
|
||||
return p
|
||||
return KdfParameters(uuid).apply {
|
||||
setParamUUID()
|
||||
setUInt32(PARAM_ROUNDS, DEFAULT_ROUNDS.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
override val defaultKeyRounds: Long
|
||||
@@ -54,8 +52,8 @@ class AesKdf internal constructor() : KdfEngine() {
|
||||
@Throws(IOException::class)
|
||||
override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray {
|
||||
var currentMasterKey = masterKey
|
||||
val rounds = p.getUInt64(ParamRounds)
|
||||
var seed = p.getByteArray(ParamSeed)
|
||||
val rounds = p.getUInt64(PARAM_ROUNDS)
|
||||
var seed = p.getByteArray(PARAM_SEED)
|
||||
|
||||
if (currentMasterKey.size != 32) {
|
||||
currentMasterKey = CryptoUtil.hashSha256(currentMasterKey)
|
||||
@@ -75,15 +73,15 @@ class AesKdf internal constructor() : KdfEngine() {
|
||||
val seed = ByteArray(32)
|
||||
random.nextBytes(seed)
|
||||
|
||||
p.setByteArray(ParamSeed, seed)
|
||||
p.setByteArray(PARAM_SEED, seed)
|
||||
}
|
||||
|
||||
override fun getKeyRounds(p: KdfParameters): Long {
|
||||
return p.getUInt64(ParamRounds)
|
||||
return p.getUInt64(PARAM_ROUNDS)
|
||||
}
|
||||
|
||||
override fun setKeyRounds(p: KdfParameters, keyRounds: Long) {
|
||||
p.setUInt64(ParamRounds, keyRounds)
|
||||
p.setUInt64(PARAM_ROUNDS, keyRounds)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -91,9 +89,24 @@ class AesKdf internal constructor() : KdfEngine() {
|
||||
private const val DEFAULT_ROUNDS = 6000
|
||||
|
||||
val CIPHER_UUID: UUID = Types.bytestoUUID(
|
||||
byteArrayOf(0xC9.toByte(), 0xD9.toByte(), 0xF3.toByte(), 0x9A.toByte(), 0x62.toByte(), 0x8A.toByte(), 0x44.toByte(), 0x60.toByte(), 0xBF.toByte(), 0x74.toByte(), 0x0D.toByte(), 0x08.toByte(), 0xC1.toByte(), 0x8A.toByte(), 0x4F.toByte(), 0xEA.toByte()))
|
||||
byteArrayOf(0xC9.toByte(),
|
||||
0xD9.toByte(),
|
||||
0xF3.toByte(),
|
||||
0x9A.toByte(),
|
||||
0x62.toByte(),
|
||||
0x8A.toByte(),
|
||||
0x44.toByte(),
|
||||
0x60.toByte(),
|
||||
0xBF.toByte(),
|
||||
0x74.toByte(),
|
||||
0x0D.toByte(),
|
||||
0x08.toByte(),
|
||||
0xC1.toByte(),
|
||||
0x8A.toByte(),
|
||||
0x4F.toByte(),
|
||||
0xEA.toByte()))
|
||||
|
||||
const val ParamRounds = "R"
|
||||
const val ParamSeed = "S"
|
||||
const val PARAM_ROUNDS = "R"
|
||||
const val PARAM_SEED = "S"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,16 +33,16 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
||||
val p = KdfParameters(uuid)
|
||||
|
||||
p.setParamUUID()
|
||||
p.setUInt32(ParamParallelism, DefaultParallelism)
|
||||
p.setUInt64(ParamMemory, DefaultMemory)
|
||||
p.setUInt64(ParamIterations, DefaultIterations)
|
||||
p.setUInt32(ParamVersion, MaxVersion)
|
||||
p.setUInt32(PARAM_PARALLELISM, DEFAULT_PARALLELISM)
|
||||
p.setUInt64(PARAM_MEMORY, DEFAULT_MEMORY)
|
||||
p.setUInt64(PARAM_ITERATIONS, DEFAULT_ITERATIONS)
|
||||
p.setUInt32(PARAM_VERSION, MAX_VERSION)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
override val defaultKeyRounds: Long
|
||||
get() = DefaultIterations
|
||||
get() = DEFAULT_ITERATIONS
|
||||
|
||||
init {
|
||||
uuid = CIPHER_UUID
|
||||
@@ -55,13 +55,13 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
||||
@Throws(IOException::class)
|
||||
override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray {
|
||||
|
||||
val salt = p.getByteArray(ParamSalt)
|
||||
val parallelism = p.getUInt32(ParamParallelism).toInt()
|
||||
val memory = p.getUInt64(ParamMemory)
|
||||
val iterations = p.getUInt64(ParamIterations)
|
||||
val version = p.getUInt32(ParamVersion)
|
||||
val secretKey = p.getByteArray(ParamSecretKey)
|
||||
val assocData = p.getByteArray(ParamAssocData)
|
||||
val salt = p.getByteArray(PARAM_SALT)
|
||||
val parallelism = p.getUInt32(PARAM_PARALLELISM).toInt()
|
||||
val memory = p.getUInt64(PARAM_MEMORY)
|
||||
val iterations = p.getUInt64(PARAM_ITERATIONS)
|
||||
val version = p.getUInt32(PARAM_VERSION)
|
||||
val secretKey = p.getByteArray(PARAM_SECRET_KEY)
|
||||
val assocData = p.getByteArray(PARAM_ASSOC_DATA)
|
||||
|
||||
return Argon2Native.transformKey(masterKey, salt, parallelism, memory, iterations,
|
||||
secretKey, assocData, version)
|
||||
@@ -73,71 +73,102 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
||||
val salt = ByteArray(32)
|
||||
random.nextBytes(salt)
|
||||
|
||||
p.setByteArray(ParamSalt, salt)
|
||||
p.setByteArray(PARAM_SALT, salt)
|
||||
}
|
||||
|
||||
override fun getKeyRounds(p: KdfParameters): Long {
|
||||
return p.getUInt64(ParamIterations)
|
||||
return p.getUInt64(PARAM_ITERATIONS)
|
||||
}
|
||||
|
||||
override fun setKeyRounds(p: KdfParameters, keyRounds: Long) {
|
||||
p.setUInt64(ParamIterations, keyRounds)
|
||||
p.setUInt64(PARAM_ITERATIONS, keyRounds)
|
||||
}
|
||||
|
||||
override val minKeyRounds: Long
|
||||
get() = MIN_ITERATIONS
|
||||
|
||||
override val maxKeyRounds: Long
|
||||
get() = MAX_ITERATIONS
|
||||
|
||||
override fun getMemoryUsage(p: KdfParameters): Long {
|
||||
return p.getUInt64(ParamMemory)
|
||||
return p.getUInt64(PARAM_MEMORY)
|
||||
}
|
||||
|
||||
override fun setMemoryUsage(p: KdfParameters, memory: Long) {
|
||||
p.setUInt64(ParamMemory, memory)
|
||||
p.setUInt64(PARAM_MEMORY, memory)
|
||||
}
|
||||
|
||||
override fun getDefaultMemoryUsage(): Long {
|
||||
return DefaultMemory
|
||||
}
|
||||
override val defaultMemoryUsage: Long
|
||||
get() = DEFAULT_MEMORY
|
||||
|
||||
override val minMemoryUsage: Long
|
||||
get() = MIN_MEMORY
|
||||
|
||||
override val maxMemoryUsage: Long
|
||||
get() = MAX_MEMORY
|
||||
|
||||
override fun getParallelism(p: KdfParameters): Int {
|
||||
return p.getUInt32(ParamParallelism).toInt() // TODO Verify
|
||||
return p.getUInt32(PARAM_PARALLELISM).toInt() // TODO Verify
|
||||
}
|
||||
|
||||
override fun setParallelism(p: KdfParameters, parallelism: Int) {
|
||||
p.setUInt32(ParamParallelism, parallelism.toLong())
|
||||
p.setUInt32(PARAM_PARALLELISM, parallelism.toLong())
|
||||
}
|
||||
|
||||
override fun getDefaultParallelism(): Int {
|
||||
return DefaultParallelism.toInt() // TODO Verify
|
||||
}
|
||||
override val defaultParallelism: Int
|
||||
get() = DEFAULT_PARALLELISM.toInt()
|
||||
|
||||
override val minParallelism: Int
|
||||
get() = MIN_PARALLELISM
|
||||
|
||||
override val maxParallelism: Int
|
||||
get() = MAX_PARALLELISM
|
||||
|
||||
companion object {
|
||||
|
||||
val CIPHER_UUID: UUID = Types.bytestoUUID(
|
||||
byteArrayOf(0xEF.toByte(), 0x63.toByte(), 0x6D.toByte(), 0xDF.toByte(), 0x8C.toByte(), 0x29.toByte(), 0x44.toByte(), 0x4B.toByte(), 0x91.toByte(), 0xF7.toByte(), 0xA9.toByte(), 0xA4.toByte(), 0x03.toByte(), 0xE3.toByte(), 0x0A.toByte(), 0x0C.toByte()))
|
||||
byteArrayOf(0xEF.toByte(),
|
||||
0x63.toByte(),
|
||||
0x6D.toByte(),
|
||||
0xDF.toByte(),
|
||||
0x8C.toByte(),
|
||||
0x29.toByte(),
|
||||
0x44.toByte(),
|
||||
0x4B.toByte(),
|
||||
0x91.toByte(),
|
||||
0xF7.toByte(),
|
||||
0xA9.toByte(),
|
||||
0xA4.toByte(),
|
||||
0x03.toByte(),
|
||||
0xE3.toByte(),
|
||||
0x0A.toByte(),
|
||||
0x0C.toByte()))
|
||||
|
||||
private const val ParamSalt = "S" // byte[]
|
||||
private const val ParamParallelism = "P" // UInt32
|
||||
private const val ParamMemory = "M" // UInt64
|
||||
private const val ParamIterations = "I" // UInt64
|
||||
private const val ParamVersion = "V" // UInt32
|
||||
private const val ParamSecretKey = "K" // byte[]
|
||||
private const val ParamAssocData = "A" // byte[]
|
||||
private const val PARAM_SALT = "S" // byte[]
|
||||
private const val PARAM_PARALLELISM = "P" // UInt32
|
||||
private const val PARAM_MEMORY = "M" // UInt64
|
||||
private const val PARAM_ITERATIONS = "I" // UInt64
|
||||
private const val PARAM_VERSION = "V" // UInt32
|
||||
private const val PARAM_SECRET_KEY = "K" // byte[]
|
||||
private const val PARAM_ASSOC_DATA = "A" // byte[]
|
||||
|
||||
private const val MinVersion: Long = 0x10
|
||||
private const val MaxVersion: Long = 0x13
|
||||
private const val MIN_VERSION: Long = 0x10
|
||||
private const val MAX_VERSION: Long = 0x13
|
||||
|
||||
private const val MinSalt = 8
|
||||
private const val MaxSalt = Integer.MAX_VALUE
|
||||
private const val MIN_SALT = 8
|
||||
private const val MAX_SALT = Integer.MAX_VALUE
|
||||
|
||||
private const val MinIterations: Long = 1
|
||||
private const val MaxIterations = 4294967295L
|
||||
private const val MIN_ITERATIONS: Long = 1
|
||||
private const val MAX_ITERATIONS = 4294967295L
|
||||
|
||||
private const val MinMemory = (1024 * 8).toLong()
|
||||
private const val MaxMemory = Integer.MAX_VALUE.toLong()
|
||||
private const val MIN_MEMORY = (1024 * 8).toLong()
|
||||
private const val MAX_MEMORY = Integer.MAX_VALUE.toLong()
|
||||
|
||||
private const val MinParallelism = 1
|
||||
private const val MaxParallelism = (1 shl 24) - 1
|
||||
private const val MIN_PARALLELISM = 1
|
||||
private const val MAX_PARALLELISM = (1 shl 24) - 1
|
||||
|
||||
private const val DefaultIterations: Long = 2
|
||||
private const val DefaultMemory = (1024 * 1024).toLong()
|
||||
private const val DefaultParallelism: Long = 2
|
||||
private const val DEFAULT_ITERATIONS: Long = 2
|
||||
private const val DEFAULT_MEMORY = (1024 * 1024).toLong()
|
||||
private const val DEFAULT_PARALLELISM: Long = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,17 +30,31 @@ abstract class KdfEngine : ObjectNameResource {
|
||||
|
||||
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 +63,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 +84,16 @@ abstract class KdfEngine : ObjectNameResource {
|
||||
// Do nothing by default
|
||||
}
|
||||
|
||||
open fun getDefaultParallelism(): Int {
|
||||
return UNKNOWN_VALUE
|
||||
}
|
||||
open val defaultParallelism: Int
|
||||
get() = UNKNOWN_VALUE
|
||||
|
||||
open val minParallelism: Int
|
||||
get() = 1
|
||||
|
||||
open val maxParallelism: Int
|
||||
get() = Int.MAX_VALUE
|
||||
|
||||
companion object {
|
||||
|
||||
const val UNKNOWN_VALUE = -1
|
||||
const val UNKNOWN_VALUE_STRING = (-1).toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,37 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
||||
|
||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||
|
||||
import java.util.ArrayList
|
||||
|
||||
object KdfFactory {
|
||||
|
||||
var aesKdf = AesKdf()
|
||||
var argon2Kdf = Argon2Kdf()
|
||||
|
||||
var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||
var kdfListV4: MutableList<KdfEngine> = ArrayList()
|
||||
|
||||
init {
|
||||
kdfListV3.add(aesKdf)
|
||||
|
||||
kdfListV4.add(aesKdf)
|
||||
kdfListV4.add(argon2Kdf)
|
||||
}
|
||||
|
||||
@Throws(UnknownKDF::class)
|
||||
fun getEngineV4(kdfParameters: KdfParameters?): KdfEngine {
|
||||
val unknownKDFException = UnknownKDF()
|
||||
if (kdfParameters == null) {
|
||||
throw unknownKDFException
|
||||
}
|
||||
for (engine in kdfListV4) {
|
||||
if (engine.uuid == kdfParameters.uuid) {
|
||||
return engine
|
||||
}
|
||||
}
|
||||
throw unknownKDFException
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ enum class SortNodeEnum {
|
||||
|
||||
override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int {
|
||||
return object1.creationTime.date
|
||||
?.compareTo(object2.creationTime.date) ?: 0
|
||||
.compareTo(object2.creationTime.date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ enum class SortNodeEnum {
|
||||
|
||||
override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int {
|
||||
return object1.lastModificationTime.date
|
||||
?.compareTo(object2.lastModificationTime.date) ?: 0
|
||||
.compareTo(object2.lastModificationTime.date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ enum class SortNodeEnum {
|
||||
|
||||
override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int {
|
||||
return object1.lastAccessTime.date
|
||||
?.compareTo(object2.lastAccessTime.date) ?: 0
|
||||
.compareTo(object2.lastAccessTime.date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.action
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidKeyFileException
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import java.io.IOException
|
||||
@@ -63,7 +63,7 @@ open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
||||
// To save the database
|
||||
super.run()
|
||||
finishRun(true)
|
||||
} catch (e: InvalidKeyFileException) {
|
||||
} catch (e: LoadDatabaseInvalidKeyFileException) {
|
||||
erase(mBackupKey)
|
||||
finishRun(false, e.message)
|
||||
} catch (e: IOException) {
|
||||
|
||||
@@ -19,118 +19,45 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContentResolver
|
||||
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.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.database.search.SearchDbHelper
|
||||
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
|
||||
import java.io.File
|
||||
|
||||
class LoadDatabaseRunnable(private val mWeakContext: WeakReference<Context>,
|
||||
private val mDatabase: Database,
|
||||
class LoadDatabaseRunnable(private val mDatabase: Database,
|
||||
private val mUri: Uri,
|
||||
private val mPass: String?,
|
||||
private val mKey: Uri?,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val cacheDirectory: File,
|
||||
private val mSearchHelper: SearchDbHelper,
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
mDatabase.loadData(mUri, mPass, mKey,
|
||||
contentResolver,
|
||||
cacheDirectory,
|
||||
mSearchHelper,
|
||||
mFixDuplicateUUID,
|
||||
progressTaskUpdater)
|
||||
finishRun(true)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private fun saveFileData(uri: Uri, key: Uri?) {
|
||||
var keyFileUri = key
|
||||
if (!mRememberKeyFile) {
|
||||
keyFileUri = null
|
||||
}
|
||||
mWeakContext.get()?.let {
|
||||
FileDatabaseHistoryAction.getInstance(it).addOrUpdateDatabaseUri(uri, keyFileUri)
|
||||
catch (e: LoadDatabaseException) {
|
||||
finishRun(false, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
if (!result.isSuccess) {
|
||||
mDatabase.closeAndClear(mWeakContext.get()?.filesDir)
|
||||
mDatabase.closeAndClear(cacheDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = LoadDatabaseRunnable::class.java.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ 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
|
||||
|
||||
@@ -37,7 +37,7 @@ abstract class SaveDatabaseRunnable(protected var context: Context,
|
||||
database.saveData(context.contentResolver)
|
||||
} catch (e: IOException) {
|
||||
finishRun(false, e.message)
|
||||
} catch (e: PwDbOutputException) {
|
||||
} catch (e: DatabaseOutputException) {
|
||||
finishRun(false, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@ class DeleteEntryRunnable constructor(
|
||||
result.data = Bundle().apply {
|
||||
putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position )
|
||||
}
|
||||
} ?: run {
|
||||
result.data?.remove(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY)
|
||||
}
|
||||
|
||||
// Return a copy of unchanged entry as old param
|
||||
|
||||
@@ -73,6 +73,8 @@ class DeleteGroupRunnable(context: FragmentActivity,
|
||||
result.data = Bundle().apply {
|
||||
putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position )
|
||||
}
|
||||
} ?: run {
|
||||
result.data?.remove(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY)
|
||||
}
|
||||
|
||||
// Return a copy of unchanged group as old param
|
||||
|
||||
@@ -33,11 +33,17 @@ class UpdateEntryRunnable constructor(
|
||||
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
|
||||
|
||||
// Keep backup of original values in case save fails
|
||||
private var mBackupEntry: EntryVersioned? = null
|
||||
private var mBackupEntryHistory: EntryVersioned? = null
|
||||
|
||||
override fun nodeAction() {
|
||||
mBackupEntry = database.addHistoryBackupTo(mOldEntry)
|
||||
mOldEntry.touch(modified = true, touchParents = true)
|
||||
mNewEntry.touch(modified = true, touchParents = true)
|
||||
|
||||
mBackupEntryHistory = EntryVersioned(mOldEntry)
|
||||
|
||||
// Create an entry history (an entry history don't have history)
|
||||
mNewEntry.addEntryToHistory(EntryVersioned(mOldEntry, copyHistory = false))
|
||||
|
||||
database.removeOldestHistory(mNewEntry)
|
||||
// Update entry with new values
|
||||
mOldEntry.updateWith(mNewEntry)
|
||||
}
|
||||
@@ -45,7 +51,7 @@ class UpdateEntryRunnable constructor(
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, back out changes to global structure
|
||||
mBackupEntry?.let {
|
||||
mBackupEntryHistory?.let {
|
||||
mOldEntry.updateWith(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -57,7 +54,8 @@ class Database {
|
||||
private var pwDatabaseV4: PwDatabaseV4? = null
|
||||
|
||||
private var mUri: Uri? = null
|
||||
private var searchHelper: SearchDbHelper? = null
|
||||
private var mSearchHelper: SearchDbHelper? = null
|
||||
|
||||
var isReadOnly = false
|
||||
|
||||
val drawFactory = IconDrawableFactory()
|
||||
@@ -69,15 +67,23 @@ class Database {
|
||||
return pwDatabaseV3?.iconFactory ?: pwDatabaseV4?.iconFactory ?: PwIconFactory()
|
||||
}
|
||||
|
||||
val name: String
|
||||
var name: String
|
||||
get() {
|
||||
return pwDatabaseV4?.name ?: ""
|
||||
}
|
||||
set(name) {
|
||||
pwDatabaseV4?.name = name
|
||||
pwDatabaseV4?.nameChanged = PwDate()
|
||||
}
|
||||
|
||||
val description: String
|
||||
var description: String
|
||||
get() {
|
||||
return pwDatabaseV4?.description ?: ""
|
||||
}
|
||||
set(description) {
|
||||
pwDatabaseV4?.description = description
|
||||
pwDatabaseV4?.descriptionChanged = PwDate()
|
||||
}
|
||||
|
||||
var defaultUsername: String
|
||||
get() {
|
||||
@@ -88,33 +94,56 @@ class Database {
|
||||
pwDatabaseV4?.defaultUserNameChanged = PwDate()
|
||||
}
|
||||
|
||||
val encryptionAlgorithm: PwEncryptionAlgorithm?
|
||||
// with format "#000000"
|
||||
var color: String
|
||||
get() {
|
||||
return pwDatabaseV4?.encryptionAlgorithm
|
||||
return pwDatabaseV4?.color ?: ""
|
||||
}
|
||||
set(value) {
|
||||
// TODO Check color string
|
||||
pwDatabaseV4?.color = value
|
||||
}
|
||||
|
||||
val availableCompressionAlgorithms: List<PwCompressionAlgorithm>
|
||||
get() = pwDatabaseV4?.availableCompressionAlgorithms ?: ArrayList()
|
||||
|
||||
var compressionAlgorithm: PwCompressionAlgorithm?
|
||||
get() = pwDatabaseV4?.compressionAlgorithm
|
||||
set(value) {
|
||||
value?.let {
|
||||
pwDatabaseV4?.compressionAlgorithm = it
|
||||
}
|
||||
// TODO Compression
|
||||
}
|
||||
|
||||
val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm>
|
||||
get() = pwDatabaseV3?.availableEncryptionAlgorithms ?: pwDatabaseV4?.availableEncryptionAlgorithms ?: ArrayList()
|
||||
|
||||
var encryptionAlgorithm: PwEncryptionAlgorithm?
|
||||
get() = pwDatabaseV3?.encryptionAlgorithm ?: pwDatabaseV4?.encryptionAlgorithm
|
||||
set(algorithm) {
|
||||
algorithm?.let {
|
||||
pwDatabaseV4?.encryptionAlgorithm = algorithm
|
||||
pwDatabaseV4?.setDataEngine(algorithm.cipherEngine)
|
||||
pwDatabaseV4?.dataCipher = algorithm.dataCipher
|
||||
}
|
||||
}
|
||||
|
||||
val availableKdfEngines: List<KdfEngine>
|
||||
get() {
|
||||
if (pwDatabaseV3 != null) {
|
||||
return KdfFactory.kdfListV3
|
||||
}
|
||||
if (pwDatabaseV4 != null) {
|
||||
return KdfFactory.kdfListV4
|
||||
}
|
||||
return ArrayList()
|
||||
}
|
||||
get() = pwDatabaseV3?.kdfAvailableList ?: pwDatabaseV4?.kdfAvailableList ?: ArrayList()
|
||||
|
||||
val kdfEngine: KdfEngine
|
||||
get() {
|
||||
return pwDatabaseV4?.kdfEngine ?: return KdfFactory.aesKdf
|
||||
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()
|
||||
|
||||
var numberKeyEncryptionRounds: Long
|
||||
get() = pwDatabaseV3?.numberKeyEncryptionRounds ?: pwDatabaseV4?.numberKeyEncryptionRounds ?: 0
|
||||
@Throws(NumberFormatException::class)
|
||||
@@ -123,9 +152,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 +160,6 @@ class Database {
|
||||
pwDatabaseV4?.memoryUsage = memory
|
||||
}
|
||||
|
||||
val parallelismAsString: String
|
||||
get() = parallelism.toString()
|
||||
|
||||
var parallelism: Int
|
||||
get() = pwDatabaseV4?.parallelism ?: KdfEngine.UNKNOWN_VALUE
|
||||
set(parallelism) {
|
||||
@@ -161,6 +184,25 @@ 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
|
||||
@@ -206,78 +248,95 @@ class Database {
|
||||
this.mUri = 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?,
|
||||
contentResolver: ContentResolver,
|
||||
cacheDirectory: File,
|
||||
searchHelper: SearchDbHelper,
|
||||
fixDuplicateUUID: Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
|
||||
mUri = uri
|
||||
isReadOnly = false
|
||||
if (uri.scheme == "file") {
|
||||
val file = File(uri.path!!)
|
||||
isReadOnly = !file.canWrite()
|
||||
}
|
||||
|
||||
// Pass Uris as InputStreams
|
||||
val inputStream: InputStream?
|
||||
try {
|
||||
inputStream = UriUtil.getUriInputStream(ctx.contentResolver, uri)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KPD", "Database::loadData", e)
|
||||
throw ContentFileNotFoundException.getInstance(uri)
|
||||
}
|
||||
|
||||
// Pass KeyFile Uri as InputStreams
|
||||
var keyFileInputStream: InputStream? = null
|
||||
keyfile?.let {
|
||||
mUri = uri
|
||||
isReadOnly = false
|
||||
if (uri.scheme == "file") {
|
||||
val file = File(uri.path!!)
|
||||
isReadOnly = !file.canWrite()
|
||||
}
|
||||
|
||||
// Pass Uris as InputStreams
|
||||
val inputStream: InputStream?
|
||||
try {
|
||||
keyFileInputStream = UriUtil.getUriInputStream(ctx.contentResolver, keyfile)
|
||||
inputStream = UriUtil.getUriInputStream(contentResolver, uri)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KPD", "Database::loadData", e)
|
||||
throw ContentFileNotFoundException.getInstance(keyfile)
|
||||
throw LoadDatabaseFileNotFoundException()
|
||||
}
|
||||
}
|
||||
|
||||
// Load Data
|
||||
// Pass KeyFile Uri as InputStreams
|
||||
var keyFileInputStream: InputStream? = null
|
||||
keyfile?.let {
|
||||
try {
|
||||
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KPD", "Database::loadData", e)
|
||||
throw LoadDatabaseFileNotFoundException()
|
||||
}
|
||||
}
|
||||
|
||||
val bufferedInputStream = BufferedInputStream(inputStream)
|
||||
if (!bufferedInputStream.markSupported()) {
|
||||
throw IOException("Input stream does not support mark.")
|
||||
}
|
||||
// Load Data
|
||||
|
||||
// We'll end up reading 8 bytes to identify the header. Might as well use two extra.
|
||||
bufferedInputStream.mark(10)
|
||||
val bufferedInputStream = BufferedInputStream(inputStream)
|
||||
if (!bufferedInputStream.markSupported()) {
|
||||
throw IOException("Input stream does not support mark.")
|
||||
}
|
||||
|
||||
// Get the file directory to save the attachments
|
||||
val sig1 = LEDataInputStream.readInt(bufferedInputStream)
|
||||
val sig2 = LEDataInputStream.readInt(bufferedInputStream)
|
||||
// We'll end up reading 8 bytes to identify the header. Might as well use two extra.
|
||||
bufferedInputStream.mark(10)
|
||||
|
||||
// Return to the start
|
||||
bufferedInputStream.reset()
|
||||
// Get the file directory to save the attachments
|
||||
val sig1 = LEDataInputStream.readInt(bufferedInputStream)
|
||||
val sig2 = LEDataInputStream.readInt(bufferedInputStream)
|
||||
|
||||
when {
|
||||
// Header of database V3
|
||||
PwDbHeaderV3.matchesHeader(sig1, sig2) -> setDatabaseV3(ImporterV3()
|
||||
.openDatabase(bufferedInputStream,
|
||||
password,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater))
|
||||
// Return to the start
|
||||
bufferedInputStream.reset()
|
||||
|
||||
// Header of database V4
|
||||
PwDbHeaderV4.matchesHeader(sig1, sig2) -> setDatabaseV4(ImporterV4(ctx.filesDir)
|
||||
.openDatabase(bufferedInputStream,
|
||||
password,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater))
|
||||
when {
|
||||
// Header of database V3
|
||||
PwDbHeaderV3.matchesHeader(sig1, sig2) -> setDatabaseV3(ImporterV3()
|
||||
.openDatabase(bufferedInputStream,
|
||||
password,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater))
|
||||
|
||||
// Header not recognized
|
||||
else -> throw InvalidDBSignatureException()
|
||||
}
|
||||
// Header of database V4
|
||||
PwDbHeaderV4.matchesHeader(sig1, sig2) -> setDatabaseV4(ImporterV4(
|
||||
cacheDirectory,
|
||||
fixDuplicateUUID)
|
||||
.openDatabase(bufferedInputStream,
|
||||
password,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater))
|
||||
|
||||
try {
|
||||
searchHelper = SearchDbHelper(PreferencesUtil.omitBackup(ctx))
|
||||
// Header not recognized
|
||||
else -> throw LoadDatabaseSignatureException()
|
||||
}
|
||||
|
||||
this.mSearchHelper = searchHelper
|
||||
loaded = true
|
||||
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw 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) {
|
||||
Log.e(TAG, "Load can't be performed with this Database version", e)
|
||||
loaded = false
|
||||
throw LoadDatabaseException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,7 +348,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 +399,14 @@ class Database {
|
||||
return entry
|
||||
}
|
||||
|
||||
@Throws(IOException::class, PwDbOutputException::class)
|
||||
@Throws(IOException::class, DatabaseOutputException::class)
|
||||
fun saveData(contentResolver: ContentResolver) {
|
||||
mUri?.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."
|
||||
|
||||
@@ -425,31 +484,30 @@ class Database {
|
||||
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 containsDefaultUsername(): Boolean {
|
||||
pwDatabaseV4?.let { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fun containsCustomColor(): Boolean {
|
||||
pwDatabaseV4?.let { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fun allowNoMasterKey(): Boolean {
|
||||
pwDatabaseV4?.let { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
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) ?: ""
|
||||
}
|
||||
@@ -458,25 +516,17 @@ class Database {
|
||||
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)
|
||||
return kdfEngine?.getName(resources) ?: ""
|
||||
}
|
||||
|
||||
fun validatePasswordEncoding(key: String?): Boolean {
|
||||
return pwDatabaseV3?.validatePasswordEncoding(key)
|
||||
?: pwDatabaseV4?.validatePasswordEncoding(key)
|
||||
fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||
return pwDatabaseV3?.validatePasswordEncoding(password, containsKeyFile)
|
||||
?: pwDatabaseV4?.validatePasswordEncoding(password, containsKeyFile)
|
||||
?: false
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
|
||||
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
|
||||
pwDatabaseV3?.retrieveMasterKey(key, keyInputStream)
|
||||
pwDatabaseV4?.retrieveMasterKey(key, keyInputStream)
|
||||
@@ -516,7 +566,7 @@ class Database {
|
||||
return null
|
||||
}
|
||||
|
||||
fun getEntryById(id: PwNodeId<*>): EntryVersioned? {
|
||||
fun getEntryById(id: PwNodeId<UUID>): EntryVersioned? {
|
||||
pwDatabaseV3?.getEntryById(id)?.let {
|
||||
return EntryVersioned(it)
|
||||
}
|
||||
@@ -527,12 +577,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
|
||||
}
|
||||
|
||||
@@ -702,31 +754,28 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
fun addHistoryBackupTo(entry: EntryVersioned): EntryVersioned {
|
||||
val backupEntry = EntryVersioned(entry)
|
||||
fun removeOldestHistory(entry: EntryVersioned) {
|
||||
|
||||
entry.addBackupToHistory()
|
||||
|
||||
// Remove oldest backup if more than max items or max memory
|
||||
// Remove oldest history if more than max items or max memory
|
||||
pwDatabaseV4?.let {
|
||||
val history = entry.getHistory()
|
||||
|
||||
val maxItems = it.historyMaxItems
|
||||
val maxItems = historyMaxItems
|
||||
if (maxItems >= 0) {
|
||||
while (history.size > maxItems) {
|
||||
entry.removeOldestEntryFromHistory()
|
||||
}
|
||||
}
|
||||
|
||||
val maxSize = it.historyMaxSize
|
||||
val maxSize = historyMaxSize
|
||||
if (maxSize >= 0) {
|
||||
while (true) {
|
||||
var histSize: Long = 0
|
||||
for (backup in history) {
|
||||
histSize += backup.size
|
||||
var historySize: Long = 0
|
||||
for (entryHistory in history) {
|
||||
historySize += entryHistory.getSize()
|
||||
}
|
||||
|
||||
if (histSize > maxSize) {
|
||||
if (historySize > maxSize) {
|
||||
entry.removeOldestEntryFromHistory()
|
||||
} else {
|
||||
break
|
||||
@@ -734,8 +783,6 @@ class Database {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return backupEntry
|
||||
}
|
||||
|
||||
companion object : SingletonHolder<Database>(::Database) {
|
||||
|
||||
@@ -15,26 +15,26 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
|
||||
var pwEntryV4: PwEntryV4? = null
|
||||
private set
|
||||
|
||||
fun updateWith(entry: EntryVersioned) {
|
||||
fun updateWith(entry: EntryVersioned, copyHistory: Boolean = true) {
|
||||
entry.pwEntryV3?.let {
|
||||
this.pwEntryV3?.updateWith(it)
|
||||
}
|
||||
entry.pwEntryV4?.let {
|
||||
this.pwEntryV4?.updateWith(it)
|
||||
this.pwEntryV4?.updateWith(it, copyHistory)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this constructor to copy an Entry with exact same values
|
||||
*/
|
||||
constructor(entry: EntryVersioned) {
|
||||
constructor(entry: EntryVersioned, copyHistory: Boolean = true) {
|
||||
if (entry.pwEntryV3 != null) {
|
||||
this.pwEntryV3 = PwEntryV3()
|
||||
}
|
||||
if (entry.pwEntryV4 != null) {
|
||||
this.pwEntryV4 = PwEntryV4()
|
||||
}
|
||||
updateWith(entry)
|
||||
updateWith(entry, copyHistory)
|
||||
}
|
||||
|
||||
constructor(entry: PwEntryV3) {
|
||||
@@ -241,6 +241,10 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
|
||||
return pwEntryV4?.allowCustomFields() ?: false
|
||||
}
|
||||
|
||||
fun removeAllFields() {
|
||||
pwEntryV4?.removeAllFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an extra field to the list (standard or custom)
|
||||
* @param label Label of field, must be unique
|
||||
@@ -258,20 +262,31 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
|
||||
pwEntryV4?.stopToManageFieldReferences()
|
||||
}
|
||||
|
||||
fun addBackupToHistory() {
|
||||
pwEntryV4?.let {
|
||||
val entryHistory = PwEntryV4()
|
||||
entryHistory.updateWith(it)
|
||||
it.addEntryToHistory(entryHistory)
|
||||
fun getHistory(): ArrayList<EntryVersioned> {
|
||||
val history = ArrayList<EntryVersioned>()
|
||||
val entryV4History = pwEntryV4?.history ?: ArrayList()
|
||||
for (entryHistory in entryV4History) {
|
||||
history.add(EntryVersioned(entryHistory))
|
||||
}
|
||||
return history
|
||||
}
|
||||
|
||||
fun addEntryToHistory(entry: EntryVersioned) {
|
||||
entry.pwEntryV4?.let {
|
||||
pwEntryV4?.addEntryToHistory(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllHistory() {
|
||||
pwEntryV4?.removeAllHistory()
|
||||
}
|
||||
|
||||
fun removeOldestEntryFromHistory() {
|
||||
pwEntryV4?.removeOldestEntryFromHistory()
|
||||
}
|
||||
|
||||
fun getHistory(): ArrayList<PwEntryV4> {
|
||||
return pwEntryV4?.history ?: ArrayList()
|
||||
fun getSize(): Long {
|
||||
return pwEntryV4?.size ?: 0L
|
||||
}
|
||||
|
||||
fun containsCustomData(): Boolean {
|
||||
|
||||
@@ -17,25 +17,24 @@
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.file
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.content.res.Resources
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.ObjectNameResource
|
||||
|
||||
// Note: We can get away with using int's to store unsigned 32-bit ints
|
||||
// since we won't do arithmetic on these values (also unlikely to
|
||||
// reach negative ids).
|
||||
enum class PwCompressionAlgorithm constructor(val id: Int) {
|
||||
enum class PwCompressionAlgorithm : ObjectNameResource {
|
||||
|
||||
None(0),
|
||||
Gzip(1);
|
||||
None,
|
||||
GZip;
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromId(num: Int): PwCompressionAlgorithm? {
|
||||
for (e in values()) {
|
||||
if (e.id == num) {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return null
|
||||
override fun getName(resources: Resources): String {
|
||||
return when (this) {
|
||||
None -> resources.getString(R.string.compression_none)
|
||||
GZip -> resources.getString(R.string.compression_gzip)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
*/
|
||||
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.LoadDatabaseInvalidKeyFileException
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseKeyFileEmptyException
|
||||
import com.kunzisoft.keepass.utils.MemoryUtil
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
@@ -34,11 +35,19 @@ import java.security.NoSuchAlgorithmException
|
||||
import java.util.LinkedHashMap
|
||||
import java.util.UUID
|
||||
|
||||
abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Group, Entry>> {
|
||||
abstract class PwDatabase<
|
||||
GroupId,
|
||||
Group : PwGroup<GroupId, Group, Entry>,
|
||||
Entry : PwEntry<Group, Entry>
|
||||
> {
|
||||
|
||||
// Algorithm used to encrypt the database
|
||||
protected var algorithm: PwEncryptionAlgorithm? = null
|
||||
|
||||
abstract val kdfEngine: KdfEngine?
|
||||
|
||||
abstract val kdfAvailableList: List<KdfEngine>
|
||||
|
||||
var masterKey = ByteArray(32)
|
||||
var finalKey: ByteArray? = null
|
||||
protected set
|
||||
@@ -46,8 +55,10 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
var iconFactory = PwIconFactory()
|
||||
protected set
|
||||
|
||||
private var groupIndexes = LinkedHashMap<PwNodeId<*>, Group>()
|
||||
private var entryIndexes = LinkedHashMap<PwNodeId<*>, Entry>()
|
||||
var changeDuplicateId = false
|
||||
|
||||
private var groupIndexes = LinkedHashMap<PwNodeId<GroupId>, Group>()
|
||||
private var entryIndexes = LinkedHashMap<PwNodeId<UUID>, Entry>()
|
||||
|
||||
abstract val version: String
|
||||
|
||||
@@ -67,15 +78,15 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
|
||||
var rootGroup: Group? = null
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
|
||||
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
|
||||
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
|
||||
masterKey = getMasterKey(key, keyInputStream)
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
|
||||
protected fun getCompositeKey(key: String, keyInputStream: InputStream): ByteArray {
|
||||
val fileKey = getFileKey(keyInputStream)
|
||||
val passwordKey = getPasswordKey(key)
|
||||
@@ -115,7 +126,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
return messageDigest.digest()
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
|
||||
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
||||
|
||||
val keyByteArrayOutputStream = ByteArrayOutputStream()
|
||||
@@ -129,7 +140,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
}
|
||||
|
||||
when (keyData.size.toLong()) {
|
||||
0L -> throw KeyFileEmptyException()
|
||||
0L -> throw LoadDatabaseKeyFileEmptyException()
|
||||
32L -> return keyData
|
||||
64L -> try {
|
||||
return hexStringToByteArray(String(keyData))
|
||||
@@ -156,15 +167,18 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
|
||||
protected abstract fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray?
|
||||
|
||||
open fun validatePasswordEncoding(key: String?): Boolean {
|
||||
if (key == null)
|
||||
open fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||
if (password == null && !containsKeyFile)
|
||||
return false
|
||||
|
||||
if (password == null)
|
||||
return true
|
||||
|
||||
val encoding = passwordEncoding
|
||||
|
||||
val bKey: ByteArray
|
||||
try {
|
||||
bKey = key.toByteArray(charset(encoding))
|
||||
bKey = password.toByteArray(charset(encoding))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
return false
|
||||
}
|
||||
@@ -175,7 +189,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
return false
|
||||
}
|
||||
return key == reEncoded
|
||||
return password == reEncoded
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -184,9 +198,9 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
* -------------------------------------
|
||||
*/
|
||||
|
||||
abstract fun newGroupId(): PwNodeId<*>
|
||||
abstract fun newGroupId(): PwNodeId<GroupId>
|
||||
|
||||
abstract fun newEntryId(): PwNodeId<*>
|
||||
abstract fun newEntryId(): PwNodeId<UUID>
|
||||
|
||||
abstract fun createGroup(): Group
|
||||
|
||||
@@ -211,7 +225,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
* ID number to check for
|
||||
* @return True if the ID is used, false otherwise
|
||||
*/
|
||||
fun isGroupIdUsed(id: PwNodeId<*>): Boolean {
|
||||
fun isGroupIdUsed(id: PwNodeId<GroupId>): Boolean {
|
||||
return groupIndexes.containsKey(id)
|
||||
}
|
||||
|
||||
@@ -226,14 +240,21 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupById(id: PwNodeId<*>): Group? {
|
||||
fun getGroupById(id: PwNodeId<GroupId>): Group? {
|
||||
return this.groupIndexes[id]
|
||||
}
|
||||
|
||||
fun addGroupIndex(group: Group) {
|
||||
val groupId = group.nodeId
|
||||
if (groupIndexes.containsKey(groupId)) {
|
||||
Log.e(TAG, "Error, a group with the same UUID $groupId already exists")
|
||||
if (changeDuplicateId) {
|
||||
val newGroupId = newGroupId()
|
||||
group.nodeId = newGroupId
|
||||
group.parent?.addChildGroup(group)
|
||||
this.groupIndexes[newGroupId] = group
|
||||
} else {
|
||||
throw LoadDatabaseDuplicateUuidException(Type.GROUP, groupId)
|
||||
}
|
||||
} else {
|
||||
this.groupIndexes[groupId] = group
|
||||
}
|
||||
@@ -253,7 +274,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
}
|
||||
}
|
||||
|
||||
fun isEntryIdUsed(id: PwNodeId<*>): Boolean {
|
||||
fun isEntryIdUsed(id: PwNodeId<UUID>): Boolean {
|
||||
return entryIndexes.containsKey(id)
|
||||
}
|
||||
|
||||
@@ -261,15 +282,21 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
|
||||
return entryIndexes.values
|
||||
}
|
||||
|
||||
fun getEntryById(id: PwNodeId<*>): Entry? {
|
||||
fun getEntryById(id: PwNodeId<UUID>): 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
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory
|
||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidKeyFileException
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -28,18 +30,25 @@ import java.security.DigestOutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
/**
|
||||
* @author Naomaru Itoi <nao></nao>@phoneid.org>
|
||||
* @author Bill Zwicky <wrzwicky></wrzwicky>@pobox.com>
|
||||
* @author Dominik Reichl <dominik.reichl></dominik.reichl>@t-online.de>
|
||||
*/
|
||||
class PwDatabaseV3 : PwDatabase<PwGroupV3, PwEntryV3>() {
|
||||
class PwDatabaseV3 : PwDatabase<Int, PwGroupV3, PwEntryV3>() {
|
||||
|
||||
private var numKeyEncRounds: Int = 0
|
||||
|
||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||
|
||||
override val version: String
|
||||
get() = "KeePass 1"
|
||||
|
||||
init {
|
||||
kdfListV3.add(KdfFactory.aesKdf)
|
||||
}
|
||||
|
||||
override val kdfEngine: KdfEngine?
|
||||
get() = kdfListV3[0]
|
||||
|
||||
override val kdfAvailableList: List<KdfEngine>
|
||||
get() = kdfListV3
|
||||
|
||||
override val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm>
|
||||
get() {
|
||||
val list = ArrayList<PwEncryptionAlgorithm>()
|
||||
@@ -103,7 +112,7 @@ class PwDatabaseV3 : PwDatabase<PwGroupV3, PwEntryV3>() {
|
||||
return newId
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
|
||||
override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
|
||||
|
||||
return if (key != null && keyInputStream != null) {
|
||||
|
||||
@@ -26,12 +26,9 @@ 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.LoadDatabaseInvalidKeyFileException
|
||||
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
|
||||
@@ -43,14 +40,15 @@ import java.util.*
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
|
||||
class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
class PwDatabaseV4 : PwDatabase<UUID, PwGroupV4, PwEntryV4> {
|
||||
|
||||
var hmacKey: ByteArray? = null
|
||||
private set
|
||||
var dataCipher = AesEngine.CIPHER_UUID
|
||||
private var dataEngine: CipherEngine = AesEngine()
|
||||
var compressionAlgorithm = PwCompressionAlgorithm.Gzip
|
||||
var compressionAlgorithm = PwCompressionAlgorithm.GZip
|
||||
var kdfParameters: KdfParameters? = null
|
||||
private var kdfV4List: MutableList<KdfEngine> = ArrayList()
|
||||
private var numKeyEncRounds: Long = 0
|
||||
var publicCustomData = VariantDictionary()
|
||||
|
||||
@@ -93,6 +91,11 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
|
||||
var localizedAppName = "KeePassDX" // TODO resource
|
||||
|
||||
init {
|
||||
kdfV4List.add(KdfFactory.aesKdf)
|
||||
kdfV4List.add(KdfFactory.argon2Kdf)
|
||||
}
|
||||
|
||||
constructor()
|
||||
|
||||
constructor(databaseName: String) {
|
||||
@@ -107,6 +110,39 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
override val version: String
|
||||
get() = "KeePass 2"
|
||||
|
||||
override val kdfEngine: KdfEngine?
|
||||
get() = try {
|
||||
getEngineV4(kdfParameters)
|
||||
} catch (unknownKDF: UnknownKDF) {
|
||||
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
|
||||
null
|
||||
}
|
||||
|
||||
override val kdfAvailableList: List<KdfEngine>
|
||||
get() = kdfV4List
|
||||
|
||||
@Throws(UnknownKDF::class)
|
||||
fun getEngineV4(kdfParameters: KdfParameters?): KdfEngine {
|
||||
val unknownKDFException = UnknownKDF()
|
||||
if (kdfParameters == null) {
|
||||
throw unknownKDFException
|
||||
}
|
||||
for (engine in kdfV4List) {
|
||||
if (engine.uuid == kdfParameters.uuid) {
|
||||
return engine
|
||||
}
|
||||
}
|
||||
throw unknownKDFException
|
||||
}
|
||||
|
||||
val availableCompressionAlgorithms: List<PwCompressionAlgorithm>
|
||||
get() {
|
||||
val list = ArrayList<PwCompressionAlgorithm>()
|
||||
list.add(PwCompressionAlgorithm.None)
|
||||
list.add(PwCompressionAlgorithm.GZip)
|
||||
return list
|
||||
}
|
||||
|
||||
override val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm>
|
||||
get() {
|
||||
val list = ArrayList<PwEncryptionAlgorithm>()
|
||||
@@ -116,45 +152,45 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
return list
|
||||
}
|
||||
|
||||
val kdfEngine: KdfEngine?
|
||||
get() {
|
||||
return try {
|
||||
KdfFactory.getEngineV4(kdfParameters)
|
||||
} catch (unknownKDF: UnknownKDF) {
|
||||
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override var numberKeyEncryptionRounds: Long
|
||||
get() {
|
||||
val kdfEngine = kdfEngine
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
numKeyEncRounds = kdfEngine!!.getKeyRounds(kdfParameters!!)
|
||||
numKeyEncRounds = kdfEngine.getKeyRounds(kdfParameters!!)
|
||||
return numKeyEncRounds
|
||||
}
|
||||
@Throws(NumberFormatException::class)
|
||||
set(rounds) {
|
||||
val kdfEngine = kdfEngine
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
kdfEngine!!.setKeyRounds(kdfParameters!!, rounds)
|
||||
kdfEngine.setKeyRounds(kdfParameters!!, rounds)
|
||||
numKeyEncRounds = rounds
|
||||
}
|
||||
|
||||
var memoryUsage: Long
|
||||
get() = if (kdfEngine != null && kdfParameters != null) {
|
||||
kdfEngine!!.getMemoryUsage(kdfParameters!!)
|
||||
} else KdfEngine.UNKNOWN_VALUE.toLong()
|
||||
get() {
|
||||
val kdfEngine = kdfEngine
|
||||
return if (kdfEngine != null && kdfParameters != null) {
|
||||
kdfEngine.getMemoryUsage(kdfParameters!!)
|
||||
} else KdfEngine.UNKNOWN_VALUE.toLong()
|
||||
}
|
||||
set(memory) {
|
||||
val kdfEngine = kdfEngine
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
kdfEngine!!.setMemoryUsage(kdfParameters!!, memory)
|
||||
kdfEngine.setMemoryUsage(kdfParameters!!, memory)
|
||||
}
|
||||
|
||||
var parallelism: Int
|
||||
get() = if (kdfEngine != null && kdfParameters != null) {
|
||||
kdfEngine!!.getParallelism(kdfParameters!!)
|
||||
} else KdfEngine.UNKNOWN_VALUE
|
||||
get() {
|
||||
val kdfEngine = kdfEngine
|
||||
return if (kdfEngine != null && kdfParameters != null) {
|
||||
kdfEngine.getParallelism(kdfParameters!!)
|
||||
} else KdfEngine.UNKNOWN_VALUE
|
||||
}
|
||||
set(parallelism) {
|
||||
val kdfEngine = kdfEngine
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
kdfEngine!!.setParallelism(kdfParameters!!, parallelism)
|
||||
kdfEngine.setParallelism(kdfParameters!!, parallelism)
|
||||
}
|
||||
|
||||
override val passwordEncoding: String
|
||||
@@ -200,7 +236,7 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
return getCustomData().isNotEmpty()
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
|
||||
public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
|
||||
|
||||
var masterKey = byteArrayOf()
|
||||
@@ -227,7 +263,7 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
fun makeFinalKey(masterSeed: ByteArray) {
|
||||
|
||||
kdfParameters?.let { keyDerivationFunctionParameters ->
|
||||
val kdfEngine = KdfFactory.getEngineV4(keyDerivationFunctionParameters)
|
||||
val kdfEngine = getEngineV4(keyDerivationFunctionParameters)
|
||||
|
||||
var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters)
|
||||
if (transformedMasterKey.size != 32) {
|
||||
@@ -360,9 +396,7 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
}
|
||||
addGroupTo(recycleBinGroup, rootGroup)
|
||||
recycleBinUUID = recycleBinGroup.id
|
||||
recycleBinGroup.lastModificationTime.date?.let {
|
||||
recycleBinChanged = it
|
||||
}
|
||||
recycleBinChanged = recycleBinGroup.lastModificationTime.date
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,10 +461,10 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
return publicCustomData.size() > 0
|
||||
}
|
||||
|
||||
override fun validatePasswordEncoding(key: String?): Boolean {
|
||||
if (key == null)
|
||||
override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||
if (password == null)
|
||||
return true
|
||||
return super.validatePasswordEncoding(key)
|
||||
return super.validatePasswordEncoding(password, containsKeyFile)
|
||||
}
|
||||
|
||||
override fun clearCache() {
|
||||
|
||||
@@ -19,14 +19,12 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import com.kunzisoft.keepass.utils.Types
|
||||
|
||||
import java.util.Arrays
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Converting from the C Date format to the Java data format is
|
||||
@@ -34,14 +32,14 @@ import java.util.Date
|
||||
*/
|
||||
class PwDate : Parcelable {
|
||||
|
||||
private var jDate: Date? = null
|
||||
private var jDate: Date = Date()
|
||||
private var jDateBuilt = false
|
||||
@Transient
|
||||
private var cDate: ByteArray? = null
|
||||
@Transient
|
||||
private var cDateBuilt = false
|
||||
|
||||
val date: Date?
|
||||
val date: Date
|
||||
get() {
|
||||
if (!jDateBuilt) {
|
||||
jDate = readTime(cDate, 0, calendar)
|
||||
@@ -68,9 +66,7 @@ class PwDate : Parcelable {
|
||||
}
|
||||
|
||||
constructor(source: PwDate) {
|
||||
if (source.jDate != null) {
|
||||
this.jDate = Date(source.jDate!!.time)
|
||||
}
|
||||
this.jDate = Date(source.jDate.time)
|
||||
this.jDateBuilt = source.jDateBuilt
|
||||
|
||||
if (source.cDate != null) {
|
||||
@@ -106,6 +102,10 @@ class PwDate : Parcelable {
|
||||
return 0
|
||||
}
|
||||
|
||||
fun getDateTimeString(resources: Resources): String {
|
||||
return Companion.getDateTimeString(resources, this.date)
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeSerializable(date)
|
||||
dest.writeByte((if (jDateBuilt) 1 else 0).toByte())
|
||||
@@ -135,7 +135,7 @@ class PwDate : Parcelable {
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = jDate?.hashCode() ?: 0
|
||||
var result = jDate.hashCode()
|
||||
result = 31 * result + jDateBuilt.hashCode()
|
||||
result = 31 * result + (cDate?.contentHashCode() ?: 0)
|
||||
result = 31 * result + cDateBuilt.hashCode()
|
||||
@@ -280,5 +280,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
* Update with deep copy of each entry element
|
||||
* @param source
|
||||
*/
|
||||
fun updateWith(source: PwEntryV4) {
|
||||
fun updateWith(source: PwEntryV4, copyHistory: Boolean = true) {
|
||||
super.updateWith(source)
|
||||
iconCustom = PwIconCustom(source.iconCustom)
|
||||
usageCount = source.usageCount
|
||||
@@ -146,7 +146,8 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
overrideURL = source.overrideURL
|
||||
autoType = AutoType(source.autoType)
|
||||
history.clear()
|
||||
history.addAll(source.history)
|
||||
if (copyHistory)
|
||||
history.addAll(source.history)
|
||||
url = source.url
|
||||
additional = source.additional
|
||||
tags = source.tags
|
||||
@@ -263,6 +264,10 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
return true
|
||||
}
|
||||
|
||||
fun removeAllFields() {
|
||||
fields.clear()
|
||||
}
|
||||
|
||||
fun addExtraField(label: String, value: ProtectedString) {
|
||||
fields[label] = value
|
||||
}
|
||||
@@ -287,6 +292,10 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
history.add(entry)
|
||||
}
|
||||
|
||||
fun removeAllHistory() {
|
||||
history.clear()
|
||||
}
|
||||
|
||||
fun removeOldestEntryFromHistory() {
|
||||
var min: Date? = null
|
||||
var index = -1
|
||||
@@ -294,7 +303,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
|
||||
for (i in history.indices) {
|
||||
val entry = history[i]
|
||||
val lastMod = entry.lastModificationTime.date
|
||||
if (min == null || lastMod == null || lastMod.before(min)) {
|
||||
if (min == null || lastMod.before(min)) {
|
||||
index = i
|
||||
min = lastMod
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ abstract class PwGroup
|
||||
protected fun updateWith(source: PwGroup<Id, Group, Entry>) {
|
||||
super.updateWith(source)
|
||||
titleGroup = source.titleGroup
|
||||
childGroups.clear()
|
||||
childGroups.addAll(source.childGroups)
|
||||
childEntries.clear()
|
||||
childEntries.addAll(source.childEntries)
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ abstract class PwNode<IdType, Parent : PwGroupInterface<Parent, Entry>, Entry :
|
||||
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
|
||||
.before(LocalDate.fromDateFields(PwDate.NEVER_EXPIRE).minusMonths(1).toDate())
|
||||
set(value) {
|
||||
if (!value) {
|
||||
expiryTime = PwDate.PW_NEVER_EXPIRE
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class ArcFourException : InvalidDBException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = 2103983626687861237L
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
import android.net.Uri
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class ContentFileNotFoundException : FileNotFoundException() {
|
||||
companion object {
|
||||
fun getInstance(uri: Uri?): FileNotFoundException {
|
||||
if (uri == null) {
|
||||
return FileNotFoundException()
|
||||
}
|
||||
|
||||
val scheme = uri.scheme
|
||||
return if (scheme != null
|
||||
&& scheme.isNotEmpty()
|
||||
&& scheme.equals("content", ignoreCase = true)) {
|
||||
ContentFileNotFoundException()
|
||||
} else FileNotFoundException()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,10 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class PwDbOutputException : Exception {
|
||||
class DatabaseOutputException : 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
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class InvalidAlgorithmException : InvalidDBException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = 3062682891863487208L
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
open class InvalidDBException : Exception {
|
||||
|
||||
constructor(str: String) : super(str)
|
||||
|
||||
constructor() : super()
|
||||
|
||||
companion object {
|
||||
private const val serialVersionUID = 5191964825154190923L
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class InvalidDBSignatureException : InvalidDBException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = -5358923878743513758L
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class InvalidDBVersionException : InvalidDBException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = -4260650987856400586L
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/package com.kunzisoft.keepass.database.exception
|
||||
|
||||
open class InvalidKeyFileException : InvalidDBException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = 5540694419562294464L
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class InvalidPasswordException : InvalidDBException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = -8729476180242058319L
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class KeyFileEmptyException : InvalidKeyFileException() {
|
||||
companion object {
|
||||
private const val serialVersionUID = -1630780661204212325L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
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
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
class LoadDatabaseArcFourException :
|
||||
LoadDatabaseException(R.string.error_arc4)
|
||||
|
||||
class LoadDatabaseFileNotFoundException :
|
||||
LoadDatabaseException(R.string.file_not_found_content)
|
||||
|
||||
class LoadDatabaseInvalidAlgorithmException :
|
||||
LoadDatabaseException(R.string.invalid_algorithm)
|
||||
|
||||
class LoadDatabaseDuplicateUuidException(type: Type, uuid: PwNodeId<*>):
|
||||
LoadDatabaseException(R.string.invalid_db_same_uuid, type.name, uuid.toString())
|
||||
|
||||
class LoadDatabaseIOException(exception: IOException) :
|
||||
LoadDatabaseException(exception, R.string.error_load_database)
|
||||
|
||||
class LoadDatabaseKDFMemoryException(exception: IOException) :
|
||||
LoadDatabaseException(exception, R.string.error_load_database_KDF_memory)
|
||||
|
||||
class LoadDatabaseSignatureException :
|
||||
LoadDatabaseException(R.string.invalid_db_sig)
|
||||
|
||||
class LoadDatabaseVersionException :
|
||||
LoadDatabaseException(R.string.unsupported_db_version)
|
||||
|
||||
open class LoadDatabaseInvalidKeyFileException :
|
||||
LoadDatabaseException(R.string.keyfile_does_not_exist)
|
||||
|
||||
class LoadDatabaseInvalidPasswordException :
|
||||
LoadDatabaseException(R.string.invalid_password)
|
||||
|
||||
class LoadDatabaseKeyFileEmptyException :
|
||||
LoadDatabaseException(R.string.keyfile_is_empty)
|
||||
|
||||
class LoadDatabaseNoMemoryException(exception: OutOfMemoryError) :
|
||||
LoadDatabaseException(exception, R.string.error_out_of_memory)
|
||||
|
||||
open class LoadDatabaseException : Exception {
|
||||
|
||||
@StringRes
|
||||
var errorId: Int = R.string.error_load_database
|
||||
var parameters: (Array<out String>)? = null
|
||||
|
||||
constructor(errorMessageId: Int) : super() {
|
||||
errorId = errorMessageId
|
||||
}
|
||||
|
||||
constructor(errorMessageId: Int, vararg params: String) : super() {
|
||||
errorId = errorMessageId
|
||||
parameters = params
|
||||
}
|
||||
|
||||
constructor(throwable: Throwable, errorMessageId: Int? = null) : super(throwable) {
|
||||
errorMessageId?.let {
|
||||
errorId = it
|
||||
}
|
||||
}
|
||||
|
||||
constructor() : super()
|
||||
|
||||
fun getLocalizedMessage(resources: Resources): String {
|
||||
parameters?.let {
|
||||
return resources.getString(errorId, *it)
|
||||
} ?: return resources.getString(errorId)
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,4 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class SamsungClipboardException(e: Exception) : Exception(e) {
|
||||
companion object {
|
||||
private const val serialVersionUID = -3168837280393843509L
|
||||
}
|
||||
|
||||
}
|
||||
class SamsungClipboardException(e: Exception) : Exception(e)
|
||||
|
||||
@@ -2,8 +2,4 @@ package com.kunzisoft.keepass.database.exception
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
class UnknownKDF : IOException(message) {
|
||||
companion object {
|
||||
private const val message = "Unknown key derivation function"
|
||||
}
|
||||
}
|
||||
class UnknownKDF : IOException("Unknown key derivation function")
|
||||
|
||||
@@ -24,11 +24,8 @@ import com.kunzisoft.keepass.crypto.keyDerivation.AesKdf
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
||||
import com.kunzisoft.keepass.database.NodeHandler
|
||||
import com.kunzisoft.keepass.database.element.PwNodeV4Interface
|
||||
import com.kunzisoft.keepass.database.element.PwDatabaseV4
|
||||
import com.kunzisoft.keepass.database.element.PwEntryV4
|
||||
import com.kunzisoft.keepass.database.element.PwGroupV4
|
||||
import com.kunzisoft.keepass.database.exception.InvalidDBVersionException
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseVersionException
|
||||
import com.kunzisoft.keepass.stream.CopyInputStream
|
||||
import com.kunzisoft.keepass.stream.HmacBlockStream
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
@@ -51,10 +48,10 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
|
||||
// version < FILE_VERSION_32_4)
|
||||
var transformSeed: ByteArray?
|
||||
get() = databaseV4.kdfParameters?.getByteArray(AesKdf.ParamSeed)
|
||||
get() = databaseV4.kdfParameters?.getByteArray(AesKdf.PARAM_SEED)
|
||||
private set(seed) {
|
||||
assignAesKdfEngineIfNotExists()
|
||||
databaseV4.kdfParameters?.setByteArray(AesKdf.ParamSeed, seed)
|
||||
databaseV4.kdfParameters?.setByteArray(AesKdf.PARAM_SEED, seed)
|
||||
}
|
||||
|
||||
object PwDbHeaderV4Fields {
|
||||
@@ -133,9 +130,9 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
/** Assumes the input stream is at the beginning of the .kdbx file
|
||||
* @param inputStream
|
||||
* @throws IOException
|
||||
* @throws InvalidDBVersionException
|
||||
* @throws LoadDatabaseVersionException
|
||||
*/
|
||||
@Throws(IOException::class, InvalidDBVersionException::class)
|
||||
@Throws(IOException::class, LoadDatabaseVersionException::class)
|
||||
fun loadFromFile(inputStream: InputStream): HeaderAndHash {
|
||||
val messageDigest: MessageDigest
|
||||
try {
|
||||
@@ -153,12 +150,12 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
val sig2 = littleEndianDataInputStream.readInt()
|
||||
|
||||
if (!matchesHeader(sig1, sig2)) {
|
||||
throw InvalidDBVersionException()
|
||||
throw LoadDatabaseVersionException()
|
||||
}
|
||||
|
||||
version = littleEndianDataInputStream.readUInt() // Erase previous value
|
||||
if (!validVersion(version)) {
|
||||
throw InvalidDBVersionException()
|
||||
throw LoadDatabaseVersionException()
|
||||
}
|
||||
|
||||
var done = false
|
||||
@@ -229,7 +226,9 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
}
|
||||
|
||||
private fun assignAesKdfEngineIfNotExists() {
|
||||
if (databaseV4.kdfParameters == null || databaseV4.kdfParameters!!.uuid != KdfFactory.aesKdf.uuid) {
|
||||
val kdfParams = databaseV4.kdfParameters
|
||||
if (kdfParams == null
|
||||
|| kdfParams.uuid != KdfFactory.aesKdf.uuid) {
|
||||
databaseV4.kdfParameters = KdfFactory.aesKdf.defaultParameters
|
||||
}
|
||||
}
|
||||
@@ -246,7 +245,7 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
private fun setTransformRound(roundsByte: ByteArray?) {
|
||||
assignAesKdfEngineIfNotExists()
|
||||
val rounds = LEDataInputStream.readLong(roundsByte!!, 0)
|
||||
databaseV4.kdfParameters?.setUInt64(AesKdf.ParamRounds, rounds)
|
||||
databaseV4.kdfParameters?.setUInt64(AesKdf.PARAM_ROUNDS, rounds)
|
||||
databaseV4.numberKeyEncryptionRounds = rounds
|
||||
}
|
||||
|
||||
@@ -261,7 +260,7 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
throw IOException("Unrecognized compression flag.")
|
||||
}
|
||||
|
||||
PwCompressionAlgorithm.fromId(flag)?.let { compression ->
|
||||
getCompressionFromFlag(flag)?.let { compression ->
|
||||
databaseV4.compressionAlgorithm = compression
|
||||
}
|
||||
}
|
||||
@@ -299,6 +298,21 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
|
||||
const val FILE_VERSION_32_3: Long = 0x00030001
|
||||
const val FILE_VERSION_32_4: Long = 0x00040000
|
||||
|
||||
fun getCompressionFromFlag(flag: Int): PwCompressionAlgorithm? {
|
||||
return when (flag) {
|
||||
0 -> PwCompressionAlgorithm.None
|
||||
1 -> PwCompressionAlgorithm.GZip
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun getFlagFromCompression(compression: PwCompressionAlgorithm): Int {
|
||||
return when (compression) {
|
||||
PwCompressionAlgorithm.GZip -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
fun matchesHeader(sig1: Int, sig2: Int): Boolean {
|
||||
return sig1 == PWM_DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
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<PwDb : PwDatabase<*, *>> {
|
||||
abstract class Importer<PwDb : PwDatabase<*, *, *>> {
|
||||
|
||||
/**
|
||||
* Load a versioned database file, return contents in a new PwDatabase.
|
||||
@@ -36,9 +36,9 @@ abstract class Importer<PwDb : PwDatabase<*, *>> {
|
||||
* @return new PwDatabase container.
|
||||
*
|
||||
* @throws IOException on any file error.
|
||||
* @throws InvalidDBException on database error.
|
||||
* @throws LoadDatabaseException on database error.
|
||||
*/
|
||||
@Throws(IOException::class, InvalidDBException::class)
|
||||
@Throws(IOException::class, LoadDatabaseException::class)
|
||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||
password: String?,
|
||||
keyInputStream: InputStream?,
|
||||
|
||||
@@ -73,7 +73,7 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
|
||||
|
||||
private lateinit var mDatabaseToOpen: PwDatabaseV3
|
||||
|
||||
@Throws(IOException::class, InvalidDBException::class)
|
||||
@Throws(IOException::class, LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
password: String?,
|
||||
keyInputStream: InputStream?,
|
||||
@@ -92,11 +92,11 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
|
||||
hdr.loadFromFile(filebuf, 0)
|
||||
|
||||
if (hdr.signature1 != PwDbHeader.PWM_DBSIG_1 || hdr.signature2 != PwDbHeaderV3.DBSIG_2) {
|
||||
throw InvalidDBSignatureException()
|
||||
throw LoadDatabaseSignatureException()
|
||||
}
|
||||
|
||||
if (!hdr.matchesVersion()) {
|
||||
throw InvalidDBVersionException()
|
||||
throw LoadDatabaseVersionException()
|
||||
}
|
||||
|
||||
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
||||
@@ -109,7 +109,7 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
|
||||
} else if (hdr.flags and PwDbHeaderV3.FLAG_TWOFISH != 0) {
|
||||
mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.Twofish
|
||||
} else {
|
||||
throw InvalidAlgorithmException()
|
||||
throw LoadDatabaseInvalidAlgorithmException()
|
||||
}
|
||||
|
||||
mDatabaseToOpen.numberKeyEncryptionRounds = hdr.numKeyEncRounds.toLong()
|
||||
@@ -152,7 +152,7 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
|
||||
} catch (e1: IllegalBlockSizeException) {
|
||||
throw IOException("Invalid block size")
|
||||
} catch (e1: BadPaddingException) {
|
||||
throw InvalidPasswordException()
|
||||
throw LoadDatabaseInvalidPasswordException()
|
||||
}
|
||||
|
||||
val md: MessageDigest
|
||||
@@ -171,7 +171,7 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
|
||||
if (!Arrays.equals(hash, hdr.contentsHash)) {
|
||||
|
||||
Log.w(TAG, "Database file did not decrypt correctly. (checksum code is broken)")
|
||||
throw InvalidPasswordException()
|
||||
throw LoadDatabaseInvalidPasswordException()
|
||||
}
|
||||
|
||||
// New manual root because V3 contains multiple root groups (here available with getRootGroups())
|
||||
|
||||
@@ -24,11 +24,11 @@ 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.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.exception.LoadDatabaseArcFourException
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidPasswordException
|
||||
import com.kunzisoft.keepass.database.file.PwDbHeaderV4
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedBinary
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
@@ -56,7 +56,8 @@ import javax.crypto.Cipher
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import kotlin.math.min
|
||||
|
||||
class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
|
||||
class ImporterV4(private val streamDir: File,
|
||||
private val fixDuplicateUUID: Boolean = false) : Importer<PwDatabaseV4>() {
|
||||
|
||||
private var randomStream: StreamCipher? = null
|
||||
private lateinit var mDatabase: PwDatabaseV4
|
||||
@@ -89,7 +90,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
|
||||
private var entryCustomDataKey: String? = null
|
||||
private var entryCustomDataValue: String? = null
|
||||
|
||||
@Throws(IOException::class, InvalidDBException::class)
|
||||
@Throws(IOException::class, LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
password: String?,
|
||||
keyInputStream: InputStream?,
|
||||
@@ -99,6 +100,9 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
|
||||
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
||||
|
||||
mDatabase = PwDatabaseV4()
|
||||
|
||||
mDatabase.changeDuplicateId = fixDuplicateUUID
|
||||
|
||||
val header = PwDbHeaderV4(mDatabase)
|
||||
|
||||
val headerAndHash = header.loadFromFile(databaseInputStream)
|
||||
@@ -138,14 +142,14 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
|
||||
try {
|
||||
storedStartBytes = dataDecrypted.readBytes(32)
|
||||
if (storedStartBytes == null || storedStartBytes.size != 32) {
|
||||
throw InvalidPasswordException()
|
||||
throw LoadDatabaseInvalidPasswordException()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw InvalidPasswordException()
|
||||
throw LoadDatabaseInvalidPasswordException()
|
||||
}
|
||||
|
||||
if (!Arrays.equals(storedStartBytes, header.streamStartBytes)) {
|
||||
throw InvalidPasswordException()
|
||||
throw LoadDatabaseInvalidPasswordException()
|
||||
}
|
||||
|
||||
isPlain = HashedBlockInputStream(dataDecrypted)
|
||||
@@ -153,18 +157,18 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
|
||||
val isData = LEDataInputStream(databaseInputStream)
|
||||
val storedHash = isData.readBytes(32)
|
||||
if (!Arrays.equals(storedHash, hashOfHeader)) {
|
||||
throw InvalidDBException()
|
||||
throw LoadDatabaseException()
|
||||
}
|
||||
|
||||
val hmacKey = mDatabase.hmacKey ?: throw InvalidDBException()
|
||||
val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException()
|
||||
val headerHmac = PwDbHeaderV4.computeHeaderHmac(pbHeader, hmacKey)
|
||||
val storedHmac = isData.readBytes(32)
|
||||
if (storedHmac == null || storedHmac.size != 32) {
|
||||
throw InvalidDBException()
|
||||
throw LoadDatabaseException()
|
||||
}
|
||||
// Mac doesn't match
|
||||
if (!Arrays.equals(headerHmac, storedHmac)) {
|
||||
throw InvalidDBException()
|
||||
throw LoadDatabaseException()
|
||||
}
|
||||
|
||||
val hmIs = HmacBlockInputStream(isData, true, hmacKey)
|
||||
@@ -173,10 +177,9 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
|
||||
}
|
||||
|
||||
val isXml: InputStream
|
||||
if (mDatabase.compressionAlgorithm === PwCompressionAlgorithm.Gzip) {
|
||||
isXml = GZIPInputStream(isPlain)
|
||||
} else {
|
||||
isXml = isPlain
|
||||
isXml = when(mDatabase.compressionAlgorithm) {
|
||||
PwCompressionAlgorithm.GZip -> GZIPInputStream(isPlain)
|
||||
else -> isPlain
|
||||
}
|
||||
|
||||
if (version >= PwDbHeaderV4.FILE_VERSION_32_4) {
|
||||
@@ -186,7 +189,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
|
||||
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
|
||||
|
||||
if (randomStream == null) {
|
||||
throw ArcFourException()
|
||||
throw LoadDatabaseArcFourException()
|
||||
}
|
||||
|
||||
readXmlStreamed(isXml)
|
||||
@@ -271,7 +274,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
|
||||
Binaries
|
||||
}
|
||||
|
||||
@Throws(IOException::class, InvalidDBException::class)
|
||||
@Throws(IOException::class, LoadDatabaseException::class)
|
||||
private fun readXmlStreamed(readerStream: InputStream) {
|
||||
try {
|
||||
readDocumentStreamed(createPullParser(readerStream))
|
||||
@@ -282,7 +285,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
|
||||
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class, InvalidDBException::class)
|
||||
@Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class)
|
||||
private fun readDocumentStreamed(xpp: XmlPullParser) {
|
||||
|
||||
ctxGroups.clear()
|
||||
@@ -313,7 +316,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
|
||||
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 +340,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
|
||||
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 +358,6 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
|
||||
} 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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<Header : PwDbHeader> 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<Header : PwDbHeader> 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
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -66,7 +66,6 @@ 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,
|
||||
@@ -122,18 +121,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.
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -83,11 +83,16 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<Preference>(getString(R.string.settings_database_change_credentials_key))?.apply {
|
||||
findPreference<Preference>(getString(R.string.settings_database_security_key))?.apply {
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
fragmentManager?.let { fragmentManager ->
|
||||
AssignMasterKeyDialogFragment().show(fragmentManager, "passwordDialog")
|
||||
}
|
||||
mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.DATABASE_SECURITY)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<Preference>(getString(R.string.settings_database_credentials_key))?.apply {
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.DATABASE_MASTER_KEY)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,36 +22,37 @@ package com.kunzisoft.keepass.settings
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.autofill.AutofillManager
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.preference.*
|
||||
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.KeyboardExplanationDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.UnderDevelopmentFeatureDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.*
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.Education
|
||||
import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm
|
||||
import com.kunzisoft.keepass.education.Education
|
||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
||||
import com.kunzisoft.keepass.settings.preference.DialogListExplanationPreference
|
||||
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
|
||||
import com.kunzisoft.keepass.settings.preference.InputNumberPreference
|
||||
import com.kunzisoft.keepass.settings.preference.InputTextPreference
|
||||
import com.kunzisoft.keepass.settings.preference.*
|
||||
import com.kunzisoft.keepass.settings.preference.DialogColorPreference.Companion.DISABLE_COLOR
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.*
|
||||
|
||||
class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener {
|
||||
@@ -61,12 +62,13 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
|
||||
private var mCount = 0
|
||||
|
||||
private var mRoundPref: InputNumberPreference? = null
|
||||
private var mMemoryPref: InputNumberPreference? = null
|
||||
private var mParallelismPref: InputNumberPreference? = null
|
||||
private var databaseCustomColorPref: DialogColorPreference? = null
|
||||
private var mRoundPref: InputKdfNumberPreference? = null
|
||||
private var mMemoryPref: InputKdfNumberPreference? = null
|
||||
private var mParallelismPref: InputKdfNumberPreference? = null
|
||||
|
||||
enum class Screen {
|
||||
APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, DATABASE, APPEARANCE
|
||||
APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -74,7 +76,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
|
||||
activity?.let { activity ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val autoFillEnablePreference: SwitchPreference? = findPreference<SwitchPreference>(getString(R.string.settings_autofill_enable_key))
|
||||
val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key))
|
||||
if (autoFillEnablePreference != null) {
|
||||
val autofillManager = activity.getSystemService(AutofillManager::class.java)
|
||||
autoFillEnablePreference.isChecked = autofillManager != null
|
||||
@@ -110,6 +112,12 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
Screen.DATABASE -> {
|
||||
onCreateDatabasePreference(rootKey)
|
||||
}
|
||||
Screen.DATABASE_SECURITY -> {
|
||||
onCreateDatabaseSecurityPreference(rootKey)
|
||||
}
|
||||
Screen.DATABASE_MASTER_KEY -> {
|
||||
onCreateDatabaseMasterKeyPreference(rootKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +147,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
setPreferencesFromResource(R.xml.preferences_form_filling, rootKey)
|
||||
|
||||
activity?.let { activity ->
|
||||
val autoFillEnablePreference: SwitchPreference? = findPreference<SwitchPreference>(getString(R.string.settings_autofill_enable_key))
|
||||
val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val autofillManager = activity.getSystemService(AutofillManager::class.java)
|
||||
if (autofillManager != null && autofillManager.hasEnabledAutofillServices())
|
||||
@@ -217,7 +225,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
setPreferencesFromResource(R.xml.preferences_advanced_unlock, rootKey)
|
||||
|
||||
activity?.let { activity ->
|
||||
val biometricUnlockEnablePreference: SwitchPreference? = findPreference<SwitchPreference>(getString(R.string.biometric_unlock_enable_key))
|
||||
val biometricUnlockEnablePreference: SwitchPreference? = findPreference(getString(R.string.biometric_unlock_enable_key))
|
||||
// < M solve verifyError exception
|
||||
var biometricUnlockSupported = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
@@ -240,7 +248,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
}
|
||||
}
|
||||
|
||||
val deleteKeysFingerprints: Preference? = findPreference<Preference>(getString(R.string.biometric_delete_all_key_key))
|
||||
val deleteKeysFingerprints: Preference? = findPreference(getString(R.string.biometric_delete_all_key_key))
|
||||
if (!biometricUnlockSupported) {
|
||||
deleteKeysFingerprints?.isEnabled = false
|
||||
} else {
|
||||
@@ -338,26 +346,54 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
|
||||
if (mDatabase.loaded) {
|
||||
|
||||
val dbGeneralPrefCategory: PreferenceCategory? = findPreference<PreferenceCategory>(getString(R.string.database_general_key))
|
||||
val dbGeneralPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_general_key))
|
||||
|
||||
// Db name
|
||||
val dbNamePref: InputTextPreference? = findPreference<InputTextPreference>(getString(R.string.database_name_key))
|
||||
// Database name
|
||||
val dbNamePref: InputTextPreference? = findPreference(getString(R.string.database_name_key))
|
||||
if (mDatabase.containsName()) {
|
||||
dbNamePref?.summary = mDatabase.name
|
||||
} else {
|
||||
dbGeneralPrefCategory?.removePreference(dbNamePref)
|
||||
}
|
||||
|
||||
// Db description
|
||||
val dbDescriptionPref: InputTextPreference? = findPreference<InputTextPreference>(getString(R.string.database_description_key))
|
||||
// Database description
|
||||
val dbDescriptionPref: InputTextPreference? = findPreference(getString(R.string.database_description_key))
|
||||
if (mDatabase.containsDescription()) {
|
||||
dbDescriptionPref?.summary = mDatabase.description
|
||||
} else {
|
||||
dbGeneralPrefCategory?.removePreference(dbDescriptionPref)
|
||||
}
|
||||
|
||||
// Database default username
|
||||
val dbDefaultUsername: InputTextPreference? = findPreference(getString(R.string.database_default_username_key))
|
||||
if (mDatabase.containsDefaultUsername()) {
|
||||
dbDefaultUsername?.summary = mDatabase.defaultUsername
|
||||
} else {
|
||||
dbGeneralPrefCategory?.removePreference(dbDefaultUsername)
|
||||
}
|
||||
|
||||
// Database custom color
|
||||
databaseCustomColorPref = findPreference(getString(R.string.database_custom_color_key))
|
||||
if (mDatabase.containsCustomColor()) {
|
||||
databaseCustomColorPref?.apply {
|
||||
try {
|
||||
color = Color.parseColor(mDatabase.color)
|
||||
summary = mDatabase.color
|
||||
} catch (e: Exception) {
|
||||
color = DISABLE_COLOR
|
||||
summary = ""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dbGeneralPrefCategory?.removePreference(databaseCustomColorPref)
|
||||
}
|
||||
|
||||
// Database compression
|
||||
findPreference<Preference>(getString(R.string.database_data_compression_key))
|
||||
?.summary = (mDatabase.compressionAlgorithm ?: PwCompressionAlgorithm.None).getName(resources)
|
||||
|
||||
// Recycle bin
|
||||
val recycleBinPref: SwitchPreference? = findPreference<SwitchPreference>(getString(R.string.recycle_bin_key))
|
||||
val recycleBinPref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_key))
|
||||
// TODO Recycle
|
||||
dbGeneralPrefCategory?.removePreference(recycleBinPref) // To delete
|
||||
if (mDatabase.isRecycleBinAvailable) {
|
||||
@@ -371,6 +407,26 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
findPreference<Preference>(getString(R.string.database_version_key))
|
||||
?.summary = mDatabase.getVersion()
|
||||
|
||||
findPreference<PreferenceCategory>(getString(R.string.database_history_key))
|
||||
?.isVisible = mDatabase.manageHistory == true
|
||||
|
||||
// Max history items
|
||||
findPreference<InputNumberPreference>(getString(R.string.max_history_items_key))
|
||||
?.summary = mDatabase.historyMaxItems.toString()
|
||||
|
||||
// Max history size
|
||||
findPreference<InputNumberPreference>(getString(R.string.max_history_size_key))
|
||||
?.summary = mDatabase.historyMaxSize.toString()
|
||||
|
||||
} else {
|
||||
Log.e(javaClass.name, "Database isn't ready")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCreateDatabaseSecurityPreference(rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preferences_database_security, rootKey)
|
||||
|
||||
if (mDatabase.loaded) {
|
||||
// Encryption Algorithm
|
||||
findPreference<DialogListExplanationPreference>(getString(R.string.encryption_algorithm_key))
|
||||
?.summary = mDatabase.getEncryptionAlgorithmName(resources)
|
||||
@@ -380,24 +436,41 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
?.summary = mDatabase.getKeyDerivationName(resources)
|
||||
|
||||
// Round encryption
|
||||
mRoundPref = findPreference<InputNumberPreference>(getString(R.string.transform_rounds_key))
|
||||
mRoundPref?.summary = mDatabase.numberKeyEncryptionRoundsAsString
|
||||
mRoundPref = findPreference(getString(R.string.transform_rounds_key))
|
||||
mRoundPref?.summary = mDatabase.numberKeyEncryptionRounds.toString()
|
||||
|
||||
// Memory Usage
|
||||
mMemoryPref = findPreference<InputNumberPreference>(getString(R.string.memory_usage_key))
|
||||
mMemoryPref?.summary = mDatabase.memoryUsageAsString
|
||||
mMemoryPref = findPreference(getString(R.string.memory_usage_key))
|
||||
mMemoryPref?.summary = mDatabase.memoryUsage.toString()
|
||||
|
||||
// Parallelism
|
||||
mParallelismPref = findPreference<InputNumberPreference>(getString(R.string.parallelism_key))
|
||||
mParallelismPref?.summary = mDatabase.parallelismAsString
|
||||
mParallelismPref = findPreference(getString(R.string.parallelism_key))
|
||||
mParallelismPref?.summary = mDatabase.parallelism.toString()
|
||||
} else {
|
||||
Log.e(javaClass.name, "Database isn't ready")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCreateDatabaseMasterKeyPreference(rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preferences_database_master_key, rootKey)
|
||||
|
||||
if (mDatabase.loaded) {
|
||||
findPreference<Preference>(getString(R.string.settings_database_change_credentials_key))?.apply {
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
fragmentManager?.let { fragmentManager ->
|
||||
AssignMasterKeyDialogFragment.getInstance(mDatabase.allowNoMasterKey())
|
||||
.show(fragmentManager, "passwordDialog")
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(javaClass.name, "Database isn't ready")
|
||||
}
|
||||
}
|
||||
|
||||
private fun allowCopyPassword() {
|
||||
val copyPasswordPreference: SwitchPreference? = findPreference<SwitchPreference>(getString(R.string.allow_copy_password_key))
|
||||
val copyPasswordPreference: SwitchPreference? = findPreference(getString(R.string.allow_copy_password_key))
|
||||
copyPasswordPreference?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue as Boolean && context != null) {
|
||||
val message = getString(R.string.allow_copy_password_warning) +
|
||||
@@ -447,6 +520,27 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
}
|
||||
}
|
||||
|
||||
private val colorSelectedListener: ((Boolean, Int)-> Unit)? = { enable, color ->
|
||||
databaseCustomColorPref?.summary = ChromaUtil.getFormattedColorString(color, false)
|
||||
if (enable) {
|
||||
databaseCustomColorPref?.color = color
|
||||
} else {
|
||||
databaseCustomColorPref?.color = DISABLE_COLOR
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
try {
|
||||
// To reassign color listener after orientation change
|
||||
val chromaDialog = fragmentManager?.findFragmentByTag(TAG_PREF_FRAGMENT) as DatabaseColorPreferenceDialogFragmentCompat?
|
||||
chromaDialog?.onColorSelectedListener = colorSelectedListener
|
||||
} catch (e: Exception) {}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onDisplayPreferenceDialog(preference: Preference?) {
|
||||
|
||||
var otherDialogFragment = false
|
||||
@@ -461,6 +555,23 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
preference.key == getString(R.string.database_description_key) -> {
|
||||
dialogFragment = DatabaseDescriptionPreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||
}
|
||||
preference.key == getString(R.string.database_default_username_key) -> {
|
||||
dialogFragment = DatabaseDefaultUsernamePreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||
}
|
||||
preference.key == getString(R.string.database_custom_color_key) -> {
|
||||
dialogFragment = DatabaseColorPreferenceDialogFragmentCompat.newInstance(preference.key).apply {
|
||||
onColorSelectedListener = colorSelectedListener
|
||||
}
|
||||
}
|
||||
preference.key == getString(R.string.database_data_compression_key) -> {
|
||||
dialogFragment = DatabaseDataCompressionPreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||
}
|
||||
preference.key == getString(R.string.max_history_items_key) -> {
|
||||
dialogFragment = MaxHistoryItemsPreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||
}
|
||||
preference.key == getString(R.string.max_history_size_key) -> {
|
||||
dialogFragment = MaxHistorySizePreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||
}
|
||||
preference.key == getString(R.string.encryption_algorithm_key) -> {
|
||||
dialogFragment = DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||
}
|
||||
@@ -486,7 +597,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
|
||||
if (dialogFragment != null && !mDatabaseReadOnly) {
|
||||
dialogFragment.setTargetFragment(this, 0)
|
||||
dialogFragment.show(fragmentManager, null)
|
||||
dialogFragment.show(fragmentManager, TAG_PREF_FRAGMENT)
|
||||
}
|
||||
// Could not be handled here. Try with the super method.
|
||||
else if (otherDialogFragment) {
|
||||
@@ -510,6 +621,8 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
|
||||
private const val TAG_KEY = "NESTED_KEY"
|
||||
|
||||
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
|
||||
|
||||
private const val REQUEST_CODE_AUTOFILL = 5201
|
||||
|
||||
@JvmOverloads
|
||||
@@ -529,8 +642,10 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
|
||||
Screen.APPLICATION -> resources.getString(R.string.menu_app_settings)
|
||||
Screen.FORM_FILLING -> resources.getString(R.string.menu_form_filling_settings)
|
||||
Screen.ADVANCED_UNLOCK -> resources.getString(R.string.menu_advanced_unlock_settings)
|
||||
Screen.DATABASE -> resources.getString(R.string.menu_database_settings)
|
||||
Screen.APPEARANCE -> resources.getString(R.string.menu_appearance_settings)
|
||||
Screen.DATABASE -> resources.getString(R.string.menu_database_settings)
|
||||
Screen.DATABASE_SECURITY -> resources.getString(R.string.menu_security_settings)
|
||||
Screen.DATABASE_MASTER_KEY -> resources.getString(R.string.menu_master_key_settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,12 @@ object PreferencesUtil {
|
||||
return prefs.getBoolean(context.getString(R.string.show_read_only_warning), true)
|
||||
}
|
||||
|
||||
fun rememberKeyFiles(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.keyfile_key),
|
||||
context.resources.getBoolean(R.bool.keyfile_default))
|
||||
}
|
||||
|
||||
fun omitBackup(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.omitbackup_key),
|
||||
@@ -109,8 +115,8 @@ object PreferencesUtil {
|
||||
fun getAppTimeout(context: Context): Long {
|
||||
return try {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
java.lang.Long.parseLong(prefs.getString(context.getString(R.string.app_timeout_key),
|
||||
context.getString(R.string.clipboard_timeout_default)) ?: "60000")
|
||||
(prefs.getString(context.getString(R.string.app_timeout_key),
|
||||
context.getString(R.string.clipboard_timeout_default)) ?: "300000").toLong()
|
||||
} catch (e: NumberFormatException) {
|
||||
TimeoutHelper.DEFAULT_TIMEOUT
|
||||
}
|
||||
@@ -185,12 +191,6 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.monospace_font_fields_enable_default))
|
||||
}
|
||||
|
||||
fun autoOpenSelectedFile(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.auto_open_file_uri_key),
|
||||
context.resources.getBoolean(R.bool.auto_open_file_uri_default))
|
||||
}
|
||||
|
||||
fun isFirstTimeAskAllowCopyPasswordAndProtectedFields(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.allow_copy_password_first_time_key),
|
||||
|
||||
@@ -115,7 +115,7 @@ open class SettingsActivity
|
||||
true)
|
||||
}
|
||||
// Show the progress dialog now or after dialog confirmation
|
||||
if (database.validatePasswordEncoding(masterPassword)) {
|
||||
if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) {
|
||||
progressDialogThread.start()
|
||||
} else {
|
||||
PasswordEncodingDialogFragment().apply {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.kunzisoft.keepass.settings.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.ColorInt
|
||||
import com.kunzisoft.androidclearchroma.ChromaPreferenceCompat
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class DialogColorPreference @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
|
||||
defStyleRes: Int = defStyleAttr)
|
||||
: ChromaPreferenceCompat(context, attrs, defStyleAttr, defStyleRes) {
|
||||
|
||||
override fun setSummary(summary: CharSequence?) {
|
||||
if (color == DISABLE_COLOR)
|
||||
super.setSummary("")
|
||||
else
|
||||
super.setSummary(summary)
|
||||
}
|
||||
|
||||
override fun getDialogLayoutResource(): Int {
|
||||
return R.layout.pref_dialog_input_color
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@ColorInt
|
||||
const val DISABLE_COLOR: Int = Color.TRANSPARENT
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.settings.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
|
||||
class InputKdfNumberPreference @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
|
||||
defStyleRes: Int = defStyleAttr)
|
||||
: InputTextPreference(context, attrs, defStyleAttr, defStyleRes) {
|
||||
|
||||
override fun setSummary(summary: CharSequence) {
|
||||
if (summary == UNKNOWN_VALUE_STRING) {
|
||||
isEnabled = false
|
||||
super.setSummary("")
|
||||
} else {
|
||||
isEnabled = true
|
||||
super.setSummary(summary)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val UNKNOWN_VALUE_STRING = KdfEngine.UNKNOWN_VALUE.toString()
|
||||
}
|
||||
}
|
||||
@@ -20,66 +20,28 @@
|
||||
package com.kunzisoft.keepass.settings.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.util.AttributeSet
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
|
||||
class InputNumberPreference @JvmOverloads constructor(context: Context,
|
||||
open class InputNumberPreference @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
|
||||
defStyleRes: Int = defStyleAttr)
|
||||
: InputTextExplanationPreference(context, attrs, defStyleAttr, defStyleRes) {
|
||||
|
||||
// Save to Shared Preferences
|
||||
var number: Long = 0
|
||||
set(number) {
|
||||
field = number
|
||||
persistLong(number)
|
||||
}
|
||||
: InputTextPreference(context, attrs, defStyleAttr, defStyleRes) {
|
||||
|
||||
override fun getDialogLayoutResource(): Int {
|
||||
return R.layout.pref_dialog_numbers
|
||||
return R.layout.pref_dialog_input_numbers
|
||||
}
|
||||
|
||||
override fun setSummary(summary: CharSequence) {
|
||||
if (summary == KdfEngine.UNKNOWN_VALUE_STRING) {
|
||||
isEnabled = false
|
||||
if (summary == INFINITE_VALUE_STRING) {
|
||||
super.setSummary("")
|
||||
} else {
|
||||
isEnabled = true
|
||||
super.setSummary(summary)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetDefaultValue(a: TypedArray?, index: Int): Any {
|
||||
// Default value from attribute. Fallback value is set to 0.
|
||||
return a?.getInt(index, 0) ?: 0
|
||||
companion object {
|
||||
const val INFINITE_VALUE_STRING = "-1"
|
||||
}
|
||||
|
||||
override fun onSetInitialValue(restorePersistedValue: Boolean,
|
||||
defaultValue: Any?) {
|
||||
// Read the value. Use the default value if it is not possible.
|
||||
var numberValue: Long
|
||||
if (!restorePersistedValue) {
|
||||
numberValue = 100000
|
||||
if (defaultValue is String) {
|
||||
numberValue = java.lang.Long.parseLong(defaultValue)
|
||||
}
|
||||
if (defaultValue is Int) {
|
||||
numberValue = defaultValue.toLong()
|
||||
}
|
||||
try {
|
||||
numberValue = defaultValue as Long
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
} else {
|
||||
numberValue = getPersistedLong(this.number)
|
||||
}
|
||||
|
||||
number = numberValue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.kunzisoft.keepass.settings.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.DialogPreference
|
||||
import android.util.AttributeSet
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
open class InputTextExplanationPreference @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
|
||||
defStyleRes: Int = defStyleAttr)
|
||||
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
|
||||
|
||||
var explanation: String? = null
|
||||
|
||||
init {
|
||||
val styleAttributes = context.theme.obtainStyledAttributes(
|
||||
attrs,
|
||||
R.styleable.explanationDialog,
|
||||
0, 0)
|
||||
try {
|
||||
explanation = styleAttributes.getString(R.styleable.explanationDialog_explanations)
|
||||
} finally {
|
||||
styleAttributes.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDialogLayoutResource(): Int {
|
||||
return R.layout.pref_dialog_input_text_explanation
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,10 @@ import android.util.AttributeSet
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class InputTextPreference @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
|
||||
defStyleRes: Int = defStyleAttr)
|
||||
open class InputTextPreference @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
|
||||
defStyleRes: Int = defStyleAttr)
|
||||
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
|
||||
|
||||
override fun getDialogLayoutResource(): Int {
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.CompoundButton
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||
import com.kunzisoft.androidclearchroma.IndicatorMode
|
||||
import com.kunzisoft.androidclearchroma.colormode.ColorMode
|
||||
import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment
|
||||
import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment.*
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import java.lang.Exception
|
||||
|
||||
class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
private lateinit var rootView: View
|
||||
private lateinit var enableSwitchView: CompoundButton
|
||||
private var chromaColorFragment: ChromaColorFragment? = null
|
||||
|
||||
var onColorSelectedListener: ((enable: Boolean, color: Int) -> Unit)? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
val alertDialogBuilder = AlertDialog.Builder(activity!!)
|
||||
|
||||
rootView = activity!!.layoutInflater.inflate(R.layout.pref_dialog_input_color, null)
|
||||
enableSwitchView = rootView.findViewById(R.id.switch_element)
|
||||
|
||||
val fragmentManager = childFragmentManager
|
||||
chromaColorFragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_COLORS) as ChromaColorFragment?
|
||||
val fragmentTransaction = fragmentManager.beginTransaction()
|
||||
|
||||
database?.let { database ->
|
||||
val initColor = try {
|
||||
enableSwitchView.isChecked = true
|
||||
Color.parseColor(database.color)
|
||||
} catch (e: Exception) {
|
||||
enableSwitchView.isChecked = false
|
||||
DEFAULT_COLOR
|
||||
}
|
||||
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
|
||||
}
|
||||
|
||||
if (chromaColorFragment == null) {
|
||||
chromaColorFragment = newInstance(arguments)
|
||||
fragmentTransaction.add(com.kunzisoft.androidclearchroma.R.id.color_dialog_container, chromaColorFragment!!, TAG_FRAGMENT_COLORS).commit()
|
||||
}
|
||||
|
||||
alertDialogBuilder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val currentColor = chromaColorFragment!!.currentColor
|
||||
val customColorEnable = enableSwitchView.isChecked
|
||||
|
||||
onColorSelectedListener?.invoke(customColorEnable, currentColor)
|
||||
|
||||
database?.let { database ->
|
||||
val newColor = if (customColorEnable) {
|
||||
ChromaUtil.getFormattedColorString(currentColor, false)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val oldColor = database.color
|
||||
database.color = newColor
|
||||
|
||||
actionInUIThreadAfterSaveDatabase = AfterColorSave(newColor, oldColor)
|
||||
}
|
||||
|
||||
super.onDialogClosed(true)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
alertDialogBuilder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
super.onDialogClosed(false)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
alertDialogBuilder.setView(rootView)
|
||||
|
||||
val dialog = alertDialogBuilder.create()
|
||||
// request a window without the title
|
||||
dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
|
||||
|
||||
dialog.setOnShowListener { measureLayout(it as Dialog) }
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new dimensions to dialog
|
||||
* @param ad dialog
|
||||
*/
|
||||
private fun measureLayout(ad: Dialog) {
|
||||
val typedValue = TypedValue()
|
||||
resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_height_multiplier, typedValue, true)
|
||||
val heightMultiplier = typedValue.float
|
||||
val height = (ad.context.resources.displayMetrics.heightPixels * heightMultiplier).toInt()
|
||||
|
||||
resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_width_multiplier, typedValue, true)
|
||||
val widthMultiplier = typedValue.float
|
||||
val width = (ad.context.resources.displayMetrics.widthPixels * widthMultiplier).toInt()
|
||||
|
||||
ad.window?.setLayout(width, height)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
return rootView
|
||||
}
|
||||
|
||||
private inner class AfterColorSave(private val mNewColor: String,
|
||||
private val mOldColor: String)
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
val defaultColorToShow =
|
||||
if (result.isSuccess) {
|
||||
mNewColor
|
||||
} else {
|
||||
database?.color = mOldColor
|
||||
mOldColor
|
||||
}
|
||||
preference.summary = defaultColorToShow
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG_FRAGMENT_COLORS = "TAG_FRAGMENT_COLORS"
|
||||
|
||||
@ColorInt
|
||||
const val DEFAULT_COLOR: Int = Color.WHITE
|
||||
|
||||
fun newInstance(key: String): DatabaseColorPreferenceDialogFragmentCompat {
|
||||
val fragment = DatabaseColorPreferenceDialogFragmentCompat()
|
||||
val bundle = Bundle(1)
|
||||
bundle.putString(ARG_KEY, key)
|
||||
bundle.putInt(ARG_INITIAL_COLOR, Color.BLACK)
|
||||
bundle.putInt(ARG_COLOR_MODE, ColorMode.RGB.ordinal)
|
||||
bundle.putInt(ARG_INDICATOR_MODE, IndicatorMode.HEX.ordinal)
|
||||
fragment.arguments = bundle
|
||||
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.adapter.ListRadioItemAdapter
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class DatabaseDataCompressionPreferenceDialogFragmentCompat
|
||||
: DatabaseSavePreferenceDialogFragmentCompat(),
|
||||
ListRadioItemAdapter.RadioItemSelectedCallback<PwCompressionAlgorithm> {
|
||||
|
||||
private var compressionSelected: PwCompressionAlgorithm? = null
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
|
||||
setExplanationText(R.string.database_data_compression_summary)
|
||||
|
||||
val recyclerView = view.findViewById<RecyclerView>(R.id.pref_dialog_list)
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
|
||||
activity?.let { activity ->
|
||||
val compressionAdapter = ListRadioItemAdapter<PwCompressionAlgorithm>(activity)
|
||||
compressionAdapter.setRadioItemSelectedCallback(this)
|
||||
recyclerView.adapter = compressionAdapter
|
||||
|
||||
database?.let { database ->
|
||||
compressionSelected = database.compressionAlgorithm?.apply {
|
||||
compressionAdapter.setItems(database.availableCompressionAlgorithms, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
|
||||
if (positiveResult) {
|
||||
database?.let { database ->
|
||||
if (compressionSelected != null) {
|
||||
val newAlgorithm = compressionSelected
|
||||
val oldAlgorithm = database.compressionAlgorithm
|
||||
database.compressionAlgorithm = newAlgorithm
|
||||
|
||||
if (oldAlgorithm != null && newAlgorithm != null)
|
||||
actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newAlgorithm, oldAlgorithm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onDialogClosed(positiveResult)
|
||||
}
|
||||
|
||||
override fun onItemSelected(item: PwCompressionAlgorithm) {
|
||||
this.compressionSelected = item
|
||||
}
|
||||
|
||||
private inner class AfterDescriptionSave(private val mNewAlgorithm: PwCompressionAlgorithm,
|
||||
private val mOldAlgorithm: PwCompressionAlgorithm)
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
val algorithmToShow =
|
||||
if (result.isSuccess) {
|
||||
mNewAlgorithm
|
||||
} else {
|
||||
database?.compressionAlgorithm = mOldAlgorithm
|
||||
mOldAlgorithm
|
||||
}
|
||||
preference.summary = algorithmToShow.getName(settingsResources)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(key: String): DatabaseDataCompressionPreferenceDialogFragmentCompat {
|
||||
val fragment = DatabaseDataCompressionPreferenceDialogFragmentCompat()
|
||||
val bundle = Bundle(1)
|
||||
bundle.putString(ARG_KEY, key)
|
||||
fragment.arguments = bundle
|
||||
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
|
||||
inputText = database?.defaultUsername?: ""
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
database?.let { database ->
|
||||
if (positiveResult) {
|
||||
val newDefaultUsername = inputText
|
||||
val oldDefaultUsername = database.defaultUsername
|
||||
database.defaultUsername = newDefaultUsername
|
||||
|
||||
actionInUIThreadAfterSaveDatabase = AfterDefaultUsernameSave(newDefaultUsername, oldDefaultUsername)
|
||||
}
|
||||
}
|
||||
|
||||
super.onDialogClosed(positiveResult)
|
||||
}
|
||||
|
||||
private inner class AfterDefaultUsernameSave(private val mNewDefaultUsername: String,
|
||||
private val mOldDefaultUsername: String)
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
val defaultUsernameToShow =
|
||||
if (result.isSuccess) {
|
||||
mNewDefaultUsername
|
||||
} else {
|
||||
database?.defaultUsername = mOldDefaultUsername
|
||||
mOldDefaultUsername
|
||||
}
|
||||
preference.summary = defaultUsernameToShow
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(key: String): DatabaseDefaultUsernamePreferenceDialogFragmentCompat {
|
||||
val fragment = DatabaseDefaultUsernamePreferenceDialogFragmentCompat()
|
||||
val bundle = Bundle(1)
|
||||
bundle.putString(ARG_KEY, key)
|
||||
fragment.arguments = bundle
|
||||
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class DatabaseDescriptionPreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() {
|
||||
class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
@@ -32,12 +32,14 @@ class DatabaseDescriptionPreferenceDialogFragmentCompat : InputDatabaseSavePrefe
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
if (database != null && positiveResult) {
|
||||
val newDescription = inputText
|
||||
val oldDescription = database!!.description
|
||||
database?.assignDescription(newDescription)
|
||||
database?.let { database ->
|
||||
if (positiveResult) {
|
||||
val newDescription = inputText
|
||||
val oldDescription = database.description
|
||||
database.description = newDescription
|
||||
|
||||
actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newDescription, oldDescription)
|
||||
actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newDescription, oldDescription)
|
||||
}
|
||||
}
|
||||
|
||||
super.onDialogClosed(positiveResult)
|
||||
@@ -48,10 +50,13 @@ class DatabaseDescriptionPreferenceDialogFragmentCompat : InputDatabaseSavePrefe
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
val descriptionToShow = mNewDescription
|
||||
if (!result.isSuccess) {
|
||||
database?.assignDescription(mOldDescription)
|
||||
}
|
||||
val descriptionToShow =
|
||||
if (result.isSuccess) {
|
||||
mNewDescription
|
||||
} else {
|
||||
database?.description = mOldDescription
|
||||
mOldDescription
|
||||
}
|
||||
preference.summary = descriptionToShow
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,17 +56,19 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
if (database != null && positiveResult && database!!.allowEncryptionAlgorithmModification()) {
|
||||
|
||||
if (algorithmSelected != null) {
|
||||
val newAlgorithm = algorithmSelected
|
||||
val oldAlgorithm = database?.encryptionAlgorithm
|
||||
newAlgorithm?.let {
|
||||
database?.assignEncryptionAlgorithm(it)
|
||||
if (positiveResult) {
|
||||
database?.let { database ->
|
||||
if (database.allowEncryptionAlgorithmModification()) {
|
||||
if (algorithmSelected != null) {
|
||||
val newAlgorithm = algorithmSelected
|
||||
val oldAlgorithm = database.encryptionAlgorithm
|
||||
database.encryptionAlgorithm = newAlgorithm
|
||||
|
||||
if (oldAlgorithm != null && newAlgorithm != null)
|
||||
actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newAlgorithm, oldAlgorithm)
|
||||
}
|
||||
}
|
||||
|
||||
if (oldAlgorithm != null && newAlgorithm != null)
|
||||
actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newAlgorithm, oldAlgorithm)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,11 +84,13 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
var algorithmToShow = mNewAlgorithm
|
||||
if (!result.isSuccess) {
|
||||
database?.assignEncryptionAlgorithm(mOldAlgorithm)
|
||||
algorithmToShow = mOldAlgorithm
|
||||
}
|
||||
val algorithmToShow =
|
||||
if (result.isSuccess) {
|
||||
mNewAlgorithm
|
||||
} else {
|
||||
database?.encryptionAlgorithm = mOldAlgorithm
|
||||
mOldAlgorithm
|
||||
}
|
||||
preference.summary = algorithmToShow.getName(settingsResources)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,21 +52,24 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
|
||||
recyclerView.adapter = kdfAdapter
|
||||
|
||||
database?.let { database ->
|
||||
kdfEngineSelected = database.kdfEngine
|
||||
if (kdfEngineSelected != null)
|
||||
kdfAdapter.setItems(database.availableKdfEngines, kdfEngineSelected!!)
|
||||
kdfEngineSelected = database.kdfEngine?.apply {
|
||||
kdfAdapter.setItems(database.availableKdfEngines, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
if (database != null && positiveResult && database!!.allowKdfModification()) {
|
||||
if (kdfEngineSelected != null) {
|
||||
val newKdfEngine = kdfEngineSelected!!
|
||||
val oldKdfEngine = database!!.kdfEngine
|
||||
database?.assignKdfEngine(newKdfEngine)
|
||||
|
||||
actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newKdfEngine, oldKdfEngine)
|
||||
if (positiveResult) {
|
||||
database?.let { database ->
|
||||
if (database.allowKdfModification()) {
|
||||
val newKdfEngine = kdfEngineSelected
|
||||
val oldKdfEngine = database.kdfEngine
|
||||
if (newKdfEngine != null && oldKdfEngine != null) {
|
||||
database.kdfEngine = newKdfEngine
|
||||
actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newKdfEngine, oldKdfEngine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,17 +97,19 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
val kdfEngineToShow = mNewKdfEngine
|
||||
|
||||
if (!result.isSuccess) {
|
||||
database?.assignKdfEngine(mOldKdfEngine)
|
||||
}
|
||||
val kdfEngineToShow =
|
||||
if (result.isSuccess) {
|
||||
mNewKdfEngine
|
||||
} else {
|
||||
database?.kdfEngine = mOldKdfEngine
|
||||
mOldKdfEngine
|
||||
}
|
||||
preference.summary = kdfEngineToShow.getName(settingsResources)
|
||||
|
||||
roundPreference?.summary = kdfEngineToShow.defaultKeyRounds.toString()
|
||||
// Disable memory and parallelism if not available
|
||||
memoryPreference?.summary = kdfEngineToShow.getDefaultMemoryUsage().toString()
|
||||
parallelismPreference?.summary = kdfEngineToShow.getDefaultParallelism().toString()
|
||||
memoryPreference?.summary = kdfEngineToShow.defaultMemoryUsage.toString()
|
||||
parallelismPreference?.summary = kdfEngineToShow.defaultParallelism.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class DatabaseNamePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() {
|
||||
class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
@@ -32,12 +32,14 @@ class DatabaseNamePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDi
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
if (database != null && positiveResult) {
|
||||
val newName = inputText
|
||||
val oldName = database!!.name
|
||||
database?.assignName(newName)
|
||||
if (positiveResult) {
|
||||
database?.let { database ->
|
||||
val newName = inputText
|
||||
val oldName = database.name
|
||||
database.name = newName
|
||||
|
||||
actionInUIThreadAfterSaveDatabase = AfterNameSave(newName, oldName)
|
||||
actionInUIThreadAfterSaveDatabase = AfterNameSave(newName, oldName)
|
||||
}
|
||||
}
|
||||
|
||||
super.onDialogClosed(positiveResult)
|
||||
@@ -48,10 +50,13 @@ class DatabaseNamePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDi
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
val nameToShow = mNewName
|
||||
if (!result.isSuccess) {
|
||||
database?.assignName(mOldName)
|
||||
}
|
||||
val nameToShow =
|
||||
if (result.isSuccess) {
|
||||
mNewName
|
||||
} else {
|
||||
database?.name = mOldName
|
||||
mOldName
|
||||
}
|
||||
preference.summary = nameToShow
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
@@ -36,10 +37,14 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat : InputPreferenceDialo
|
||||
|
||||
protected lateinit var settingsResources: Resources
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
this.database = Database.getInstance()
|
||||
}
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
|
||||
activity?.resources?.let { settingsResources = it }
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
open class InputDatabaseSavePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
private var inputTextView: EditText? = null
|
||||
|
||||
var inputText: String
|
||||
get() = this.inputTextView?.text?.toString() ?: ""
|
||||
set(inputText) {
|
||||
if (inputTextView != null) {
|
||||
this.inputTextView?.setText(inputText)
|
||||
this.inputTextView?.setSelection(this.inputTextView!!.text.length)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
|
||||
inputTextView = view.findViewById(R.id.input_text)
|
||||
}
|
||||
}
|
||||
@@ -19,36 +19,71 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.preference.PreferenceDialogFragmentCompat
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() {
|
||||
|
||||
private var inputTextView: EditText? = null
|
||||
private var textExplanationView: TextView? = null
|
||||
private var switchElementView: CompoundButton? = null
|
||||
|
||||
var inputText: String
|
||||
get() = this.inputTextView?.text?.toString() ?: ""
|
||||
set(inputText) {
|
||||
if (inputTextView != null) {
|
||||
this.inputTextView?.setText(inputText)
|
||||
this.inputTextView?.setSelection(this.inputTextView!!.text.length)
|
||||
}
|
||||
}
|
||||
|
||||
var explanationText: String?
|
||||
get() = textExplanationView?.text?.toString() ?: ""
|
||||
set(explanationText) {
|
||||
if (explanationText != null && explanationText.isNotEmpty()) {
|
||||
textExplanationView?.text = explanationText
|
||||
textExplanationView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
textExplanationView?.text = explanationText
|
||||
textExplanationView?.visibility = View.VISIBLE
|
||||
textExplanationView?.apply {
|
||||
if (explanationText != null && explanationText.isNotEmpty()) {
|
||||
text = explanationText
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
text = ""
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
|
||||
inputTextView = view.findViewById(R.id.input_text)
|
||||
textExplanationView = view.findViewById(R.id.explanation_text)
|
||||
textExplanationView?.visibility = View.GONE
|
||||
switchElementView = view.findViewById(R.id.switch_element)
|
||||
switchElementView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
fun setInoutText(@StringRes inputTextId: Int) {
|
||||
inputText = getString(inputTextId)
|
||||
}
|
||||
|
||||
fun showInputText(show: Boolean) {
|
||||
inputTextView?.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
fun setExplanationText(@StringRes explanationTextId: Int) {
|
||||
explanationText = getString(explanationTextId)
|
||||
}
|
||||
|
||||
fun setSwitchAction(onCheckedChange: ((isChecked: Boolean)-> Unit)?, defaultChecked: Boolean) {
|
||||
switchElementView?.visibility = if (onCheckedChange == null) View.GONE else View.VISIBLE
|
||||
switchElementView?.isChecked = defaultChecked
|
||||
inputTextView?.visibility = if (defaultChecked) View.VISIBLE else View.GONE
|
||||
switchElementView?.setOnCheckedChangeListener { _, isChecked ->
|
||||
onCheckedChange?.invoke(isChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class MaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
|
||||
setExplanationText(R.string.max_history_items_summary)
|
||||
database?.historyMaxItems?.let { maxItemsDatabase ->
|
||||
inputText = maxItemsDatabase.toString()
|
||||
setSwitchAction({ isChecked ->
|
||||
inputText = if (!isChecked) {
|
||||
INFINITE_MAX_HISTORY_ITEMS.toString()
|
||||
} else
|
||||
DEFAULT_MAX_HISTORY_ITEMS.toString()
|
||||
showInputText(isChecked)
|
||||
}, maxItemsDatabase > INFINITE_MAX_HISTORY_ITEMS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
if (positiveResult) {
|
||||
database?.let { database ->
|
||||
var maxHistoryItems: Int = try {
|
||||
inputText.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
DEFAULT_MAX_HISTORY_ITEMS
|
||||
}
|
||||
if (maxHistoryItems < INFINITE_MAX_HISTORY_ITEMS) {
|
||||
maxHistoryItems = INFINITE_MAX_HISTORY_ITEMS
|
||||
}
|
||||
|
||||
val oldMaxHistoryItems = database.historyMaxItems
|
||||
database.historyMaxItems = maxHistoryItems
|
||||
|
||||
actionInUIThreadAfterSaveDatabase = AfterMaxHistoryItemsSave(maxHistoryItems, oldMaxHistoryItems)
|
||||
}
|
||||
}
|
||||
|
||||
super.onDialogClosed(positiveResult)
|
||||
}
|
||||
|
||||
private inner class AfterMaxHistoryItemsSave(private val mNewMaxHistoryItems: Int,
|
||||
private val mOldMaxHistoryItems: Int)
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
val maxHistoryItemsToShow =
|
||||
if (result.isSuccess) {
|
||||
mNewMaxHistoryItems
|
||||
} else {
|
||||
database?.historyMaxItems = mOldMaxHistoryItems
|
||||
mOldMaxHistoryItems
|
||||
}
|
||||
preference.summary = maxHistoryItemsToShow.toString()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val DEFAULT_MAX_HISTORY_ITEMS = 10
|
||||
const val INFINITE_MAX_HISTORY_ITEMS = -1
|
||||
|
||||
fun newInstance(key: String): MaxHistoryItemsPreferenceDialogFragmentCompat {
|
||||
val fragment = MaxHistoryItemsPreferenceDialogFragmentCompat()
|
||||
val bundle = Bundle(1)
|
||||
bundle.putString(ARG_KEY, key)
|
||||
fragment.arguments = bundle
|
||||
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
|
||||
setExplanationText(R.string.max_history_size_summary)
|
||||
database?.historyMaxSize?.let { maxItemsDatabase ->
|
||||
inputText = maxItemsDatabase.toString()
|
||||
setSwitchAction({ isChecked ->
|
||||
inputText = if (!isChecked) {
|
||||
INFINITE_MAX_HISTORY_SIZE.toString()
|
||||
} else
|
||||
DEFAULT_MAX_HISTORY_SIZE.toString()
|
||||
showInputText(isChecked)
|
||||
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
if (positiveResult) {
|
||||
database?.let { database ->
|
||||
var maxHistorySize: Long = try {
|
||||
inputText.toLong()
|
||||
} catch (e: NumberFormatException) {
|
||||
DEFAULT_MAX_HISTORY_SIZE
|
||||
}
|
||||
if (maxHistorySize < INFINITE_MAX_HISTORY_SIZE) {
|
||||
maxHistorySize = INFINITE_MAX_HISTORY_SIZE
|
||||
}
|
||||
|
||||
val oldMaxHistorySize = database.historyMaxSize
|
||||
database.historyMaxSize = maxHistorySize
|
||||
|
||||
actionInUIThreadAfterSaveDatabase = AfterMaxHistorySizeSave(maxHistorySize, oldMaxHistorySize)
|
||||
}
|
||||
}
|
||||
|
||||
super.onDialogClosed(positiveResult)
|
||||
}
|
||||
|
||||
private inner class AfterMaxHistorySizeSave(private val mNewMaxHistorySize: Long,
|
||||
private val mOldMaxHistorySize: Long)
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
val maxHistorySizeToShow =
|
||||
if (result.isSuccess) {
|
||||
mNewMaxHistorySize
|
||||
} else {
|
||||
database?.historyMaxSize = mOldMaxHistorySize
|
||||
mOldMaxHistorySize
|
||||
}
|
||||
preference.summary = maxHistorySizeToShow.toString()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val DEFAULT_MAX_HISTORY_SIZE = 134217728L
|
||||
const val INFINITE_MAX_HISTORY_SIZE = -1L
|
||||
|
||||
fun newInstance(key: String): MaxHistorySizePreferenceDialogFragmentCompat {
|
||||
val fragment = MaxHistorySizePreferenceDialogFragmentCompat()
|
||||
val bundle = Bundle(1)
|
||||
bundle.putString(ARG_KEY, key)
|
||||
fragment.arguments = bundle
|
||||
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,38 +21,36 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class MemoryUsagePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() {
|
||||
class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
|
||||
setExplanationText(R.string.memory_usage_explanation)
|
||||
inputText = database?.memoryUsageAsString ?: ""
|
||||
inputText = database?.memoryUsage?.toString()?: MIN_MEMORY_USAGE.toString()
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
if (database != null && positiveResult) {
|
||||
var memoryUsage: Long
|
||||
try {
|
||||
val stringMemory = inputText
|
||||
memoryUsage = java.lang.Long.parseLong(stringMemory)
|
||||
} catch (e: NumberFormatException) {
|
||||
Toast.makeText(context, R.string.error_rounds_not_number, Toast.LENGTH_LONG).show() // TODO change error
|
||||
return
|
||||
if (positiveResult) {
|
||||
database?.let { database ->
|
||||
var memoryUsage: Long = try {
|
||||
inputText.toLong()
|
||||
} catch (e: NumberFormatException) {
|
||||
MIN_MEMORY_USAGE
|
||||
}
|
||||
if (memoryUsage < MIN_MEMORY_USAGE) {
|
||||
memoryUsage = MIN_MEMORY_USAGE
|
||||
}
|
||||
// TODO Max Memory
|
||||
|
||||
val oldMemoryUsage = database.memoryUsage
|
||||
database.memoryUsage = memoryUsage
|
||||
|
||||
actionInUIThreadAfterSaveDatabase = AfterMemorySave(memoryUsage, oldMemoryUsage)
|
||||
}
|
||||
|
||||
if (memoryUsage < 1) {
|
||||
memoryUsage = 1
|
||||
}
|
||||
|
||||
val oldMemoryUsage = database!!.memoryUsage
|
||||
database!!.memoryUsage = memoryUsage
|
||||
|
||||
actionInUIThreadAfterSaveDatabase = AfterMemorySave(memoryUsage, oldMemoryUsage)
|
||||
}
|
||||
|
||||
super.onDialogClosed(positiveResult)
|
||||
@@ -63,16 +61,21 @@ class MemoryUsagePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDia
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
val memoryToShow = mNewMemory
|
||||
if (!result.isSuccess) {
|
||||
database?.memoryUsage = mOldMemory
|
||||
}
|
||||
val memoryToShow =
|
||||
if (result.isSuccess) {
|
||||
mNewMemory
|
||||
} else {
|
||||
database?.memoryUsage = mOldMemory
|
||||
mOldMemory
|
||||
}
|
||||
preference.summary = memoryToShow.toString()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val MIN_MEMORY_USAGE = 1L
|
||||
|
||||
fun newInstance(key: String): MemoryUsagePreferenceDialogFragmentCompat {
|
||||
val fragment = MemoryUsagePreferenceDialogFragmentCompat()
|
||||
val bundle = Bundle(1)
|
||||
|
||||
@@ -21,38 +21,36 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class ParallelismPreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() {
|
||||
class ParallelismPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
|
||||
setExplanationText(R.string.parallelism_explanation)
|
||||
inputText = database?.parallelismAsString ?: ""
|
||||
inputText = database?.parallelism?.toString() ?: MIN_PARALLELISM.toString()
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
if (database != null && positiveResult) {
|
||||
var parallelism: Int
|
||||
try {
|
||||
val stringParallelism = inputText
|
||||
parallelism = Integer.parseInt(stringParallelism)
|
||||
} catch (e: NumberFormatException) {
|
||||
Toast.makeText(context, R.string.error_rounds_not_number, Toast.LENGTH_LONG).show() // TODO change error
|
||||
return
|
||||
if (positiveResult) {
|
||||
database?.let { database ->
|
||||
var parallelism: Int = try {
|
||||
inputText.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
MIN_PARALLELISM
|
||||
}
|
||||
if (parallelism < MIN_PARALLELISM) {
|
||||
parallelism = MIN_PARALLELISM
|
||||
}
|
||||
// TODO Max Parallelism
|
||||
|
||||
val oldParallelism = database.parallelism
|
||||
database.parallelism = parallelism
|
||||
|
||||
actionInUIThreadAfterSaveDatabase = AfterParallelismSave(parallelism, oldParallelism)
|
||||
}
|
||||
|
||||
if (parallelism < 1) {
|
||||
parallelism = 1
|
||||
}
|
||||
|
||||
val oldParallelism = database!!.parallelism
|
||||
database?.parallelism = parallelism
|
||||
|
||||
actionInUIThreadAfterSaveDatabase = AfterParallelismSave(parallelism, oldParallelism)
|
||||
}
|
||||
|
||||
super.onDialogClosed(positiveResult)
|
||||
@@ -63,16 +61,21 @@ class ParallelismPreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDia
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
val parallelismToShow = mNewParallelism
|
||||
if (!result.isSuccess) {
|
||||
database?.parallelism = mOldParallelism
|
||||
}
|
||||
val parallelismToShow =
|
||||
if (result.isSuccess) {
|
||||
mNewParallelism
|
||||
} else {
|
||||
database?.parallelism = mOldParallelism
|
||||
mOldParallelism
|
||||
}
|
||||
preference.summary = parallelismToShow.toString()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val MIN_PARALLELISM = 1
|
||||
|
||||
fun newInstance(key: String): ParallelismPreferenceDialogFragmentCompat {
|
||||
val fragment = ParallelismPreferenceDialogFragmentCompat()
|
||||
val bundle = Bundle(1)
|
||||
|
||||
@@ -25,39 +25,38 @@ import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class RoundsPreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() {
|
||||
class RoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
|
||||
explanationText = getString(R.string.rounds_explanation)
|
||||
inputText = database?.numberKeyEncryptionRoundsAsString ?: ""
|
||||
inputText = database?.numberKeyEncryptionRounds?.toString() ?: MIN_ITERATIONS.toString()
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
if (database != null && positiveResult) {
|
||||
var rounds: Long
|
||||
try {
|
||||
val strRounds = inputText
|
||||
rounds = java.lang.Long.parseLong(strRounds)
|
||||
} catch (e: NumberFormatException) {
|
||||
Toast.makeText(context, R.string.error_rounds_not_number, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
if (positiveResult) {
|
||||
database?.let { database ->
|
||||
var rounds: Long = try {
|
||||
inputText.toLong()
|
||||
} catch (e: NumberFormatException) {
|
||||
MIN_ITERATIONS
|
||||
}
|
||||
if (rounds < MIN_ITERATIONS) {
|
||||
rounds = MIN_ITERATIONS
|
||||
}
|
||||
// TODO Max iterations
|
||||
|
||||
if (rounds < 1) {
|
||||
rounds = 1
|
||||
}
|
||||
val oldRounds = database.numberKeyEncryptionRounds
|
||||
try {
|
||||
database.numberKeyEncryptionRounds = rounds
|
||||
} catch (e: NumberFormatException) {
|
||||
Toast.makeText(context, R.string.error_rounds_too_large, Toast.LENGTH_LONG).show()
|
||||
database.numberKeyEncryptionRounds = Long.MAX_VALUE
|
||||
}
|
||||
|
||||
val oldRounds = database!!.numberKeyEncryptionRounds
|
||||
try {
|
||||
database?.numberKeyEncryptionRounds = rounds
|
||||
} catch (e: NumberFormatException) {
|
||||
Toast.makeText(context, R.string.error_rounds_too_large, Toast.LENGTH_LONG).show()
|
||||
database?.numberKeyEncryptionRounds = Integer.MAX_VALUE.toLong()
|
||||
actionInUIThreadAfterSaveDatabase = AfterRoundSave(rounds, oldRounds)
|
||||
}
|
||||
|
||||
actionInUIThreadAfterSaveDatabase = AfterRoundSave(rounds, oldRounds)
|
||||
}
|
||||
|
||||
super.onDialogClosed(positiveResult)
|
||||
@@ -67,17 +66,21 @@ class RoundsPreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFr
|
||||
private val mOldRounds: Long) : ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
val roundsToShow = mNewRounds
|
||||
if (!result.isSuccess) {
|
||||
database?.numberKeyEncryptionRounds = mOldRounds
|
||||
}
|
||||
|
||||
val roundsToShow =
|
||||
if (result.isSuccess) {
|
||||
mNewRounds
|
||||
} else {
|
||||
database?.numberKeyEncryptionRounds = mOldRounds
|
||||
mOldRounds
|
||||
}
|
||||
preference.summary = roundsToShow.toString()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val MIN_ITERATIONS = 1L
|
||||
|
||||
fun newInstance(key: String): RoundsPreferenceDialogFragmentCompat {
|
||||
val fragment = RoundsPreferenceDialogFragmentCompat()
|
||||
val bundle = Bundle(1)
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
|
||||
/**
|
||||
* Callback after a task is completed.
|
||||
@@ -52,8 +53,21 @@ abstract class ActionRunnable(private var nestedActionRunnable: ActionRunnable?
|
||||
* launch the nested action runnable if exists and finish,
|
||||
* else directly finish
|
||||
*/
|
||||
protected fun finishRun(isSuccess: Boolean, message: String? = null) {
|
||||
protected fun finishRun(isSuccess: Boolean,
|
||||
message: String? = null) {
|
||||
finishRun(isSuccess, null, message)
|
||||
}
|
||||
|
||||
/**
|
||||
* If [success] or [executeNestedActionIfResultFalse] true,
|
||||
* launch the nested action runnable if exists and finish,
|
||||
* else directly finish
|
||||
*/
|
||||
protected fun finishRun(isSuccess: Boolean,
|
||||
exception: LoadDatabaseException?,
|
||||
message: String? = null) {
|
||||
result.isSuccess = isSuccess
|
||||
result.exception = exception
|
||||
result.message = message
|
||||
if (isSuccess || executeNestedActionIfResultFalse) {
|
||||
execute()
|
||||
@@ -89,5 +103,8 @@ abstract class ActionRunnable(private var nestedActionRunnable: ActionRunnable?
|
||||
/**
|
||||
* Class to manage result from ActionRunnable
|
||||
*/
|
||||
data class Result(var isSuccess: Boolean = true, var message: String? = null, var data: Bundle? = null)
|
||||
data class Result(var isSuccess: Boolean = true,
|
||||
var message: String? = null,
|
||||
var exception: LoadDatabaseException? = null,
|
||||
var data: Bundle? = null)
|
||||
}
|
||||
|
||||
@@ -60,8 +60,7 @@ class ClipboardHelper(private val context: Context) {
|
||||
val sClipClear = prefs.getString(context.getString(R.string.clipboard_timeout_key),
|
||||
context.getString(R.string.clipboard_timeout_default))
|
||||
|
||||
val clipClearTime = java.lang.Long.parseLong(sClipClear ?: "60000")
|
||||
|
||||
val clipClearTime = (sClipClear ?: "300000").toLong()
|
||||
if (clipClearTime > 0) {
|
||||
mTimer.schedule(ClearClipboardTask(context, text), clipClearTime)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.core.content.ContextCompat
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
@@ -29,9 +28,14 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.PwDate
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
@@ -59,9 +63,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
private val extrasContainerView: View
|
||||
private val extrasView: ViewGroup
|
||||
|
||||
private val dateFormat: DateFormat = android.text.format.DateFormat.getDateFormat(context)
|
||||
private val timeFormat: DateFormat = android.text.format.DateFormat.getTimeFormat(context)
|
||||
|
||||
private val creationDateView: TextView
|
||||
private val modificationDateView: TextView
|
||||
private val lastAccessDateView: TextView
|
||||
@@ -69,6 +70,10 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
|
||||
private val uuidView: TextView
|
||||
|
||||
private val historyContainerView: View
|
||||
private val historyListView: RecyclerView
|
||||
private val historyAdapter = EntryHistoryAdapter(context)
|
||||
|
||||
val isUserNamePresent: Boolean
|
||||
get() = userNameContainerView.visibility == View.VISIBLE
|
||||
|
||||
@@ -103,6 +108,13 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
|
||||
uuidView = findViewById(R.id.entry_UUID)
|
||||
|
||||
historyContainerView = findViewById(R.id.entry_history_container)
|
||||
historyListView = findViewById(R.id.entry_history_list)
|
||||
historyListView?.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
|
||||
adapter = historyAdapter
|
||||
}
|
||||
|
||||
val attrColorAccent = intArrayOf(R.attr.colorAccent)
|
||||
val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent)
|
||||
colorAccent = taColorAccent.getColor(0, Color.BLACK)
|
||||
@@ -230,24 +242,20 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
extrasContainerView.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun getDateTime(date: Date): String {
|
||||
return dateFormat.format(date) + " " + timeFormat.format(date)
|
||||
fun assignCreationDate(date: PwDate) {
|
||||
creationDateView.text = date.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
fun assignCreationDate(date: Date) {
|
||||
creationDateView.text = getDateTime(date)
|
||||
fun assignModificationDate(date: PwDate) {
|
||||
modificationDateView.text = date.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
fun assignModificationDate(date: Date) {
|
||||
modificationDateView.text = getDateTime(date)
|
||||
fun assignLastAccessDate(date: PwDate) {
|
||||
lastAccessDateView.text = date.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
fun assignLastAccessDate(date: Date) {
|
||||
lastAccessDateView.text = getDateTime(date)
|
||||
}
|
||||
|
||||
fun assignExpiresDate(date: Date) {
|
||||
expiresDateView.text = getDateTime(date)
|
||||
fun assignExpiresDate(date: PwDate) {
|
||||
expiresDateView.text = date.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
fun assignExpiresDate(constString: String) {
|
||||
@@ -258,6 +266,21 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
uuidView.text = uuid.toString()
|
||||
}
|
||||
|
||||
fun showHistory(show: Boolean) {
|
||||
historyContainerView.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
fun assignHistory(history: ArrayList<EntryVersioned>) {
|
||||
historyAdapter.clear()
|
||||
historyAdapter.entryHistoryList.addAll(history)
|
||||
}
|
||||
|
||||
fun onHistoryClick(action: (historyItem: EntryVersioned, position: Int)->Unit) {
|
||||
historyAdapter.onItemClickListener = { item, position ->
|
||||
action.invoke(item, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun generateDefaultLayoutParams(): LayoutParams {
|
||||
return LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import java.util.HashMap
|
||||
|
||||
class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@@ -167,10 +168,12 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
/**
|
||||
* Add a new view to fill in the information of the customized field
|
||||
*/
|
||||
fun addNewCustomField(name: String = "", value:ProtectedString = ProtectedString(false, "")) {
|
||||
val entryEditCustomField = EntryEditCustomField(context)
|
||||
entryEditCustomField.setData(name, value)
|
||||
entryEditCustomField.setFontVisibility(fontInVisibility)
|
||||
fun addNewCustomField(name: String = "", value: ProtectedString = ProtectedString(false, "")) {
|
||||
val entryEditCustomField = EntryEditCustomField(context).apply {
|
||||
setData(name, value)
|
||||
setFontVisibility(fontInVisibility)
|
||||
requestFocus()
|
||||
}
|
||||
entryExtraFieldsContainer.addView(entryEditCustomField)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,13 +19,17 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.view
|
||||
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.Activity
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
/**
|
||||
@@ -56,3 +60,40 @@ fun Activity.lockScreenOrientation() {
|
||||
fun Activity.unlockScreenOrientation() {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
|
||||
private var actionBarHeight: Int = 0
|
||||
|
||||
fun Toolbar.collapse(animate: Boolean = true) {
|
||||
|
||||
if (layoutParams.height > 5)
|
||||
actionBarHeight = layoutParams.height
|
||||
|
||||
val slideAnimator = ValueAnimator
|
||||
.ofInt(height, 0)
|
||||
if (animate)
|
||||
slideAnimator.duration = 300L
|
||||
slideAnimator.addUpdateListener { animation ->
|
||||
layoutParams.height = animation.animatedValue as Int
|
||||
requestLayout()
|
||||
}
|
||||
AnimatorSet().apply {
|
||||
play(slideAnimator)
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun Toolbar.expand(animate: Boolean = true) {
|
||||
|
||||
val slideAnimator = ValueAnimator
|
||||
.ofInt(0, actionBarHeight)
|
||||
if (animate)
|
||||
slideAnimator.duration = 300L
|
||||
slideAnimator.addUpdateListener { animation ->
|
||||
layoutParams.height = animation.animatedValue as Int
|
||||
requestLayout()
|
||||
}
|
||||
AnimatorSet().apply {
|
||||
play(slideAnimator)
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
}.start()
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/prefs_autorenew_24dp.xml
Normal file
10
app/src/main/res/drawable/prefs_autorenew_24dp.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="?attr/iconPreferenceColor"
|
||||
android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/prefs_security_24dp.xml
Normal file
10
app/src/main/res/drawable/prefs_security_24dp.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="?attr/iconPreferenceColor"
|
||||
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94L12,12L5,12L5,6.3l7,-3.11v8.8z"/>
|
||||
</vector>
|
||||
@@ -75,7 +75,6 @@
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/entry_scroll"
|
||||
android:layout_width="match_parent"
|
||||
@@ -86,12 +85,30 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/history_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:background="?attr/colorAccent"
|
||||
android:padding="12dp"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:textColor="?attr/textColorInverse"
|
||||
android:text="@string/entry_history"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.kunzisoft.keepass.view.EntryContentsView
|
||||
android:id="@+id/entry_contents"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="0dp"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/history_container"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/browse_button"
|
||||
android:id="@+id/open_database_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/select_database_file"
|
||||
@@ -194,72 +194,10 @@
|
||||
android:paddingStart="32dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/file_select_expandable"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/file_select_expandable_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:elevation="8dp"
|
||||
app:layout_constraintTop_toTopOf="@+id/browse_button"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/browse_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:focusable="true"
|
||||
android:padding="12dp"
|
||||
android:tint="?attr/textColorInverse"
|
||||
android:src="@drawable/ic_link_white_24dp"
|
||||
android:contentDescription="@string/content_description_show_file_link"/>
|
||||
|
||||
<net.cachapa.expandablelayout.ExpandableLayout
|
||||
android:id="@+id/file_select_expandable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toTopOf="@+id/create_database"
|
||||
app:el_duration="300"
|
||||
app:el_expanded="false"
|
||||
app:el_parallax="0.5">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/file_select"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorPrimaryDark">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/file_filename"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:inputType="textUri"
|
||||
android:textColor="?attr/textColorInverse"
|
||||
android:textColorHint="?android:attr/textColorHintInverse"
|
||||
android:layout_toLeftOf="@+id/open_database"
|
||||
android:layout_toStartOf="@+id/open_database"
|
||||
android:layout_marginLeft="@dimen/default_margin"
|
||||
android:layout_marginStart="@dimen/default_margin"
|
||||
android:layout_marginRight="@dimen/default_margin"
|
||||
android:layout_marginEnd="@dimen/default_margin"
|
||||
android:maxLines="1"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/open_database"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:padding="12dp"
|
||||
android:layout_alignTop="@+id/file_filename"
|
||||
android:layout_alignBottom="@+id/file_filename"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_send_white_24dp"
|
||||
android:contentDescription="@string/content_description_open_file_link"
|
||||
android:tint="?attr/textColorInverse"/>
|
||||
</RelativeLayout>
|
||||
|
||||
</net.cachapa.expandablelayout.ExpandableLayout>
|
||||
app:layout_constraintBottom_toTopOf="@+id/create_database_button"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/create_database"
|
||||
android:id="@+id/create_database_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="true"
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@+id/expandable_toolbar_paste_layout">
|
||||
android:layout_above="@+id/toolbar_paste">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
@@ -100,6 +100,7 @@
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/node_list_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
@@ -112,6 +113,7 @@
|
||||
android:padding="6dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:background="?attr/colorAccent"
|
||||
android:textColor="?attr/textColorInverse"
|
||||
android:text="@string/selection_mode"/>
|
||||
<FrameLayout
|
||||
android:id="@+id/nodes_list_fragment_container"
|
||||
@@ -124,28 +126,18 @@
|
||||
android:id="@+id/add_node_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_anchor="@+id/nodes_list"
|
||||
app:layout_anchor="@+id/node_list_container"
|
||||
app:layout_anchorGravity="right|bottom" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<net.cachapa.expandablelayout.ExpandableLayout
|
||||
android:id="@+id/expandable_toolbar_paste_layout"
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar_paste"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_alignParentBottom="true"
|
||||
app:el_duration="300"
|
||||
app:el_expanded="false"
|
||||
app:el_parallax="0.5">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar_paste"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:elevation="4dp"
|
||||
android:theme="?attr/toolbarBottomAppearance"
|
||||
android:background="?attr/colorAccent"
|
||||
tools:targetApi="lollipop" />
|
||||
|
||||
</net.cachapa.expandablelayout.ExpandableLayout>
|
||||
android:elevation="4dp"
|
||||
android:theme="?attr/toolbarBottomAppearance"
|
||||
android:background="?attr/colorAccent"
|
||||
tools:targetApi="lollipop" />
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -186,8 +186,8 @@
|
||||
android:importantForAutofill="no"
|
||||
android:layout_toEndOf="@+id/keyfile_checkox"
|
||||
android:layout_toRightOf="@+id/keyfile_checkox"
|
||||
android:layout_toLeftOf="@+id/browse_button"
|
||||
android:layout_toStartOf="@+id/browse_button">
|
||||
android:layout_toLeftOf="@+id/open_database_button"
|
||||
android:layout_toStartOf="@+id/open_database_button">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/pass_keyfile"
|
||||
@@ -203,7 +203,7 @@
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/browse_button"
|
||||
android:id="@+id/open_database_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:padding="12dp"
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/browse_button"
|
||||
android:id="@+id/open_database_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
@@ -130,8 +130,8 @@
|
||||
android:id="@+id/keyfile_input_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toLeftOf="@+id/browse_button"
|
||||
android:layout_toStartOf="@+id/browse_button"
|
||||
android:layout_toLeftOf="@+id/open_database_button"
|
||||
android:layout_toStartOf="@+id/open_database_button"
|
||||
android:importantForAccessibility="no"
|
||||
android:importantForAutofill="no"
|
||||
app:passwordToggleEnabled="true"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user