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)
* Manage data merge #840 #977
* Manage Tags #633

View File

@@ -12,8 +12,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 15
targetSdkVersion 31
versionCode = 97
versionName = "3.2.0"
versionCode = 102
versionName = "3.3.0"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -69,10 +69,14 @@ android {
buildConfigField "boolean", "FULL_VERSION", "false"
buildConfigField "boolean", "CLOSED_STORE", "true"
buildConfigField "String[]", "STYLES_DISABLED",
"{\"KeepassDXStyle_Blue\"," +
"{\"KeepassDXStyle_Simple\"," +
"\"KeepassDXStyle_Simple_Night\"," +
"\"KeepassDXStyle_Blue\"," +
"\"KeepassDXStyle_Blue_Night\"," +
"\"KeepassDXStyle_Red\"," +
"\"KeepassDXStyle_Red_Night\"," +
"\"KeepassDXStyle_Reply\"," +
"\"KeepassDXStyle_Reply_Night\"," +
"\"KeepassDXStyle_Purple\"," +
"\"KeepassDXStyle_Purple_Dark\"}"
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
@@ -104,17 +108,18 @@ def room_version = "2.4.1"
dependencies {
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.preference:preference-ktx:1.1.1'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.documentfile:documentfile:1.0.1'
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
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"
// Token auto complete
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed

View File

@@ -40,7 +40,6 @@
<activity
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
android:theme="@style/KeepassDXStyle.SplashScreen"
android:label="@string/app_name"
android:launchMode="singleTop"
android:exported="true"
android:configChanges="keyboardHidden"
@@ -51,7 +50,7 @@
</intent-filter>
</activity>
<activity
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
android:name="com.kunzisoft.keepass.activities.MainCredentialActivity"
android:exported="true"
android:configChanges="keyboardHidden"
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.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.UuidUtil
import com.kunzisoft.keepass.view.changeControlColor
@@ -92,6 +91,8 @@ class EntryActivity : DatabaseLockActivity() {
private val mEntryViewModel: EntryViewModel by viewModels()
private val mEntryActivityEducation = EntryActivityEducation(this)
private var mMainEntryId: NodeId<UUID>? = null
private var mHistoryPosition: Int = -1
private var mEntryIsHistory: Boolean = false
@@ -370,7 +371,6 @@ class EntryActivity : DatabaseLockActivity() {
super.onCreateOptionsMenu(menu)
if (mEntryLoaded) {
val inflater = menuInflater
MenuUtil.contributionMenuInflater(inflater, menu)
inflater.inflate(R.menu.entry, menu)
inflater.inflate(R.menu.database, menu)
@@ -381,11 +381,7 @@ class EntryActivity : DatabaseLockActivity() {
// Show education views
Handler(Looper.getMainLooper()).post {
performedNextEducation(
EntryActivityEducation(
this
), menu
)
performedNextEducation(menu)
}
}
return true
@@ -411,31 +407,30 @@ class EntryActivity : DatabaseLockActivity() {
return super.onPrepareOptionsMenu(menu)
}
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
menu: Menu) {
private fun performedNextEducation(menu: Menu) {
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
as? EntryFragment?
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
val entryCopyEducationPerformed = entryFieldCopyView != null
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
&& mEntryActivityEducation.checkAndPerformedEntryCopyEducation(
entryFieldCopyView,
{
entryFragment.launchEntryCopyEducationAction()
},
{
performedNextEducation(entryActivityEducation, menu)
performedNextEducation(menu)
})
if (!entryCopyEducationPerformed) {
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
// entryEditEducationPerformed
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
menuEditView != null && mEntryActivityEducation.checkAndPerformedEntryEditEducation(
menuEditView,
{
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 {
when (item.itemId) {
R.id.menu_contribute -> {
MenuUtil.onContributionItemSelected(this)
return true
}
R.id.menu_edit -> {
mDatabase?.let { database ->
mMainEntryId?.let { entryId ->

View File

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

View File

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

View File

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

View File

@@ -53,6 +53,10 @@ class GroupDialogFragment : DatabaseDialogFragment() {
private lateinit var expirationView: DateTimeFieldView
private lateinit var creationView: 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 uuidReferenceView: TextView
@@ -62,6 +66,25 @@ class GroupDialogFragment : DatabaseDialogFragment() {
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
}
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 {
@@ -75,6 +98,10 @@ class GroupDialogFragment : DatabaseDialogFragment() {
expirationView = root.findViewById(R.id.group_expiration)
creationView = root.findViewById(R.id.group_created)
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)
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
@@ -123,6 +150,9 @@ class GroupDialogFragment : DatabaseDialogFragment() {
expirationView.dateTime = mGroupInfo.expiryTime
creationView.text = mGroupInfo.creationTime.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)
if (uuid == null || uuid.isEmpty()) {
uuidContainerView.visibility = View.GONE
@@ -143,6 +173,15 @@ class GroupDialogFragment : DatabaseDialogFragment() {
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) {
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
super.onSaveInstanceState(outState)

View File

@@ -23,9 +23,8 @@ import android.app.Dialog
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.view.ViewGroup
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
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.model.GroupInfo
import com.kunzisoft.keepass.view.DateTimeEditFieldView
import com.kunzisoft.keepass.view.InheritedCompletionView
import com.kunzisoft.keepass.view.TagsCompletionView
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.tokenautocomplete.FilteredArrayAdapter
@@ -58,6 +58,12 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
private lateinit var notesTextLayoutView: TextInputLayout
private lateinit var notesTextView: TextView
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 var tagsAdapter: FilteredArrayAdapter<String>? = null
@@ -118,11 +124,24 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
}
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)
tagsCompletionView.apply {
threshold = 1
setAdapter(tagsAdapter)
}
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -134,6 +153,12 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
notesTextView = root.findViewById(R.id.group_edit_note)
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)
// Retrieve the textColor to tint the icon
@@ -211,6 +236,11 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
expirationView.activation = groupInfo.expires
expirationView.dateTime = groupInfo.expiryTime
// Set searchable
searchableView.setValue(groupInfo.searchable)
// Set auto-type
autoTypeInheritedView.setValue(groupInfo.enableAutoType)
autoTypeSequenceView.text = groupInfo.defaultAutoTypeSequence
// Set Tags
groupInfo.tags.let { tags ->
tagsCompletionView.setText("")
@@ -229,6 +259,9 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
}
mGroupInfo.expires = expirationView.activation
mGroupInfo.expiryTime = expirationView.dateTime
mGroupInfo.searchable = searchableView.getValue()
mGroupInfo.enableAutoType = autoTypeInheritedView.getValue()
mGroupInfo.defaultAutoTypeSequence = autoTypeSequenceView.text.toString()
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.view.KeyFileSelectionView
class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null
@@ -56,7 +56,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
private var keyFileCheckBox: CompoundButton? = null
private var keyFileSelectionView: KeyFileSelectionView? = null
private var mListener: AssignPasswordDialogListener? = null
private var mListener: AssignMainCredentialDialogListener? = null
private var mExternalFileHelper: ExternalFileHelper? = null
@@ -74,7 +74,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
}
}
interface AssignPasswordDialogListener {
interface AssignMainCredentialDialogListener {
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
}
@@ -82,10 +82,10 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
override fun onAttach(activity: Context) {
super.onAttach(activity)
try {
mListener = activity as AssignPasswordDialogListener
mListener = activity as AssignMainCredentialDialogListener
} catch (e: ClassCastException) {
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 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)
// Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ -> }
@@ -254,7 +254,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyKeyFile()) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@AssignMasterKeyDialogFragment.dismiss()
this@SetMainCredentialDialogFragment.dismiss()
}
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
@@ -269,7 +269,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
builder.setMessage(R.string.warning_no_encryption_key)
.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@AssignMasterKeyDialogFragment.dismiss()
this@SetMainCredentialDialogFragment.dismiss()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
mNoKeyConfirmationDialog = builder.create()
@@ -301,8 +301,8 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment {
val fragment = AssignMasterKeyDialogFragment()
fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
val fragment = SetMainCredentialDialogFragment()
val args = Bundle()
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
fragment.arguments = args

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
package com.kunzisoft.keepass.activities.legacy
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.Toast
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.view.SpecialModeView
/**
* Activity to manage database special mode (ie: selection mode)
*/
@@ -63,8 +66,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
if (mSpecialMode != SpecialMode.DEFAULT) {
// To move the app in background
moveTaskToBack(true)
backToTheMainAppAndFinish()
}
}
}
@@ -77,8 +79,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
EntrySelectionHelper.removeModesFromIntent(intent)
EntrySelectionHelper.removeInfoFromIntent(intent)
if (mSpecialMode != SpecialMode.DEFAULT) {
// To move the app in background
moveTaskToBack(true)
backToTheMainAppAndFinish()
}
}
}
@@ -88,11 +89,19 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
// To get the app caller, only for IntentSender
super.onBackPressed()
} else {
// To move the app in background
moveTaskToBack(true)
backToTheMainAppAndFinish()
}
}
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?) {
super.onCreate(savedInstanceState)
@@ -160,12 +169,17 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
}
// To hide home button from the regular toolbar in special mode
if (mSpecialMode != SpecialMode.DEFAULT) {
if (mSpecialMode != SpecialMode.DEFAULT
&& hideHomeButtonIfModeIsNotDefault()) {
supportActionBar?.setDisplayHomeAsUpEnabled(false)
supportActionBar?.setDisplayShowHomeEnabled(false)
}
}
open fun hideHomeButtonIfModeIsNotDefault(): Boolean {
return true
}
private fun blockAutofill(searchInfo: SearchInfo?) {
val webDomain = searchInfo?.webDomain
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_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_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_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)
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_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_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_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)
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_clear) -> R.style.KeepassDXStyle_Clear
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_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_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_dark) -> R.style.KeepassDXStyle_Purple_Dark
else -> R.style.KeepassDXStyle_Light

View File

@@ -23,11 +23,13 @@ import android.content.Context
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StyleRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
abstract class StylishFragment : Fragment() {
@@ -47,27 +49,41 @@ abstract class StylishFragment : Fragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = requireActivity().window
val defaultColor = Color.BLACK
val windowInset = WindowInsetsControllerCompat(window, window.decorView)
try {
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
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) {
try {
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
if (taWindowStatusLight?.getBoolean(0, false) == true) {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
windowInset.isAppearanceLightStatusBars = taWindowStatusLight
?.getBoolean(0, false) == true
taWindowStatusLight?.recycle()
} catch (e: Exception) {}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve theme : window light status bar", e)
}
}
try {
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
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)
}
@@ -76,4 +92,8 @@ abstract class StylishFragment : Fragment() {
contextThemed = null
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.Toast
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
@@ -87,8 +86,6 @@ class NodesAdapter (private val context: Context,
private var mNodeClickCallback: NodeClickCallback? = null
private var mClipboardHelper = ClipboardHelper(context)
@ColorInt
private val mContentSelectionColor: Int
@ColorInt
private val mTextColorPrimary: Int
@ColorInt
@@ -98,7 +95,7 @@ class NodesAdapter (private val context: Context,
@ColorInt
private val mColorAccentLight: Int
@ColorInt
private val mTextColorSelected: Int
private val mColorOnAccentColor: Int
/**
* Determine if the adapter contains or not any element
@@ -115,8 +112,6 @@ class NodesAdapter (private val context: Context,
this.mNodeSortedListCallback = NodeSortedListCallback()
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
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK)
@@ -130,13 +125,13 @@ class NodesAdapter (private val context: Context,
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
taTextColorSecondary.recycle()
// To get background color for selection
val taSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
this.mColorAccentLight = taSelectionColor.getColor(0, Color.GRAY)
taSelectionColor.recycle()
val taColorAccentLight = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
this.mColorAccentLight = taColorAccentLight.getColor(0, Color.GRAY)
taColorAccentLight.recycle()
// To get text color for selection
val taSelectionTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor))
this.mTextColorSelected = taSelectionTextColor.getColor(0, Color.WHITE)
taSelectionTextColor.recycle()
val taColorOnAccentColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor))
this.mColorOnAccentColor = taColorOnAccentColor.getColor(0, Color.WHITE)
taColorOnAccentColor.recycle()
}
private fun assignPreferences() {
@@ -352,23 +347,6 @@ class NodesAdapter (private val context: Context,
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
holder.text.apply {
text = subNode.title
@@ -396,6 +374,14 @@ class NodesAdapter (private val context: Context,
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
if (subNode.type == Type.ENTRY) {
val entry = subNode as Entry
@@ -457,13 +443,7 @@ class NodesAdapter (private val context: Context,
holder.otpProgress?.setIndicatorColor(foregroundColor)
holder.attachmentIcon?.setColorFilter(foregroundColor)
holder.meta.setTextColor(foregroundColor)
holder.icon.apply {
database.iconDrawableFactory.assignDatabaseIcon(
this,
subNode.icon,
foregroundColor
)
}
iconColor = foregroundColor
} else {
holder.text.setTextColor(mTextColor)
holder.subText?.setTextColor(mTextColorSecondary)
@@ -473,12 +453,12 @@ class NodesAdapter (private val context: Context,
holder.meta.setTextColor(mTextColor)
}
} else {
holder.text.setTextColor(mTextColorSelected)
holder.subText?.setTextColor(mTextColorSelected)
holder.otpToken?.setTextColor(mTextColorSelected)
holder.otpProgress?.setIndicatorColor(mTextColorSelected)
holder.attachmentIcon?.setColorFilter(mTextColorSelected)
holder.meta.setTextColor(mTextColorSelected)
holder.text.setTextColor(mColorOnAccentColor)
holder.subText?.setTextColor(mColorOnAccentColor)
holder.otpToken?.setTextColor(mColorOnAccentColor)
holder.otpProgress?.setIndicatorColor(mColorOnAccentColor)
holder.attachmentIcon?.setColorFilter(mColorOnAccentColor)
holder.meta.setTextColor(mColorOnAccentColor)
}
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
holder.container.setOnClickListener {
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.net.Uri
import android.os.IBinder
import android.util.Base64
import android.util.Log
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter
@@ -125,15 +127,40 @@ class CipherDatabaseAction(context: Context) {
}
fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
cipherDatabaseResultListener: (CipherEncryptDatabase?) -> Unit) {
if (useTempDao) {
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 {
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)
@@ -149,8 +176,16 @@ class CipherDatabaseAction(context: Context) {
}
}
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
fun addOrUpdateCipherDatabase(cipherEncryptDatabase: CipherEncryptDatabase,
cipherDatabaseResultListener: (() -> Unit)? = null) {
cipherEncryptDatabase.databaseUri?.let { databaseUri ->
val cipherDatabaseEntity = CipherDatabaseEntity(
databaseUri.toString(),
Base64.encodeToString(cipherEncryptDatabase.encryptedValue, Base64.NO_WRAP),
Base64.encodeToString(cipherEncryptDatabase.specParameters, Base64.NO_WRAP),
)
if (useTempDao) {
// The only case to create service (not needed to get an info)
serviceActionTask(true) {
@@ -160,7 +195,8 @@ class CipherDatabaseAction(context: Context) {
} else {
IOActionTask(
{
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
val cipherDatabaseRetrieve =
cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
// Update values if element not yet in the database
if (cipherDatabaseRetrieve == null) {
cipherDatabaseDao.add(cipherDatabaseEntity)
@@ -174,6 +210,7 @@ class CipherDatabaseAction(context: Context) {
).execute()
}
}
}
fun deleteByDatabaseUri(databaseUri: Uri,
cipherDatabaseResultListener: (() -> Unit)? = null) {

View File

@@ -40,6 +40,9 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException
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.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
@@ -60,6 +63,9 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
var databaseFileUri: Uri? = null
private set
// TODO Retrieve credential storage from app database
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
/**
* Manage setting to auto open biometric prompt
*/
@@ -477,6 +483,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} ?: checkUnlockAvailability()
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) {
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 ->
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
databaseFileUri?.let {
mBuilderListener?.onCredentialDecrypted(it, decryptedValue)
databaseFileUri?.let { databaseUri ->
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)
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onGenericException(e: Exception) {
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
setAdvancedUnlockedMessageView(errorMessage)
@@ -580,6 +601,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.message = text
@@ -617,10 +639,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
}
interface BuilderListener {
fun retrieveCredentialForEncryption(): String
fun retrieveCredentialForEncryption(): ByteArray
fun conditionToStoreCredential(): Boolean
fun onCredentialEncrypted(databaseUri: Uri, encryptedCredential: String, ivSpec: String)
fun onCredentialDecrypted(databaseUri: Uri, decryptedCredential: String)
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase)
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase)
}
override fun onPause() {

View File

@@ -27,7 +27,6 @@ import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log
import android.widget.Toast
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) {
return
}
try {
val encrypted = cipher?.doFinal(value.toByteArray())
val encryptedBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP)
val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
// passes updated iv spec on to callback so this can be stored for decryption
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP)
advancedUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
advancedUnlockCallback?.handleEncryptedResult(encrypted, spec.iv)
}
} catch (e: Exception) {
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) {
initDecryptData(ivSpecValue, actionIfCypherInit, true)
}
private fun initDecryptData(ivSpecValue: String,
private fun initDecryptData(ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean = true) {
if (!isKeyManagerInitialized) {
@@ -246,9 +242,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
try {
// important to restore spec here that was used for decryption
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP)
val spec = IvParameterSpec(iv)
val spec = IvParameterSpec(ivSpecValue)
getSecretKey()?.let { secretKey ->
cipher?.let { cipher ->
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) {
return
}
try {
// actual decryption here
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP)
cipher?.doFinal(encrypted)?.let { decrypted ->
advancedUnlockCallback?.handleDecryptedResult(String(decrypted))
cipher?.doFinal(encryptedValue)?.let { decrypted ->
advancedUnlockCallback?.handleDecryptedResult(decrypted)
}
} catch (badPaddingException: BadPaddingException) {
Log.e(TAG, "Unable to decrypt data", badPaddingException)
@@ -367,8 +360,8 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
fun onAuthenticationSucceeded()
fun onAuthenticationFailed()
fun onAuthenticationError(errorCode: Int, errString: CharSequence)
fun handleEncryptedResult(encryptedValue: String, ivSpec: String)
fun handleDecryptedResult(decryptedValue: String)
fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray)
fun handleDecryptedResult(decryptedValue: ByteArray)
}
companion object {
@@ -469,9 +462,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
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) {
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.utils.UriUtil
open class AssignPasswordInDatabaseRunnable (
open class AssignMainCredentialInDatabaseRunnable (
context: Context,
database: Database,
protected val mDatabaseUri: Uri,

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,13 +20,15 @@
package com.kunzisoft.keepass.database.action
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.tasks.ActionRunnable
open class SaveDatabaseRunnable(protected var context: Context,
protected var database: Database,
private var saveDatabase: Boolean)
private var saveDatabase: Boolean,
private var databaseCopyUri: Uri? = null)
: ActionRunnable() {
var mAfterSaveDatabase: ((Result) -> Unit)? = null
@@ -37,7 +39,7 @@ open class SaveDatabaseRunnable(protected var context: Context,
database.checkVersion()
if (saveDatabase && result.isSuccess) {
try {
database.saveData(context.contentResolver)
database.saveData(databaseCopyUri, context.contentResolver)
} catch (e: DatabaseException) {
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
import android.os.Parcel
@@ -17,7 +36,10 @@ class CustomData : Parcelable {
}
constructor(parcel: Parcel) {
ParcelableUtil.readStringParcelableMap(parcel, CustomDataItem::class.java)
mCustomDataItems.clear()
mCustomDataItems.putAll(ParcelableUtil
.readStringParcelableMap(parcel, CustomDataItem::class.java)
)
}
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) {
ParcelableUtil.writeStringParcelableMap(parcel, flags, mCustomDataItems)
}

View File

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

View File

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

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.database.DatabaseKDBX
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.EntryKDBX
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 {
return title == PMS_TAN_ENTRY && username.isNotEmpty()
}
@@ -460,6 +473,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryInfo.tags = tags
entryInfo.backgroundColor = backgroundColor
entryInfo.foregroundColor = foregroundColor
entryInfo.customData = customData
entryInfo.autoType = autoType
entryInfo.customFields = getExtraFields().toMutableList()
// Add otpElement to generate token
entryInfo.otpModel = getOtpElement()?.otpModel
@@ -497,6 +512,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
tags = newEntryInfo.tags
backgroundColor = newEntryInfo.backgroundColor
foregroundColor = newEntryInfo.foregroundColor
customData = newEntryInfo.customData
autoType = newEntryInfo.autoType
addExtraFields(newEntryInfo.customFields)
database?.attachmentPool?.let { binaryPool ->
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> {
return groupKDB?.getChildGroups()?.map {
Group(it)
@@ -434,12 +440,35 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupKDBX?.nodeId = id
}
fun setEnableAutoType(enableAutoType: Boolean?) {
groupKDBX?.enableAutoType = enableAutoType
var searchable: Boolean?
get() = groupKDBX?.enableSearching
set(value) {
groupKDBX?.enableSearching = value
}
fun setEnableSearching(enableSearching: Boolean?) {
groupKDBX?.enableSearching = enableSearching
fun isSearchable(): Boolean {
val searchableGroup = searchable
if (searchableGroup == null) {
val parenGroup = parent
if (parenGroup == null)
return true
else
return parenGroup.isSearchable()
} else {
return searchableGroup
}
}
var enableAutoType: Boolean?
get() = groupKDBX?.enableAutoType
set(value) {
groupKDBX?.enableAutoType = value
}
var defaultAutoTypeSequence: String
get() = groupKDBX?.defaultAutoTypeSequence ?: ""
set(value) {
groupKDBX?.defaultAutoTypeSequence = value
}
fun setExpanded(expanded: Boolean) {
@@ -462,7 +491,11 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
groupInfo.expires = expires
groupInfo.expiryTime = expiryTime
groupInfo.notes = notes
groupInfo.searchable = searchable
groupInfo.enableAutoType = enableAutoType
groupInfo.defaultAutoTypeSequence = defaultAutoTypeSequence
groupInfo.tags = tags
groupInfo.customData = customData
return groupInfo
}
@@ -475,7 +508,11 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
expires = groupInfo.expires
expiryTime = groupInfo.expiryTime
notes = groupInfo.notes
searchable = groupInfo.searchable
enableAutoType = groupInfo.enableAutoType
defaultAutoTypeSequence = groupInfo.defaultAutoTypeSequence
tags = groupInfo.tags
customData = groupInfo.customData
}
override fun equals(other: Any?): Boolean {

View File

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

View File

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

View File

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

View File

@@ -62,6 +62,7 @@ abstract class DatabaseVersioned<
protected set
abstract val version: String
abstract val defaultFileExtension: String
/**
* To manage binaries in faster way
@@ -325,14 +326,6 @@ abstract class DatabaseVersioned<
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() {
iconsManager.doForEachCustomIcon { _, binary ->
try {

View File

@@ -41,9 +41,9 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
override var customData = CustomData()
var notes = ""
var isExpanded = true
var defaultAutoTypeSequence = ""
var enableAutoType: Boolean? = null
var enableSearching: Boolean? = null
var enableAutoType: Boolean? = null
var defaultAutoTypeSequence: String = ""
var lastTopVisibleEntry: UUID = DatabaseVersioned.UUID_ZERO
override var tags = Tags()
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()
notes = parcel.readString() ?: notes
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()
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
tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags
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.writeString(notes)
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 (enableAutoType == null) -1 else if (enableAutoType!!) 1 else 0)
dest.writeString(defaultAutoTypeSequence)
dest.writeSerializable(lastTopVisibleEntry)
dest.writeParcelable(tags, flags)
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
@@ -111,9 +111,9 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
customData = CustomData(source.customData)
notes = source.notes
isExpanded = source.isExpanded
defaultAutoTypeSequence = source.defaultAutoTypeSequence
enableAutoType = source.enableAutoType
enableSearching = source.enableSearching
enableAutoType = source.enableAutoType
defaultAutoTypeSequence = source.defaultAutoTypeSequence
lastTopVisibleEntry = source.lastTopVisibleEntry
tags = source.tags
previousParentGroup = source.previousParentGroup

View File

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

View File

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

View File

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

View File

@@ -101,27 +101,8 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
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 {
assignMasterKey: (() -> Unit)): DatabaseKDBX {
try {
startKeyTimer(progressTaskUpdater)
@@ -133,7 +114,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
hashOfHeader = headerAndHash.hash
val pbHeader = headerAndHash.header
assignMasterKey?.invoke()
assignMasterKey.invoke()
mDatabase.makeFinalKey(header.masterSeed)
stopKeyTimer()

View File

@@ -663,7 +663,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeTags(tags: Tags) {
if (!tags.isEmpty()) {
if (tags.isNotEmpty()) {
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.GroupKDBX
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.utils.readAllBytes
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
*/
fun merge(databaseToMerge: DatabaseKDB) {
// TODO Test KDB merge
val rootGroup = database.rootGroup
val rootGroupId = rootGroup?.nodeId
val rootGroupToMerge = databaseToMerge.rootGroup
@@ -53,6 +53,11 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
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
rootGroupToMerge.doForEachChild(
object : NodeHandler<EntryKDB>() {
@@ -87,12 +92,35 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
val entry = database.getEntryById(entryId)
databaseToMerge.getEntryById(entryId)?.let { srcEntryToMerge ->
// Do not merge meta stream elements
if (!srcEntryToMerge.isMetaStream()) {
// Retrieve parent in current database
var parentEntryToMerge: GroupKDBX? = null
srcEntryToMerge.parent?.nodeId?.let {
val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it)
parentEntryToMerge = database.getGroupById(parentGroupIdToMerge)
}
// Copy attachment
var newAttachment: Attachment? = null
srcEntryToMerge.getAttachment(databaseToMerge.attachmentPool)?.let { attachment ->
val binarySize = attachment.binaryData.getSize()
val binaryData = database.buildNewBinaryAttachment(
isRAMSufficient.invoke(binarySize),
attachment.binaryData.isCompressed,
attachment.binaryData.isProtected
)
attachment.binaryData.getInputDataStream(databaseToMerge.binaryCache)
.use { inputStream ->
binaryData.getOutputDataStream(database.binaryCache)
.use { outputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
}
newAttachment = Attachment(attachment.name, binaryData)
}
// Create new entry format
val entryToMerge = EntryKDBX().apply {
this.nodeId = srcEntryToMerge.nodeId
this.icon = srcEntryToMerge.icon
@@ -106,7 +134,9 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
this.password = srcEntryToMerge.password
this.url = srcEntryToMerge.url
this.notes = srcEntryToMerge.notes
// TODO attachment
newAttachment?.let {
this.putAttachment(it, database.attachmentPool)
}
}
if (entry != null) {
entry.updateWith(entryToMerge, false)
@@ -115,6 +145,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
}
}
}
}
/**
* Utility method to merge a KDB group

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.Entry
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.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD
@@ -37,7 +38,7 @@ class SearchHelper {
fun createVirtualGroupWithSearchResult(database: Database,
searchParameters: SearchParameters,
omitBackup: Boolean,
fromGroup: NodeId<*>? = null,
max: Int): Group? {
val searchGroup = database.createGroup(virtual = true)
@@ -45,7 +46,15 @@ class SearchHelper {
// Search all entries
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>() {
override fun operate(node: Entry): Boolean {
if (incrementEntry >= max)
@@ -62,19 +71,43 @@ class SearchHelper {
},
object : NodeHandler<Group>() {
override fun operate(node: Group): Boolean {
return when {
incrementEntry >= max -> false
database.isGroupSearchable(node, omitBackup) -> true
else -> false
}
return groupConditions(database,
node,
searchParameters,
allowCustomSearchable,
max
)
}
},
false)
false
)
}
searchGroup?.refreshNumberOfChildEntries()
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,
entry: Entry,
searchParameters: SearchParameters): Boolean {
@@ -88,7 +121,18 @@ class SearchHelper {
}
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]
@@ -110,7 +154,6 @@ class SearchHelper {
// If search provide results
database.createVirtualGroupFromSearchInfo(
searchInfo.toString(),
PreferencesUtil.omitBackup(context),
MAX_SEARCH_ENTRY
)?.let { searchGroup ->
if (searchGroup.numberOfChildEntries > 0) {
@@ -132,16 +175,23 @@ class SearchHelper {
fun searchInEntry(entry: Entry,
searchParameters: SearchParameters): Boolean {
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())
return false
// Exclude entry expired
if (searchParameters.excludeExpired) {
if (entry.isCurrentlyExpires)
return false
}
// Search all strings in the KDBX entry
if (searchParameters.searchInTitles) {
if (checkSearchQuery(entry.title, searchParameters))
return true
}
if (searchParameters.searchInUserNames) {
if (searchParameters.searchInUsernames) {
if (checkSearchQuery(entry.username, searchParameters))
return true
}
@@ -158,8 +208,8 @@ class SearchHelper {
return true
}
if (searchParameters.searchInUUIDs) {
val hexString = UuidUtil.toHexString(entry.nodeId.id)
if (hexString != null && hexString.contains(searchQuery, true))
val hexString = UuidUtil.toHexString(entry.nodeId.id) ?: ""
if (checkSearchQuery(hexString, searchParameters))
return true
}
if (searchParameters.searchInOther) {
@@ -171,21 +221,31 @@ class SearchHelper {
}
}
}
if (searchParameters.searchInTags) {
if (checkSearchQuery(entry.tags.toString(), searchParameters))
return true
}
return false
}
private fun checkSearchQuery(stringToCheck: String, searchParameters: SearchParameters): Boolean {
/*
// TODO Search settings
var regularExpression = false
var ignoreCase = true
var removeAccents = true <- Too much time, to study
var excludeExpired = false
var searchOnlyInCurrentGroup = false
*/
return stringToCheck.isNotEmpty()
&& stringToCheck.contains(
searchParameters.searchQuery, true)
if (stringToCheck.isEmpty())
return false
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
import android.os.Parcel
import android.os.Parcelable
/**
* Parameters for searching strings in the database.
*/
class SearchParameters {
class SearchParameters() : Parcelable{
var searchQuery: String = ""
var caseSensitive = false
var isRegex = false
var searchInTitles = true
var searchInUserNames = true
var searchInUsernames = true
var searchInPasswords = false
var searchInUrls = true
var excludeExpired = false
var searchInNotes = true
var searchInOTP = false
var searchInOther = true
var searchInUUIDs = false
var searchInTags = true
var searchInTags = false
var searchInCurrentGroup = false
var searchInSearchableGroup = true
var searchInRecycleBin = 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,6 +31,7 @@ import com.kunzisoft.keepass.R
open class Education(val activity: Activity) {
private var mOneEducationHintOpen = false
/**
* Utility method to save preference after an education action
*/
@@ -39,17 +40,38 @@ open class Education(val activity: Activity) {
listener: TapTargetView.Listener,
saveEducationStringId: Int): Boolean {
var doEducation = false
if (isEducationScreensEnabled()) {
if (isEducationAlreadyPerformed) {
if (isEducationScreensEnabled()
&& !mOneEducationHintOpen
&& !isEducationAlreadyPerformed) {
try {
TapTargetView.showFor(activity, tapTarget, listener)
TapTargetView.showFor(activity, tapTarget, object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) {
mOneEducationHintOpen = false
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
}
@@ -117,6 +139,29 @@ open class Education(val activity: Activity) {
R.string.education_add_attachment_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
*/

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ class PasswordActivityEducation(activity: Activity)
fun checkAndPerformedUnlockEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationUnlockPerformed(activity),
return checkAndPerformedEducation(isEducationUnlockPerformed(activity),
TapTarget.forView(educationView,
activity.getString(R.string.education_unlock_title),
activity.getString(R.string.education_unlock_summary))
@@ -60,7 +60,7 @@ class PasswordActivityEducation(activity: Activity)
fun checkAndPerformedReadOnlyEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationReadOnlyPerformed(activity),
return checkAndPerformedEducation(isEducationReadOnlyPerformed(activity),
TapTarget.forView(educationView,
activity.getString(R.string.education_read_only_title),
activity.getString(R.string.education_read_only_summary))
@@ -87,7 +87,7 @@ class PasswordActivityEducation(activity: Activity)
fun checkAndPerformedBiometricEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationBiometricPerformed(activity),
return checkAndPerformedEducation(isEducationBiometricPerformed(activity),
TapTarget.forView(educationView,
activity.getString(R.string.education_advanced_unlock_title),
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.Parcelable
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.template.TemplateField
import com.kunzisoft.keepass.otp.OtpElement
@@ -42,6 +43,7 @@ class EntryInfo : NodeInfo {
var foregroundColor: Int? = null
var customFields: MutableList<Field> = mutableListOf()
var attachments: MutableList<Attachment> = mutableListOf()
var autoType: AutoType = AutoType()
var otpModel: OtpModel? = null
var isTemplate: Boolean = false
@@ -60,6 +62,7 @@ class EntryInfo : NodeInfo {
foregroundColor = if (readFgColor == -1) null else readFgColor
parcel.readList(customFields, Field::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
isTemplate = parcel.readByte().toInt() != 0
}
@@ -80,6 +83,7 @@ class EntryInfo : NodeInfo {
parcel.writeInt(foregroundColor ?: -1)
parcel.writeList(customFields)
parcel.writeList(attachments)
parcel.writeParcelable(autoType, flags)
parcel.writeParcelable(otpModel, flags)
parcel.writeByte((if (isTemplate) 1 else 0).toByte())
}
@@ -105,6 +109,7 @@ class EntryInfo : NodeInfo {
return customFields.lastOrNull { it.name == label }?.protectedValue?.toString() ?: ""
}
// Return true if modified
private fun addUniqueField(field: Field, number: Int = 0) {
var sameName = false
var sameValue = false
@@ -126,7 +131,19 @@ class EntryInfo : NodeInfo {
(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 ->
// Replace the OTP field
OtpEntryFields.parseOTPUri(otpString)?.let { otpElement ->
@@ -141,32 +158,46 @@ class EntryInfo : NodeInfo {
mutableCustomFields.remove(otpField)
}
mutableCustomFields.add(otpField)
modification = true
}
} ?: searchInfo.webDomain?.let { webDomain ->
// If unable to save web domain in custom field or URL not populated, save in URL
val scheme = searchInfo.webScheme
val webScheme = if (scheme.isNullOrEmpty()) "http" else scheme
val webScheme = if (scheme.isNullOrEmpty()) "https" else scheme
val webDomainToStore = "$webScheme://$webDomain"
if (!containsDomainOrApplicationId(webDomain)) {
if (database?.allowEntryCustomFields() != true || url.isEmpty()) {
url = webDomainToStore
} else if (url != webDomainToStore) {
} else {
// Save web domain in custom field
addUniqueField(Field(WEB_DOMAIN_FIELD_NAME,
ProtectedString(false, webDomainToStore)),
addUniqueField(
Field(
WEB_DOMAIN_FIELD_NAME,
ProtectedString(false, webDomainToStore)
),
1 // Start to one because URL is a standard field name
)
}
modification = true
}
} ?: run {
// Save application id in custom field
if (database?.allowEntryCustomFields() == true) {
searchInfo.applicationId?.let { applicationId ->
addUniqueField(Field(APPLICATION_ID_FIELD_NAME,
ProtectedString(false, applicationId))
if (!containsDomainOrApplicationId(applicationId)) {
addUniqueField(
Field(
APPLICATION_ID_FIELD_NAME,
ProtectedString(false, applicationId)
)
)
modification = true
}
}
}
}
return modification
}
fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo) {
registerInfo.username?.let {
@@ -209,6 +240,7 @@ class EntryInfo : NodeInfo {
if (foregroundColor != other.foregroundColor) return false
if (customFields != other.customFields) return false
if (attachments != other.attachments) return false
if (autoType != other.autoType) return false
if (otpModel != other.otpModel) return false
if (isTemplate != other.isTemplate) return false
@@ -227,6 +259,7 @@ class EntryInfo : NodeInfo {
result = 31 * result + foregroundColor.hashCode()
result = 31 * result + customFields.hashCode()
result = 31 * result + attachments.hashCode()
result = 31 * result + autoType.hashCode()
result = 31 * result + (otpModel?.hashCode() ?: 0)
result = 31 * result + isTemplate.hashCode()
return result

View File

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

View File

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

View File

@@ -22,12 +22,14 @@ package com.kunzisoft.keepass.services
import android.app.PendingIntent
import android.content.Intent
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 androidx.media.app.NotificationCompat
import com.kunzisoft.keepass.R
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.history.DeleteEntryHistoryDatabaseRunnable
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.NodeId
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.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
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 kotlinx.coroutines.*
import java.util.*
import kotlin.collections.ArrayList
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
@@ -226,7 +231,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val actionRunnable: ActionRunnable? = when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(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_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(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(MAIN_CREDENTIAL_KEY)
intent?.removeExtra(READ_ONLY_KEY)
intent?.removeExtra(CIPHER_ENTITY_KEY)
intent?.removeExtra(CIPHER_DATABASE_KEY)
intent?.removeExtra(FIX_DUPLICATE_UUID_KEY)
intent?.removeExtra(GROUP_KEY)
intent?.removeExtra(ENTRY_KEY)
@@ -570,13 +575,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MAIN_CREDENTIAL_KEY)
&& intent.hasExtra(READ_ONLY_KEY)
&& intent.hasExtra(CIPHER_ENTITY_KEY)
&& intent.hasExtra(CIPHER_DATABASE_KEY)
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
) {
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
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)
return null
@@ -589,7 +594,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
databaseUri,
mainCredential,
readOnly,
cipherEntity,
cipherEncryptDatabase,
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
this
) { result ->
@@ -598,7 +603,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
putParcelable(DATABASE_URI_KEY, databaseUri)
putParcelable(MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(READ_ONLY_KEY, readOnly)
putParcelable(CIPHER_ENTITY_KEY, cipherEntity)
putParcelable(CIPHER_DATABASE_KEY, cipherEncryptDatabase)
}
}
} 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(
this,
database,
databaseToMergeUri,
databaseToMergeMainCredential,
this
) { result ->
// No need to add each info to reload database
@@ -633,7 +649,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
&& intent.hasExtra(MAIN_CREDENTIAL_KEY)
) {
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
AssignPasswordInDatabaseRunnable(this,
AssignMainCredentialInDatabaseRunnable(this,
database,
databaseUri,
intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
@@ -911,9 +927,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
*/
private fun buildDatabaseSave(intent: Intent, database: Database): ActionRunnable? {
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,
database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
databaseCopyUri)
} else {
null
}
@@ -963,7 +986,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
const val MAIN_CREDENTIAL_KEY = "MAIN_CREDENTIAL_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 GROUP_KEY = "GROUP_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 dialogFragment: DialogFragment? = null
when (preference?.key) {
when (preference.key) {
getString(R.string.autofill_application_id_blocklist_key) -> {
dialogFragment = AutofillBlocklistAppIdPreferenceDialogFragmentCompat.newInstance(preference.key)
}

View File

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

View File

@@ -365,8 +365,10 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
) { _, _ ->
validate?.invoke()
deleteKeysAlertDialog?.setOnDismissListener(null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
}
}
.setNegativeButton(resources.getString(android.R.string.cancel)
) { _, _ ->}
.create()
@@ -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
when (preference?.key) {
when (preference.key) {
getString(R.string.setting_style_key),
getString(R.string.setting_style_brightness_key),
getString(R.string.setting_icon_pack_choose_key),
@@ -459,13 +461,13 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
return super.onPreferenceTreeClick(preference)
}
override fun onDisplayPreferenceDialog(preference: Preference?) {
override fun onDisplayPreferenceDialog(preference: Preference) {
var otherDialogFragment = false
var dialogFragment: DialogFragment? = null
// Main Preferences
when (preference?.key) {
when (preference.key) {
getString(R.string.app_timeout_key),
getString(R.string.clipboard_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 com.kunzisoft.androidclearchroma.ChromaUtil
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.resetAppTimeoutWhenViewTouchedOrFocused
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.preferencedialogfragment.*
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
@@ -155,18 +154,22 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
// Database name
dbNamePref = findPreference(getString(R.string.database_name_key))
dbNamePref?.let { namePreference ->
if (database.allowName) {
dbNamePref?.summary = database.name
namePreference.summary = database.name
} else {
dbGeneralPrefCategory?.removePreference(dbNamePref)
dbGeneralPrefCategory?.removePreference(namePreference)
}
}
// Database description
dbDescriptionPref = findPreference(getString(R.string.database_description_key))
dbDescriptionPref?.let { descriptionPreference ->
if (database.allowDescription) {
dbDescriptionPref?.summary = database.description
} else {
dbGeneralPrefCategory?.removePreference(dbDescriptionPref)
dbGeneralPrefCategory?.removePreference(descriptionPreference)
}
}
// Database default username
@@ -238,7 +241,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
// Templates
val templatesGroupPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_templates_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))
templatesEnablePref?.apply {
isChecked = database.isTemplatesEnabled
@@ -334,7 +337,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
findPreference<Preference>(getString(R.string.settings_database_change_credentials_key))?.apply {
isEnabled = if (!mDatabaseReadOnly) {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
AssignMasterKeyDialogFragment.getInstance(database.allowNoMasterKey)
SetMainCredentialDialogFragment.getInstance(database.allowNoMasterKey)
.show(parentFragmentManager, "passwordDialog")
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)
try {
@@ -565,13 +568,13 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
}
override fun onDisplayPreferenceDialog(preference: Preference?) {
override fun onDisplayPreferenceDialog(preference: Preference) {
var otherDialogFragment = false
var dialogFragment: DialogFragment? = null
// Main Preferences
when (preference?.key) {
when (preference.key) {
getString(R.string.database_name_key) -> {
dialogFragment = DatabaseNamePreferenceDialogFragmentCompat.newInstance(preference.key)
}
@@ -669,27 +672,29 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
}
R.id.menu_merge_database -> {
mergeDatabase()
return true
true
}
R.id.menu_reload_database -> {
reloadDatabase()
return true
true
}
else -> {
R.id.menu_app_settings -> {
// Check the time lock before launching settings
// TODO activity menu
(activity as SettingsActivity?)?.let {
MenuUtil.onDefaultMenuOptionsItemSelected(it, item, true)
SettingsActivity.launch(it, true)
}
true
}
else -> {
super.onOptionsItemSelected(item)
}
}
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
override fun onPreferenceTreeClick(preference: Preference): Boolean {
// To reload group when database settings are modified
when (preference?.key) {
when (preference.key) {
getString(R.string.database_name_key),
getString(R.string.database_description_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))
}
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 {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
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_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.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.subdomain_search_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.app_timeout_key) -> editor.putString(name, value.toLong().toString())
@@ -678,23 +671,7 @@ object PreferencesUtil {
putPropertiesInPreferences(properties,
Education.getEducationSharedPreferences(context)) { editor, name, value ->
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())
}
Education.putPropertiesInEducationPreferences(context, editor, name, value)
}
}
}

View File

@@ -32,7 +32,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment
import com.google.android.material.floatingactionbutton.FloatingActionButton
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.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.database.element.Database
@@ -46,7 +46,7 @@ import java.util.*
open class SettingsActivity
: DatabaseLockActivity(),
MainPreferenceFragment.Callback,
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
SetMainCredentialDialogFragment.AssignMainCredentialDialogListener {
private var backupManager: BackupManager? = 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 {
a?.getString(index)?.toLongOrNull() ?: mDuration
a.getString(index)?.toLongOrNull() ?: mDuration
} catch (e: Exception) {
mDuration
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,18 +32,11 @@ import com.kunzisoft.keepass.settings.SettingsActivity
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) {
contributionMenuInflater(inflater, menu)
inflater.inflate(R.menu.default_menu, menu)
}
fun onContributionItemSelected(context: Context) {
UriUtil.gotoUrl(context, R.string.contribution_url)
inflater.inflate(R.menu.settings, menu)
inflater.inflate(R.menu.about, menu)
if (!(BuildConfig.FULL_VERSION && BuildConfig.CLOSED_STORE))
menu.findItem(R.id.menu_contribute)?.isVisible = false
}
/*
@@ -54,7 +47,7 @@ object MenuUtil {
timeoutEnable: Boolean = false) {
when (item.itemId) {
R.id.menu_contribute -> {
onContributionItemSelected(activity)
UriUtil.gotoUrl(activity, R.string.contribution_url)
}
R.id.menu_app_settings -> {
// 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 android.graphics.drawable.Drawable
import android.view.ViewGroup
import android.widget.LinearLayout
import com.google.android.material.appbar.CollapsingToolbarLayout
@@ -113,8 +115,7 @@ fun View.collapse(animate: Boolean = true,
onCollapseFinished: (() -> Unit)? = null) {
val recordViewHeight = layoutParams.height
val slideAnimator = ValueAnimator.ofInt(height, 0)
if (animate)
slideAnimator.duration = 300L
slideAnimator.duration = if (animate) 300L else 0L
slideAnimator.addUpdateListener { animation ->
layoutParams.height = animation.animatedValue as Int
requestLayout()
@@ -143,8 +144,7 @@ fun View.expand(animate: Boolean = true,
layoutParams.height = 0
val slideAnimator = ValueAnimator
.ofInt(0, viewHeight)
if (animate)
slideAnimator.duration = 300L
slideAnimator.duration = if (animate) 300L else 0L
var alreadyVisible = false
slideAnimator.addUpdateListener { animation ->
layoutParams.height = animation.animatedValue as Int
@@ -168,12 +168,38 @@ fun View.expand(animate: Boolean = true,
}.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() {
alpha = 1f
animate()
.alpha(0f)
.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() {

View File

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

View File

@@ -2,5 +2,5 @@
<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_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>

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"?>
<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"/>
</selector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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" android:state_enabled="true" />
<item android:color="?attr/colorOnAccentColor" android:state_enabled="true" />
</selector>

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
<item android:state_pressed="true">
<shape
android:shape="oval">
<stroke android:color="@color/orange_light" android:width="1dp"/>
<stroke android:color="@color/orange_lighter" android:width="1dp"/>
<padding
android:left="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"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
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>

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:top="12dp"
android:bottom="12dp"/>
<stroke android:width="1dp" android:color="@color/grey_blue" />
<solid android:color="@color/grey_blue"/>
<stroke android:width="1dp" android:color="@color/grey_blue_slight" />
<solid android:color="@color/grey_blue_slight"/>
</shape>

View File

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

View File

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

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