mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'release/3.3.0'
This commit is contained in:
12
CHANGELOG
12
CHANGELOG
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
},
|
||||
{}
|
||||
)
|
||||
{
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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
|
||||
* -------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -53,6 +53,10 @@ class Tags: Parcelable {
|
||||
return mTags.isEmpty()
|
||||
}
|
||||
|
||||
fun isNotEmpty(): Boolean {
|
||||
return !isEmpty()
|
||||
}
|
||||
|
||||
fun size(): Int {
|
||||
return mTags.size
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) -> {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
6
app/src/main/res/color/background_color_chip.xml
Normal file
6
app/src/main/res/color/background_color_chip.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
11
app/src/main/res/drawable/ic_case_sensitive_white_24dp.xml
Normal file
11
app/src/main/res/drawable/ic_case_sensitive_white_24dp.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
11
app/src/main/res/drawable/ic_current_folder_white_24dp.xml
Normal file
11
app/src/main/res/drawable/ic_current_folder_white_24dp.xml
Normal 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>
|
||||
11
app/src/main/res/drawable/ic_database_white_36dp.xml
Normal file
11
app/src/main/res/drawable/ic_database_white_36dp.xml
Normal 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>
|
||||
13
app/src/main/res/drawable/ic_merge_from_white_24dp.xml
Normal file
13
app/src/main/res/drawable/ic_merge_from_white_24dp.xml
Normal 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>
|
||||
13
app/src/main/res/drawable/ic_modified_white_12dp.xml
Normal file
13
app/src/main/res/drawable/ic_modified_white_12dp.xml
Normal 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>
|
||||
12
app/src/main/res/drawable/ic_regex_white_24dp.xml
Normal file
12
app/src/main/res/drawable/ic_regex_white_24dp.xml
Normal 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>
|
||||
13
app/src/main/res/drawable/ic_save_copy_to_white_24dp.xml
Normal file
13
app/src/main/res/drawable/ic_save_copy_to_white_24dp.xml
Normal 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>
|
||||
16
app/src/main/res/drawable/ic_templates_white_24dp.xml
Normal file
16
app/src/main/res/drawable/ic_templates_white_24dp.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user