Merge branch 'release/3.3.0'

This commit is contained in:
J-Jamet
2022-02-19 11:01:02 +01:00
200 changed files with 5320 additions and 2035 deletions

View File

@@ -1,3 +1,15 @@
KeePassDX(3.3.0)
* Quick search and dynamic filters #163 #462 #521
* Keep search context #1141
* Add searchable groups #905 #1006
* Search with regular expression #175
* Merge from file and save as copy #1221 #1204 #840
* Fix custom data #1236
* Fix education hints #1192
* Fix save and app instance in selection mode
* New UI and fix styles
* Add "Simple" and "Reply" themes
KeePassDX(3.2.0) KeePassDX(3.2.0)
* Manage data merge #840 #977 * Manage data merge #840 #977
* Manage Tags #633 * Manage Tags #633

View File

@@ -12,8 +12,8 @@ android {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 31 targetSdkVersion 31
versionCode = 97 versionCode = 102
versionName = "3.2.0" versionName = "3.3.0"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
@@ -69,10 +69,14 @@ android {
buildConfigField "boolean", "FULL_VERSION", "false" buildConfigField "boolean", "FULL_VERSION", "false"
buildConfigField "boolean", "CLOSED_STORE", "true" buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED", buildConfigField "String[]", "STYLES_DISABLED",
"{\"KeepassDXStyle_Blue\"," + "{\"KeepassDXStyle_Simple\"," +
"\"KeepassDXStyle_Simple_Night\"," +
"\"KeepassDXStyle_Blue\"," +
"\"KeepassDXStyle_Blue_Night\"," + "\"KeepassDXStyle_Blue_Night\"," +
"\"KeepassDXStyle_Red\"," + "\"KeepassDXStyle_Red\"," +
"\"KeepassDXStyle_Red_Night\"," + "\"KeepassDXStyle_Red_Night\"," +
"\"KeepassDXStyle_Reply\"," +
"\"KeepassDXStyle_Reply_Night\"," +
"\"KeepassDXStyle_Purple\"," + "\"KeepassDXStyle_Purple\"," +
"\"KeepassDXStyle_Purple_Dark\"}" "\"KeepassDXStyle_Purple_Dark\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}" buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
@@ -104,17 +108,18 @@ def room_version = "2.4.1"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "com.android.support:multidex:1.0.3"
implementation "androidx.appcompat:appcompat:$android_appcompat_version" implementation "androidx.appcompat:appcompat:$android_appcompat_version"
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.biometric:biometric:1.1.0'
implementation 'androidx.media:media:1.4.3' implementation 'androidx.media:media:1.5.0'
// Lifecycle - LiveData - ViewModel - Coroutines // Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:$android_core_version" implementation "androidx.core:core-ktx:$android_core_version"
implementation 'androidx.fragment:fragment-ktx:1.4.0' implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation "com.google.android.material:material:$android_material_version" implementation "com.google.android.material:material:$android_material_version"
// Token auto complete // Token auto complete
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed // From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed

View File

@@ -40,7 +40,6 @@
<activity <activity
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity" android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
android:theme="@style/KeepassDXStyle.SplashScreen" android:theme="@style/KeepassDXStyle.SplashScreen"
android:label="@string/app_name"
android:launchMode="singleTop" android:launchMode="singleTop"
android:exported="true" android:exported="true"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
@@ -51,7 +50,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name="com.kunzisoft.keepass.activities.PasswordActivity" android:name="com.kunzisoft.keepass.activities.MainCredentialActivity"
android:exported="true" android:exported="true"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustResize|stateUnchanged"> android:windowSoftInputMode="adjustResize|stateUnchanged">

View File

@@ -66,7 +66,6 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.UuidUtil import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.view.changeControlColor import com.kunzisoft.keepass.view.changeControlColor
@@ -92,6 +91,8 @@ class EntryActivity : DatabaseLockActivity() {
private val mEntryViewModel: EntryViewModel by viewModels() private val mEntryViewModel: EntryViewModel by viewModels()
private val mEntryActivityEducation = EntryActivityEducation(this)
private var mMainEntryId: NodeId<UUID>? = null private var mMainEntryId: NodeId<UUID>? = null
private var mHistoryPosition: Int = -1 private var mHistoryPosition: Int = -1
private var mEntryIsHistory: Boolean = false private var mEntryIsHistory: Boolean = false
@@ -370,7 +371,6 @@ class EntryActivity : DatabaseLockActivity() {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
if (mEntryLoaded) { if (mEntryLoaded) {
val inflater = menuInflater val inflater = menuInflater
MenuUtil.contributionMenuInflater(inflater, menu)
inflater.inflate(R.menu.entry, menu) inflater.inflate(R.menu.entry, menu)
inflater.inflate(R.menu.database, menu) inflater.inflate(R.menu.database, menu)
@@ -381,11 +381,7 @@ class EntryActivity : DatabaseLockActivity() {
// Show education views // Show education views
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
performedNextEducation( performedNextEducation(menu)
EntryActivityEducation(
this
), menu
)
} }
} }
return true return true
@@ -411,31 +407,30 @@ class EntryActivity : DatabaseLockActivity() {
return super.onPrepareOptionsMenu(menu) return super.onPrepareOptionsMenu(menu)
} }
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation, private fun performedNextEducation(menu: Menu) {
menu: Menu) {
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG) val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
as? EntryFragment? as? EntryFragment?
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView() val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
val entryCopyEducationPerformed = entryFieldCopyView != null val entryCopyEducationPerformed = entryFieldCopyView != null
&& entryActivityEducation.checkAndPerformedEntryCopyEducation( && mEntryActivityEducation.checkAndPerformedEntryCopyEducation(
entryFieldCopyView, entryFieldCopyView,
{ {
entryFragment.launchEntryCopyEducationAction() entryFragment.launchEntryCopyEducationAction()
}, },
{ {
performedNextEducation(entryActivityEducation, menu) performedNextEducation(menu)
}) })
if (!entryCopyEducationPerformed) { if (!entryCopyEducationPerformed) {
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit) val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
// entryEditEducationPerformed // entryEditEducationPerformed
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation( menuEditView != null && mEntryActivityEducation.checkAndPerformedEntryEditEducation(
menuEditView, menuEditView,
{ {
onOptionsItemSelected(menu.findItem(R.id.menu_edit)) onOptionsItemSelected(menu.findItem(R.id.menu_edit))
}, },
{ {
performedNextEducation(entryActivityEducation, menu) performedNextEducation(menu)
} }
) )
} }
@@ -443,10 +438,6 @@ class EntryActivity : DatabaseLockActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_contribute -> {
MenuUtil.onContributionItemSelected(this)
return true
}
R.id.menu_edit -> { R.id.menu_edit -> {
mDatabase?.let { database -> mDatabase?.let { database ->
mMainEntryId?.let { entryId -> mMainEntryId?.let { entryId ->

View File

@@ -113,7 +113,7 @@ class EntryEditActivity : DatabaseLockActivity(),
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
// Education // Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null private var mEntryEditActivityEducation = EntryEditActivityEducation(this)
private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon -> private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
mEntryEditViewModel.selectIcon(icon) mEntryEditViewModel.selectIcon(icon)
@@ -183,8 +183,6 @@ class EntryEditActivity : DatabaseLockActivity(),
} }
mAttachmentFileBinderManager = AttachmentFileBinderManager(this) mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
// Verify the education views
entryEditActivityEducation = EntryEditActivityEducation(this)
// Lock button // Lock button
lockView?.setOnClickListener { lockAndExit() } lockView?.setOnClickListener { lockAndExit() }
@@ -538,10 +536,8 @@ class EntryEditActivity : DatabaseLockActivity(),
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
if (mEntryLoaded) { if (mEntryLoaded) {
menuInflater.inflate(R.menu.entry_edit, menu) menuInflater.inflate(R.menu.entry_edit, menu)
entryEditActivityEducation?.let { Handler(Looper.getMainLooper()).post {
Handler(Looper.getMainLooper()).post { performedNextEducation()
performedNextEducation(it)
}
} }
} }
return true return true
@@ -568,19 +564,19 @@ class EntryEditActivity : DatabaseLockActivity(),
return super.onPrepareOptionsMenu(menu) return super.onPrepareOptionsMenu(menu)
} }
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) { private fun performedNextEducation() {
val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content) val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content)
as? EntryEditFragment? as? EntryEditFragment?
val generatePasswordView = entryEditFragment?.getActionImageView() val generatePasswordView = entryEditFragment?.getActionImageView()
val generatePasswordEductionPerformed = generatePasswordView != null val generatePasswordEductionPerformed = generatePasswordView != null
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( && mEntryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
generatePasswordView, generatePasswordView,
{ {
entryEditFragment.launchGeneratePasswordEductionAction() entryEditFragment.launchGeneratePasswordEductionAction()
}, },
{ {
performedNextEducation(entryEditActivityEducation) performedNextEducation()
} }
) )
@@ -589,33 +585,33 @@ class EntryEditActivity : DatabaseLockActivity(),
val addNewFieldEducationPerformed = mAllowCustomFields val addNewFieldEducationPerformed = mAllowCustomFields
&& addNewFieldView != null && addNewFieldView != null
&& addNewFieldView.isVisible && addNewFieldView.isVisible
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation( && mEntryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView, addNewFieldView,
{ {
addNewCustomField() addNewCustomField()
}, },
{ {
performedNextEducation(entryEditActivityEducation) performedNextEducation()
} }
) )
if (!addNewFieldEducationPerformed) { if (!addNewFieldEducationPerformed) {
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment) val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
val addAttachmentEducationPerformed = attachmentView != null val addAttachmentEducationPerformed = attachmentView != null
&& attachmentView.isVisible && attachmentView.isVisible
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation( && mEntryEditActivityEducation.checkAndPerformedAttachmentEducation(
attachmentView, attachmentView,
{ {
addNewAttachment() addNewAttachment()
}, },
{ {
performedNextEducation(entryEditActivityEducation) performedNextEducation()
} }
) )
if (!addAttachmentEducationPerformed) { if (!addAttachmentEducationPerformed) {
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp) val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
setupOtpView != null setupOtpView != null
&& setupOtpView.isVisible && setupOtpView.isVisible
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation( && mEntryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
setupOtpView, setupOtpView,
{ {
setupOtp() setupOtp()
@@ -662,8 +658,8 @@ class EntryEditActivity : DatabaseLockActivity(),
override fun acceptPassword(passwordField: Field) { override fun acceptPassword(passwordField: Field) {
mEntryEditViewModel.selectPassword(passwordField) mEntryEditViewModel.selectPassword(passwordField)
entryEditActivityEducation?.let { Handler(Looper.getMainLooper()).post {
Handler(Looper.getMainLooper()).post { performedNextEducation(it) } performedNextEducation()
} }
} }

View File

@@ -42,7 +42,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.SpecialMode
@@ -69,7 +69,7 @@ import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
import java.io.FileNotFoundException import java.io.FileNotFoundException
class FileDatabaseSelectActivity : DatabaseModeActivity(), class FileDatabaseSelectActivity : DatabaseModeActivity(),
AssignMasterKeyDialogFragment.AssignPasswordDialogListener { SetMainCredentialDialogFragment.AssignMainCredentialDialogListener {
// Views // Views
private lateinit var coordinatorLayout: CoordinatorLayout private lateinit var coordinatorLayout: CoordinatorLayout
@@ -78,6 +78,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels() private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
private val mFileDatabaseSelectActivityEducation = FileDatabaseSelectActivityEducation(this)
// Adapter to manage database history list // Adapter to manage database history list
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
@@ -124,7 +126,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri -> mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) { if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true) SetMainCredentialDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog") .show(supportFragmentManager, "passwordDialog")
} else { } else {
val error = getString(R.string.error_create_database) val error = getString(R.string.error_create_database)
@@ -132,7 +134,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
Log.e(TAG, error) Log.e(TAG, error)
} }
} }
openDatabaseButtonView = findViewById(R.id.open_keyfile_button) openDatabaseButtonView = findViewById(R.id.open_database_button)
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper) openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
// History list // History list
@@ -291,7 +293,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) { private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
PasswordActivity.launch(this, MainCredentialActivity.launch(this,
databaseUri, databaseUri,
keyFile, keyFile,
{ exception -> { exception ->
@@ -392,39 +394,40 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
MenuUtil.defaultMenuInflater(menuInflater, menu) MenuUtil.defaultMenuInflater(menuInflater, menu)
} }
Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) } Handler(Looper.getMainLooper()).post {
performedNextEducation()
}
return true return true
} }
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) { private fun performedNextEducation() {
// If no recent files // If no recent files
val createDatabaseEducationPerformed = val createDatabaseEducationPerformed =
createDatabaseButtonView != null createDatabaseButtonView != null
&& createDatabaseButtonView!!.visibility == View.VISIBLE && createDatabaseButtonView!!.visibility == View.VISIBLE
&& mAdapterDatabaseHistory != null && mFileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
&& mAdapterDatabaseHistory!!.itemCount == 0
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
createDatabaseButtonView!!, createDatabaseButtonView!!,
{ {
createNewFile() createNewFile()
}, },
{ {
// But if the user cancel, it can also select a database // But if the user cancel, it can also select a database
performedNextEducation(fileDatabaseSelectActivityEducation) performedNextEducation()
}) })
if (!createDatabaseEducationPerformed) { if (!createDatabaseEducationPerformed) {
// selectDatabaseEducationPerformed // selectDatabaseEducationPerformed
openDatabaseButtonView != null openDatabaseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation( && mFileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
openDatabaseButtonView!!, openDatabaseButtonView!!,
{ tapTargetView -> { tapTargetView ->
tapTargetView?.let { tapTargetView?.let {
mExternalFileHelper?.openDocument() mExternalFileHelper?.openDocument()
} }
}, },
{} {
)
})
} }
} }

View File

@@ -26,14 +26,14 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.Editable
import android.text.TextWatcher
import android.util.Log import android.util.Log
import android.view.* import android.view.Menu
import android.view.KeyEvent.KEYCODE_ENTER import android.view.MenuItem
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.ViewGroup
import android.widget.* import android.widget.Button
import android.widget.CompoundButton
import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@@ -44,10 +44,11 @@ import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.* import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
@@ -55,11 +56,9 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
@@ -68,23 +67,20 @@ import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.MainCredentialView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import java.io.FileNotFoundException import java.io.FileNotFoundException
class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener { class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
// Views // Views
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
private var filenameView: TextView? = null private var filenameView: TextView? = null
private var passwordView: EditText? = null private var mainCredentialView: MainCredentialView? = null
private var keyFileSelectionView: KeyFileSelectionView? = null
private var confirmButtonView: Button? = null private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null
private var infoContainerView: ViewGroup? = null private var infoContainerView: ViewGroup? = null
private lateinit var coordinatorLayout: CoordinatorLayout private lateinit var coordinatorLayout: CoordinatorLayout
private var advancedUnlockFragment: AdvancedUnlockFragment? = null private var advancedUnlockFragment: AdvancedUnlockFragment? = null
@@ -92,9 +88,10 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels() private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels() private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
private val mPasswordActivityEducation = PasswordActivityEducation(this)
private var mDefaultDatabase: Boolean = false private var mDefaultDatabase: Boolean = false
private var mDatabaseFileUri: Uri? = null private var mDatabaseFileUri: Uri? = null
private var mDatabaseKeyFileUri: Uri? = null
private var mRememberKeyFile: Boolean = false private var mRememberKeyFile: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
@@ -110,7 +107,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_password) setContentView(R.layout.activity_main_credential)
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
toolbar?.title = getString(R.string.app_name) toolbar?.title = getString(R.string.app_name)
@@ -118,12 +115,9 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
confirmButtonView = findViewById(R.id.activity_password_open_button)
filenameView = findViewById(R.id.filename) filenameView = findViewById(R.id.filename)
passwordView = findViewById(R.id.password) mainCredentialView = findViewById(R.id.activity_password_credentials)
keyFileSelectionView = findViewById(R.id.keyfile_selection) confirmButtonView = findViewById(R.id.activity_password_open_button)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
infoContainerView = findViewById(R.id.activity_password_info_container) infoContainerView = findViewById(R.id.activity_password_info_container)
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout) coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
@@ -134,41 +128,19 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} }
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity) mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity)
mExternalFileHelper?.buildOpenDocument { uri -> mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) { if (uri != null) {
mDatabaseKeyFileUri = uri mainCredentialView?.populateKeyFileTextView(uri)
populateKeyFileTextView(uri)
} }
} }
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper) mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
mainCredentialView?.onValidateListener = {
passwordView?.setOnEditorActionListener(onEditorActionListener) loadDatabase()
passwordView?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
if (editable.toString().isNotEmpty() && checkboxPasswordView?.isChecked != true)
checkboxPasswordView?.isChecked = true
}
})
passwordView?.setOnKeyListener { _, _, keyEvent ->
var handled = false
if (keyEvent.action == KeyEvent.ACTION_DOWN
&& keyEvent?.keyCode == KEYCODE_ENTER) {
verifyCheckboxesAndLoadDatabase()
handled = true
}
handled
} }
// If is a view intent // If is a view intent
getUriFromIntent(intent) getUriFromIntent(intent)
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
}
// Init Biometric elements // Init Biometric elements
advancedUnlockFragment = supportFragmentManager advancedUnlockFragment = supportFragmentManager
@@ -183,10 +155,11 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} }
// Listen password checkbox to init advanced unlock and confirmation button // Listen password checkbox to init advanced unlock and confirmation button
checkboxPasswordView?.setOnCheckedChangeListener { _, _ -> mainCredentialView?.onPasswordChecked =
mAdvancedUnlockViewModel.checkUnlockAvailability() CompoundButton.OnCheckedChangeListener { _, _ ->
enableOrNotTheConfirmationButton() mAdvancedUnlockViewModel.checkUnlockAvailability()
} enableConfirmationButton()
}
// Observe if default database // Observe if default database
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
@@ -211,12 +184,13 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
invalidateOptionsMenu() invalidateOptionsMenu()
// Post init uri with KeyFile only if needed // Post init uri with KeyFile only if needed
val databaseKeyFileUri = mainCredentialView?.getMainCredential()?.keyFileUri
val keyFileUri = val keyFileUri =
if (mRememberKeyFile if (mRememberKeyFile
&& (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) { && (databaseKeyFileUri == null || databaseKeyFileUri.toString().isEmpty())) {
databaseFile?.keyFileUri databaseFile?.keyFileUri
} else { } else {
mDatabaseKeyFileUri databaseKeyFileUri
} }
// Define title // Define title
@@ -229,10 +203,10 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@PasswordActivity) mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
// Back to previous keyboard is setting activated // Back to previous keyboard is setting activated
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@PasswordActivity)) { if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION)) sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
} }
@@ -271,7 +245,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
if (result.isSuccess) { if (result.isSuccess) {
launchGroupActivityIfLoaded(database) launchGroupActivityIfLoaded(database)
} else { } else {
passwordView?.requestFocusFromTouch() mainCredentialView?.requestPasswordFocus()
var resultError = "" var resultError = ""
val resultException = result.exception val resultException = result.exception
@@ -288,7 +262,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
var databaseUri: Uri? = null var databaseUri: Uri? = null
var mainCredential = MainCredential() var mainCredential = MainCredential()
var readOnly = true var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null var cipherEncryptDatabase: CipherEncryptDatabase? = null
result.data?.let { resultData -> result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY) databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
@@ -296,8 +270,8 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
resultData.getParcelable(MAIN_CREDENTIAL_KEY) resultData.getParcelable(MAIN_CREDENTIAL_KEY)
?: mainCredential ?: mainCredential
readOnly = resultData.getBoolean(READ_ONLY_KEY) readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity = cipherEncryptDatabase =
resultData.getParcelable(CIPHER_ENTITY_KEY) resultData.getParcelable(CIPHER_DATABASE_KEY)
} }
databaseUri?.let { databaseFileUri -> databaseUri?.let { databaseFileUri ->
@@ -305,7 +279,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
databaseFileUri, databaseFileUri,
mainCredential, mainCredential,
readOnly, readOnly,
cipherEntity, cipherEncryptDatabase,
true true
) )
} }
@@ -341,11 +315,16 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
if (action != null if (action != null
&& action == VIEW_INTENT) { && action == VIEW_INTENT) {
mDatabaseFileUri = intent.data mDatabaseFileUri = intent.data
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE) mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE))
} else { } else {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME) mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE) intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let {
mainCredentialView?.populateKeyFileTextView(it)
}
} }
try {
intent?.removeExtra(KEY_KEYFILE)
} catch (e: Exception) {}
mDatabaseFileUri?.let { mDatabaseFileUri?.let {
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it) mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
} }
@@ -380,51 +359,68 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
finish() finish()
} }
override fun retrieveCredentialForEncryption(): String { override fun retrieveCredentialForEncryption(): ByteArray {
return passwordView?.text?.toString() ?: "" return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
?: byteArrayOf()
} }
override fun conditionToStoreCredential(): Boolean { override fun conditionToStoreCredential(): Boolean {
return checkboxPasswordView?.isChecked == true return mainCredentialView?.conditionToStoreCredential() == true
} }
override fun onCredentialEncrypted(databaseUri: Uri, override fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
encryptedCredential: String,
ivSpec: String) {
// Load the database if password is registered with biometric // Load the database if password is registered with biometric
verifyCheckboxesAndLoadDatabase( loadDatabase(mDatabaseFileUri,
CipherDatabaseEntity( mainCredentialView?.getMainCredential(),
databaseUri.toString(), cipherEncryptDatabase
encryptedCredential,
ivSpec)
) )
} }
override fun onCredentialDecrypted(databaseUri: Uri, private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
decryptedCredential: String) { override fun passwordToStore(password: String?): ByteArray? {
// Load the database if password is retrieve from biometric return password?.toByteArray()
// Retrieve from biometric }
verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential)
override fun keyfileToStore(keyfile: Uri?): ByteArray? {
// TODO create byte array to store keyfile
return null
}
override fun hardwareKeyToStore(): ByteArray? {
// TODO create byte array to store hardware key
return null
}
} }
private val onEditorActionListener = object : TextView.OnEditorActionListener { override fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { // Load the database if password is retrieve from biometric
if (actionId == IME_ACTION_DONE) { // Retrieve from biometric
verifyCheckboxesAndLoadDatabase() val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
return true when (cipherDecryptDatabase.credentialStorage) {
CredentialStorage.PASSWORD -> {
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue)
}
CredentialStorage.KEY_FILE -> {
// TODO advanced unlock key file
}
CredentialStorage.HARDWARE_KEY -> {
// TODO advanced unlock hardware key
} }
return false
} }
loadDatabase(mDatabaseFileUri,
mainCredential,
null
)
} }
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) { private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
// Define Key File text // Define Key File text
if (mRememberKeyFile) { if (mRememberKeyFile) {
populateKeyFileTextView(keyFileUri) mainCredentialView?.populateKeyFileTextView(keyFileUri)
} }
// Define listener for validate button // Define listener for validate button
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() } confirmButtonView?.setOnClickListener { loadDatabase() }
// If Activity is launch with a password and want to open directly // If Activity is launch with a password and want to open directly
val intent = intent val intent = intent
@@ -433,66 +429,33 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
intent.removeExtra(KEY_PASSWORD) intent.removeExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false) val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) { if (password != null) {
populatePasswordTextView(password) mainCredentialView?.populatePasswordTextView(password)
} }
if (launchImmediately) { if (launchImmediately) {
verifyCheckboxesAndLoadDatabase(password, keyFileUri) loadDatabase()
} else { } else {
// Init Biometric elements // Init Biometric elements
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri) mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
} }
enableOrNotTheConfirmationButton() enableConfirmationButton()
// Auto select the password field and open keyboard mainCredentialView?.focusPasswordFieldAndOpenKeyboard()
passwordView?.postDelayed({
passwordView?.requestFocusFromTouch()
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager?
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
}, 100)
} }
private fun enableOrNotTheConfirmationButton() { private fun enableConfirmationButton() {
// Enable or not the open button if setting is checked // Enable or not the open button if setting is checked
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) { if (!PreferencesUtil.emptyPasswordAllowed(this@MainCredentialActivity)) {
checkboxPasswordView?.let { confirmButtonView?.isEnabled = mainCredentialView?.isFill() ?: false
confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true
|| checkboxKeyFileView?.isChecked == true)
}
} else { } else {
confirmButtonView?.isEnabled = true confirmButtonView?.isEnabled = true
} }
} }
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) { private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
populatePasswordTextView(null) mainCredentialView?.populatePasswordTextView(null)
if (clearKeyFile) { if (clearKeyFile) {
mDatabaseKeyFileUri = null mainCredentialView?.populateKeyFileTextView(null)
populateKeyFileTextView(null)
}
}
private fun populatePasswordTextView(text: String?) {
if (text == null || text.isEmpty()) {
passwordView?.setText("")
if (checkboxPasswordView?.isChecked == true)
checkboxPasswordView?.isChecked = false
} else {
passwordView?.setText(text)
if (checkboxPasswordView?.isChecked != true)
checkboxPasswordView?.isChecked = true
}
}
private fun populateKeyFileTextView(uri: Uri?) {
if (uri == null || uri.toString().isEmpty()) {
keyFileSelectionView?.uri = null
if (checkboxKeyFileView?.isChecked == true)
checkboxKeyFileView?.isChecked = false
} else {
keyFileSelectionView?.uri = uri
if (checkboxKeyFileView?.isChecked != true)
checkboxKeyFileView?.isChecked = true
} }
} }
@@ -504,41 +467,20 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
mDatabaseKeyFileUri?.let {
outState.putString(KEY_KEYFILE, it.toString())
}
outState.putBoolean(KEY_READ_ONLY, mReadOnly) outState.putBoolean(KEY_READ_ONLY, mReadOnly)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) { private fun loadDatabase() {
val password: String? = passwordView?.text?.toString() loadDatabase(mDatabaseFileUri,
val keyFile: Uri? = keyFileSelectionView?.uri mainCredentialView?.getMainCredential(),
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity) null
} )
private fun verifyCheckboxesAndLoadDatabase(password: String?,
keyFile: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
}
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
val keyFile: Uri? = keyFileSelectionView?.uri
verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
}
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
} }
private fun loadDatabase(databaseFileUri: Uri?, private fun loadDatabase(databaseFileUri: Uri?,
password: String?, mainCredential: MainCredential?,
keyFileUri: Uri?, cipherEncryptDatabase: CipherEncryptDatabase?) {
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) { if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
clearCredentialsViews() clearCredentialsViews()
@@ -556,11 +498,12 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
databaseFileUri?.let { databaseUri -> databaseFileUri?.let { databaseUri ->
// Show the progress dialog and load the database // Show the progress dialog and load the database
showProgressDialogAndLoadDatabase( showProgressDialogAndLoadDatabase(
databaseUri, databaseUri,
MainCredential(password, keyFileUri), mainCredential ?: MainCredential(),
mReadOnly, mReadOnly,
cipherDatabaseEntity, cipherEncryptDatabase,
false) false
)
} }
} }
} }
@@ -568,14 +511,14 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri, private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
mainCredential: MainCredential, mainCredential: MainCredential,
readOnly: Boolean, readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?, cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUUID: Boolean) { fixDuplicateUUID: Boolean) {
loadDatabase( loadDatabase(
databaseUri, databaseUri,
mainCredential, mainCredential,
readOnly, readOnly,
cipherDatabaseEntity, cipherEncryptDatabase,
fixDuplicateUUID fixDuplicateUUID
) )
} }
@@ -612,26 +555,27 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
if (!performedEductionInProgress) { if (!performedEductionInProgress) {
performedEductionInProgress = true performedEductionInProgress = true
// Show education views // Show education views
Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) } Handler(Looper.getMainLooper()).post {
performedNextEducation(menu)
}
} }
} }
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation, private fun performedNextEducation(menu: Menu) {
menu: Menu) {
val educationToolbar = toolbar val educationToolbar = toolbar
val unlockEducationPerformed = educationToolbar != null val unlockEducationPerformed = educationToolbar != null
&& passwordActivityEducation.checkAndPerformedUnlockEducation( && mPasswordActivityEducation.checkAndPerformedUnlockEducation(
educationToolbar, educationToolbar,
{ {
performedNextEducation(passwordActivityEducation, menu) performedNextEducation(menu)
}, },
{ {
performedNextEducation(passwordActivityEducation, menu) performedNextEducation(menu)
}) })
if (!unlockEducationPerformed) { if (!unlockEducationPerformed) {
val readOnlyEducationPerformed = val readOnlyEducationPerformed =
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation( && mPasswordActivityEducation.checkAndPerformedReadOnlyEducation(
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key), educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
{ {
try { try {
@@ -639,19 +583,19 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to find read mode menu") Log.e(TAG, "Unable to find read mode menu")
} }
performedNextEducation(passwordActivityEducation, menu) performedNextEducation(menu)
}, },
{ {
performedNextEducation(passwordActivityEducation, menu) performedNextEducation(menu)
}) })
advancedUnlockFragment?.performEducation(passwordActivityEducation, advancedUnlockFragment?.performEducation(mPasswordActivityEducation,
readOnlyEducationPerformed, readOnlyEducationPerformed,
{ {
performedNextEducation(passwordActivityEducation, menu) performedNextEducation(menu)
}, },
{ {
performedNextEducation(passwordActivityEducation, menu) performedNextEducation(menu)
}) })
} }
} }
@@ -682,7 +626,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
companion object { companion object {
private val TAG = PasswordActivity::class.java.name private val TAG = MainCredentialActivity::class.java.name
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG" private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
@@ -696,7 +640,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?, private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
intentBuildLauncher: (Intent) -> Unit) { intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, PasswordActivity::class.java) val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile) intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null) if (keyFile != null)
intent.putExtra(KEY_KEYFILE, keyFile) intent.putExtra(KEY_KEYFILE, keyFile)
@@ -832,30 +776,30 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
try { try {
EntrySelectionHelper.doSpecialAction(activity.intent, EntrySelectionHelper.doSpecialAction(activity.intent,
{ {
PasswordActivity.launch(activity, MainCredentialActivity.launch(activity,
databaseUri, keyFile) databaseUri, keyFile)
}, },
{ searchInfo -> // Search Action { searchInfo -> // Search Action
PasswordActivity.launchForSearchResult(activity, MainCredentialActivity.launchForSearchResult(activity,
databaseUri, keyFile, databaseUri, keyFile,
searchInfo) searchInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo -> // Save Action { searchInfo -> // Save Action
PasswordActivity.launchForSaveResult(activity, MainCredentialActivity.launchForSaveResult(activity,
databaseUri, keyFile, databaseUri, keyFile,
searchInfo) searchInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo -> // Keyboard Selection Action { searchInfo -> // Keyboard Selection Action
PasswordActivity.launchForKeyboardResult(activity, MainCredentialActivity.launchForKeyboardResult(activity,
databaseUri, keyFile, databaseUri, keyFile,
searchInfo) searchInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo, autofillComponent -> // Autofill Selection Action { searchInfo, autofillComponent -> // Autofill Selection Action
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PasswordActivity.launchForAutofillResult(activity, MainCredentialActivity.launchForAutofillResult(activity,
databaseUri, keyFile, databaseUri, keyFile,
autofillActivityResultLauncher, autofillActivityResultLauncher,
autofillComponent, autofillComponent,
@@ -866,7 +810,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} }
}, },
{ registerInfo -> // Registration Action { registerInfo -> // Registration Action
PasswordActivity.launchForRegistration(activity, MainCredentialActivity.launchForRegistration(activity,
databaseUri, keyFile, databaseUri, keyFile,
registerInfo) registerInfo)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()

View File

@@ -53,6 +53,10 @@ class GroupDialogFragment : DatabaseDialogFragment() {
private lateinit var expirationView: DateTimeFieldView private lateinit var expirationView: DateTimeFieldView
private lateinit var creationView: TextView private lateinit var creationView: TextView
private lateinit var modificationView: TextView private lateinit var modificationView: TextView
private lateinit var searchableLabelView: TextView
private lateinit var searchableView: TextView
private lateinit var autoTypeLabelView: TextView
private lateinit var autoTypeView: TextView
private lateinit var uuidContainerView: ViewGroup private lateinit var uuidContainerView: ViewGroup
private lateinit var uuidReferenceView: TextView private lateinit var uuidReferenceView: TextView
@@ -62,6 +66,25 @@ class GroupDialogFragment : DatabaseDialogFragment() {
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor) database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
} }
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon) mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
if (database?.allowCustomSearchableGroup() == true) {
searchableLabelView.visibility = View.VISIBLE
searchableView.visibility = View.VISIBLE
} else {
searchableLabelView.visibility = View.GONE
searchableView.visibility = View.GONE
}
// TODO Auto-Type
/*
if (database?.allowAutoType() == true) {
autoTypeLabelView.visibility = View.VISIBLE
autoTypeView.visibility = View.VISIBLE
} else {
autoTypeLabelView.visibility = View.GONE
autoTypeView.visibility = View.GONE
}
*/
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -75,6 +98,10 @@ class GroupDialogFragment : DatabaseDialogFragment() {
expirationView = root.findViewById(R.id.group_expiration) expirationView = root.findViewById(R.id.group_expiration)
creationView = root.findViewById(R.id.group_created) creationView = root.findViewById(R.id.group_created)
modificationView = root.findViewById(R.id.group_modified) modificationView = root.findViewById(R.id.group_modified)
searchableLabelView = root.findViewById(R.id.group_searchable_label)
searchableView = root.findViewById(R.id.group_searchable)
autoTypeLabelView = root.findViewById(R.id.group_auto_type_label)
autoTypeView = root.findViewById(R.id.group_auto_type)
uuidContainerView = root.findViewById(R.id.group_UUID_container) uuidContainerView = root.findViewById(R.id.group_UUID_container)
uuidReferenceView = root.findViewById(R.id.group_UUID_reference) uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
@@ -123,6 +150,9 @@ class GroupDialogFragment : DatabaseDialogFragment() {
expirationView.dateTime = mGroupInfo.expiryTime expirationView.dateTime = mGroupInfo.expiryTime
creationView.text = mGroupInfo.creationTime.getDateTimeString(resources) creationView.text = mGroupInfo.creationTime.getDateTimeString(resources)
modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources) modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources)
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
mGroupInfo.defaultAutoTypeSequence)
val uuid = UuidUtil.toHexString(mGroupInfo.id) val uuid = UuidUtil.toHexString(mGroupInfo.id)
if (uuid == null || uuid.isEmpty()) { if (uuid == null || uuid.isEmpty()) {
uuidContainerView.visibility = View.GONE uuidContainerView.visibility = View.GONE
@@ -143,6 +173,15 @@ class GroupDialogFragment : DatabaseDialogFragment() {
return super.onCreateDialog(savedInstanceState) return super.onCreateDialog(savedInstanceState)
} }
private fun stringFromInheritableBoolean(enable: Boolean?, value: String? = null): String {
val valueString = if (value != null && value.isNotEmpty()) " [$value]" else ""
return when {
enable == null -> getString(R.string.inherited) + valueString
enable -> getString(R.string.enable) + valueString
else -> getString(R.string.disable)
}
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo) outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)

View File

@@ -23,9 +23,8 @@ import android.app.Dialog
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Button import android.view.ViewGroup
import android.widget.ImageView import android.widget.*
import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
@@ -37,6 +36,7 @@ import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.view.DateTimeEditFieldView import com.kunzisoft.keepass.view.DateTimeEditFieldView
import com.kunzisoft.keepass.view.InheritedCompletionView
import com.kunzisoft.keepass.view.TagsCompletionView import com.kunzisoft.keepass.view.TagsCompletionView
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.tokenautocomplete.FilteredArrayAdapter import com.tokenautocomplete.FilteredArrayAdapter
@@ -58,6 +58,12 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
private lateinit var notesTextLayoutView: TextInputLayout private lateinit var notesTextLayoutView: TextInputLayout
private lateinit var notesTextView: TextView private lateinit var notesTextView: TextView
private lateinit var expirationView: DateTimeEditFieldView private lateinit var expirationView: DateTimeEditFieldView
private lateinit var searchableContainerView: TextInputLayout
private lateinit var searchableView: InheritedCompletionView
private lateinit var autoTypeContainerView: ViewGroup
private lateinit var autoTypeInheritedView: InheritedCompletionView
private lateinit var autoTypeSequenceView: TextView
private lateinit var tagsContainerView: TextInputLayout
private lateinit var tagsCompletionView: TagsCompletionView private lateinit var tagsCompletionView: TagsCompletionView
private var tagsAdapter: FilteredArrayAdapter<String>? = null private var tagsAdapter: FilteredArrayAdapter<String>? = null
@@ -118,11 +124,24 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
} }
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon) mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
View.VISIBLE
} else {
View.GONE
}
if (database?.allowAutoType() == true) {
autoTypeContainerView.visibility = View.VISIBLE
} else {
autoTypeContainerView.visibility = View.GONE
}
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool) tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
tagsCompletionView.apply { tagsCompletionView.apply {
threshold = 1 threshold = 1
setAdapter(tagsAdapter) setAdapter(tagsAdapter)
} }
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -134,6 +153,12 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container) notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
notesTextView = root.findViewById(R.id.group_edit_note) notesTextView = root.findViewById(R.id.group_edit_note)
expirationView = root.findViewById(R.id.group_edit_expiration) expirationView = root.findViewById(R.id.group_edit_expiration)
searchableContainerView = root.findViewById(R.id.group_edit_searchable_container)
searchableView = root.findViewById(R.id.group_edit_searchable)
autoTypeContainerView = root.findViewById(R.id.group_edit_auto_type_container)
autoTypeInheritedView = root.findViewById(R.id.group_edit_auto_type_inherited)
autoTypeSequenceView = root.findViewById(R.id.group_edit_auto_type_sequence)
tagsContainerView = root.findViewById(R.id.group_tags_label)
tagsCompletionView = root.findViewById(R.id.group_tags_completion_view) tagsCompletionView = root.findViewById(R.id.group_tags_completion_view)
// Retrieve the textColor to tint the icon // Retrieve the textColor to tint the icon
@@ -211,6 +236,11 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
expirationView.activation = groupInfo.expires expirationView.activation = groupInfo.expires
expirationView.dateTime = groupInfo.expiryTime expirationView.dateTime = groupInfo.expiryTime
// Set searchable
searchableView.setValue(groupInfo.searchable)
// Set auto-type
autoTypeInheritedView.setValue(groupInfo.enableAutoType)
autoTypeSequenceView.text = groupInfo.defaultAutoTypeSequence
// Set Tags // Set Tags
groupInfo.tags.let { tags -> groupInfo.tags.let { tags ->
tagsCompletionView.setText("") tagsCompletionView.setText("")
@@ -229,6 +259,9 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
} }
mGroupInfo.expires = expirationView.activation mGroupInfo.expires = expirationView.activation
mGroupInfo.expiryTime = expirationView.dateTime mGroupInfo.expiryTime = expirationView.dateTime
mGroupInfo.searchable = searchableView.getValue()
mGroupInfo.enableAutoType = autoTypeInheritedView.getValue()
mGroupInfo.defaultAutoTypeSequence = autoTypeSequenceView.text.toString()
mGroupInfo.tags = tagsCompletionView.getTags() mGroupInfo.tags = tagsCompletionView.getTags()
} }

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.MainCredentialView
class MainCredentialDialogFragment : DatabaseDialogFragment() {
private var mainCredentialView: MainCredentialView? = null
private var mListener: AskMainCredentialDialogListener? = null
private var mExternalFileHelper: ExternalFileHelper? = null
interface AskMainCredentialDialogListener {
fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?, mainCredential: MainCredential)
fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?, mainCredential: MainCredential)
}
override fun onAttach(activity: Context) {
super.onAttach(activity)
try {
mListener = activity as AskMainCredentialDialogListener
} catch (e: ClassCastException) {
throw ClassCastException(activity.toString()
+ " must implement " + AskMainCredentialDialogListener::class.java.name)
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
var databaseUri: Uri? = null
arguments?.apply {
if (containsKey(KEY_ASK_CREDENTIAL_URI))
databaseUri = getParcelable(KEY_ASK_CREDENTIAL_URI)
}
val builder = AlertDialog.Builder(activity)
val root = activity.layoutInflater.inflate(R.layout.fragment_main_credential, null)
mainCredentialView = root.findViewById(R.id.main_credential_view)
databaseUri?.let {
root.findViewById<TextView>(R.id.title_database)?.text =
UriUtil.getFileData(requireContext(), it)?.name
}
builder.setView(root)
// Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onAskMainCredentialDialogPositiveClick(
databaseUri,
retrieveMainCredential()
)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
mListener?.onAskMainCredentialDialogNegativeClick(
databaseUri,
retrieveMainCredential()
)
}
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) {
mainCredentialView?.populateKeyFileTextView(uri)
}
}
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun retrieveMainCredential(): MainCredential {
return mainCredentialView?.getMainCredential() ?: MainCredential()
}
companion object {
private const val KEY_ASK_CREDENTIAL_URI = "KEY_ASK_CREDENTIAL_URI"
const val TAG_ASK_MAIN_CREDENTIAL = "TAG_ASK_MAIN_CREDENTIAL"
fun getInstance(uri: Uri?): MainCredentialDialogFragment {
val fragment = MainCredentialDialogFragment()
val args = Bundle()
args.putParcelable(KEY_ASK_CREDENTIAL_URI, uri)
fragment.arguments = args
return fragment
}
}
}

View File

@@ -39,7 +39,7 @@ import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mMasterPassword: String? = null private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null private var mKeyFile: Uri? = null
@@ -56,7 +56,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
private var keyFileCheckBox: CompoundButton? = null private var keyFileCheckBox: CompoundButton? = null
private var keyFileSelectionView: KeyFileSelectionView? = null private var keyFileSelectionView: KeyFileSelectionView? = null
private var mListener: AssignPasswordDialogListener? = null private var mListener: AssignMainCredentialDialogListener? = null
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
@@ -74,7 +74,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
} }
} }
interface AssignPasswordDialogListener { interface AssignMainCredentialDialogListener {
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
} }
@@ -82,10 +82,10 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
override fun onAttach(activity: Context) { override fun onAttach(activity: Context) {
super.onAttach(activity) super.onAttach(activity)
try { try {
mListener = activity as AssignPasswordDialogListener mListener = activity as AssignMainCredentialDialogListener
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
throw ClassCastException(activity.toString() throw ClassCastException(activity.toString()
+ " must implement " + AssignPasswordDialogListener::class.java.name) + " must implement " + AssignMainCredentialDialogListener::class.java.name)
} }
} }
@@ -112,7 +112,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater val inflater = activity.layoutInflater
rootView = inflater.inflate(R.layout.fragment_set_password, null) rootView = inflater.inflate(R.layout.fragment_set_main_credential, null)
builder.setView(rootView) builder.setView(rootView)
// Add action buttons // Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ -> } .setPositiveButton(android.R.string.ok) { _, _ -> }
@@ -254,7 +254,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyKeyFile()) { if (!verifyKeyFile()) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@AssignMasterKeyDialogFragment.dismiss() this@SetMainCredentialDialogFragment.dismiss()
} }
} }
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
@@ -269,7 +269,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
builder.setMessage(R.string.warning_no_encryption_key) builder.setMessage(R.string.warning_no_encryption_key)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@AssignMasterKeyDialogFragment.dismiss() this@SetMainCredentialDialogFragment.dismiss()
} }
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
mNoKeyConfirmationDialog = builder.create() mNoKeyConfirmationDialog = builder.create()
@@ -301,8 +301,8 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG" private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment { fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
val fragment = AssignMasterKeyDialogFragment() val fragment = SetMainCredentialDialogFragment()
val args = Bundle() val args = Bundle()
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey) args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
fragment.arguments = args fragment.arguments = args

View File

@@ -29,6 +29,7 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
@@ -55,6 +56,7 @@ class EntryEditFragment: DatabaseFragment() {
private lateinit var attachmentsContainerView: ViewGroup private lateinit var attachmentsContainerView: ViewGroup
private lateinit var attachmentsListView: RecyclerView private lateinit var attachmentsListView: RecyclerView
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private lateinit var tagsContainerView: TextInputLayout
private lateinit var tagsCompletionView: TagsCompletionView private lateinit var tagsCompletionView: TagsCompletionView
private var tagsAdapter: FilteredArrayAdapter<String>? = null private var tagsAdapter: FilteredArrayAdapter<String>? = null
@@ -89,6 +91,7 @@ class EntryEditFragment: DatabaseFragment() {
templateView = view.findViewById(R.id.template_view) templateView = view.findViewById(R.id.template_view)
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container) attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
attachmentsListView = view.findViewById(R.id.entry_attachments_list) attachmentsListView = view.findViewById(R.id.entry_attachments_list)
tagsContainerView = view.findViewById(R.id.entry_tags_label)
tagsCompletionView = view.findViewById(R.id.entry_tags_completion_view) tagsCompletionView = view.findViewById(R.id.entry_tags_completion_view)
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext()) attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
@@ -157,11 +160,11 @@ class EntryEditFragment: DatabaseFragment() {
templateView.setIcon(iconImage) templateView.setIcon(iconImage)
} }
mEntryEditViewModel.onBackgroundColorSelected.observe(this) { color -> mEntryEditViewModel.onBackgroundColorSelected.observe(viewLifecycleOwner) { color ->
templateView.setBackgroundColor(color) templateView.setBackgroundColor(color)
} }
mEntryEditViewModel.onForegroundColorSelected.observe(this) { color -> mEntryEditViewModel.onForegroundColorSelected.observe(viewLifecycleOwner) { color ->
templateView.setForegroundColor(color) templateView.setForegroundColor(color)
} }
@@ -287,6 +290,7 @@ class EntryEditFragment: DatabaseFragment() {
threshold = 1 threshold = 1
setAdapter(tagsAdapter) setAdapter(tagsAdapter)
} }
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
} }
private fun assignEntryInfo(entryInfo: EntryInfo?) { private fun assignEntryInfo(entryInfo: EntryInfo?) {

View File

@@ -41,6 +41,8 @@ class EntryFragment: DatabaseFragment() {
private lateinit var attachmentsListView: RecyclerView private lateinit var attachmentsListView: RecyclerView
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private lateinit var customDataView: TextView
private lateinit var uuidContainerView: View private lateinit var uuidContainerView: View
private lateinit var uuidReferenceView: TextView private lateinit var uuidReferenceView: TextView
@@ -83,6 +85,9 @@ class EntryFragment: DatabaseFragment() {
creationDateView = view.findViewById(R.id.entry_created) creationDateView = view.findViewById(R.id.entry_created)
modificationDateView = view.findViewById(R.id.entry_modified) modificationDateView = view.findViewById(R.id.entry_modified)
// TODO Custom data
// customDataView = view.findViewById(R.id.entry_custom_data)
uuidContainerView = view.findViewById(R.id.entry_UUID_container) uuidContainerView = view.findViewById(R.id.entry_UUID_container)
uuidContainerView.apply { uuidContainerView.apply {
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
@@ -154,11 +159,14 @@ class EntryFragment: DatabaseFragment() {
assignAttachments(entryInfo?.attachments ?: listOf()) assignAttachments(entryInfo?.attachments ?: listOf())
// Assign dates // Assign dates
assignCreationDate(entryInfo?.creationTime) creationDateView.text = entryInfo?.creationTime?.getDateTimeString(resources)
assignModificationDate(entryInfo?.lastModificationTime) modificationDateView.text = entryInfo?.lastModificationTime?.getDateTimeString(resources)
// TODO Custom data
// customDataView.text = entryInfo?.customData?.toString()
// Assign special data // Assign special data
assignUUID(entryInfo?.id) uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id)
} }
private fun showClipboardDialog() { private fun showClipboardDialog() {
@@ -189,18 +197,6 @@ class EntryFragment: DatabaseFragment() {
templateView.reload() templateView.reload()
} }
private fun assignCreationDate(date: DateInstant?) {
creationDateView.text = date?.getDateTimeString(resources)
}
private fun assignModificationDate(date: DateInstant?) {
modificationDateView.text = date?.getDateTimeString(resources)
}
private fun assignUUID(uuid: UUID?) {
uuidReferenceView.text = UuidUtil.toHexString(uuid)
}
/* ------------- /* -------------
* Attachments * Attachments
* ------------- * -------------

View File

@@ -152,7 +152,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
mAdapter = NodesAdapter(context, database).apply { mAdapter = NodesAdapter(context, database).apply {
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback { setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
override fun onNodeClick(database: Database, node: Node) { override fun onNodeClick(database: Database, node: Node) {
if (nodeActionSelectionMode) { if (mCurrentGroup?.isVirtual == false
&& nodeActionSelectionMode) {
if (listActionNodes.contains(node)) { if (listActionNodes.contains(node)) {
// Remove selected item if already selected // Remove selected item if already selected
listActionNodes.remove(node) listActionNodes.remove(node)
@@ -169,7 +170,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} }
override fun onNodeLongClick(database: Database, node: Node): Boolean { override fun onNodeLongClick(database: Database, node: Node): Boolean {
if (nodeActionPasteMode == PasteMode.UNDEFINED) { if (mCurrentGroup?.isVirtual == false
&& nodeActionPasteMode == PasteMode.UNDEFINED) {
// Select the first item after a long click // Select the first item after a long click
if (!listActionNodes.contains(node)) if (!listActionNodes.contains(node))
listActionNodes.add(node) listActionNodes.add(node)
@@ -257,9 +259,9 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private fun rebuildList() { private fun rebuildList() {
try { try {
// Add elements to the list // Add elements to the list
mCurrentGroup?.let { mainGroup -> mCurrentGroup?.let { currentGroup ->
// Thrown an exception when sort cannot be performed // Thrown an exception when sort cannot be performed
mAdapter?.rebuildList(mainGroup) mAdapter?.rebuildList(currentGroup)
} }
} catch (e:Exception) { } catch (e:Exception) {
Log.e(TAG, "Unable to rebuild the list", e) Log.e(TAG, "Unable to rebuild the list", e)

View File

@@ -4,9 +4,9 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels import androidx.activity.viewModels
import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
@@ -59,9 +59,9 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
fun loadDatabase(databaseUri: Uri, fun loadDatabase(databaseUri: Uri,
mainCredential: MainCredential, mainCredential: MainCredential,
readOnly: Boolean, readOnly: Boolean,
cipherEntity: CipherDatabaseEntity?, cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean) { fixDuplicateUuid: Boolean) {
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEntity, fixDuplicateUuid) mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
} }
protected fun closeDatabase() { protected fun closeDatabase() {

View File

@@ -88,8 +88,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseSave(save) mDatabaseTaskProvider?.startDatabaseSave(save)
} }
mDatabaseViewModel.mergeDatabase.observe(this) { fixDuplicateUuid -> mDatabaseViewModel.mergeDatabase.observe(this) {
mDatabaseTaskProvider?.startDatabaseMerge(fixDuplicateUuid) mDatabaseTaskProvider?.startDatabaseMerge()
} }
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid -> mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
@@ -263,8 +263,16 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseSave(true) mDatabaseTaskProvider?.startDatabaseSave(true)
} }
fun saveDatabaseTo(uri: Uri) {
mDatabaseTaskProvider?.startDatabaseSave(true, uri)
}
fun mergeDatabase() { fun mergeDatabase() {
mDatabaseTaskProvider?.startDatabaseMerge(false) mDatabaseTaskProvider?.startDatabaseMerge()
}
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
mDatabaseTaskProvider?.startDatabaseMerge(uri, mainCredential)
} }
fun reloadDatabase() { fun reloadDatabase() {

View File

@@ -1,6 +1,8 @@
package com.kunzisoft.keepass.activities.legacy package com.kunzisoft.keepass.activities.legacy
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
@@ -11,6 +13,7 @@ import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.SpecialModeView import com.kunzisoft.keepass.view.SpecialModeView
/** /**
* Activity to manage database special mode (ie: selection mode) * Activity to manage database special mode (ie: selection mode)
*/ */
@@ -63,8 +66,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
EntrySelectionHelper.removeModesFromIntent(intent) EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent) EntrySelectionHelper.removeInfoFromIntent(intent)
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
// To move the app in background backToTheMainAppAndFinish()
moveTaskToBack(true)
} }
} }
} }
@@ -77,8 +79,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
EntrySelectionHelper.removeModesFromIntent(intent) EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent) EntrySelectionHelper.removeInfoFromIntent(intent)
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT) {
// To move the app in background backToTheMainAppAndFinish()
moveTaskToBack(true)
} }
} }
} }
@@ -88,11 +89,19 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
// To get the app caller, only for IntentSender // To get the app caller, only for IntentSender
super.onBackPressed() super.onBackPressed()
} else { } else {
// To move the app in background backToTheMainAppAndFinish()
moveTaskToBack(true)
} }
} }
private fun backToTheMainAppAndFinish() {
// To move the app in background and return to the main app
moveTaskToBack(true)
// To remove this instance in the OS app selector
Handler(Looper.getMainLooper()).postDelayed({
finish()
}, 500)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -160,12 +169,17 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
} }
// To hide home button from the regular toolbar in special mode // To hide home button from the regular toolbar in special mode
if (mSpecialMode != SpecialMode.DEFAULT) { if (mSpecialMode != SpecialMode.DEFAULT
&& hideHomeButtonIfModeIsNotDefault()) {
supportActionBar?.setDisplayHomeAsUpEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(false)
supportActionBar?.setDisplayShowHomeEnabled(false) supportActionBar?.setDisplayShowHomeEnabled(false)
} }
} }
open fun hideHomeButtonIfModeIsNotDefault(): Boolean {
return true
}
private fun blockAutofill(searchInfo: SearchInfo?) { private fun blockAutofill(searchInfo: SearchInfo?) {
val webDomain = searchInfo?.webDomain val webDomain = searchInfo?.webDomain
val applicationId = searchInfo?.applicationId val applicationId = searchInfo?.applicationId

View File

@@ -69,8 +69,10 @@ object Stylish {
context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light) context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light)
context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white) context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white)
context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear) context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear)
context.getString(R.string.list_style_name_simple_night) -> context.getString(R.string.list_style_name_simple)
context.getString(R.string.list_style_name_blue_night) -> context.getString(R.string.list_style_name_blue) context.getString(R.string.list_style_name_blue_night) -> context.getString(R.string.list_style_name_blue)
context.getString(R.string.list_style_name_red_night) -> context.getString(R.string.list_style_name_red) context.getString(R.string.list_style_name_red_night) -> context.getString(R.string.list_style_name_red)
context.getString(R.string.list_style_name_reply_night) -> context.getString(R.string.list_style_name_reply)
context.getString(R.string.list_style_name_purple_dark) -> context.getString(R.string.list_style_name_purple) context.getString(R.string.list_style_name_purple_dark) -> context.getString(R.string.list_style_name_purple)
else -> styleString else -> styleString
} }
@@ -81,8 +83,10 @@ object Stylish {
context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night) context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night)
context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black) context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black)
context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark) context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark)
context.getString(R.string.list_style_name_simple) -> context.getString(R.string.list_style_name_simple_night)
context.getString(R.string.list_style_name_blue) -> context.getString(R.string.list_style_name_blue_night) context.getString(R.string.list_style_name_blue) -> context.getString(R.string.list_style_name_blue_night)
context.getString(R.string.list_style_name_red) -> context.getString(R.string.list_style_name_red_night) context.getString(R.string.list_style_name_red) -> context.getString(R.string.list_style_name_red_night)
context.getString(R.string.list_style_name_reply) -> context.getString(R.string.list_style_name_reply_night)
context.getString(R.string.list_style_name_purple) -> context.getString(R.string.list_style_name_purple_dark) context.getString(R.string.list_style_name_purple) -> context.getString(R.string.list_style_name_purple_dark)
else -> styleString else -> styleString
} }
@@ -113,10 +117,14 @@ object Stylish {
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
context.getString(R.string.list_style_name_clear) -> R.style.KeepassDXStyle_Clear context.getString(R.string.list_style_name_clear) -> R.style.KeepassDXStyle_Clear
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
context.getString(R.string.list_style_name_simple) -> R.style.KeepassDXStyle_Simple
context.getString(R.string.list_style_name_simple_night) -> R.style.KeepassDXStyle_Simple_Night
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
context.getString(R.string.list_style_name_blue_night) -> R.style.KeepassDXStyle_Blue_Night context.getString(R.string.list_style_name_blue_night) -> R.style.KeepassDXStyle_Blue_Night
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
context.getString(R.string.list_style_name_red_night) -> R.style.KeepassDXStyle_Red_Night context.getString(R.string.list_style_name_red_night) -> R.style.KeepassDXStyle_Red_Night
context.getString(R.string.list_style_name_reply) -> R.style.KeepassDXStyle_Reply
context.getString(R.string.list_style_name_reply_night) -> R.style.KeepassDXStyle_Reply_Night
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
else -> R.style.KeepassDXStyle_Light else -> R.style.KeepassDXStyle_Light

View File

@@ -23,11 +23,13 @@ import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
abstract class StylishFragment : Fragment() { abstract class StylishFragment : Fragment() {
@@ -47,27 +49,41 @@ abstract class StylishFragment : Fragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = requireActivity().window val window = requireActivity().window
val defaultColor = Color.BLACK val defaultColor = Color.BLACK
val windowInset = WindowInsetsControllerCompat(window, window.decorView)
try { try {
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor)) val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
taStatusBarColor?.recycle() taStatusBarColor?.recycle()
} catch (e: Exception) {} } catch (e: Exception) {
Log.e(TAG, "Unable to retrieve theme : status bar color", e)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try { try {
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar)) val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
if (taWindowStatusLight?.getBoolean(0, false) == true) { windowInset.isAppearanceLightStatusBars = taWindowStatusLight
@Suppress("DEPRECATION") ?.getBoolean(0, false) == true
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
taWindowStatusLight?.recycle() taWindowStatusLight?.recycle()
} catch (e: Exception) {} } catch (e: Exception) {
Log.e(TAG, "Unable to retrieve theme : window light status bar", e)
}
} }
try { try {
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor)) val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
taNavigationBarColor?.recycle() taNavigationBarColor?.recycle()
} catch (e: Exception) {} } catch (e: Exception) {
Log.e(TAG, "Unable to retrieve theme : navigation bar color", e)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
try {
val taWindowLightNavigationBar = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightNavigationBar))
windowInset.isAppearanceLightNavigationBars = taWindowLightNavigationBar
?.getBoolean(0, false) == true
taWindowLightNavigationBar?.recycle()
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve theme : navigation light navigation bar", e)
}
}
} }
return super.onCreateView(inflater, container, savedInstanceState) return super.onCreateView(inflater, container, savedInstanceState)
} }
@@ -76,4 +92,8 @@ abstract class StylishFragment : Fragment() {
contextThemed = null contextThemed = null
super.onDetach() super.onDetach()
} }
companion object {
private val TAG = StylishFragment::class.java.simpleName
}
} }

View File

@@ -29,7 +29,6 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback import androidx.recyclerview.widget.SortedListAdapterCallback
@@ -87,8 +86,6 @@ class NodesAdapter (private val context: Context,
private var mNodeClickCallback: NodeClickCallback? = null private var mNodeClickCallback: NodeClickCallback? = null
private var mClipboardHelper = ClipboardHelper(context) private var mClipboardHelper = ClipboardHelper(context)
@ColorInt
private val mContentSelectionColor: Int
@ColorInt @ColorInt
private val mTextColorPrimary: Int private val mTextColorPrimary: Int
@ColorInt @ColorInt
@@ -98,7 +95,7 @@ class NodesAdapter (private val context: Context,
@ColorInt @ColorInt
private val mColorAccentLight: Int private val mColorAccentLight: Int
@ColorInt @ColorInt
private val mTextColorSelected: Int private val mColorOnAccentColor: Int
/** /**
* Determine if the adapter contains or not any element * Determine if the adapter contains or not any element
@@ -115,8 +112,6 @@ class NodesAdapter (private val context: Context,
this.mNodeSortedListCallback = NodeSortedListCallback() this.mNodeSortedListCallback = NodeSortedListCallback()
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback) this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
// Color of content selection
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
// Retrieve the color to tint the icon // Retrieve the color to tint the icon
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary)) val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK) this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK)
@@ -130,13 +125,13 @@ class NodesAdapter (private val context: Context,
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK) this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
taTextColorSecondary.recycle() taTextColorSecondary.recycle()
// To get background color for selection // To get background color for selection
val taSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight)) val taColorAccentLight = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
this.mColorAccentLight = taSelectionColor.getColor(0, Color.GRAY) this.mColorAccentLight = taColorAccentLight.getColor(0, Color.GRAY)
taSelectionColor.recycle() taColorAccentLight.recycle()
// To get text color for selection // To get text color for selection
val taSelectionTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor)) val taColorOnAccentColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor))
this.mTextColorSelected = taSelectionTextColor.getColor(0, Color.WHITE) this.mColorOnAccentColor = taColorOnAccentColor.getColor(0, Color.WHITE)
taSelectionTextColor.recycle() taColorOnAccentColor.recycle()
} }
private fun assignPreferences() { private fun assignPreferences() {
@@ -352,23 +347,6 @@ class NodesAdapter (private val context: Context,
isSelected = mActionNodesList.contains(subNode) isSelected = mActionNodesList.contains(subNode)
} }
// Assign image
val iconColor = if (holder.container.isSelected)
mContentSelectionColor
else when (subNode.type) {
Type.GROUP -> mTextColorPrimary
Type.ENTRY -> mTextColor
}
holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply {
database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
// Relative size of the icon
layoutParams?.apply {
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
}
}
// Assign text // Assign text
holder.text.apply { holder.text.apply {
text = subNode.title text = subNode.title
@@ -396,6 +374,14 @@ class NodesAdapter (private val context: Context,
holder.path?.visibility = View.GONE holder.path?.visibility = View.GONE
} }
// Assign icon colors
var iconColor = if (holder.container.isSelected)
mColorOnAccentColor
else when (subNode.type) {
Type.GROUP -> mTextColorPrimary
Type.ENTRY -> mTextColor
}
// Specific elements for entry // Specific elements for entry
if (subNode.type == Type.ENTRY) { if (subNode.type == Type.ENTRY) {
val entry = subNode as Entry val entry = subNode as Entry
@@ -457,13 +443,7 @@ class NodesAdapter (private val context: Context,
holder.otpProgress?.setIndicatorColor(foregroundColor) holder.otpProgress?.setIndicatorColor(foregroundColor)
holder.attachmentIcon?.setColorFilter(foregroundColor) holder.attachmentIcon?.setColorFilter(foregroundColor)
holder.meta.setTextColor(foregroundColor) holder.meta.setTextColor(foregroundColor)
holder.icon.apply { iconColor = foregroundColor
database.iconDrawableFactory.assignDatabaseIcon(
this,
subNode.icon,
foregroundColor
)
}
} else { } else {
holder.text.setTextColor(mTextColor) holder.text.setTextColor(mTextColor)
holder.subText?.setTextColor(mTextColorSecondary) holder.subText?.setTextColor(mTextColorSecondary)
@@ -473,12 +453,12 @@ class NodesAdapter (private val context: Context,
holder.meta.setTextColor(mTextColor) holder.meta.setTextColor(mTextColor)
} }
} else { } else {
holder.text.setTextColor(mTextColorSelected) holder.text.setTextColor(mColorOnAccentColor)
holder.subText?.setTextColor(mTextColorSelected) holder.subText?.setTextColor(mColorOnAccentColor)
holder.otpToken?.setTextColor(mTextColorSelected) holder.otpToken?.setTextColor(mColorOnAccentColor)
holder.otpProgress?.setIndicatorColor(mTextColorSelected) holder.otpProgress?.setIndicatorColor(mColorOnAccentColor)
holder.attachmentIcon?.setColorFilter(mTextColorSelected) holder.attachmentIcon?.setColorFilter(mColorOnAccentColor)
holder.meta.setTextColor(mTextColorSelected) holder.meta.setTextColor(mColorOnAccentColor)
} }
database.stopManageEntry(entry) database.stopManageEntry(entry)
@@ -499,6 +479,17 @@ class NodesAdapter (private val context: Context,
} }
} }
// Assign image
holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply {
database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
// Relative size of the icon
layoutParams?.apply {
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
}
}
// Assign click // Assign click
holder.container.setOnClickListener { holder.container.setOnClickListener {
mNodeClickCallback?.onNodeClick(database, subNode) mNodeClickCallback?.onNodeClick(database, subNode)

View File

@@ -1,189 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.database.Cursor
import android.database.MatrixCursor
import android.graphics.Color
import android.provider.BaseColumns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.cursoradapter.widget.CursorAdapter
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut
import java.util.*
class SearchEntryCursorAdapter(private val context: Context,
private val database: Database)
: CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
private val cursorInflater: LayoutInflater? = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
private var mDisplayUsername: Boolean = false
private var mOmitBackup: Boolean = true
private val iconColor: Int
init {
// Get the icon color
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
this.iconColor = taTextColor.getColor(0, Color.WHITE)
taTextColor.recycle()
reInit(context)
}
fun reInit(context: Context) {
this.mDisplayUsername = PreferencesUtil.showUsernamesListEntries(context)
this.mOmitBackup = PreferencesUtil.omitBackup(context)
}
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false)
val viewHolder = ViewHolder()
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext)
viewHolder.textViewPath = view.findViewById(R.id.entry_path)
view.tag = viewHolder
return view
}
override fun bindView(view: View, context: Context, cursor: Cursor?) {
getEntryFrom(cursor)?.let { currentEntry ->
val viewHolder = view.tag as ViewHolder
// Assign image
viewHolder.imageViewIcon?.let { iconView ->
database.iconDrawableFactory.assignDatabaseIcon(iconView, currentEntry.icon, iconColor)
}
// Assign title
viewHolder.textViewTitle?.apply {
text = currentEntry.getVisualTitle()
strikeOut(currentEntry.isCurrentlyExpires)
}
// Assign subtitle
viewHolder.textViewSubTitle?.apply {
val entryUsername = currentEntry.username
text = if (mDisplayUsername && entryUsername.isNotEmpty()) {
String.format("(%s)", entryUsername)
} else {
""
}
visibility = if (text.isEmpty()) View.GONE else View.VISIBLE
strikeOut(currentEntry.isCurrentlyExpires)
}
viewHolder.textViewPath?.apply {
text = currentEntry.getPathString()
}
}
}
private fun getEntryFrom(cursor: Cursor?): Entry? {
val entryCursor = cursor as? EntryCursor?
entryCursor?.getNodeId()?.let {
return database.getEntryById(it)
}
return null
}
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
return searchEntries(context, constraint.toString())
}
private fun searchEntries(context: Context, query: String): Cursor {
val cursor = EntryCursor()
val searchGroup = database.createVirtualGroupFromSearch(query,
mOmitBackup,
SearchHelper.MAX_SEARCH_ENTRY)
if (searchGroup != null) {
// Search in hide entries but not meta-stream
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
database.startManageEntry(entry)
cursor.addEntry(entry)
database.stopManageEntry(entry)
}
}
return cursor
}
fun getEntryFromPosition(position: Int): Entry? {
var pwEntry: Entry? = null
val cursor = this.cursor
if (cursor.moveToFirst() && cursor.move(position)) {
pwEntry = getEntryFrom(cursor)
}
return pwEntry
}
private class ViewHolder {
var imageViewIcon: ImageView? = null
var textViewTitle: TextView? = null
var textViewSubTitle: TextView? = null
var textViewPath: TextView? = null
}
private class EntryCursor : MatrixCursor(arrayOf(
ID,
COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS,
COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS
)) {
private var entryId: Long = 0
fun addEntry(entry: Entry) {
addRow(arrayOf(
entryId,
entry.nodeId.id.mostSignificantBits,
entry.nodeId.id.leastSignificantBits
))
entryId++
}
fun getNodeId(): NodeId<UUID> {
return NodeIdUUID(
UUID(getLong(getColumnIndex(COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
getLong(getColumnIndex(COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS)))
)
}
companion object {
const val ID = BaseColumns._ID
const val COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS = "UUID_most_significant_bits"
const val COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS = "UUID_least_significant_bits"
}
}
}

View File

@@ -22,7 +22,9 @@ package com.kunzisoft.keepass.app.database
import android.content.* import android.content.*
import android.net.Uri import android.net.Uri
import android.os.IBinder import android.os.IBinder
import android.util.Base64
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter import com.kunzisoft.keepass.utils.SingletonHolderParameter
@@ -125,15 +127,40 @@ class CipherDatabaseAction(context: Context) {
} }
fun getCipherDatabase(databaseUri: Uri, fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) { cipherDatabaseResultListener: (CipherEncryptDatabase?) -> Unit) {
if (useTempDao) { if (useTempDao) {
serviceActionTask { serviceActionTask {
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri)) mBinder?.getCipherDatabase(databaseUri)?.let { cipherDatabaseEntity ->
val cipherDatabase = CipherEncryptDatabase().apply {
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
this.encryptedValue = Base64.decode(
cipherDatabaseEntity.encryptedValue,
Base64.NO_WRAP
)
this.specParameters = Base64.decode(
cipherDatabaseEntity.specParameters,
Base64.NO_WRAP
)
}
cipherDatabaseResultListener.invoke(cipherDatabase)
}
} }
} else { } else {
IOActionTask( IOActionTask(
{ {
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString()) cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())?.let { cipherDatabaseEntity ->
CipherEncryptDatabase().apply {
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
this.encryptedValue = Base64.decode(
cipherDatabaseEntity.encryptedValue,
Base64.NO_WRAP
)
this.specParameters = Base64.decode(
cipherDatabaseEntity.specParameters,
Base64.NO_WRAP
)
}
}
}, },
{ {
cipherDatabaseResultListener.invoke(it) cipherDatabaseResultListener.invoke(it)
@@ -149,18 +176,27 @@ class CipherDatabaseAction(context: Context) {
} }
} }
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity, fun addOrUpdateCipherDatabase(cipherEncryptDatabase: CipherEncryptDatabase,
cipherDatabaseResultListener: (() -> Unit)? = null) { cipherDatabaseResultListener: (() -> Unit)? = null) {
if (useTempDao) { cipherEncryptDatabase.databaseUri?.let { databaseUri ->
// The only case to create service (not needed to get an info)
serviceActionTask(true) { val cipherDatabaseEntity = CipherDatabaseEntity(
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity) databaseUri.toString(),
cipherDatabaseResultListener?.invoke() Base64.encodeToString(cipherEncryptDatabase.encryptedValue, Base64.NO_WRAP),
} Base64.encodeToString(cipherEncryptDatabase.specParameters, Base64.NO_WRAP),
} else { )
IOActionTask(
if (useTempDao) {
// The only case to create service (not needed to get an info)
serviceActionTask(true) {
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
cipherDatabaseResultListener?.invoke()
}
} else {
IOActionTask(
{ {
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri) val cipherDatabaseRetrieve =
cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
// Update values if element not yet in the database // Update values if element not yet in the database
if (cipherDatabaseRetrieve == null) { if (cipherDatabaseRetrieve == null) {
cipherDatabaseDao.add(cipherDatabaseEntity) cipherDatabaseDao.add(cipherDatabaseEntity)
@@ -171,7 +207,8 @@ class CipherDatabaseAction(context: Context) {
{ {
cipherDatabaseResultListener?.invoke() cipherDatabaseResultListener?.invoke()
} }
).execute() ).execute()
}
} }
} }

View File

@@ -40,6 +40,9 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.model.CipherDecryptDatabase
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.CredentialStorage
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
@@ -60,6 +63,9 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
var databaseFileUri: Uri? = null var databaseFileUri: Uri? = null
private set private set
// TODO Retrieve credential storage from app database
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
/** /**
* Manage setting to auto open biometric prompt * Manage setting to auto open biometric prompt
*/ */
@@ -477,6 +483,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} ?: checkUnlockAvailability() } ?: checkUnlockAvailability()
} }
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
@@ -528,16 +535,29 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
} }
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) { override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {
databaseFileUri?.let { databaseUri -> databaseFileUri?.let { databaseUri ->
mBuilderListener?.onCredentialEncrypted(databaseUri, encryptedValue, ivSpec) mBuilderListener?.onCredentialEncrypted(
CipherEncryptDatabase().apply {
this.databaseUri = databaseUri
this.credentialStorage = credentialDatabaseStorage
this.encryptedValue = encryptedValue
this.specParameters = ivSpec
}
)
} }
} }
override fun handleDecryptedResult(decryptedValue: String) { override fun handleDecryptedResult(decryptedValue: ByteArray) {
// Load database directly with password retrieve // Load database directly with password retrieve
databaseFileUri?.let { databaseFileUri?.let { databaseUri ->
mBuilderListener?.onCredentialDecrypted(it, decryptedValue) mBuilderListener?.onCredentialDecrypted(
CipherDecryptDatabase().apply {
this.databaseUri = databaseUri
this.credentialStorage = credentialDatabaseStorage
this.decryptedValue = decryptedValue
}
)
} }
} }
@@ -551,6 +571,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
} }
@RequiresApi(Build.VERSION_CODES.M)
override fun onGenericException(e: Exception) { override fun onGenericException(e: Exception) {
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: "" val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
setAdvancedUnlockedMessageView(errorMessage) setAdvancedUnlockedMessageView(errorMessage)
@@ -580,6 +601,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
} }
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(text: CharSequence) { private fun setAdvancedUnlockedMessageView(text: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.message = text mAdvancedUnlockInfoView?.message = text
@@ -617,10 +639,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
interface BuilderListener { interface BuilderListener {
fun retrieveCredentialForEncryption(): String fun retrieveCredentialForEncryption(): ByteArray
fun conditionToStoreCredential(): Boolean fun conditionToStoreCredential(): Boolean
fun onCredentialEncrypted(databaseUri: Uri, encryptedCredential: String, ivSpec: String) fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase)
fun onCredentialDecrypted(databaseUri: Uri, decryptedCredential: String) fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase)
} }
override fun onPause() { override fun onPause() {

View File

@@ -27,7 +27,6 @@ import android.os.Build
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
@@ -214,18 +213,15 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
fun encryptData(value: String) { fun encryptData(value: ByteArray) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
try { try {
val encrypted = cipher?.doFinal(value.toByteArray()) val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
val encryptedBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP)
// passes updated iv spec on to callback so this can be stored for decryption // passes updated iv spec on to callback so this can be stored for decryption
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec -> cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP) advancedUnlockCallback?.handleEncryptedResult(encrypted, spec.iv)
advancedUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to encrypt data", e) Log.e(TAG, "Unable to encrypt data", e)
@@ -233,12 +229,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
fun initDecryptData(ivSpecValue: String, fun initDecryptData(ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) { actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
initDecryptData(ivSpecValue, actionIfCypherInit, true) initDecryptData(ivSpecValue, actionIfCypherInit, true)
} }
private fun initDecryptData(ivSpecValue: String, private fun initDecryptData(ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit, actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean = true) { firstLaunch: Boolean = true) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
@@ -246,9 +242,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
try { try {
// important to restore spec here that was used for decryption // important to restore spec here that was used for decryption
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP) val spec = IvParameterSpec(ivSpecValue)
val spec = IvParameterSpec(iv)
getSecretKey()?.let { secretKey -> getSecretKey()?.let { secretKey ->
cipher?.let { cipher -> cipher?.let { cipher ->
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
@@ -284,15 +278,14 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
fun decryptData(encryptedValue: String) { fun decryptData(encryptedValue: ByteArray) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
try { try {
// actual decryption here // actual decryption here
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP) cipher?.doFinal(encryptedValue)?.let { decrypted ->
cipher?.doFinal(encrypted)?.let { decrypted -> advancedUnlockCallback?.handleDecryptedResult(decrypted)
advancedUnlockCallback?.handleDecryptedResult(String(decrypted))
} }
} catch (badPaddingException: BadPaddingException) { } catch (badPaddingException: BadPaddingException) {
Log.e(TAG, "Unable to decrypt data", badPaddingException) Log.e(TAG, "Unable to decrypt data", badPaddingException)
@@ -367,8 +360,8 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
fun onAuthenticationSucceeded() fun onAuthenticationSucceeded()
fun onAuthenticationFailed() fun onAuthenticationFailed()
fun onAuthenticationError(errorCode: Int, errString: CharSequence) fun onAuthenticationError(errorCode: Int, errString: CharSequence)
fun handleEncryptedResult(encryptedValue: String, ivSpec: String) fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray)
fun handleDecryptedResult(decryptedValue: String) fun handleDecryptedResult(decryptedValue: ByteArray)
} }
companion object { companion object {
@@ -469,9 +462,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {} override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {} override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {}
override fun handleDecryptedResult(decryptedValue: String) {} override fun handleDecryptedResult(decryptedValue: ByteArray) {}
override fun onUnrecoverableKeyException(e: Exception) { override fun onUnrecoverableKeyException(e: Exception) {
advancedCallback.onUnrecoverableKeyException(e) advancedCallback.onUnrecoverableKeyException(e)

View File

@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
open class AssignPasswordInDatabaseRunnable ( open class AssignMainCredentialInDatabaseRunnable (
context: Context, context: Context,
database: Database, database: Database,
protected val mDatabaseUri: Uri, protected val mDatabaseUri: Uri,

View File

@@ -35,7 +35,7 @@ class CreateDatabaseRunnable(context: Context,
private val templateGroupName: String?, private val templateGroupName: String?,
mainCredential: MainCredential, mainCredential: MainCredential,
private val createDatabaseResult: ((Result) -> Unit)?) private val createDatabaseResult: ((Result) -> Unit)?)
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) { : AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
override fun onStartRun() { override fun onStartRun() {
try { try {

View File

@@ -33,7 +33,6 @@ import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
@@ -43,6 +42,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
@@ -84,7 +84,6 @@ import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
import kotlin.collections.ArrayList
/** /**
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService, * Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
@@ -344,21 +343,23 @@ class DatabaseTaskProvider {
fun startDatabaseLoad(databaseUri: Uri, fun startDatabaseLoad(databaseUri: Uri,
mainCredential: MainCredential, mainCredential: MainCredential,
readOnly: Boolean, readOnly: Boolean,
cipherEntity: CipherDatabaseEntity?, cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean) { fixDuplicateUuid: Boolean) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly) putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity) putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
} }
, ACTION_DATABASE_LOAD_TASK) , ACTION_DATABASE_LOAD_TASK)
} }
fun startDatabaseMerge(fixDuplicateUuid: Boolean) { fun startDatabaseMerge(fromDatabaseUri: Uri? = null,
mainCredential: MainCredential? = null) {
start(Bundle().apply { start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
} }
, ACTION_DATABASE_MERGE_TASK) , ACTION_DATABASE_MERGE_TASK)
} }
@@ -693,9 +694,10 @@ class DatabaseTaskProvider {
/** /**
* Save Database without parameter * Save Database without parameter
*/ */
fun startDatabaseSave(save: Boolean) { fun startDatabaseSave(save: Boolean, saveToUri: Uri? = null) {
start(Bundle().apply { start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
} }
, ACTION_DATABASE_SAVE) , ACTION_DATABASE_SAVE)
} }

View File

@@ -22,12 +22,11 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -39,7 +38,7 @@ class LoadDatabaseRunnable(private val context: Context,
private val mUri: Uri, private val mUri: Uri,
private val mMainCredential: MainCredential, private val mMainCredential: MainCredential,
private val mReadonly: Boolean, private val mReadonly: Boolean,
private val mCipherEntity: CipherDatabaseEntity?, private val mCipherEncryptDatabase: CipherEncryptDatabase?,
private val mFixDuplicateUUID: Boolean, private val mFixDuplicateUUID: Boolean,
private val progressTaskUpdater: ProgressTaskUpdater?, private val progressTaskUpdater: ProgressTaskUpdater?,
private val mLoadDatabaseResult: ((Result) -> Unit)?) private val mLoadDatabaseResult: ((Result) -> Unit)?)
@@ -76,9 +75,9 @@ class LoadDatabaseRunnable(private val context: Context,
} }
// Register the biometric // Register the biometric
mCipherEntity?.let { cipherDatabaseEntity -> mCipherEncryptDatabase?.let { cipherDatabase ->
CipherDatabaseAction.getInstance(context) CipherDatabaseAction.getInstance(context)
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called .addOrUpdateCipherDatabase(cipherDatabase) // return value not called
} }
// Register the current time to init the lock timer // Register the current time to init the lock timer

View File

@@ -20,17 +20,19 @@
package com.kunzisoft.keepass.database.action package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.UriUtil
class MergeDatabaseRunnable(private val context: Context, class MergeDatabaseRunnable(private val context: Context,
private val mDatabase: Database, private val mDatabase: Database,
private val mDatabaseToMergeUri: Uri?,
private val mDatabaseToMergeMainCredential: MainCredential?,
private val progressTaskUpdater: ProgressTaskUpdater?, private val progressTaskUpdater: ProgressTaskUpdater?,
private val mLoadDatabaseResult: ((Result) -> Unit)?) private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() { : ActionRunnable() {
@@ -41,11 +43,14 @@ class MergeDatabaseRunnable(private val context: Context,
override fun onActionRun() { override fun onActionRun() {
try { try {
mDatabase.mergeData(context.contentResolver, mDatabase.mergeData(mDatabaseToMergeUri,
{ memoryWanted -> mDatabaseToMergeMainCredential,
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) context.contentResolver,
}, { memoryWanted ->
progressTaskUpdater) BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
progressTaskUpdater
)
} catch (e: LoadDatabaseException) { } catch (e: LoadDatabaseException) {
setError(e) setError(e)
} }

View File

@@ -20,13 +20,15 @@
package com.kunzisoft.keepass.database.action package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DatabaseException import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
open class SaveDatabaseRunnable(protected var context: Context, open class SaveDatabaseRunnable(protected var context: Context,
protected var database: Database, protected var database: Database,
private var saveDatabase: Boolean) private var saveDatabase: Boolean,
private var databaseCopyUri: Uri? = null)
: ActionRunnable() { : ActionRunnable() {
var mAfterSaveDatabase: ((Result) -> Unit)? = null var mAfterSaveDatabase: ((Result) -> Unit)? = null
@@ -37,7 +39,7 @@ open class SaveDatabaseRunnable(protected var context: Context,
database.checkVersion() database.checkVersion()
if (saveDatabase && result.isSuccess) { if (saveDatabase && result.isSuccess) {
try { try {
database.saveData(context.contentResolver) database.saveData(databaseCopyUri, context.contentResolver)
} catch (e: DatabaseException) { } catch (e: DatabaseException) {
setError(e) setError(e)
} }

View File

@@ -1,3 +1,22 @@
/*
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.os.Parcel import android.os.Parcel
@@ -17,7 +36,10 @@ class CustomData : Parcelable {
} }
constructor(parcel: Parcel) { constructor(parcel: Parcel) {
ParcelableUtil.readStringParcelableMap(parcel, CustomDataItem::class.java) mCustomDataItems.clear()
mCustomDataItems.putAll(ParcelableUtil
.readStringParcelableMap(parcel, CustomDataItem::class.java)
)
} }
fun get(key: String): CustomDataItem? { fun get(key: String): CustomDataItem? {
@@ -46,6 +68,10 @@ class CustomData : Parcelable {
} }
} }
override fun toString(): String {
return mCustomDataItems.toString()
}
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
ParcelableUtil.writeStringParcelableMap(parcel, flags, mCustomDataItems) ParcelableUtil.writeStringParcelableMap(parcel, flags, mCustomDataItems)
} }

View File

@@ -21,6 +21,10 @@ class CustomDataItem : Parcelable {
this.lastModificationTime = lastModificationTime this.lastModificationTime = lastModificationTime
} }
override fun toString(): String {
return value
}
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(key) parcel.writeString(key)
parcel.writeString(value) parcel.writeString(value)

View File

@@ -62,7 +62,6 @@ import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.readBytes4ToUInt import com.kunzisoft.keepass.utils.readBytes4ToUInt
import java.io.* import java.io.*
import java.util.* import java.util.*
import kotlin.collections.ArrayList
class Database { class Database {
@@ -275,6 +274,9 @@ class Database {
} }
} }
val defaultFileExtension: String
get() = mDatabaseKDB?.defaultFileExtension ?: mDatabaseKDBX?.defaultFileExtension ?: ".bin"
val type: Class<*>? val type: Class<*>?
get() = mDatabaseKDB?.javaClass ?: mDatabaseKDBX?.javaClass get() = mDatabaseKDB?.javaClass ?: mDatabaseKDBX?.javaClass
@@ -496,7 +498,7 @@ class Database {
* Determine if a configurable templates group is available or not for this version of database * Determine if a configurable templates group is available or not for this version of database
* @return true if a configurable templates group available * @return true if a configurable templates group available
*/ */
val allowConfigurableTemplatesGroup: Boolean val allowTemplatesGroup: Boolean
get() = mDatabaseKDBX != null get() = mDatabaseKDBX != null
// Maybe another templates method with KDBX5 // Maybe another templates method with KDBX5
@@ -635,9 +637,13 @@ class Database {
} }
DatabaseInputKDB(databaseKDB) DatabaseInputKDB(databaseKDB)
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream,
mainCredential.masterPassword, progressTaskUpdater
keyFileInputStream, ) {
progressTaskUpdater) databaseKDB.retrieveMasterKey(
mainCredential.masterPassword,
keyFileInputStream
)
}
databaseKDB databaseKDB
}, },
{ databaseInputStream -> { databaseInputStream ->
@@ -648,9 +654,12 @@ class Database {
DatabaseInputKDBX(databaseKDBX).apply { DatabaseInputKDBX(databaseKDBX).apply {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient) setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream, openDatabase(databaseInputStream,
mainCredential.masterPassword, progressTaskUpdater) {
keyFileInputStream, databaseKDBX.retrieveMasterKey(
progressTaskUpdater) mainCredential.masterPassword,
keyFileInputStream,
)
}
} }
databaseKDBX databaseKDBX
} }
@@ -672,7 +681,9 @@ class Database {
} }
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
fun mergeData(contentResolver: ContentResolver, fun mergeData(databaseToMergeUri: Uri?,
databaseToMergeMainCredential: MainCredential?,
contentResolver: ContentResolver,
isRAMSufficient: (memoryWanted: Long) -> Boolean, isRAMSufficient: (memoryWanted: Long) -> Boolean,
progressTaskUpdater: ProgressTaskUpdater?) { progressTaskUpdater: ProgressTaskUpdater?) {
@@ -682,30 +693,52 @@ class Database {
// New database instance to get new changes // New database instance to get new changes
val databaseToMerge = Database() val databaseToMerge = Database()
databaseToMerge.fileUri = this.fileUri databaseToMerge.fileUri = databaseToMergeUri ?: this.fileUri
// Pass KeyFile Uri as InputStreams
var keyFileInputStream: InputStream? = null
try { try {
databaseToMerge.fileUri?.let { databaseUri -> val databaseUri = databaseToMerge.fileUri
if (databaseUri != null) {
val databaseKDB = DatabaseKDB() if (databaseToMergeMainCredential != null) {
val databaseKDBX = DatabaseKDBX() // Get keyFile inputStream
databaseToMergeMainCredential.keyFileUri?.let { keyFile ->
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
}
}
databaseToMerge.readDatabaseStream(contentResolver, databaseUri, databaseToMerge.readDatabaseStream(contentResolver, databaseUri,
{ databaseInputStream -> { databaseInputStream ->
DatabaseInputKDB(databaseKDB) val databaseToMergeKDB = DatabaseKDB()
.openDatabase(databaseInputStream, DatabaseInputKDB(databaseToMergeKDB)
masterKey, .openDatabase(databaseInputStream, progressTaskUpdater) {
progressTaskUpdater) if (databaseToMergeMainCredential != null) {
databaseKDB databaseToMergeKDB.retrieveMasterKey(
databaseToMergeMainCredential.masterPassword,
keyFileInputStream,
)
} else {
databaseToMergeKDB.masterKey = masterKey
}
}
databaseToMergeKDB
}, },
{ databaseInputStream -> { databaseInputStream ->
DatabaseInputKDBX(databaseKDBX).apply { val databaseToMergeKDBX = DatabaseKDBX()
DatabaseInputKDBX(databaseToMergeKDBX).apply {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient) setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream, openDatabase(databaseInputStream, progressTaskUpdater) {
masterKey, if (databaseToMergeMainCredential != null) {
progressTaskUpdater) databaseToMergeKDBX.retrieveMasterKey(
databaseToMergeMainCredential.masterPassword,
keyFileInputStream,
)
} else {
databaseToMergeKDBX.masterKey = masterKey
}
}
} }
databaseKDBX databaseToMergeKDBX
} }
) )
@@ -715,13 +748,19 @@ class Database {
} }
databaseToMerge.mDatabaseKDB?.let { databaseKDBToMerge -> databaseToMerge.mDatabaseKDB?.let { databaseKDBToMerge ->
databaseMerger.merge(databaseKDBToMerge) databaseMerger.merge(databaseKDBToMerge)
if (databaseToMergeUri != null) {
this.dataModifiedSinceLastLoading = true
}
} }
databaseToMerge.mDatabaseKDBX?.let { databaseKDBXToMerge -> databaseToMerge.mDatabaseKDBX?.let { databaseKDBXToMerge ->
databaseMerger.merge(databaseKDBXToMerge) databaseMerger.merge(databaseKDBXToMerge)
if (databaseToMergeUri != null) {
this.dataModifiedSinceLastLoading = true
}
} }
} }
} ?: run { } else {
throw IODatabaseException("Database URI is null, database cannot be reloaded") throw IODatabaseException("Database URI is null, database cannot be merged")
} }
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
throw FileNotFoundDatabaseException("Unable to load the keyfile") throw FileNotFoundDatabaseException("Unable to load the keyfile")
@@ -730,6 +769,7 @@ class Database {
} catch (e: Exception) { } catch (e: Exception) {
throw LoadDatabaseException(e) throw LoadDatabaseException(e)
} finally { } finally {
keyFileInputStream?.close()
databaseToMerge.clearAndClose() databaseToMerge.clearAndClose()
} }
} }
@@ -741,7 +781,8 @@ class Database {
// Retrieve the stream from the old database URI // Retrieve the stream from the old database URI
try { try {
fileUri?.let { oldDatabaseUri -> val oldDatabaseUri = fileUri
if (oldDatabaseUri != null) {
readDatabaseStream(contentResolver, oldDatabaseUri, readDatabaseStream(contentResolver, oldDatabaseUri,
{ databaseInputStream -> { databaseInputStream ->
val databaseKDB = DatabaseKDB() val databaseKDB = DatabaseKDB()
@@ -749,9 +790,9 @@ class Database {
databaseKDB.binaryCache = it.binaryCache databaseKDB.binaryCache = it.binaryCache
} }
DatabaseInputKDB(databaseKDB) DatabaseInputKDB(databaseKDB)
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream, progressTaskUpdater) {
masterKey, databaseKDB.masterKey = masterKey
progressTaskUpdater) }
databaseKDB databaseKDB
}, },
{ databaseInputStream -> { databaseInputStream ->
@@ -761,14 +802,14 @@ class Database {
} }
DatabaseInputKDBX(databaseKDBX).apply { DatabaseInputKDBX(databaseKDBX).apply {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient) setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream, openDatabase(databaseInputStream, progressTaskUpdater) {
masterKey, databaseKDBX.masterKey = masterKey
progressTaskUpdater) }
} }
databaseKDBX databaseKDBX
} }
) )
} ?: run { } else {
throw IODatabaseException("Database URI is null, database cannot be reloaded") throw IODatabaseException("Database URI is null, database cannot be reloaded")
} }
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
@@ -782,29 +823,39 @@ class Database {
} }
} }
fun isGroupSearchable(group: Group, omitBackup: Boolean): Boolean { fun groupIsInRecycleBin(group: Group): Boolean {
return mDatabaseKDB?.isGroupSearchable(group.groupKDB, omitBackup) ?: val groupKDB = group.groupKDB
mDatabaseKDBX?.isGroupSearchable(group.groupKDBX, omitBackup) ?: val groupKDBX = group.groupKDBX
false if (groupKDB != null) {
return mDatabaseKDB?.isInRecycleBin(groupKDB) ?: false
} else if (groupKDBX != null) {
return mDatabaseKDBX?.isInRecycleBin(groupKDBX) ?: false
}
return false
} }
fun createVirtualGroupFromSearch(searchQuery: String, fun groupIsInTemplates(group: Group): Boolean {
omitBackup: Boolean, val groupKDBX = group.groupKDBX
if (groupKDBX != null) {
return mDatabaseKDBX?.getTemplatesGroup() == groupKDBX
}
return false
}
fun createVirtualGroupFromSearch(searchParameters: SearchParameters,
fromGroup: NodeId<*>? = null,
max: Int = Integer.MAX_VALUE): Group? { max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this, return mSearchHelper?.createVirtualGroupWithSearchResult(this,
SearchParameters().apply { searchParameters, fromGroup, max)
this.searchQuery = searchQuery
}, omitBackup, max)
} }
fun createVirtualGroupFromSearchInfo(searchInfoString: String, fun createVirtualGroupFromSearchInfo(searchInfoString: String,
omitBackup: Boolean,
max: Int = Integer.MAX_VALUE): Group? { max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this, return mSearchHelper?.createVirtualGroupWithSearchResult(this,
SearchParameters().apply { SearchParameters().apply {
searchQuery = searchInfoString searchQuery = searchInfoString
searchInTitles = true searchInTitles = true
searchInUserNames = false searchInUsernames = false
searchInPasswords = false searchInPasswords = false
searchInUrls = true searchInUrls = true
searchInNotes = true searchInNotes = true
@@ -812,8 +863,11 @@ class Database {
searchInOther = true searchInOther = true
searchInUUIDs = false searchInUUIDs = false
searchInTags = false searchInTags = false
searchInCurrentGroup = false
searchInSearchableGroup = true
searchInRecycleBin = false
searchInTemplates = false searchInTemplates = false
}, omitBackup, max) }, null, max)
} }
val tagPool: Tags val tagPool: Tags
@@ -855,10 +909,61 @@ class Database {
} }
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
fun saveData(contentResolver: ContentResolver) { fun saveData(databaseCopyUri: Uri?, contentResolver: ContentResolver) {
try { try {
this.fileUri?.let { val saveUri = databaseCopyUri ?: this.fileUri
saveData(contentResolver, it) if (saveUri != null) {
if (saveUri.scheme == "file") {
saveUri.path?.let { filename ->
val tempFile = File("$filename.tmp")
var fileOutputStream: FileOutputStream? = null
try {
fileOutputStream = FileOutputStream(tempFile)
val pmo = mDatabaseKDB?.let { DatabaseOutputKDB(it, fileOutputStream) }
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, fileOutputStream) }
pmo?.output()
} catch (e: Exception) {
throw IOException(e)
} finally {
fileOutputStream?.close()
}
// Force data to disk before continuing
try {
fileOutputStream?.fd?.sync()
} catch (e: SyncFailedException) {
// Ignore if fsync fails. We tried.
}
if (!tempFile.renameTo(File(filename))) {
throw IOException()
}
}
} else {
var outputStream: OutputStream? = null
try {
outputStream = contentResolver.openOutputStream(saveUri, "rwt")
outputStream?.let { definedOutputStream ->
val databaseOutput =
mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
?: mDatabaseKDBX?.let {
DatabaseOutputKDBX(
it,
definedOutputStream
)
}
databaseOutput?.output()
}
} catch (e: Exception) {
throw IOException(e)
} finally {
outputStream?.close()
}
}
if (databaseCopyUri == null) {
this.dataModifiedSinceLastLoading = false
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to save database", e) Log.e(TAG, "Unable to save database", e)
@@ -866,55 +971,6 @@ class Database {
} }
} }
@Throws(IOException::class, DatabaseOutputException::class)
private fun saveData(contentResolver: ContentResolver, uri: Uri) {
if (uri.scheme == "file") {
uri.path?.let { filename ->
val tempFile = File("$filename.tmp")
var fileOutputStream: FileOutputStream? = null
try {
fileOutputStream = FileOutputStream(tempFile)
val pmo = mDatabaseKDB?.let { DatabaseOutputKDB(it, fileOutputStream) }
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, fileOutputStream) }
pmo?.output()
} catch (e: Exception) {
throw IOException(e)
} finally {
fileOutputStream?.close()
}
// Force data to disk before continuing
try {
fileOutputStream?.fd?.sync()
} catch (e: SyncFailedException) {
// Ignore if fsync fails. We tried.
}
if (!tempFile.renameTo(File(filename))) {
throw IOException()
}
}
} else {
var outputStream: OutputStream? = null
try {
outputStream = contentResolver.openOutputStream(uri, "rwt")
outputStream?.let { definedOutputStream ->
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
databaseOutput?.output()
}
} catch (e: Exception) {
throw IOException(e)
} finally {
outputStream?.close()
}
}
this.fileUri = uri
this.dataModifiedSinceLastLoading = false
}
fun clearIndexesAndBinaries(filesDirectory: File? = null) { fun clearIndexesAndBinaries(filesDirectory: File? = null) {
this.mDatabaseKDB?.clearIndexes() this.mDatabaseKDB?.clearIndexes()
this.mDatabaseKDBX?.clearIndexes() this.mDatabaseKDBX?.clearIndexes()
@@ -1245,6 +1301,18 @@ class Database {
return mDatabaseKDBX != null return mDatabaseKDBX != null
} }
fun allowCustomSearchableGroup(): Boolean {
return mDatabaseKDBX != null
}
fun allowAutoType(): Boolean {
return mDatabaseKDBX != null
}
fun allowTags(): Boolean {
return mDatabaseKDBX != null
}
/** /**
* Remove oldest history for each entry if more than max items or max memory * Remove oldest history for each entry if more than max items or max memory
*/ */

View File

@@ -26,6 +26,7 @@ import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.AutoType
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
@@ -276,6 +277,18 @@ class Entry : Node, EntryVersionedInterface<Group> {
} }
} }
var customData: CustomData
get() = entryKDBX?.customData ?: CustomData()
set(value) {
entryKDBX?.customData = value
}
var autoType: AutoType
get() = entryKDBX?.autoType ?: AutoType()
set(value) {
entryKDBX?.autoType = value
}
private fun isTan(): Boolean { private fun isTan(): Boolean {
return title == PMS_TAN_ENTRY && username.isNotEmpty() return title == PMS_TAN_ENTRY && username.isNotEmpty()
} }
@@ -460,6 +473,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryInfo.tags = tags entryInfo.tags = tags
entryInfo.backgroundColor = backgroundColor entryInfo.backgroundColor = backgroundColor
entryInfo.foregroundColor = foregroundColor entryInfo.foregroundColor = foregroundColor
entryInfo.customData = customData
entryInfo.autoType = autoType
entryInfo.customFields = getExtraFields().toMutableList() entryInfo.customFields = getExtraFields().toMutableList()
// Add otpElement to generate token // Add otpElement to generate token
entryInfo.otpModel = getOtpElement()?.otpModel entryInfo.otpModel = getOtpElement()?.otpModel
@@ -497,6 +512,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
tags = newEntryInfo.tags tags = newEntryInfo.tags
backgroundColor = newEntryInfo.backgroundColor backgroundColor = newEntryInfo.backgroundColor
foregroundColor = newEntryInfo.foregroundColor foregroundColor = newEntryInfo.foregroundColor
customData = newEntryInfo.customData
autoType = newEntryInfo.autoType
addExtraFields(newEntryInfo.customFields) addExtraFields(newEntryInfo.customFields)
database?.attachmentPool?.let { binaryPool -> database?.attachmentPool?.let { binaryPool ->
newEntryInfo.attachments.forEach { attachment -> newEntryInfo.attachments.forEach { attachment ->

View File

@@ -262,6 +262,12 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
} }
} }
var customData: CustomData
get() = groupKDBX?.customData ?: CustomData()
set(value) {
groupKDBX?.customData = value
}
override fun getChildGroups(): List<Group> { override fun getChildGroups(): List<Group> {
return groupKDB?.getChildGroups()?.map { return groupKDB?.getChildGroups()?.map {
Group(it) Group(it)
@@ -434,13 +440,36 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDBX?.nodeId = id groupKDBX?.nodeId = id
} }
fun setEnableAutoType(enableAutoType: Boolean?) { var searchable: Boolean?
groupKDBX?.enableAutoType = enableAutoType get() = groupKDBX?.enableSearching
set(value) {
groupKDBX?.enableSearching = value
}
fun isSearchable(): Boolean {
val searchableGroup = searchable
if (searchableGroup == null) {
val parenGroup = parent
if (parenGroup == null)
return true
else
return parenGroup.isSearchable()
} else {
return searchableGroup
}
} }
fun setEnableSearching(enableSearching: Boolean?) { var enableAutoType: Boolean?
groupKDBX?.enableSearching = enableSearching get() = groupKDBX?.enableAutoType
} set(value) {
groupKDBX?.enableAutoType = value
}
var defaultAutoTypeSequence: String
get() = groupKDBX?.defaultAutoTypeSequence ?: ""
set(value) {
groupKDBX?.defaultAutoTypeSequence = value
}
fun setExpanded(expanded: Boolean) { fun setExpanded(expanded: Boolean) {
groupKDBX?.isExpanded = expanded groupKDBX?.isExpanded = expanded
@@ -462,7 +491,11 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupInfo.expires = expires groupInfo.expires = expires
groupInfo.expiryTime = expiryTime groupInfo.expiryTime = expiryTime
groupInfo.notes = notes groupInfo.notes = notes
groupInfo.searchable = searchable
groupInfo.enableAutoType = enableAutoType
groupInfo.defaultAutoTypeSequence = defaultAutoTypeSequence
groupInfo.tags = tags groupInfo.tags = tags
groupInfo.customData = customData
return groupInfo return groupInfo
} }
@@ -475,7 +508,11 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
expires = groupInfo.expires expires = groupInfo.expires
expiryTime = groupInfo.expiryTime expiryTime = groupInfo.expiryTime
notes = groupInfo.notes notes = groupInfo.notes
searchable = groupInfo.searchable
enableAutoType = groupInfo.enableAutoType
defaultAutoTypeSequence = groupInfo.defaultAutoTypeSequence
tags = groupInfo.tags tags = groupInfo.tags
customData = groupInfo.customData
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

View File

@@ -53,6 +53,10 @@ class Tags: Parcelable {
return mTags.isEmpty() return mTags.isEmpty()
} }
fun isNotEmpty(): Boolean {
return !isEmpty()
}
fun size(): Int { fun size(): Int {
return mTags.size return mTags.size
} }

View File

@@ -64,6 +64,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
override val version: String override val version: String
get() = "V1" get() = "V1"
override val defaultFileExtension: String
get() = ".kdb"
init { init {
// New manual root because KDB contains multiple root groups (here available with getRootGroups()) // New manual root because KDB contains multiple root groups (here available with getRootGroups())
rootGroup = createGroup().apply { rootGroup = createGroup().apply {

View File

@@ -201,6 +201,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return "V2 - KDBX$kdbxStringVersion" return "V2 - KDBX$kdbxStringVersion"
} }
override val defaultFileExtension: String
get() = ".kdbx"
private open class NodeOperationHandler<T: NodeKDBXInterface> : NodeHandler<T>() { private open class NodeOperationHandler<T: NodeKDBXInterface> : NodeHandler<T>() {
var containsCustomData = false var containsCustomData = false
override fun operate(node: T): Boolean { override fun operate(node: T): Boolean {
@@ -224,7 +227,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private inner class GroupOperationHandler: NodeOperationHandler<GroupKDBX>() { private inner class GroupOperationHandler: NodeOperationHandler<GroupKDBX>() {
var containsTags = false var containsTags = false
override fun operate(node: GroupKDBX): Boolean { override fun operate(node: GroupKDBX): Boolean {
if (!node.tags.isEmpty()) if (node.tags.isNotEmpty())
containsTags = true containsTags = true
return super.operate(node) return super.operate(node)
} }

View File

@@ -62,6 +62,7 @@ abstract class DatabaseVersioned<
protected set protected set
abstract val version: String abstract val version: String
abstract val defaultFileExtension: String
/** /**
* To manage binaries in faster way * To manage binaries in faster way
@@ -325,14 +326,6 @@ abstract class DatabaseVersioned<
abstract fun isInRecycleBin(group: Group): Boolean abstract fun isInRecycleBin(group: Group): Boolean
fun isGroupSearchable(group: Group?, omitBackup: Boolean): Boolean {
if (group == null)
return false
if (omitBackup && isInRecycleBin(group))
return false
return true
}
fun clearIconsCache() { fun clearIconsCache() {
iconsManager.doForEachCustomIcon { _, binary -> iconsManager.doForEachCustomIcon { _, binary ->
try { try {

View File

@@ -41,9 +41,9 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
override var customData = CustomData() override var customData = CustomData()
var notes = "" var notes = ""
var isExpanded = true var isExpanded = true
var defaultAutoTypeSequence = ""
var enableAutoType: Boolean? = null
var enableSearching: Boolean? = null var enableSearching: Boolean? = null
var enableAutoType: Boolean? = null
var defaultAutoTypeSequence: String = ""
var lastTopVisibleEntry: UUID = DatabaseVersioned.UUID_ZERO var lastTopVisibleEntry: UUID = DatabaseVersioned.UUID_ZERO
override var tags = Tags() override var tags = Tags()
override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO override var previousParentGroup: UUID = DatabaseVersioned.UUID_ZERO
@@ -69,11 +69,11 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData() customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: CustomData()
notes = parcel.readString() ?: notes notes = parcel.readString() ?: notes
isExpanded = parcel.readByte().toInt() != 0 isExpanded = parcel.readByte().toInt() != 0
defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence
val isAutoTypeEnabled = parcel.readInt()
enableAutoType = if (isAutoTypeEnabled == -1) null else isAutoTypeEnabled == 1
val isSearchingEnabled = parcel.readInt() val isSearchingEnabled = parcel.readInt()
enableSearching = if (isSearchingEnabled == -1) null else isSearchingEnabled == 1 enableSearching = if (isSearchingEnabled == -1) null else isSearchingEnabled == 1
val isAutoTypeEnabled = parcel.readInt()
enableAutoType = if (isAutoTypeEnabled == -1) null else isAutoTypeEnabled == 1
defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence
lastTopVisibleEntry = parcel.readSerializable() as UUID lastTopVisibleEntry = parcel.readSerializable() as UUID
tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags
previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO previousParentGroup = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
@@ -94,9 +94,9 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
dest.writeParcelable(customData, flags) dest.writeParcelable(customData, flags)
dest.writeString(notes) dest.writeString(notes)
dest.writeByte((if (isExpanded) 1 else 0).toByte()) dest.writeByte((if (isExpanded) 1 else 0).toByte())
dest.writeString(defaultAutoTypeSequence)
dest.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0)
dest.writeInt(if (enableSearching == null) -1 else if (enableSearching!!) 1 else 0) dest.writeInt(if (enableSearching == null) -1 else if (enableSearching!!) 1 else 0)
dest.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0)
dest.writeString(defaultAutoTypeSequence)
dest.writeSerializable(lastTopVisibleEntry) dest.writeSerializable(lastTopVisibleEntry)
dest.writeParcelable(tags, flags) dest.writeParcelable(tags, flags)
dest.writeParcelable(ParcelUuid(previousParentGroup), flags) dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
@@ -111,9 +111,9 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
customData = CustomData(source.customData) customData = CustomData(source.customData)
notes = source.notes notes = source.notes
isExpanded = source.isExpanded isExpanded = source.isExpanded
defaultAutoTypeSequence = source.defaultAutoTypeSequence
enableAutoType = source.enableAutoType
enableSearching = source.enableSearching enableSearching = source.enableSearching
enableAutoType = source.enableAutoType
defaultAutoTypeSequence = source.defaultAutoTypeSequence
lastTopVisibleEntry = source.lastTopVisibleEntry lastTopVisibleEntry = source.lastTopVisibleEntry
tags = source.tags tags = source.tags
previousParentGroup = source.previousParentGroup previousParentGroup = source.previousParentGroup

View File

@@ -103,7 +103,7 @@ object TemplateField {
LABEL_SSID.equals(name, true) -> context.getString(R.string.ssid) LABEL_SSID.equals(name, true) -> context.getString(R.string.ssid)
LABEL_TYPE.equals(name, true) -> context.getString(R.string.type) LABEL_TYPE.equals(name, true) -> context.getString(R.string.type)
LABEL_CRYPTOCURRENCY.equals(name, true) -> context.getString(R.string.cryptocurrency) LABEL_CRYPTOCURRENCY.equals(name, true) -> context.getString(R.string.cryptocurrency)
LABEL_TOKEN.equals(name, true) -> context.getString(R.string.token) LABEL_TOKEN.equals(name, false) -> context.getString(R.string.token)
LABEL_PUBLIC_KEY.equals(name, true) -> context.getString(R.string.public_key) LABEL_PUBLIC_KEY.equals(name, true) -> context.getString(R.string.public_key)
LABEL_PRIVATE_KEY.equals(name, true) -> context.getString(R.string.private_key) LABEL_PRIVATE_KEY.equals(name, true) -> context.getString(R.string.private_key)
LABEL_SEED.equals(name, true) -> context.getString(R.string.seed) LABEL_SEED.equals(name, true) -> context.getString(R.string.seed)

View File

@@ -43,15 +43,8 @@ abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> (protected var m
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream, abstract fun openDatabase(databaseInputStream: InputStream,
password: String?, progressTaskUpdater: ProgressTaskUpdater?,
keyfileInputStream: InputStream?, assignMasterKey: (() -> Unit)): D
progressTaskUpdater: ProgressTaskUpdater?): D
@Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
progressTaskUpdater: ProgressTaskUpdater?): D
protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) { protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) {
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)

View File

@@ -39,7 +39,6 @@ import java.security.MessageDigest
import java.util.* import java.util.*
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherInputStream import javax.crypto.CipherInputStream
import kotlin.collections.HashMap
/** /**
@@ -50,27 +49,8 @@ class DatabaseInputKDB(database: DatabaseKDB)
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
password: String?, progressTaskUpdater: ProgressTaskUpdater?,
keyfileInputStream: InputStream?, assignMasterKey: (() -> Unit)): DatabaseKDB {
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater) {
mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater) {
mDatabase.masterKey = masterKey
}
}
@Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
try { try {
startKeyTimer(progressTaskUpdater) startKeyTimer(progressTaskUpdater)
@@ -96,7 +76,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
throw VersionDatabaseException() throw VersionDatabaseException()
} }
assignMasterKey?.invoke() assignMasterKey.invoke()
// Select algorithm // Select algorithm
when { when {

View File

@@ -101,27 +101,8 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
password: String?, progressTaskUpdater: ProgressTaskUpdater?,
keyfileInputStream: InputStream?, assignMasterKey: (() -> Unit)): DatabaseKDBX {
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater) {
mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater) {
mDatabase.masterKey = masterKey
}
}
@Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
try { try {
startKeyTimer(progressTaskUpdater) startKeyTimer(progressTaskUpdater)
@@ -133,7 +114,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
hashOfHeader = headerAndHash.hash hashOfHeader = headerAndHash.hash
val pbHeader = headerAndHash.header val pbHeader = headerAndHash.header
assignMasterKey?.invoke() assignMasterKey.invoke()
mDatabase.makeFinalKey(header.masterSeed) mDatabase.makeFinalKey(header.masterSeed)
stopKeyTimer() stopKeyTimer()

View File

@@ -663,7 +663,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeTags(tags: Tags) { private fun writeTags(tags: Tags) {
if (!tags.isEmpty()) { if (tags.isNotEmpty()) {
writeString(DatabaseKDBXXML.ElemTags, tags.toString()) writeString(DatabaseKDBXXML.ElemTags, tags.toString())
} }
} }

View File

@@ -30,6 +30,7 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.utils.readAllBytes import com.kunzisoft.keepass.utils.readAllBytes
import java.io.IOException import java.io.IOException
@@ -43,7 +44,6 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
* Merge a KDB database in a KDBX database, by default all data are copied from the KDB * Merge a KDB database in a KDBX database, by default all data are copied from the KDB
*/ */
fun merge(databaseToMerge: DatabaseKDB) { fun merge(databaseToMerge: DatabaseKDB) {
// TODO Test KDB merge
val rootGroup = database.rootGroup val rootGroup = database.rootGroup
val rootGroupId = rootGroup?.nodeId val rootGroupId = rootGroup?.nodeId
val rootGroupToMerge = databaseToMerge.rootGroup val rootGroupToMerge = databaseToMerge.rootGroup
@@ -53,6 +53,11 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
throw IOException("Database is not open") throw IOException("Database is not open")
} }
// Replace the UUID of the KDB root group to init seed
databaseToMerge.removeGroupIndex(rootGroupToMerge)
rootGroupToMerge.nodeId = NodeIdInt(0)
databaseToMerge.addGroupIndex(rootGroupToMerge)
// Merge children // Merge children
rootGroupToMerge.doForEachChild( rootGroupToMerge.doForEachChild(
object : NodeHandler<EntryKDB>() { object : NodeHandler<EntryKDB>() {
@@ -87,31 +92,57 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
val entry = database.getEntryById(entryId) val entry = database.getEntryById(entryId)
databaseToMerge.getEntryById(entryId)?.let { srcEntryToMerge -> databaseToMerge.getEntryById(entryId)?.let { srcEntryToMerge ->
// Retrieve parent in current database // Do not merge meta stream elements
var parentEntryToMerge: GroupKDBX? = null if (!srcEntryToMerge.isMetaStream()) {
srcEntryToMerge.parent?.nodeId?.let { // Retrieve parent in current database
val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it) var parentEntryToMerge: GroupKDBX? = null
parentEntryToMerge = database.getGroupById(parentGroupIdToMerge) srcEntryToMerge.parent?.nodeId?.let {
} val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it)
val entryToMerge = EntryKDBX().apply { parentEntryToMerge = database.getGroupById(parentGroupIdToMerge)
this.nodeId = srcEntryToMerge.nodeId }
this.icon = srcEntryToMerge.icon // Copy attachment
this.creationTime = DateInstant(srcEntryToMerge.creationTime) var newAttachment: Attachment? = null
this.lastModificationTime = DateInstant(srcEntryToMerge.lastModificationTime) srcEntryToMerge.getAttachment(databaseToMerge.attachmentPool)?.let { attachment ->
this.lastAccessTime = DateInstant(srcEntryToMerge.lastAccessTime) val binarySize = attachment.binaryData.getSize()
this.expiryTime = DateInstant(srcEntryToMerge.expiryTime) val binaryData = database.buildNewBinaryAttachment(
this.expires = srcEntryToMerge.expires isRAMSufficient.invoke(binarySize),
this.title = srcEntryToMerge.title attachment.binaryData.isCompressed,
this.username = srcEntryToMerge.username attachment.binaryData.isProtected
this.password = srcEntryToMerge.password )
this.url = srcEntryToMerge.url attachment.binaryData.getInputDataStream(databaseToMerge.binaryCache)
this.notes = srcEntryToMerge.notes .use { inputStream ->
// TODO attachment binaryData.getOutputDataStream(database.binaryCache)
} .use { outputStream ->
if (entry != null) { inputStream.readAllBytes { buffer ->
entry.updateWith(entryToMerge, false) outputStream.write(buffer)
} else if (parentEntryToMerge != null) { }
database.addEntryTo(entryToMerge, parentEntryToMerge) }
}
newAttachment = Attachment(attachment.name, binaryData)
}
// Create new entry format
val entryToMerge = EntryKDBX().apply {
this.nodeId = srcEntryToMerge.nodeId
this.icon = srcEntryToMerge.icon
this.creationTime = DateInstant(srcEntryToMerge.creationTime)
this.lastModificationTime = DateInstant(srcEntryToMerge.lastModificationTime)
this.lastAccessTime = DateInstant(srcEntryToMerge.lastAccessTime)
this.expiryTime = DateInstant(srcEntryToMerge.expiryTime)
this.expires = srcEntryToMerge.expires
this.title = srcEntryToMerge.title
this.username = srcEntryToMerge.username
this.password = srcEntryToMerge.password
this.url = srcEntryToMerge.url
this.notes = srcEntryToMerge.notes
newAttachment?.let {
this.putAttachment(it, database.attachmentPool)
}
}
if (entry != null) {
entry.updateWith(entryToMerge, false)
} else if (parentEntryToMerge != null) {
database.addEntryTo(entryToMerge, parentEntryToMerge)
}
} }
} }
} }

View File

@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD
@@ -37,7 +38,7 @@ class SearchHelper {
fun createVirtualGroupWithSearchResult(database: Database, fun createVirtualGroupWithSearchResult(database: Database,
searchParameters: SearchParameters, searchParameters: SearchParameters,
omitBackup: Boolean, fromGroup: NodeId<*>? = null,
max: Int): Group? { max: Int): Group? {
val searchGroup = database.createGroup(virtual = true) val searchGroup = database.createGroup(virtual = true)
@@ -45,7 +46,15 @@ class SearchHelper {
// Search all entries // Search all entries
incrementEntry = 0 incrementEntry = 0
database.rootGroup?.doForEachChild(
val allowCustomSearchable = database.allowCustomSearchableGroup()
val startGroup = if (searchParameters.searchInCurrentGroup && fromGroup != null) {
database.getGroupById(fromGroup) ?: database.rootGroup
} else {
database.rootGroup
}
if (groupConditions(database, startGroup, searchParameters, allowCustomSearchable, max)) {
startGroup?.doForEachChild(
object : NodeHandler<Entry>() { object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean { override fun operate(node: Entry): Boolean {
if (incrementEntry >= max) if (incrementEntry >= max)
@@ -62,19 +71,43 @@ class SearchHelper {
}, },
object : NodeHandler<Group>() { object : NodeHandler<Group>() {
override fun operate(node: Group): Boolean { override fun operate(node: Group): Boolean {
return when { return groupConditions(database,
incrementEntry >= max -> false node,
database.isGroupSearchable(node, omitBackup) -> true searchParameters,
else -> false allowCustomSearchable,
} max
)
} }
}, },
false) false
)
}
searchGroup?.refreshNumberOfChildEntries() searchGroup?.refreshNumberOfChildEntries()
return searchGroup return searchGroup
} }
private fun groupConditions(database: Database,
group: Group?,
searchParameters: SearchParameters,
allowCustomSearchable: Boolean,
max: Int): Boolean {
return if (group == null)
false
else if (incrementEntry >= max)
false
else if (database.groupIsInRecycleBin(group))
searchParameters.searchInRecycleBin
else if (database.groupIsInTemplates(group))
searchParameters.searchInTemplates
else if (!allowCustomSearchable)
true
else if (searchParameters.searchInSearchableGroup)
group.isSearchable()
else
true
}
private fun entryContainsString(database: Database, private fun entryContainsString(database: Database,
entry: Entry, entry: Entry,
searchParameters: SearchParameters): Boolean { searchParameters: SearchParameters): Boolean {
@@ -88,7 +121,18 @@ class SearchHelper {
} }
companion object { companion object {
const val MAX_SEARCH_ENTRY = 10 const val MAX_SEARCH_ENTRY = 1000
/**
* Method to show the number of search results with max results
*/
fun showNumberOfSearchResults(number: Int): String {
return if (number >= MAX_SEARCH_ENTRY) {
(MAX_SEARCH_ENTRY-1).toString() + "+"
} else {
number.toString()
}
}
/** /**
* Utility method to perform actions if item is found or not after an auto search in [database] * Utility method to perform actions if item is found or not after an auto search in [database]
@@ -110,7 +154,6 @@ class SearchHelper {
// If search provide results // If search provide results
database.createVirtualGroupFromSearchInfo( database.createVirtualGroupFromSearchInfo(
searchInfo.toString(), searchInfo.toString(),
PreferencesUtil.omitBackup(context),
MAX_SEARCH_ENTRY MAX_SEARCH_ENTRY
)?.let { searchGroup -> )?.let { searchGroup ->
if (searchGroup.numberOfChildEntries > 0) { if (searchGroup.numberOfChildEntries > 0) {
@@ -132,16 +175,23 @@ class SearchHelper {
fun searchInEntry(entry: Entry, fun searchInEntry(entry: Entry,
searchParameters: SearchParameters): Boolean { searchParameters: SearchParameters): Boolean {
val searchQuery = searchParameters.searchQuery val searchQuery = searchParameters.searchQuery
// Entry don't contains string if the search string is empty
// Not found if the search string is empty
if (searchQuery.isEmpty()) if (searchQuery.isEmpty())
return false return false
// Exclude entry expired
if (searchParameters.excludeExpired) {
if (entry.isCurrentlyExpires)
return false
}
// Search all strings in the KDBX entry // Search all strings in the KDBX entry
if (searchParameters.searchInTitles) { if (searchParameters.searchInTitles) {
if (checkSearchQuery(entry.title, searchParameters)) if (checkSearchQuery(entry.title, searchParameters))
return true return true
} }
if (searchParameters.searchInUserNames) { if (searchParameters.searchInUsernames) {
if (checkSearchQuery(entry.username, searchParameters)) if (checkSearchQuery(entry.username, searchParameters))
return true return true
} }
@@ -158,8 +208,8 @@ class SearchHelper {
return true return true
} }
if (searchParameters.searchInUUIDs) { if (searchParameters.searchInUUIDs) {
val hexString = UuidUtil.toHexString(entry.nodeId.id) val hexString = UuidUtil.toHexString(entry.nodeId.id) ?: ""
if (hexString != null && hexString.contains(searchQuery, true)) if (checkSearchQuery(hexString, searchParameters))
return true return true
} }
if (searchParameters.searchInOther) { if (searchParameters.searchInOther) {
@@ -171,21 +221,31 @@ class SearchHelper {
} }
} }
} }
if (searchParameters.searchInTags) {
if (checkSearchQuery(entry.tags.toString(), searchParameters))
return true
}
return false return false
} }
private fun checkSearchQuery(stringToCheck: String, searchParameters: SearchParameters): Boolean { private fun checkSearchQuery(stringToCheck: String, searchParameters: SearchParameters): Boolean {
/* /*
// TODO Search settings // TODO Search settings
var regularExpression = false
var ignoreCase = true
var removeAccents = true <- Too much time, to study var removeAccents = true <- Too much time, to study
var excludeExpired = false
var searchOnlyInCurrentGroup = false
*/ */
return stringToCheck.isNotEmpty() if (stringToCheck.isEmpty())
&& stringToCheck.contains( return false
searchParameters.searchQuery, true) return if (searchParameters.isRegex) {
val regex = if (searchParameters.caseSensitive) {
searchParameters.searchQuery.toRegex(RegexOption.DOT_MATCHES_ALL)
} else {
searchParameters.searchQuery
.toRegex(setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE))
}
regex.matches(stringToCheck)
} else {
stringToCheck.contains(searchParameters.searchQuery, !searchParameters.caseSensitive)
}
} }
} }
} }

View File

@@ -19,21 +19,82 @@
*/ */
package com.kunzisoft.keepass.database.search package com.kunzisoft.keepass.database.search
import android.os.Parcel
import android.os.Parcelable
/** /**
* Parameters for searching strings in the database. * Parameters for searching strings in the database.
*/ */
class SearchParameters { class SearchParameters() : Parcelable{
var searchQuery: String = "" var searchQuery: String = ""
var caseSensitive = false
var isRegex = false
var searchInTitles = true var searchInTitles = true
var searchInUserNames = true var searchInUsernames = true
var searchInPasswords = false var searchInPasswords = false
var searchInUrls = true var searchInUrls = true
var excludeExpired = false
var searchInNotes = true var searchInNotes = true
var searchInOTP = false var searchInOTP = false
var searchInOther = true var searchInOther = true
var searchInUUIDs = false var searchInUUIDs = false
var searchInTags = true var searchInTags = false
var searchInCurrentGroup = false
var searchInSearchableGroup = true
var searchInRecycleBin = false
var searchInTemplates = false var searchInTemplates = false
constructor(parcel: Parcel) : this() {
searchQuery = parcel.readString() ?: searchQuery
caseSensitive = parcel.readByte() != 0.toByte()
searchInTitles = parcel.readByte() != 0.toByte()
searchInUsernames = parcel.readByte() != 0.toByte()
searchInPasswords = parcel.readByte() != 0.toByte()
searchInUrls = parcel.readByte() != 0.toByte()
excludeExpired = parcel.readByte() != 0.toByte()
searchInNotes = parcel.readByte() != 0.toByte()
searchInOTP = parcel.readByte() != 0.toByte()
searchInOther = parcel.readByte() != 0.toByte()
searchInUUIDs = parcel.readByte() != 0.toByte()
searchInTags = parcel.readByte() != 0.toByte()
searchInCurrentGroup = parcel.readByte() != 0.toByte()
searchInSearchableGroup = parcel.readByte() != 0.toByte()
searchInRecycleBin = parcel.readByte() != 0.toByte()
searchInTemplates = parcel.readByte() != 0.toByte()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(searchQuery)
parcel.writeByte(if (caseSensitive) 1 else 0)
parcel.writeByte(if (searchInTitles) 1 else 0)
parcel.writeByte(if (searchInUsernames) 1 else 0)
parcel.writeByte(if (searchInPasswords) 1 else 0)
parcel.writeByte(if (searchInUrls) 1 else 0)
parcel.writeByte(if (excludeExpired) 1 else 0)
parcel.writeByte(if (searchInNotes) 1 else 0)
parcel.writeByte(if (searchInOTP) 1 else 0)
parcel.writeByte(if (searchInOther) 1 else 0)
parcel.writeByte(if (searchInUUIDs) 1 else 0)
parcel.writeByte(if (searchInTags) 1 else 0)
parcel.writeByte(if (searchInCurrentGroup) 1 else 0)
parcel.writeByte(if (searchInSearchableGroup) 1 else 0)
parcel.writeByte(if (searchInRecycleBin) 1 else 0)
parcel.writeByte(if (searchInTemplates) 1 else 0)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<SearchParameters> {
override fun createFromParcel(parcel: Parcel): SearchParameters {
return SearchParameters(parcel)
}
override fun newArray(size: Int): Array<SearchParameters?> {
return arrayOfNulls(size)
}
}
} }

View File

@@ -31,23 +31,45 @@ import com.kunzisoft.keepass.R
open class Education(val activity: Activity) { open class Education(val activity: Activity) {
private var mOneEducationHintOpen = false
/** /**
* Utility method to save preference after an education action * Utility method to save preference after an education action
*/ */
protected fun checkAndPerformedEducation(isEducationAlreadyPerformed: Boolean, protected fun checkAndPerformedEducation(isEducationAlreadyPerformed: Boolean,
tapTarget: TapTarget, tapTarget: TapTarget,
listener: TapTargetView.Listener, listener: TapTargetView.Listener,
saveEducationStringId: Int): Boolean { saveEducationStringId: Int): Boolean {
var doEducation = false var doEducation = false
if (isEducationScreensEnabled()) { if (isEducationScreensEnabled()
if (isEducationAlreadyPerformed) { && !mOneEducationHintOpen
try { && !isEducationAlreadyPerformed) {
TapTargetView.showFor(activity, tapTarget, listener) try {
saveEducationPreference(activity, saveEducationStringId) TapTargetView.showFor(activity, tapTarget, object : TapTargetView.Listener() {
doEducation = true override fun onTargetClick(view: TapTargetView) {
} catch (e: Exception) { mOneEducationHintOpen = false
Log.w(Education::class.java.name, "Can't performed education " + e.message) saveEducationPreference(activity, saveEducationStringId)
} super.onTargetClick(view)
listener.onTargetClick(view)
}
override fun onOuterCircleClick(view: TapTargetView?) {
mOneEducationHintOpen = false
saveEducationPreference(activity, saveEducationStringId)
super.onOuterCircleClick(view)
listener.onOuterCircleClick(view)
view?.dismiss(false)
}
override fun onTargetCancel(view: TapTargetView?) {
mOneEducationHintOpen = false
saveEducationPreference(activity, saveEducationStringId)
super.onTargetCancel(view)
}
})
mOneEducationHintOpen = true
doEducation = true
} catch (e: Exception) {
Log.w(Education::class.java.name, "Can't performed education " + e.message)
} }
} }
return doEducation return doEducation
@@ -117,6 +139,29 @@ open class Education(val activity: Activity) {
R.string.education_add_attachment_key, R.string.education_add_attachment_key,
R.string.education_setup_OTP_key) R.string.education_setup_OTP_key)
fun putPropertiesInEducationPreferences(context: Context,
editor: SharedPreferences.Editor,
name: String,
value: String) {
when (name) {
context.getString(R.string.education_create_db_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_select_db_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_unlock_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_read_only_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_biometric_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_new_node_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_sort_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_lock_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_copy_username_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_entry_edit_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_password_generator_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_entry_new_field_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_add_attachment_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_setup_OTP_key) -> editor.putBoolean(name, value.toBoolean())
}
}
/** /**
* Get preferences bundle for education * Get preferences bundle for education
*/ */

View File

@@ -32,8 +32,7 @@ class EntryActivityEducation(activity: Activity)
fun checkAndPerformedEntryCopyEducation(educationView: View, fun checkAndPerformedEntryCopyEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation( return checkAndPerformedEducation(isEducationCopyUsernamePerformed(activity),
!isEducationCopyUsernamePerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_field_copy_title), activity.getString(R.string.education_field_copy_title),
activity.getString(R.string.education_field_copy_summary)) activity.getString(R.string.education_field_copy_summary))
@@ -64,8 +63,7 @@ class EntryActivityEducation(activity: Activity)
fun checkAndPerformedEntryEditEducation(educationView: View, fun checkAndPerformedEntryEditEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation( return checkAndPerformedEducation(isEducationEntryEditPerformed(activity),
!isEducationEntryEditPerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_entry_edit_title), activity.getString(R.string.education_entry_edit_title),
activity.getString(R.string.education_entry_edit_summary)) activity.getString(R.string.education_entry_edit_summary))

View File

@@ -35,7 +35,7 @@ class EntryEditActivityEducation(activity: Activity)
fun checkAndPerformedGeneratePasswordEducation(educationView: View, fun checkAndPerformedGeneratePasswordEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationPasswordGeneratorPerformed(activity), return checkAndPerformedEducation(isEducationPasswordGeneratorPerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_generate_password_title), activity.getString(R.string.education_generate_password_title),
activity.getString(R.string.education_generate_password_summary)) activity.getString(R.string.education_generate_password_summary))
@@ -66,7 +66,7 @@ class EntryEditActivityEducation(activity: Activity)
fun checkAndPerformedEntryNewFieldEducation(educationView: View, fun checkAndPerformedEntryNewFieldEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationEntryNewFieldPerformed(activity), return checkAndPerformedEducation(isEducationEntryNewFieldPerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_entry_new_field_title), activity.getString(R.string.education_entry_new_field_title),
activity.getString(R.string.education_entry_new_field_summary)) activity.getString(R.string.education_entry_new_field_summary))
@@ -97,7 +97,7 @@ class EntryEditActivityEducation(activity: Activity)
fun checkAndPerformedAttachmentEducation(educationView: View, fun checkAndPerformedAttachmentEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationAddAttachmentPerformed(activity), return checkAndPerformedEducation(isEducationAddAttachmentPerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_add_attachment_title), activity.getString(R.string.education_add_attachment_title),
activity.getString(R.string.education_add_attachment_summary)) activity.getString(R.string.education_add_attachment_summary))
@@ -128,7 +128,7 @@ class EntryEditActivityEducation(activity: Activity)
fun checkAndPerformedSetUpOTPEducation(educationView: View, fun checkAndPerformedSetUpOTPEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationSetupOTPPerformed(activity), return checkAndPerformedEducation(isEducationSetupOTPPerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_setup_OTP_title), activity.getString(R.string.education_setup_OTP_title),
activity.getString(R.string.education_setup_OTP_summary)) activity.getString(R.string.education_setup_OTP_summary))

View File

@@ -39,7 +39,7 @@ class FileDatabaseSelectActivityEducation(activity: Activity)
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
// Try to open the creation base education // Try to open the creation base education
return checkAndPerformedEducation(!isEducationCreateDatabasePerformed(activity), return checkAndPerformedEducation(isEducationCreateDatabasePerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_create_database_title), activity.getString(R.string.education_create_database_title),
activity.getString(R.string.education_create_database_summary)) activity.getString(R.string.education_create_database_summary))
@@ -71,7 +71,7 @@ class FileDatabaseSelectActivityEducation(activity: Activity)
fun checkAndPerformedSelectDatabaseEducation(educationView: View, fun checkAndPerformedSelectDatabaseEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationSelectDatabasePerformed(activity), return checkAndPerformedEducation(isEducationSelectDatabasePerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_select_database_title), activity.getString(R.string.education_select_database_title),
activity.getString(R.string.education_select_database_summary)) activity.getString(R.string.education_select_database_summary))

View File

@@ -31,7 +31,7 @@ class GroupActivityEducation(activity: Activity)
fun checkAndPerformedAddNodeButtonEducation(educationView: View, fun checkAndPerformedAddNodeButtonEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationNewNodePerformed(activity), return checkAndPerformedEducation(isEducationNewNodePerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_new_node_title), activity.getString(R.string.education_new_node_title),
activity.getString(R.string.education_new_node_summary)) activity.getString(R.string.education_new_node_summary))
@@ -58,7 +58,7 @@ class GroupActivityEducation(activity: Activity)
fun checkAndPerformedSearchMenuEducation(educationView: View, fun checkAndPerformedSearchMenuEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationSearchPerformed(activity), return checkAndPerformedEducation(isEducationSearchPerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_search_title), activity.getString(R.string.education_search_title),
activity.getString(R.string.education_search_summary)) activity.getString(R.string.education_search_summary))
@@ -85,7 +85,7 @@ class GroupActivityEducation(activity: Activity)
fun checkAndPerformedSortMenuEducation(educationView: View, fun checkAndPerformedSortMenuEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationSortPerformed(activity), return checkAndPerformedEducation(isEducationSortPerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_sort_title), activity.getString(R.string.education_sort_title),
activity.getString(R.string.education_sort_summary)) activity.getString(R.string.education_sort_summary))
@@ -112,7 +112,7 @@ class GroupActivityEducation(activity: Activity)
fun checkAndPerformedLockMenuEducation(educationView: View, fun checkAndPerformedLockMenuEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationLockPerformed(activity), return checkAndPerformedEducation(isEducationLockPerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_lock_title), activity.getString(R.string.education_lock_title),
activity.getString(R.string.education_lock_summary)) activity.getString(R.string.education_lock_summary))

View File

@@ -32,7 +32,7 @@ class PasswordActivityEducation(activity: Activity)
fun checkAndPerformedUnlockEducation(educationView: View, fun checkAndPerformedUnlockEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationUnlockPerformed(activity), return checkAndPerformedEducation(isEducationUnlockPerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_unlock_title), activity.getString(R.string.education_unlock_title),
activity.getString(R.string.education_unlock_summary)) activity.getString(R.string.education_unlock_summary))
@@ -60,7 +60,7 @@ class PasswordActivityEducation(activity: Activity)
fun checkAndPerformedReadOnlyEducation(educationView: View, fun checkAndPerformedReadOnlyEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationReadOnlyPerformed(activity), return checkAndPerformedEducation(isEducationReadOnlyPerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_read_only_title), activity.getString(R.string.education_read_only_title),
activity.getString(R.string.education_read_only_summary)) activity.getString(R.string.education_read_only_summary))
@@ -87,7 +87,7 @@ class PasswordActivityEducation(activity: Activity)
fun checkAndPerformedBiometricEducation(educationView: View, fun checkAndPerformedBiometricEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null, onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationBiometricPerformed(activity), return checkAndPerformedEducation(isEducationBiometricPerformed(activity),
TapTarget.forView(educationView, TapTarget.forView(educationView,
activity.getString(R.string.education_advanced_unlock_title), activity.getString(R.string.education_advanced_unlock_title),
activity.getString(R.string.education_advanced_unlock_summary)) activity.getString(R.string.education_advanced_unlock_summary))

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.model
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
class CipherDecryptDatabase(): Parcelable {
var databaseUri: Uri? = null
var credentialStorage: CredentialStorage = CredentialStorage.DEFAULT
var decryptedValue: ByteArray = byteArrayOf()
constructor(parcel: Parcel): this() {
databaseUri = parcel.readParcelable(Uri::class.java.classLoader)
credentialStorage = parcel.readEnum<CredentialStorage>() ?: credentialStorage
decryptedValue = ByteArray(parcel.readInt())
parcel.readByteArray(decryptedValue)
}
fun replaceContent(copy: CipherDecryptDatabase) {
this.decryptedValue = copy.decryptedValue
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(databaseUri, flags)
parcel.writeEnum(credentialStorage)
parcel.writeInt(decryptedValue.size)
parcel.writeByteArray(decryptedValue)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CipherDecryptDatabase> {
override fun createFromParcel(parcel: Parcel): CipherDecryptDatabase {
return CipherDecryptDatabase(parcel)
}
override fun newArray(size: Int): Array<CipherDecryptDatabase?> {
return arrayOfNulls(size)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CipherDecryptDatabase
if (databaseUri != other.databaseUri) return false
return true
}
override fun hashCode(): Int {
return databaseUri.hashCode()
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.model
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
class CipherEncryptDatabase(): Parcelable {
var databaseUri: Uri? = null
var credentialStorage: CredentialStorage = CredentialStorage.DEFAULT
var encryptedValue: ByteArray = byteArrayOf()
var specParameters: ByteArray = byteArrayOf()
constructor(parcel: Parcel): this() {
databaseUri = parcel.readParcelable(Uri::class.java.classLoader)
credentialStorage = parcel.readEnum<CredentialStorage>() ?: credentialStorage
encryptedValue = ByteArray(parcel.readInt())
parcel.readByteArray(encryptedValue)
specParameters = ByteArray(parcel.readInt())
parcel.readByteArray(specParameters)
}
fun replaceContent(copy: CipherEncryptDatabase) {
this.encryptedValue = copy.encryptedValue
this.specParameters = copy.specParameters
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(databaseUri, flags)
parcel.writeEnum(credentialStorage)
parcel.writeInt(encryptedValue.size)
parcel.writeByteArray(encryptedValue)
parcel.writeInt(specParameters.size)
parcel.writeByteArray(specParameters)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CipherEncryptDatabase> {
override fun createFromParcel(parcel: Parcel): CipherEncryptDatabase {
return CipherEncryptDatabase(parcel)
}
override fun newArray(size: Int): Array<CipherEncryptDatabase?> {
return arrayOfNulls(size)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CipherEncryptDatabase
if (databaseUri != other.databaseUri) return false
return true
}
override fun hashCode(): Int {
return databaseUri.hashCode()
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.model
enum class CredentialStorage {
PASSWORD, KEY_FILE, HARDWARE_KEY;
companion object {
fun getFromOrdinal(ordinal: Int): CredentialStorage {
return when (ordinal) {
0 -> PASSWORD
1 -> KEY_FILE
2 -> HARDWARE_KEY
else -> DEFAULT
}
}
val DEFAULT: CredentialStorage
get() = PASSWORD
}
}

View File

@@ -23,6 +23,7 @@ import android.os.Parcel
import android.os.ParcelUuid import android.os.ParcelUuid
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.entry.AutoType
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.element.template.TemplateField import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
@@ -42,6 +43,7 @@ class EntryInfo : NodeInfo {
var foregroundColor: Int? = null var foregroundColor: Int? = null
var customFields: MutableList<Field> = mutableListOf() var customFields: MutableList<Field> = mutableListOf()
var attachments: MutableList<Attachment> = mutableListOf() var attachments: MutableList<Attachment> = mutableListOf()
var autoType: AutoType = AutoType()
var otpModel: OtpModel? = null var otpModel: OtpModel? = null
var isTemplate: Boolean = false var isTemplate: Boolean = false
@@ -60,6 +62,7 @@ class EntryInfo : NodeInfo {
foregroundColor = if (readFgColor == -1) null else readFgColor foregroundColor = if (readFgColor == -1) null else readFgColor
parcel.readList(customFields, Field::class.java.classLoader) parcel.readList(customFields, Field::class.java.classLoader)
parcel.readList(attachments, Attachment::class.java.classLoader) parcel.readList(attachments, Attachment::class.java.classLoader)
autoType = parcel.readParcelable(AutoType::class.java.classLoader) ?: autoType
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
isTemplate = parcel.readByte().toInt() != 0 isTemplate = parcel.readByte().toInt() != 0
} }
@@ -80,6 +83,7 @@ class EntryInfo : NodeInfo {
parcel.writeInt(foregroundColor ?: -1) parcel.writeInt(foregroundColor ?: -1)
parcel.writeList(customFields) parcel.writeList(customFields)
parcel.writeList(attachments) parcel.writeList(attachments)
parcel.writeParcelable(autoType, flags)
parcel.writeParcelable(otpModel, flags) parcel.writeParcelable(otpModel, flags)
parcel.writeByte((if (isTemplate) 1 else 0).toByte()) parcel.writeByte((if (isTemplate) 1 else 0).toByte())
} }
@@ -105,6 +109,7 @@ class EntryInfo : NodeInfo {
return customFields.lastOrNull { it.name == label }?.protectedValue?.toString() ?: "" return customFields.lastOrNull { it.name == label }?.protectedValue?.toString() ?: ""
} }
// Return true if modified
private fun addUniqueField(field: Field, number: Int = 0) { private fun addUniqueField(field: Field, number: Int = 0) {
var sameName = false var sameName = false
var sameValue = false var sameValue = false
@@ -126,7 +131,19 @@ class EntryInfo : NodeInfo {
(customFields as ArrayList<Field>).add(Field(field.name + suffix, field.protectedValue)) (customFields as ArrayList<Field>).add(Field(field.name + suffix, field.protectedValue))
} }
fun saveSearchInfo(database: Database?, searchInfo: SearchInfo) { private fun containsDomainOrApplicationId(search: String): Boolean {
if (url.contains(search))
return true
return customFields.find {
it.protectedValue.stringValue.contains(search)
} != null
}
/**
* Add searchInfo to current EntryInfo, return true if new data, false if no modification
*/
fun saveSearchInfo(database: Database?, searchInfo: SearchInfo): Boolean {
var modification = false
searchInfo.otpString?.let { otpString -> searchInfo.otpString?.let { otpString ->
// Replace the OTP field // Replace the OTP field
OtpEntryFields.parseOTPUri(otpString)?.let { otpElement -> OtpEntryFields.parseOTPUri(otpString)?.let { otpElement ->
@@ -141,31 +158,45 @@ class EntryInfo : NodeInfo {
mutableCustomFields.remove(otpField) mutableCustomFields.remove(otpField)
} }
mutableCustomFields.add(otpField) mutableCustomFields.add(otpField)
modification = true
} }
} ?: searchInfo.webDomain?.let { webDomain -> } ?: searchInfo.webDomain?.let { webDomain ->
// If unable to save web domain in custom field or URL not populated, save in URL // If unable to save web domain in custom field or URL not populated, save in URL
val scheme = searchInfo.webScheme val scheme = searchInfo.webScheme
val webScheme = if (scheme.isNullOrEmpty()) "http" else scheme val webScheme = if (scheme.isNullOrEmpty()) "https" else scheme
val webDomainToStore = "$webScheme://$webDomain" val webDomainToStore = "$webScheme://$webDomain"
if (database?.allowEntryCustomFields() != true || url.isEmpty()) { if (!containsDomainOrApplicationId(webDomain)) {
url = webDomainToStore if (database?.allowEntryCustomFields() != true || url.isEmpty()) {
} else if (url != webDomainToStore) { url = webDomainToStore
// Save web domain in custom field } else {
addUniqueField(Field(WEB_DOMAIN_FIELD_NAME, // Save web domain in custom field
ProtectedString(false, webDomainToStore)), addUniqueField(
Field(
WEB_DOMAIN_FIELD_NAME,
ProtectedString(false, webDomainToStore)
),
1 // Start to one because URL is a standard field name 1 // Start to one because URL is a standard field name
) )
}
modification = true
} }
} ?: run { } ?: run {
// Save application id in custom field // Save application id in custom field
if (database?.allowEntryCustomFields() == true) { if (database?.allowEntryCustomFields() == true) {
searchInfo.applicationId?.let { applicationId -> searchInfo.applicationId?.let { applicationId ->
addUniqueField(Field(APPLICATION_ID_FIELD_NAME, if (!containsDomainOrApplicationId(applicationId)) {
ProtectedString(false, applicationId)) addUniqueField(
) Field(
APPLICATION_ID_FIELD_NAME,
ProtectedString(false, applicationId)
)
)
modification = true
}
} }
} }
} }
return modification
} }
fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo) { fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo) {
@@ -209,6 +240,7 @@ class EntryInfo : NodeInfo {
if (foregroundColor != other.foregroundColor) return false if (foregroundColor != other.foregroundColor) return false
if (customFields != other.customFields) return false if (customFields != other.customFields) return false
if (attachments != other.attachments) return false if (attachments != other.attachments) return false
if (autoType != other.autoType) return false
if (otpModel != other.otpModel) return false if (otpModel != other.otpModel) return false
if (isTemplate != other.isTemplate) return false if (isTemplate != other.isTemplate) return false
@@ -227,6 +259,7 @@ class EntryInfo : NodeInfo {
result = 31 * result + foregroundColor.hashCode() result = 31 * result + foregroundColor.hashCode()
result = 31 * result + customFields.hashCode() result = 31 * result + customFields.hashCode()
result = 31 * result + attachments.hashCode() result = 31 * result + attachments.hashCode()
result = 31 * result + autoType.hashCode()
result = 31 * result + (otpModel?.hashCode() ?: 0) result = 31 * result + (otpModel?.hashCode() ?: 0)
result = 31 * result + isTemplate.hashCode() result = 31 * result + isTemplate.hashCode()
return result return result

View File

@@ -12,6 +12,9 @@ class GroupInfo : NodeInfo {
var id: UUID? = null var id: UUID? = null
var notes: String? = null var notes: String? = null
var searchable: Boolean? = null
var enableAutoType: Boolean? = null
var defaultAutoTypeSequence: String = ""
var tags: Tags = Tags() var tags: Tags = Tags()
init { init {
@@ -23,6 +26,11 @@ class GroupInfo : NodeInfo {
constructor(parcel: Parcel): super(parcel) { constructor(parcel: Parcel): super(parcel) {
id = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: id id = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: id
notes = parcel.readString() notes = parcel.readString()
val isSearchingEnabled = parcel.readInt()
searchable = if (isSearchingEnabled == -1) null else isSearchingEnabled == 1
val isAutoTypeEnabled = parcel.readInt()
enableAutoType = if (isAutoTypeEnabled == -1) null else isAutoTypeEnabled == 1
defaultAutoTypeSequence = parcel.readString() ?: defaultAutoTypeSequence
tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags
} }
@@ -31,6 +39,9 @@ class GroupInfo : NodeInfo {
val uuid = if (id != null) ParcelUuid(id) else null val uuid = if (id != null) ParcelUuid(id) else null
parcel.writeParcelable(uuid, flags) parcel.writeParcelable(uuid, flags)
parcel.writeString(notes) parcel.writeString(notes)
parcel.writeInt(if (searchable == null) -1 else if (searchable!!) 1 else 0)
parcel.writeInt(if (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0)
parcel.writeString(defaultAutoTypeSequence)
parcel.writeParcelable(tags, flags) parcel.writeParcelable(tags, flags)
} }
@@ -41,6 +52,9 @@ class GroupInfo : NodeInfo {
if (id != other.id) return false if (id != other.id) return false
if (notes != other.notes) return false if (notes != other.notes) return false
if (searchable != other.searchable) return false
if (enableAutoType != other.enableAutoType) return false
if (defaultAutoTypeSequence != other.defaultAutoTypeSequence) return false
if (tags != other.tags) return false if (tags != other.tags) return false
return true return true
@@ -50,6 +64,9 @@ class GroupInfo : NodeInfo {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (id?.hashCode() ?: 0) result = 31 * result + (id?.hashCode() ?: 0)
result = 31 * result + (notes?.hashCode() ?: 0) result = 31 * result + (notes?.hashCode() ?: 0)
result = 31 * result + searchable.hashCode()
result = 31 * result + enableAutoType.hashCode()
result = 31 * result + defaultAutoTypeSequence.hashCode()
result = 31 * result + tags.hashCode() result = 31 * result + tags.hashCode()
return result return result
} }

View File

@@ -2,6 +2,7 @@ package com.kunzisoft.keepass.model
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.utils.UuidUtil import com.kunzisoft.keepass.utils.UuidUtil
@@ -15,6 +16,7 @@ open class NodeInfo() : Parcelable {
var lastModificationTime: DateInstant = DateInstant() var lastModificationTime: DateInstant = DateInstant()
var expires: Boolean = false var expires: Boolean = false
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH_DATE_TIME var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH_DATE_TIME
var customData: CustomData = CustomData()
constructor(parcel: Parcel) : this() { constructor(parcel: Parcel) : this() {
title = parcel.readString() ?: title title = parcel.readString() ?: title
@@ -23,6 +25,7 @@ open class NodeInfo() : Parcelable {
lastModificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: lastModificationTime lastModificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: lastModificationTime
expires = parcel.readInt() != 0 expires = parcel.readInt() != 0
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
customData = parcel.readParcelable(CustomData::class.java.classLoader) ?: customData
} }
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
@@ -32,6 +35,7 @@ open class NodeInfo() : Parcelable {
parcel.writeParcelable(lastModificationTime, flags) parcel.writeParcelable(lastModificationTime, flags)
parcel.writeInt(if (expires) 1 else 0) parcel.writeInt(if (expires) 1 else 0)
parcel.writeParcelable(expiryTime, flags) parcel.writeParcelable(expiryTime, flags)
parcel.writeParcelable(customData, flags)
} }
override fun describeContents(): Int { override fun describeContents(): Int {
@@ -48,6 +52,7 @@ open class NodeInfo() : Parcelable {
if (lastModificationTime != other.lastModificationTime) return false if (lastModificationTime != other.lastModificationTime) return false
if (expires != other.expires) return false if (expires != other.expires) return false
if (expiryTime != other.expiryTime) return false if (expiryTime != other.expiryTime) return false
if (customData != other.customData) return false
return true return true
} }
@@ -59,6 +64,7 @@ open class NodeInfo() : Parcelable {
result = 31 * result + lastModificationTime.hashCode() result = 31 * result + lastModificationTime.hashCode()
result = 31 * result + expires.hashCode() result = 31 * result + expires.hashCode()
result = 31 * result + expiryTime.hashCode() result = 31 * result + expiryTime.hashCode()
result = 31 * result + customData.hashCode()
return result return result
} }

View File

@@ -22,12 +22,14 @@ package com.kunzisoft.keepass.services
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.* import android.os.Binder
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log import android.util.Log
import androidx.media.app.NotificationCompat import androidx.media.app.NotificationCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.action.* import com.kunzisoft.keepass.database.action.*
import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable
import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable
@@ -39,16 +41,19 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.closeDatabase
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.* import java.util.*
import kotlin.collections.ArrayList
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater { open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
@@ -226,7 +231,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val actionRunnable: ActionRunnable? = when (intentAction) { val actionRunnable: ActionRunnable? = when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent, database) ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent, database)
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent, database) ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent, database)
ACTION_DATABASE_MERGE_TASK -> buildDatabaseMergeActionTask(database) ACTION_DATABASE_MERGE_TASK -> buildDatabaseMergeActionTask(intent, database)
ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask(database) ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask(database)
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent, database) ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent, database)
ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent, database) ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent, database)
@@ -469,7 +474,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
intent?.removeExtra(DATABASE_URI_KEY) intent?.removeExtra(DATABASE_URI_KEY)
intent?.removeExtra(MAIN_CREDENTIAL_KEY) intent?.removeExtra(MAIN_CREDENTIAL_KEY)
intent?.removeExtra(READ_ONLY_KEY) intent?.removeExtra(READ_ONLY_KEY)
intent?.removeExtra(CIPHER_ENTITY_KEY) intent?.removeExtra(CIPHER_DATABASE_KEY)
intent?.removeExtra(FIX_DUPLICATE_UUID_KEY) intent?.removeExtra(FIX_DUPLICATE_UUID_KEY)
intent?.removeExtra(GROUP_KEY) intent?.removeExtra(GROUP_KEY)
intent?.removeExtra(ENTRY_KEY) intent?.removeExtra(ENTRY_KEY)
@@ -570,13 +575,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
if (intent.hasExtra(DATABASE_URI_KEY) if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MAIN_CREDENTIAL_KEY) && intent.hasExtra(MAIN_CREDENTIAL_KEY)
&& intent.hasExtra(READ_ONLY_KEY) && intent.hasExtra(READ_ONLY_KEY)
&& intent.hasExtra(CIPHER_ENTITY_KEY) && intent.hasExtra(CIPHER_DATABASE_KEY)
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY) && intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
) { ) {
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY) val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true) val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY) val cipherEncryptDatabase: CipherEncryptDatabase? = intent.getParcelableExtra(CIPHER_DATABASE_KEY)
if (databaseUri == null) if (databaseUri == null)
return null return null
@@ -589,7 +594,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
databaseUri, databaseUri,
mainCredential, mainCredential,
readOnly, readOnly,
cipherEntity, cipherEncryptDatabase,
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false), intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
this this
) { result -> ) { result ->
@@ -598,7 +603,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
putParcelable(DATABASE_URI_KEY, databaseUri) putParcelable(DATABASE_URI_KEY, databaseUri)
putParcelable(MAIN_CREDENTIAL_KEY, mainCredential) putParcelable(MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(READ_ONLY_KEY, readOnly) putBoolean(READ_ONLY_KEY, readOnly)
putParcelable(CIPHER_ENTITY_KEY, cipherEntity) putParcelable(CIPHER_DATABASE_KEY, cipherEncryptDatabase)
} }
} }
} else { } else {
@@ -606,10 +611,21 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
} }
private fun buildDatabaseMergeActionTask(database: Database): ActionRunnable { private fun buildDatabaseMergeActionTask(intent: Intent, database: Database): ActionRunnable {
var databaseToMergeUri: Uri? = null
var databaseToMergeMainCredential: MainCredential? = null
if (intent.hasExtra(DATABASE_URI_KEY)) {
databaseToMergeUri = intent.getParcelableExtra(DATABASE_URI_KEY)
}
if (intent.hasExtra(MAIN_CREDENTIAL_KEY)) {
databaseToMergeMainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY)
}
return MergeDatabaseRunnable( return MergeDatabaseRunnable(
this, this,
database, database,
databaseToMergeUri,
databaseToMergeMainCredential,
this this
) { result -> ) { result ->
// No need to add each info to reload database // No need to add each info to reload database
@@ -633,7 +649,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
&& intent.hasExtra(MAIN_CREDENTIAL_KEY) && intent.hasExtra(MAIN_CREDENTIAL_KEY)
) { ) {
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
AssignPasswordInDatabaseRunnable(this, AssignMainCredentialInDatabaseRunnable(this,
database, database,
databaseUri, databaseUri,
intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
@@ -911,9 +927,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
*/ */
private fun buildDatabaseSave(intent: Intent, database: Database): ActionRunnable? { private fun buildDatabaseSave(intent: Intent, database: Database): ActionRunnable? {
return if (intent.hasExtra(SAVE_DATABASE_KEY)) { return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
var databaseCopyUri: Uri? = null
if (intent.hasExtra(DATABASE_URI_KEY)) {
databaseCopyUri = intent.getParcelableExtra(DATABASE_URI_KEY)
}
SaveDatabaseRunnable(this, SaveDatabaseRunnable(this,
database, database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)) !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
databaseCopyUri)
} else { } else {
null null
} }
@@ -963,7 +986,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val DATABASE_URI_KEY = "DATABASE_URI_KEY" const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
const val MAIN_CREDENTIAL_KEY = "MAIN_CREDENTIAL_KEY" const val MAIN_CREDENTIAL_KEY = "MAIN_CREDENTIAL_KEY"
const val READ_ONLY_KEY = "READ_ONLY_KEY" const val READ_ONLY_KEY = "READ_ONLY_KEY"
const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY" const val CIPHER_DATABASE_KEY = "CIPHER_DATABASE_KEY"
const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY" const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY"
const val GROUP_KEY = "GROUP_KEY" const val GROUP_KEY = "GROUP_KEY"
const val ENTRY_KEY = "ENTRY_KEY" const val ENTRY_KEY = "ENTRY_KEY"

View File

@@ -41,12 +41,12 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
} }
} }
override fun onDisplayPreferenceDialog(preference: Preference?) { override fun onDisplayPreferenceDialog(preference: Preference) {
var otherDialogFragment = false var otherDialogFragment = false
var dialogFragment: DialogFragment? = null var dialogFragment: DialogFragment? = null
when (preference?.key) { when (preference.key) {
getString(R.string.autofill_application_id_blocklist_key) -> { getString(R.string.autofill_application_id_blocklist_key) -> {
dialogFragment = AutofillBlocklistAppIdPreferenceDialogFragmentCompat.newInstance(preference.key) dialogFragment = AutofillBlocklistAppIdPreferenceDialogFragmentCompat.newInstance(preference.key)
} }

View File

@@ -34,13 +34,13 @@ class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
setPreferencesFromResource(R.xml.preferences_keyboard, rootKey) setPreferencesFromResource(R.xml.preferences_keyboard, rootKey)
} }
override fun onDisplayPreferenceDialog(preference: Preference?) { override fun onDisplayPreferenceDialog(preference: Preference) {
var otherDialogFragment = false var otherDialogFragment = false
var dialogFragment: DialogFragment? = null var dialogFragment: DialogFragment? = null
// Main Preferences // Main Preferences
when (preference?.key) { when (preference.key) {
getString(R.string.keyboard_entry_timeout_key) -> { getString(R.string.keyboard_entry_timeout_key) -> {
dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key) dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key)
} }

View File

@@ -365,7 +365,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
) { _, _ -> ) { _, _ ->
validate?.invoke() validate?.invoke()
deleteKeysAlertDialog?.setOnDismissListener(null) deleteKeysAlertDialog?.setOnDismissListener(null)
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
}
} }
.setNegativeButton(resources.getString(android.R.string.cancel) .setNegativeButton(resources.getString(android.R.string.cancel)
) { _, _ ->} ) { _, _ ->}
@@ -437,9 +439,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
} }
} }
override fun onPreferenceTreeClick(preference: Preference?): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
// To reload group when appearance settings are modified // To reload group when appearance settings are modified
when (preference?.key) { when (preference.key) {
getString(R.string.setting_style_key), getString(R.string.setting_style_key),
getString(R.string.setting_style_brightness_key), getString(R.string.setting_style_brightness_key),
getString(R.string.setting_icon_pack_choose_key), getString(R.string.setting_icon_pack_choose_key),
@@ -459,13 +461,13 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
return super.onPreferenceTreeClick(preference) return super.onPreferenceTreeClick(preference)
} }
override fun onDisplayPreferenceDialog(preference: Preference?) { override fun onDisplayPreferenceDialog(preference: Preference) {
var otherDialogFragment = false var otherDialogFragment = false
var dialogFragment: DialogFragment? = null var dialogFragment: DialogFragment? = null
// Main Preferences // Main Preferences
when (preference?.key) { when (preference.key) {
getString(R.string.app_timeout_key), getString(R.string.app_timeout_key),
getString(R.string.clipboard_timeout_key), getString(R.string.clipboard_timeout_key),
getString(R.string.temp_advanced_unlock_timeout_key) -> { getString(R.string.temp_advanced_unlock_timeout_key) -> {

View File

@@ -30,7 +30,7 @@ import androidx.preference.PreferenceCategory
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import com.kunzisoft.androidclearchroma.ChromaUtil import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
@@ -43,7 +43,6 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.preference.* import com.kunzisoft.keepass.settings.preference.*
import com.kunzisoft.keepass.settings.preferencedialogfragment.* import com.kunzisoft.keepass.settings.preferencedialogfragment.*
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval { class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
@@ -155,18 +154,22 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
// Database name // Database name
dbNamePref = findPreference(getString(R.string.database_name_key)) dbNamePref = findPreference(getString(R.string.database_name_key))
if (database.allowName) { dbNamePref?.let { namePreference ->
dbNamePref?.summary = database.name if (database.allowName) {
} else { namePreference.summary = database.name
dbGeneralPrefCategory?.removePreference(dbNamePref) } else {
dbGeneralPrefCategory?.removePreference(namePreference)
}
} }
// Database description // Database description
dbDescriptionPref = findPreference(getString(R.string.database_description_key)) dbDescriptionPref = findPreference(getString(R.string.database_description_key))
if (database.allowDescription) { dbDescriptionPref?.let { descriptionPreference ->
dbDescriptionPref?.summary = database.description if (database.allowDescription) {
} else { dbDescriptionPref?.summary = database.description
dbGeneralPrefCategory?.removePreference(dbDescriptionPref) } else {
dbGeneralPrefCategory?.removePreference(descriptionPreference)
}
} }
// Database default username // Database default username
@@ -238,7 +241,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
// Templates // Templates
val templatesGroupPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_templates_key)) val templatesGroupPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_templates_key))
templatesGroupPref = findPreference(getString(R.string.templates_group_uuid_key)) templatesGroupPref = findPreference(getString(R.string.templates_group_uuid_key))
if (database.allowConfigurableTemplatesGroup) { if (database.allowTemplatesGroup) {
val templatesEnablePref: SwitchPreference? = findPreference(getString(R.string.templates_group_enable_key)) val templatesEnablePref: SwitchPreference? = findPreference(getString(R.string.templates_group_enable_key))
templatesEnablePref?.apply { templatesEnablePref?.apply {
isChecked = database.isTemplatesEnabled isChecked = database.isTemplatesEnabled
@@ -334,7 +337,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
findPreference<Preference>(getString(R.string.settings_database_change_credentials_key))?.apply { findPreference<Preference>(getString(R.string.settings_database_change_credentials_key))?.apply {
isEnabled = if (!mDatabaseReadOnly) { isEnabled = if (!mDatabaseReadOnly) {
onPreferenceClickListener = Preference.OnPreferenceClickListener { onPreferenceClickListener = Preference.OnPreferenceClickListener {
AssignMasterKeyDialogFragment.getInstance(database.allowNoMasterKey) SetMainCredentialDialogFragment.getInstance(database.allowNoMasterKey)
.show(parentFragmentManager, "passwordDialog") .show(parentFragmentManager, "passwordDialog")
false false
} }
@@ -355,7 +358,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = super.onCreateView(inflater, container, savedInstanceState) val view = super.onCreateView(inflater, container, savedInstanceState)
try { try {
@@ -565,13 +568,13 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} }
} }
override fun onDisplayPreferenceDialog(preference: Preference?) { override fun onDisplayPreferenceDialog(preference: Preference) {
var otherDialogFragment = false var otherDialogFragment = false
var dialogFragment: DialogFragment? = null var dialogFragment: DialogFragment? = null
// Main Preferences // Main Preferences
when (preference?.key) { when (preference.key) {
getString(R.string.database_name_key) -> { getString(R.string.database_name_key) -> {
dialogFragment = DatabaseNamePreferenceDialogFragmentCompat.newInstance(preference.key) dialogFragment = DatabaseNamePreferenceDialogFragmentCompat.newInstance(preference.key)
} }
@@ -669,27 +672,29 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
} }
R.id.menu_merge_database -> { R.id.menu_merge_database -> {
mergeDatabase() mergeDatabase()
return true true
} }
R.id.menu_reload_database -> { R.id.menu_reload_database -> {
reloadDatabase() reloadDatabase()
return true true
} }
R.id.menu_app_settings -> {
else -> {
// Check the time lock before launching settings // Check the time lock before launching settings
// TODO activity menu // TODO activity menu
(activity as SettingsActivity?)?.let { (activity as SettingsActivity?)?.let {
MenuUtil.onDefaultMenuOptionsItemSelected(it, item, true) SettingsActivity.launch(it, true)
} }
true
}
else -> {
super.onOptionsItemSelected(item) super.onOptionsItemSelected(item)
} }
} }
} }
override fun onPreferenceTreeClick(preference: Preference?): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
// To reload group when database settings are modified // To reload group when database settings are modified
when (preference?.key) { when (preference.key) {
getString(R.string.database_name_key), getString(R.string.database_name_key),
getString(R.string.database_description_key), getString(R.string.database_description_key),
getString(R.string.database_default_username_key), getString(R.string.database_default_username_key),

View File

@@ -92,12 +92,6 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.remember_keyfile_locations_default)) context.resources.getBoolean(R.bool.remember_keyfile_locations_default))
} }
fun omitBackup(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.omit_backup_search_key),
context.resources.getBoolean(R.bool.omit_backup_search_default))
}
fun automaticallyFocusSearch(context: Context): Boolean { fun automaticallyFocusSearch(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.auto_focus_search_key), return prefs.getBoolean(context.getString(R.string.auto_focus_search_key),
@@ -608,7 +602,6 @@ object PreferencesUtil {
context.getString(R.string.enable_read_only_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.enable_read_only_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.enable_auto_save_database_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.enable_auto_save_database_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.enable_keep_screen_on_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.enable_keep_screen_on_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.omit_backup_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.auto_focus_search_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.auto_focus_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.subdomain_search_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.subdomain_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.app_timeout_key) -> editor.putString(name, value.toLong().toString()) context.getString(R.string.app_timeout_key) -> editor.putString(name, value.toLong().toString())
@@ -678,23 +671,7 @@ object PreferencesUtil {
putPropertiesInPreferences(properties, putPropertiesInPreferences(properties,
Education.getEducationSharedPreferences(context)) { editor, name, value -> Education.getEducationSharedPreferences(context)) { editor, name, value ->
when (name) { Education.putPropertiesInEducationPreferences(context, editor, name, value)
context.getString(R.string.education_create_db_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_select_db_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_unlock_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_read_only_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_biometric_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_new_node_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_sort_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_lock_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_copy_username_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_entry_edit_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_password_generator_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_entry_new_field_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_add_attachment_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.education_setup_OTP_key) -> editor.putBoolean(name, value.toBoolean())
}
} }
} }
} }

View File

@@ -32,7 +32,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
@@ -46,7 +46,7 @@ import java.util.*
open class SettingsActivity open class SettingsActivity
: DatabaseLockActivity(), : DatabaseLockActivity(),
MainPreferenceFragment.Callback, MainPreferenceFragment.Callback,
AssignMasterKeyDialogFragment.AssignPasswordDialogListener { SetMainCredentialDialogFragment.AssignMainCredentialDialogListener {
private var backupManager: BackupManager? = null private var backupManager: BackupManager? = null
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null

View File

@@ -61,9 +61,9 @@ class DurationDialogPreference @JvmOverloads constructor(context: Context,
} }
} }
override fun onGetDefaultValue(a: TypedArray?, index: Int): Any { override fun onGetDefaultValue(a: TypedArray, index: Int): Any {
return try { return try {
a?.getString(index)?.toLongOrNull() ?: mDuration a.getString(index)?.toLongOrNull() ?: mDuration
} catch (e: Exception) { } catch (e: Exception) {
mDuration mDuration
} }

View File

@@ -35,7 +35,7 @@ open class InputKdfNumberPreference @JvmOverloads constructor(context: Context,
return R.layout.pref_dialog_input_numbers return R.layout.pref_dialog_input_numbers
} }
override fun setSummary(summary: CharSequence) { override fun setSummary(summary: CharSequence?) {
if (summary == UNKNOWN_VALUE_STRING) { if (summary == UNKNOWN_VALUE_STRING) {
isEnabled = false isEnabled = false
super.setSummary("") super.setSummary("")

View File

@@ -30,7 +30,7 @@ class InputKdfSizePreference @JvmOverloads constructor(context: Context,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: InputKdfNumberPreference(context, attrs, defStyleAttr, defStyleRes) { : InputKdfNumberPreference(context, attrs, defStyleAttr, defStyleRes) {
override fun setSummary(summary: CharSequence) { override fun setSummary(summary: CharSequence?) {
if (summary == UNKNOWN_VALUE_STRING) { if (summary == UNKNOWN_VALUE_STRING) {
super.setSummary(summary) super.setSummary(summary)
} else { } else {

View File

@@ -34,7 +34,7 @@ open class InputNumberPreference @JvmOverloads constructor(context: Context,
return R.layout.pref_dialog_input_numbers return R.layout.pref_dialog_input_numbers
} }
override fun setSummary(summary: CharSequence) { override fun setSummary(summary: CharSequence?) {
if (summary == INFINITE_VALUE_STRING) { if (summary == INFINITE_VALUE_STRING) {
super.setSummary("") super.setSummary("")
} else { } else {

View File

@@ -30,7 +30,7 @@ open class InputSizePreference @JvmOverloads constructor(context: Context,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: InputNumberPreference(context, attrs, defStyleAttr, defStyleRes) { : InputNumberPreference(context, attrs, defStyleAttr, defStyleRes) {
override fun setSummary(summary: CharSequence) { override fun setSummary(summary: CharSequence?) {
var summaryString = summary var summaryString = summary
try { try {
val memorySize = summary.toString().toLong() val memorySize = summary.toString().toLong()

View File

@@ -64,7 +64,7 @@ class DatabaseTemplatesGroupPreferenceDialogFragmentCompat
super.onDialogClosed(database, positiveResult) super.onDialogClosed(database, positiveResult)
if (positiveResult) { if (positiveResult) {
database?.let { database?.let {
if (database.allowConfigurableTemplatesGroup) { if (database.allowTemplatesGroup) {
val oldGroup = database.templatesGroup val oldGroup = database.templatesGroup
val newGroup = mGroupTemplates val newGroup = mGroupTemplates
database.setTemplatesGroup(newGroup) database.setTemplatesGroup(newGroup)

View File

@@ -32,18 +32,11 @@ import com.kunzisoft.keepass.settings.SettingsActivity
object MenuUtil { object MenuUtil {
fun contributionMenuInflater(inflater: MenuInflater, menu: Menu) {
if (!(BuildConfig.FULL_VERSION && BuildConfig.CLOSED_STORE))
inflater.inflate(R.menu.contribution, menu)
}
fun defaultMenuInflater(inflater: MenuInflater, menu: Menu) { fun defaultMenuInflater(inflater: MenuInflater, menu: Menu) {
contributionMenuInflater(inflater, menu) inflater.inflate(R.menu.settings, menu)
inflater.inflate(R.menu.default_menu, menu) inflater.inflate(R.menu.about, menu)
} if (!(BuildConfig.FULL_VERSION && BuildConfig.CLOSED_STORE))
menu.findItem(R.id.menu_contribute)?.isVisible = false
fun onContributionItemSelected(context: Context) {
UriUtil.gotoUrl(context, R.string.contribution_url)
} }
/* /*
@@ -54,7 +47,7 @@ object MenuUtil {
timeoutEnable: Boolean = false) { timeoutEnable: Boolean = false) {
when (item.itemId) { when (item.itemId) {
R.id.menu_contribute -> { R.id.menu_contribute -> {
onContributionItemSelected(activity) UriUtil.gotoUrl(activity, R.string.contribution_url)
} }
R.id.menu_app_settings -> { R.id.menu_app_settings -> {
// To avoid flickering when launch settings in a LockingActivity // To avoid flickering when launch settings in a LockingActivity

View File

@@ -0,0 +1,63 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.text.InputType
import android.util.AttributeSet
import android.widget.ArrayAdapter
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
import com.kunzisoft.keepass.R
class InheritedCompletionView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : AppCompatAutoCompleteTextView(context, attrs) {
val adapter = ArrayAdapter(
context,
android.R.layout.simple_list_item_1,
InheritedStatus.listOfStrings(context))
init {
setAdapter(adapter)
inputType = InputType.TYPE_NULL
adapter.filter.filter(null)
}
fun getValue(): Boolean? {
return InheritedStatus.getStatusFromString(context, text.toString()).value
}
fun setValue(inherited: Boolean?) {
setText(context.getString(InheritedStatus.getStatusFromValue(inherited).stringId))
adapter.filter.filter(null)
}
private enum class InheritedStatus(val stringId: Int, val value: Boolean?) {
INHERITED(R.string.inherited, null),
ENABLE(R.string.enable, true),
DISABLE(R.string.disable, false);
companion object {
fun listOfStrings(context: Context): List<String> {
return listOf(
context.getString(INHERITED.stringId),
context.getString(ENABLE.stringId),
context.getString(DISABLE.stringId)
)
}
fun getStatusFromValue(value: Boolean?): InheritedStatus {
values().find { it.value == value }?.let {
return it
}
return INHERITED
}
fun getStatusFromString(context: Context, text: String): InheritedStatus {
values().find { context.getString(it.stringId) == text }?.let {
return it
}
return INHERITED
}
}
}
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.view
import android.content.Context
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.CredentialStorage
import com.kunzisoft.keepass.model.MainCredential
class MainCredentialView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
private var passwordView: EditText
private var keyFileSelectionView: KeyFileSelectionView
private var checkboxPasswordView: CompoundButton
private var checkboxKeyFileView: CompoundButton
var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onValidateListener: (() -> Unit)? = null
private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_main_credentials, this)
passwordView = findViewById(R.id.password)
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
val onEditorActionListener = object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (actionId == EditorInfo.IME_ACTION_DONE) {
onValidateListener?.invoke()
return true
}
return false
}
}
passwordView.setOnEditorActionListener(onEditorActionListener)
passwordView.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
if (editable.toString().isNotEmpty() && !checkboxPasswordView.isChecked)
checkboxPasswordView.isChecked = true
}
})
passwordView.setOnKeyListener { _, _, keyEvent ->
var handled = false
if (keyEvent.action == KeyEvent.ACTION_DOWN
&& keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
) {
onValidateListener?.invoke()
handled = true
}
handled
}
checkboxPasswordView.setOnCheckedChangeListener { view, checked ->
onPasswordChecked?.onCheckedChanged(view, checked)
}
}
fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) {
keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper)
}
fun populatePasswordTextView(text: String?) {
if (text == null || text.isEmpty()) {
passwordView.setText("")
if (checkboxPasswordView.isChecked)
checkboxPasswordView.isChecked = false
} else {
passwordView.setText(text)
if (checkboxPasswordView.isChecked)
checkboxPasswordView.isChecked = true
}
}
fun populateKeyFileTextView(uri: Uri?) {
if (uri == null || uri.toString().isEmpty()) {
keyFileSelectionView.uri = null
if (checkboxKeyFileView.isChecked)
checkboxKeyFileView.isChecked = false
} else {
keyFileSelectionView.uri = uri
if (!checkboxKeyFileView.isChecked)
checkboxKeyFileView.isChecked = true
}
}
fun isFill(): Boolean {
return checkboxPasswordView.isChecked || checkboxKeyFileView.isChecked
}
fun getMainCredential(): MainCredential {
return MainCredential().apply {
this.masterPassword = if (checkboxPasswordView.isChecked)
passwordView.text?.toString() else null
this.keyFileUri = if (checkboxKeyFileView.isChecked)
keyFileSelectionView.uri else null
}
}
fun changeConditionToStoreCredential(credentialStorage: CredentialStorage) {
this.mCredentialStorage = credentialStorage
}
fun conditionToStoreCredential(): Boolean {
// TODO HARDWARE_KEY
return when (mCredentialStorage) {
CredentialStorage.PASSWORD -> checkboxPasswordView.isChecked
CredentialStorage.KEY_FILE -> checkboxPasswordView.isChecked
CredentialStorage.HARDWARE_KEY -> false
}
}
/**
* Return content of the store credential view allowed,
* String? for password
*
*/
fun retrieveCredentialForStorage(listener: CredentialStorageListener): ByteArray? {
return when (mCredentialStorage) {
CredentialStorage.PASSWORD -> listener.passwordToStore(passwordView.text?.toString())
CredentialStorage.KEY_FILE -> listener.keyfileToStore(keyFileSelectionView.uri)
CredentialStorage.HARDWARE_KEY -> listener.hardwareKeyToStore()
}
}
interface CredentialStorageListener {
fun passwordToStore(password: String?): ByteArray?
fun keyfileToStore(keyfile: Uri?): ByteArray?
fun hardwareKeyToStore(): ByteArray?
}
fun requestPasswordFocus() {
passwordView.requestFocusFromTouch()
}
// Auto select the password field and open keyboard
fun focusPasswordFieldAndOpenKeyboard() {
passwordView.postDelayed({
passwordView.requestFocusFromTouch()
val inputMethodManager = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as? InputMethodManager?
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
}, 100)
}
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
val saveState = SavedState(superState)
saveState.mCredentialStorage = this.mCredentialStorage
return saveState
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state !is SavedState) {
super.onRestoreInstanceState(state)
return
}
super.onRestoreInstanceState(state.superState)
this.mCredentialStorage = state.mCredentialStorage ?: CredentialStorage.DEFAULT
}
internal class SavedState : BaseSavedState {
var mCredentialStorage: CredentialStorage? = null
constructor(superState: Parcelable?) : super(superState) {}
private constructor(parcel: Parcel) : super(parcel) {
mCredentialStorage = CredentialStorage.getFromOrdinal(parcel.readInt())
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeInt(mCredentialStorage?.ordinal ?: 0)
}
companion object CREATOR : Parcelable.Creator<SavedState> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -0,0 +1,73 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.view.isVisible
import com.google.android.material.navigation.NavigationView
import com.kunzisoft.keepass.R
class NavigationDatabaseView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: NavigationView(context, attrs, defStyle) {
private var databaseNavContainerView: View? = null
private var databaseNavIconView: ImageView? = null
private var databaseNavModifiedView: ImageView? = null
private var databaseNavColorView: ImageView? = null
private var databaseNavNameView: TextView? = null
private var databaseNavPathView: TextView? = null
private var databaseNavVersionView: TextView? = null
init {
inflateHeaderView(R.layout.nav_header_database)
databaseNavIconView = databaseNavContainerView?.findViewById(R.id.nav_database_icon)
databaseNavModifiedView = databaseNavContainerView?.findViewById(R.id.nav_database_modified)
databaseNavColorView = databaseNavContainerView?.findViewById(R.id.nav_database_color)
databaseNavNameView = databaseNavContainerView?.findViewById(R.id.nav_database_name)
databaseNavPathView = databaseNavContainerView?.findViewById(R.id.nav_database_path)
databaseNavVersionView = databaseNavContainerView?.findViewById(R.id.nav_database_version)
}
override fun inflateHeaderView(res: Int): View {
val headerView = super.inflateHeaderView(res)
databaseNavContainerView = headerView
return headerView
}
fun setDatabaseName(name: String) {
databaseNavNameView?.text = name
}
fun setDatabasePath(path: String?) {
if (path != null) {
databaseNavPathView?.text = path
databaseNavPathView?.visibility = View.VISIBLE
} else {
databaseNavPathView?.visibility = View.GONE
}
}
fun setDatabaseVersion(version: String) {
databaseNavVersionView?.text = version
}
fun setDatabaseModifiedSinceLastLoading(modified: Boolean) {
databaseNavModifiedView?.isVisible = modified
}
fun setDatabaseColor(color: Int?) {
if (color != null) {
databaseNavColorView?.drawable?.colorFilter = BlendModeColorFilterCompat
.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN)
databaseNavColorView?.visibility = View.VISIBLE
} else {
databaseNavColorView?.visibility = View.GONE
}
}
}

View File

@@ -0,0 +1,252 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
class SearchFiltersView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: LinearLayout(context, attrs, defStyle) {
private var searchContainer: ViewGroup
private var searchAdvanceFiltersContainer: ViewGroup? = null
private var searchExpandButton: ImageView
private var searchNumbers: TextView
private var searchCurrentGroup: CompoundButton
private var searchCaseSensitive: CompoundButton
private var searchRegex: CompoundButton
private var searchTitle: CompoundButton
private var searchUsername: CompoundButton
private var searchPassword: CompoundButton
private var searchURL: CompoundButton
private var searchExpires: CompoundButton
private var searchNotes: CompoundButton
private var searchOther: CompoundButton
private var searchUUID: CompoundButton
private var searchTag: CompoundButton
private var searchGroupSearchable: CompoundButton
private var searchRecycleBin: CompoundButton
private var searchTemplate: CompoundButton
var searchParameters = SearchParameters()
get() {
return field.apply {
this.searchInCurrentGroup = searchCurrentGroup.isChecked
this.caseSensitive = searchCaseSensitive.isChecked
this.isRegex = searchRegex.isChecked
this.searchInTitles = searchTitle.isChecked
this.searchInUsernames = searchUsername.isChecked
this.searchInPasswords = searchPassword.isChecked
this.searchInUrls = searchURL.isChecked
this.excludeExpired = !(searchExpires.isChecked)
this.searchInNotes = searchNotes.isChecked
this.searchInOther = searchOther.isChecked
this.searchInUUIDs = searchUUID.isChecked
this.searchInTags = searchTag.isChecked
this.searchInRecycleBin = searchRecycleBin.isChecked
this.searchInTemplates = searchTemplate.isChecked
}
}
set(value) {
field = value
val tempListener = mOnParametersChangeListener
mOnParametersChangeListener = null
searchCurrentGroup.isChecked = value.searchInCurrentGroup
searchCaseSensitive.isChecked = value.caseSensitive
searchRegex.isChecked = value.isRegex
searchTitle.isChecked = value.searchInTitles
searchUsername.isChecked = value.searchInUsernames
searchPassword.isChecked = value.searchInPasswords
searchURL.isChecked = value.searchInUrls
searchExpires.isChecked = !value.excludeExpired
searchNotes.isChecked = value.searchInNotes
searchOther.isChecked = value.searchInOther
searchUUID.isChecked = value.searchInUUIDs
searchTag.isChecked = value.searchInTags
searchGroupSearchable.isChecked = value.searchInRecycleBin
searchRecycleBin.isChecked = value.searchInRecycleBin
searchTemplate.isChecked = value.searchInTemplates
mOnParametersChangeListener = tempListener
}
var onParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = null
private var mOnParametersChangeListener: ((searchParameters: SearchParameters) -> Unit)? = {
// To recalculate height
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
searchAdvanceFiltersContainer?.expand(
false,
searchAdvanceFiltersContainer?.getFullHeight()
)
}
onParametersChangeListener?.invoke(searchParameters)
}
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_search_filters, this)
searchContainer = findViewById(R.id.search_container)
searchAdvanceFiltersContainer = findViewById(R.id.search_advance_filters)
searchExpandButton = findViewById(R.id.search_expand)
searchNumbers = findViewById(R.id.search_numbers)
searchCurrentGroup = findViewById(R.id.search_chip_current_group)
searchCaseSensitive = findViewById(R.id.search_chip_case_sensitive)
searchRegex = findViewById(R.id.search_chip_regex)
searchTitle = findViewById(R.id.search_chip_title)
searchUsername = findViewById(R.id.search_chip_username)
searchPassword = findViewById(R.id.search_chip_password)
searchURL = findViewById(R.id.search_chip_url)
searchExpires = findViewById(R.id.search_chip_expires)
searchNotes = findViewById(R.id.search_chip_note)
searchUUID = findViewById(R.id.search_chip_uuid)
searchOther = findViewById(R.id.search_chip_other)
searchTag = findViewById(R.id.search_chip_tag)
searchGroupSearchable = findViewById(R.id.search_chip_group_searchable)
searchRecycleBin = findViewById(R.id.search_chip_recycle_bin)
searchTemplate = findViewById(R.id.search_chip_template)
// Expand menu with button
searchExpandButton.setOnClickListener {
val isVisible = searchAdvanceFiltersContainer?.visibility == View.VISIBLE
if (isVisible)
closeAdvancedFilters()
else
openAdvancedFilters()
}
searchCurrentGroup.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInCurrentGroup = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchCaseSensitive.setOnCheckedChangeListener { _, isChecked ->
searchParameters.caseSensitive = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchRegex.setOnCheckedChangeListener { _, isChecked ->
searchParameters.isRegex = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchTitle.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInTitles = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchUsername.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInUsernames = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchPassword.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInPasswords = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchURL.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInUrls = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchExpires.setOnCheckedChangeListener { _, isChecked ->
searchParameters.excludeExpired = !isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchNotes.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInNotes = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchUUID.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInUUIDs = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchOther.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInOther = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchTag.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInTags = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchGroupSearchable.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInSearchableGroup = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchRecycleBin.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInRecycleBin = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
searchTemplate.setOnCheckedChangeListener { _, isChecked ->
searchParameters.searchInTemplates = isChecked
mOnParametersChangeListener?.invoke(searchParameters)
}
}
fun setNumbers(numbers: Int) {
searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers)
}
fun setCurrentGroupText(text: String) {
val maxChars = 12
searchCurrentGroup.text = when {
text.isEmpty() -> context.getString(R.string.current_group)
text.length > maxChars -> text.substring(0, maxChars) + ""
else -> text
}
}
fun availableOther(available: Boolean) {
searchOther.isVisible = available
}
fun availableTags(available: Boolean) {
searchTag.isVisible = available
}
fun enableTags(enable: Boolean) {
searchTag.isEnabled = enable
}
fun availableSearchableGroup(available: Boolean) {
searchGroupSearchable.isVisible = available
}
fun availableTemplates(available: Boolean) {
searchTemplate.isVisible = available
}
fun enableTemplates(enable: Boolean) {
searchTemplate.isEnabled = enable
}
fun closeAdvancedFilters() {
searchAdvanceFiltersContainer?.collapse()
}
private fun openAdvancedFilters() {
searchAdvanceFiltersContainer?.expand(true,
searchAdvanceFiltersContainer?.getFullHeight()
)
}
override fun setVisibility(visibility: Int) {
when (visibility) {
View.VISIBLE -> {
searchAdvanceFiltersContainer?.visibility = View.GONE
searchContainer.showByFading()
}
else -> {
searchContainer.hideByFading()
if (searchAdvanceFiltersContainer?.visibility == View.VISIBLE) {
searchAdvanceFiltersContainer?.visibility = View.INVISIBLE
searchAdvanceFiltersContainer?.collapse()
}
}
}
}
}

View File

@@ -51,6 +51,8 @@ import androidx.appcompat.widget.ActionMenuView
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.ViewGroup
import android.widget.LinearLayout
import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.appbar.CollapsingToolbarLayout
@@ -113,8 +115,7 @@ fun View.collapse(animate: Boolean = true,
onCollapseFinished: (() -> Unit)? = null) { onCollapseFinished: (() -> Unit)? = null) {
val recordViewHeight = layoutParams.height val recordViewHeight = layoutParams.height
val slideAnimator = ValueAnimator.ofInt(height, 0) val slideAnimator = ValueAnimator.ofInt(height, 0)
if (animate) slideAnimator.duration = if (animate) 300L else 0L
slideAnimator.duration = 300L
slideAnimator.addUpdateListener { animation -> slideAnimator.addUpdateListener { animation ->
layoutParams.height = animation.animatedValue as Int layoutParams.height = animation.animatedValue as Int
requestLayout() requestLayout()
@@ -143,8 +144,7 @@ fun View.expand(animate: Boolean = true,
layoutParams.height = 0 layoutParams.height = 0
val slideAnimator = ValueAnimator val slideAnimator = ValueAnimator
.ofInt(0, viewHeight) .ofInt(0, viewHeight)
if (animate) slideAnimator.duration = if (animate) 300L else 0L
slideAnimator.duration = 300L
var alreadyVisible = false var alreadyVisible = false
slideAnimator.addUpdateListener { animation -> slideAnimator.addUpdateListener { animation ->
layoutParams.height = animation.animatedValue as Int layoutParams.height = animation.animatedValue as Int
@@ -168,12 +168,38 @@ fun View.expand(animate: Boolean = true,
}.start() }.start()
} }
/***
* This function returns the actual height the layout.
* The getHeight() function returns the current height which might be zero if
* the layout's visibility is GONE
*/
fun ViewGroup.getFullHeight(): Int {
measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val initialVisibility = visibility
visibility = LinearLayout.VISIBLE
val desiredWidth = View.MeasureSpec.makeMeasureSpec(
width,
View.MeasureSpec.AT_MOST
)
measure(desiredWidth, View.MeasureSpec.UNSPECIFIED)
val totalHeight = measuredHeight
visibility = initialVisibility
return totalHeight
}
fun View.hideByFading() { fun View.hideByFading() {
alpha = 1f alpha = 1f
animate() animate()
.alpha(0f) .alpha(0f)
.setDuration(140) .setDuration(140)
.setListener(null) .setListener(object: Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator?) {}
override fun onAnimationEnd(p0: Animator?) {
isVisible = false
}
override fun onAnimationCancel(p0: Animator?) {}
override fun onAnimationRepeat(p0: Animator?) {}
})
} }
fun View.showByFading() { fun View.showByFading() {

View File

@@ -22,26 +22,30 @@ package com.kunzisoft.keepass.viewmodels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.app.database.IOActionTask import com.kunzisoft.keepass.app.database.IOActionTask
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
class GroupViewModel: ViewModel() { class GroupViewModel: ViewModel() {
val mainGroup : LiveData<SuperGroup> get() = _mainGroup
private val _mainGroup = MutableLiveData<SuperGroup>()
val group : LiveData<SuperGroup> get() = _group val group : LiveData<SuperGroup> get() = _group
private val _group = MutableLiveData<SuperGroup>() private val _group = MutableLiveData<SuperGroup>()
val firstPositionVisible : LiveData<Int> get() = _firstPositionVisible val firstPositionVisible : LiveData<Int> get() = _firstPositionVisible
private val _firstPositionVisible = MutableLiveData<Int>() private val _firstPositionVisible = MutableLiveData<Int>()
fun loadGroup(database: Database?, fun loadMainGroup(database: Database?,
groupState: GroupActivity.GroupState?) { groupId: NodeId<*>?,
showFromPosition: Int?) {
IOActionTask( IOActionTask(
{ {
val groupId = groupState?.groupId
if (groupId != null) { if (groupId != null) {
database?.getGroupById(groupId) database?.getGroupById(groupId)
} else { } else {
@@ -50,44 +54,46 @@ class GroupViewModel: ViewModel() {
}, },
{ group -> { group ->
if (group != null) { if (group != null) {
_group.value = SuperGroup(group, _mainGroup.value = SuperGroup(group,
database?.recycleBin == group, database?.recycleBin == group,
groupState?.firstVisibleItem) showFromPosition)
_group.value = _mainGroup.value
} }
} }
).execute() ).execute()
} }
fun loadGroup(database: Database?, fun loadSearchGroup(database: Database?,
group: Group, searchParameters: SearchParameters,
showFromPosition: Int?) { fromGroup: NodeId<*>?,
_group.value = SuperGroup(group, showFromPosition: Int?) {
database?.recycleBin == group, IOActionTask(
showFromPosition) {
database?.createVirtualGroupFromSearch(
searchParameters,
fromGroup,
SearchHelper.MAX_SEARCH_ENTRY
)
},
{ group ->
if (group != null) {
_group.value = SuperGroup(group,
database?.recycleBin == group,
showFromPosition,
searchParameters)
}
}
).execute()
} }
fun assignPosition(position: Int) { fun assignPosition(position: Int) {
_firstPositionVisible.value = position _firstPositionVisible.value = position
} }
fun loadGroupFromSearch(database: Database?, data class SuperGroup(val group: Group,
searchQuery: String, val isRecycleBin: Boolean,
omitBackup: Boolean) { var showFromPosition: Int?,
IOActionTask( var searchParameters: SearchParameters = SearchParameters())
{
database?.createVirtualGroupFromSearch(searchQuery, omitBackup)
},
{ group ->
if (group != null) {
_group.value = SuperGroup(group,
database?.recycleBin == group,
0)
}
}
).execute()
}
data class SuperGroup(val group: Group, val isRecycleBin: Boolean, var showFromPosition: Int?)
companion object { companion object {
private val TAG = GroupViewModel::class.java.name private val TAG = GroupViewModel::class.java.name

View File

@@ -2,5 +2,5 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/white_grey" android:state_activated="true" /> <item android:color="@color/white_grey" android:state_activated="true" />
<item android:color="@color/white_grey_darker" android:state_enabled="false" /> <item android:color="@color/white_grey_darker" android:state_enabled="false" />
<item android:color="?android:attr/textColorHintInverse" android:state_enabled="true" /> <item android:color="?android:attr/textColorSecondaryInverse" android:state_enabled="true" />
</selector> </selector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.97" android:color="?attr/chipFilterBackgroundColorDisabled" android:state_enabled="false" />
<item android:alpha="1.00" android:color="?attr/chipFilterBackgroundColor" android:state_checked="true" />
<item android:alpha="0.98" android:color="?attr/chipFilterBackgroundColor" />
</selector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" > <selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_selected="true" android:color="@color/white"/> <item android:state_selected="true" android:color="?attr/colorOnAccentColor"/>
<item android:color="?android:attr/textColorPrimary"/> <item android:color="?android:attr/textColorPrimary"/>
</selector> </selector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/white_grey" android:state_enabled="false" /> <item android:color="@color/white_grey" android:state_enabled="false" />
<item android:color="@color/white" android:state_enabled="true" /> <item android:color="?attr/colorOnAccentColor" android:state_enabled="true" />
</selector> </selector>

View File

@@ -9,7 +9,7 @@
android:right="8dp" android:right="8dp"
android:top="12dp" android:top="12dp"
android:bottom="12dp"/> android:bottom="12dp"/>
<solid android:color="@color/orange_light"/> <solid android:color="@color/orange_lighter"/>
</shape> </shape>
</item> </item>
<item> <item>

View File

@@ -11,7 +11,7 @@
android:right="14dp" android:right="14dp"
android:top="4dp" android:top="4dp"
android:bottom="8dp"/> android:bottom="8dp"/>
<solid android:color="@color/orange_light"/> <solid android:color="@color/orange_lighter"/>
</shape> </shape>
</item> </item>
</layer-list> </layer-list>

View File

@@ -3,7 +3,7 @@
<item android:state_pressed="true"> <item android:state_pressed="true">
<shape <shape
android:shape="oval"> android:shape="oval">
<stroke android:color="@color/orange_light" android:width="1dp"/> <stroke android:color="@color/orange_lighter" android:width="1dp"/>
<padding <padding
android:left="12dp" android:left="12dp"
android:right="12dp" android:right="12dp"

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:strokeWidth="0.59883046"
android:pathData="M 8.6660156 3.5371094 L 2 21 L 4.4921875 21 L 6.0820312 16.519531 L 10.287109 16.519531 C 10.36083 15.806007 10.5606 15.14402 10.871094 14.554688 L 6.7949219 14.554688 L 10 5.8652344 L 11.210938 9.1367188 L 11.210938 7.8105469 L 11.849609 7.5644531 C 12.1567 7.4458738 12.458907 7.3490018 12.761719 7.2519531 L 11.345703 3.5371094 L 8.6660156 3.5371094 z M 16.666016 7.5839844 C 15.964262 7.5839844 15.242694 7.6624139 14.501953 7.8183594 C 13.761212 7.9665075 12.998462 8.1939533 12.210938 8.4980469 L 12.210938 10.486328 C 12.865908 10.127654 13.54826 9.8570716 14.257812 9.6777344 C 14.967364 9.4983971 15.696774 9.4101562 16.445312 9.4101562 C 17.622701 9.4101562 18.534467 9.6827067 19.181641 10.228516 C 19.836611 10.766528 20.164062 11.529278 20.164062 12.519531 L 20.164062 12.730469 L 17.146484 12.730469 C 15.189369 12.730469 13.714863 13.104978 12.724609 13.853516 C 11.742153 14.602054 11.251953 15.713816 11.251953 17.1875 C 11.251953 18.450658 11.64607 19.460462 12.433594 20.216797 C 13.228916 20.965335 14.295588 21.339844 15.636719 21.339844 C 16.697147 21.339844 17.595182 21.151612 18.328125 20.777344 C 19.061068 20.395277 19.672835 19.80704 20.164062 19.011719 L 20.164062 21 L 22.316406 21 L 22.316406 13.525391 C 22.316406 11.529289 21.847782 10.04105 20.912109 9.0585938 C 19.976437 8.0761376 18.560753 7.5839844 16.666016 7.5839844 z M 14.103516 10.777344 C 13.620766 10.928312 13.14846 11.11299 12.691406 11.363281 L 12.146484 11.662109 L 12.554688 12.767578 C 13.13989 12.410612 13.81914 12.16373 14.568359 11.996094 L 14.103516 10.777344 z M 18.023438 14.416016 L 20.164062 14.416016 L 20.164062 14.894531 C 20.164062 16.29804 19.801318 17.425488 19.076172 18.275391 C 18.358823 19.117496 17.40392 19.537109 16.210938 19.537109 C 15.34544 19.537109 14.659166 19.315538 14.152344 18.871094 C 13.653318 18.418852 13.404297 17.811007 13.404297 17.046875 C 13.404297 16.08781 13.739592 15.409379 14.410156 15.011719 C 15.080722 14.614058 16.284645 14.416016 18.023438 14.416016 z M 15.933594 15.576172 C 15.436083 15.663112 15.086511 15.772302 14.919922 15.871094 C 14.55273 16.088847 14.404297 16.320872 14.404297 17.046875 C 14.404297 17.589232 14.533543 17.860825 14.820312 18.123047 C 15.114517 18.378191 15.521588 18.537109 16.210938 18.537109 C 16.518099 18.537109 16.779218 18.49703 17.023438 18.4375 L 15.933594 15.576172 z" />
</vector>

View File

@@ -1,5 +1,9 @@
<vector android:height="24dp" android:tint="#FFFFFF" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
<path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/> android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector> </vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:strokeWidth="0.0163282"
android:pathData="M 2.5,1 C 2.223,1 2,1.223 2,1.5 v 12 9 C 2,22.777 2.223,23 2.5,23 2.777,23 3,22.777 3,22.5 V 14 h 3 v 2.474609 C 6,17.313879 6.6858008,18 7.5234375,18 H 20.476562 C 21.314199,18 22,17.314199 22,16.476562 V 9.8085938 C 22,8.9709571 21.314199,8.2851562 20.476562,8.2851562 H 14 L 12.476562,6.5703125 H 7.5234375 C 6.6858008,6.5703125 6,7.2561133 6,8.09375 V 13 H 3 V 1.5 C 3,1.223 2.777,1 2.5,1 Z" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#ffffff"
android:strokeWidth="0.04285714"
android:pathData="M 23.835938,5.7167969 C 13.70451,5.7167969 5,9.3383264 5,13.855469 c 0,4.512856 8.70451,8.167969 18.835938,8.167969 10.135713,0 18.835937,-3.655113 18.835937,-8.167969 0,-4.5171426 -8.700224,-8.1386721 -18.835937,-8.1386721 z M 42.671875,18.09375 c 0,4.517142 -8.700224,8.171875 -18.835937,8.171875 C 13.70451,26.265625 5,22.610513 5,18.097656 v 6.248047 c 0,4.512857 8.70451,8.167969 18.835938,8.167969 10.135713,0 18.835937,-3.655112 18.835937,-8.167969 z M 5,28.582031 v 6.25 C 5,39.344888 13.70451,43 23.835938,43 33.971651,43 42.671875,39.344888 42.671875,34.832031 v -6.25 c 0,4.517143 -8.700224,8.169922 -18.835937,8.169922 C 13.70451,36.751953 5,33.094888 5,28.582031 Z" />
</vector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M 6 3 C 5.446 3 5 3.446 5 4 L 5 21 C 5 21.554 5.4966699 22 6 22 L 19 22 C 19.554 22 20 21.554 20 21 L 20 8 L 15 3 L 14 3 L 7 3 L 6 3 z M 14 4 L 15 5 L 18 8 L 19 9 L 15.541016 9 L 16.955078 10.414062 L 14.183594 13.183594 L 12.769531 11.769531 L 15.541016 9 L 14 9 L 14 4 z M 9 9 L 12.955078 12.955078 L 12.910156 13 L 13 13 L 13 16 L 17 16 L 12 21 L 7 16 L 11 16 L 11 13.828125 L 7.5859375 10.414062 L 9 9 z" />
</vector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:strokeWidth="2.28571415"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M 12 8 C 14.2091389993 8 16 9.79086100068 16 12 C 16 14.2091389993 14.2091389993 16 12 16 C 9.79086100068 16 8 14.2091389993 8 12 C 8 9.79086100068 9.79086100068 8 12 8 Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<path
android:fillColor="#ffffff"
android:pathData="M 4.3105469,5.0878906 a 0.9014779,0.9014779 0 0 0 -0.00586,0.00195 0.9014779,0.9014779 0 0 0 -0.6679687,0.3671874 c -1.6307378,2.1650454 -2.540913,4.467681 -2.5664063,6.853516 -0.025422,2.385835 0.8330899,4.807465 2.5527344,7.212891 a 0.90239526,0.90239526 0 0 0 1.46875,-1.048829 C 3.5374502,16.300396 2.8522647,14.272141 2.8730469,12.328125 2.8938469,10.384107 3.6199739,8.4788746 5.078125,6.5429688 A 0.9014779,0.9014779 0 0 0 4.3105469,5.0878906 Z m 15.2246091,0 a 0.9014779,0.9014779 0 0 0 -0.08984,0.00195 0.9014779,0.9014779 0 0 0 -0.658203,1.453125 c 1.458152,1.9359058 2.184296,3.8411384 2.205079,5.7851564 0.0208,1.944016 -0.664404,3.972271 -2.21875,6.146484 a 0.90239526,0.90239526 0 1 0 1.46875,1.048829 c 1.719644,-2.405426 2.576274,-4.827056 2.550781,-7.212891 C 22.767547,9.9247122 21.8573,7.6220766 20.226562,5.4570312 A 0.9014779,0.9014779 0 0 0 19.535156,5.0878906 Z m -5.050781,1.0292969 a 0.40003999,0.40003999 0 0 0 -0.353516,0.2695313 l -0.833984,2.4628906 -2.597656,0.03125 a 0.40003999,0.40003999 0 0 0 -0.236328,0.71875 l 2.08789,1.5527346 -0.77539,2.478515 a 0.40003999,0.40003999 0 0 0 0.611328,0.447266 l 2.121093,-1.503906 2.119141,1.503906 a 0.40003999,0.40003999 0 0 0 0.615235,-0.447266 l -0.775391,-2.478515 2.083984,-1.5527346 a 0.40003999,0.40003999 0 0 0 -0.232422,-0.71875 L 15.716797,8.8496094 14.886719,6.3867188 A 0.40003999,0.40003999 0 0 0 14.484375,6.1171875 Z M 10,16 a 2,2 0 0 0 -2,2 2,2 0 0 0 2,2 2,2 0 0 0 2,-2 2,2 0 0 0 -2,-2 z" />
</group>
</vector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:strokeWidth="2"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M 3 0 C 2.446 0 2 0.446 2 1 L 2 18 C 2 18.554 2.446 19 3 19 C 3.554 19 4 18.554 4 18 L 4 2 L 14 2 C 14.554 2 15 1.554 15 1 C 15 0.446 14.554 0 14 0 L 3 0 z M 7 4 C 6.446 4 6 4.446 6 5 L 6 22 C 6 22.554 6.49667 23 7 23 L 20 23 C 20.554 23 21 22.554 21 22 L 21 9 L 16 4 L 15 4 L 8 4 L 7 4 z M 15 5 L 16 6 L 19 9 L 20 10 L 15 10 L 15 5 z M 13 11 L 18 16 L 14 16 L 14 21 L 12 21 L 12 16 L 8 16 L 13 11 z" />
</vector>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="32"
android:viewportHeight="32"
android:width="24dp"
android:height="24dp">
<group
android:scaleX="1.777778"
android:scaleY="1.777778"
android:translateX="-205.4844"
android:translateY="-31.99788">
<path
android:pathData="M131.1496 25.505423l-0.59661 -0.59661c-0.24714 -0.24714 -0.64781 -0.24714 -0.89493 0l-0.14916 0.14916 -2.98312 -2.9831 0.14916 -0.14915c0.24714 -0.24714 0.24714 -0.64782 0 -0.89493l-0.59661 -0.59664c-0.24714 -0.24713 -0.64781 -0.24713 -0.89492 0l-3.28142 3.28142c-0.24714 0.24714 -0.24714 0.64781 0 0.89492l0.59661 0.59661c0.24714 0.24714 0.64782 0.24714 0.89493 0l0.14916 -0.14916 1.04408 1.04409 -2.13679 2.13679 -0.14916 -0.14916c-0.32951 -0.32951 -0.86373 -0.32951 -1.19324 0l-3.02547 3.02552c-0.32951 0.32951 -0.32951 0.86373 0 1.19324l1.19324 1.19325c0.32951 0.32951 0.86374 0.32951 1.19325 0l3.02549 -3.0255c0.32951 -0.32951 0.32951 -0.86373 0 -1.19324l-0.14916 -0.14916 2.13679 -2.13679 1.04409 1.04409 -0.14916 0.14915c-0.24714 0.24714 -0.24714 0.64782 0 0.89493l0.59661 0.59661c0.24714 0.24714 0.64781 0.24714 0.89493 0l3.28141 -3.28141c0.24711 -0.24712 0.24711 -0.64779 0 -0.89493z"
android:fillColor="#ffffff" />
</group>
</vector>

View File

@@ -25,6 +25,6 @@
android:right="0dp" android:right="0dp"
android:top="12dp" android:top="12dp"
android:bottom="12dp"/> android:bottom="12dp"/>
<stroke android:width="1dp" android:color="@color/grey_blue" /> <stroke android:width="1dp" android:color="@color/grey_blue_slight" />
<solid android:color="@color/grey_blue"/> <solid android:color="@color/grey_blue_slight"/>
</shape> </shape>

View File

@@ -25,6 +25,6 @@
android:right="0dp" android:right="0dp"
android:top="12dp" android:top="12dp"
android:bottom="12dp"/> android:bottom="12dp"/>
<stroke android:width="1dp" android:color="@color/grey_blue" /> <stroke android:width="1dp" android:color="@color/grey_blue_slight" />
<solid android:color="@color/grey_blue_dark"/> <solid android:color="@color/grey_blue_deep"/>
</shape> </shape>

View File

@@ -104,7 +104,7 @@
android:visibility="gone" android:visibility="gone"
android:background="?attr/colorAccent" android:background="?attr/colorAccent"
android:padding="12dp" android:padding="12dp"
android:textColor="?attr/textColorInverse" android:textColor="?attr/colorOnAccentColor"
android:text="@string/entry_history"/> android:text="@string/entry_history"/>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView

Some files were not shown because too many files have changed in this diff Show More