diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..80a899f6a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,44 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +** Keepass Database ** + - Created with: [e.g Windows KeePass 2.42] + - Version: [e.g. 2] + - Location: [e.g. Remote file retrieved with GDrive app] + - Size: [e.g. 150Mo] + - Contains attachment: [e.g. Yes] + +**KeePass DX (please complete the following information):** + - Version: [e.g. 2.5.0.0beta23] + - Build: [e.g. Free] + - Language: [e.g. French] + +**Android (please complete the following information):** + - Device: [e.g. GalaxyS8] + - Version: [e.g. 8.1] + - Browser: [e.g. Chrome] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..4fe86d5ec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore index be230663e..b6de83e58 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,13 @@ proguard/ # Android Studio captures folder captures/ +# Eclipse/VS Code +.project +.settings/* +*/.project +*/.classpath +*/.settings/* + # Intellij *.iml .idea/workspace.xml diff --git a/CHANGELOG b/CHANGELOG index 2612ef808..f796ec313 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,13 @@ +KeepassDX (2.5.0.0beta24) + * Add OTP (HOTP / TOTP) + * Add settings (Color, Security, Master Key) + * Show history of each entry + * Auto repair database for nodes with same UUID + * Management of expired nodes + * Multi-selection for actions (Cut - Copy - Delete) + * Open/Save database as service / Add persistent notification + * Fix settings / edit group / small bugs + KeepassDX (2.5.0.0beta23) * New, more secure database creation workflow * Recognize more database files diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 52ccfe1a4..000000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,45 +0,0 @@ -Original author: -Brian Pellin - -Achim Weimert -Johan Berts - search patches -Mike Mohr - Better native code for aes and sha -Tobias Selig - icon support -Tolga Onbay, Dirk Bergstrom - password generator -Space Cowboy - holo theme -josefwells -Nicholas FitzRoy-Dale - auto launch intents -yulin2 - responsiveness improvements -Tadashi Saito -vhschlenker -bumper314 - Samsung multiwindow support -Hans Cappelle - fingerprint sensor integration -Jeremy Jamet - Keepass DX Material Design - Patches - -Translations: -Diego Pierotto - Italian -Laurent, Norman Obry, Nam, Bruno Parmentier, Credomo - French -Maciej Bieniek, cod3r - Polish -Максим Сёмочкин, i.nedoboy, filimonic, bboa - Russian -MaWi, rvs2008, meviox, MaDill, EdlerProgrammierer, Jan Thomas - German -yslandro - Norwegian Nynorsk -王科峰 - Chinese -Typhoon - Slovak -Masahiro Inamura - Japanese -Matsuu Takuto - Japanese -Carlos Schlyter - Portugese (Brazil) -YSmhXQDd6Z - Portugese (Portugal) -andriykopanytsia - Ukranian -intel, Zoltán Antal - Hungarian -H Vanek - Czech -jipanos - Spanish -Erik Fdevriendt, Erik Jan Meijer - Dutch -Frederik Svarre - Danish -Oriol Garrote - Catalan -Mika Takala - Finnish -Niclas Burgren - Swedish -Raimonds - Latvian -dgarciabad - Basque -Arthur Zamarin - Hebrew -RaptorTFX - Greek -zygimantus - Lithuanian diff --git a/ReadMe.md b/ReadMe.md index 5ac23fc0f..1fbedeedd 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -10,7 +10,8 @@ * Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm * **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePass XC...) * Allows **fast copy** of fields and opening of URI / URL - * **Fingerprint** for fast unlocking + * **Biometric recognition** for fast unlocking *(Fingerprint / Face unlock / ...)* + * **One-Time Password** management *(HOTP / TOTP)* * Material design with **themes** * **AutoFill** and Integration * Field filling **keyboard** diff --git a/app/build.gradle b/app/build.gradle index 4e3978343..e86c1b10d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,13 +6,14 @@ apply plugin: 'kotlin-kapt' android { compileSdkVersion 28 buildToolsVersion '28.0.3' + ndkVersion "20.1.5948944" defaultConfig { applicationId "com.kunzisoft.keepass" minSdkVersion 14 targetSdkVersion 28 - versionCode = 23 - versionName = "2.5.0.0beta23" + versionCode = 24 + versionName = "2.5.0.0beta24" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" @@ -80,7 +81,7 @@ android { } def spongycastleVersion = "1.58.0.0" -def room_version = "2.1.0" +def room_version = "2.2.1" dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -89,7 +90,7 @@ dependencies { implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'androidx.biometric:biometric:1.0.0-beta01' + implementation 'androidx.biometric:biometric:1.0.0' implementation 'com.google.android.material:material:1.0.0' implementation "androidx.room:room-runtime:$room_version" @@ -97,15 +98,17 @@ dependencies { implementation "com.madgag.spongycastle:core:$spongycastleVersion" implementation "com.madgag.spongycastle:prov:$spongycastleVersion" - // Expandable view - implementation 'net.cachapa.expandablelayout:expandablelayout:2.9.2' // Time implementation 'joda-time:joda-time:2.9.9' + // Color + implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3' // Education implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0' // Apache Commons Collections implementation 'commons-collections:commons-collections:3.2.1' implementation 'org.apache.commons:commons-io:1.3.2' + // Apache Commons Codec + implementation 'commons-codec:commons-codec:1.11' // Base64 implementation 'biz.source_code:base64coder:2010-12-19' // Icon pack diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b19a964e5..0613db3eb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -131,7 +131,8 @@ + android:label="@string/keyboard_name" + android:exported="true"> @@ -140,7 +141,10 @@ - + = intent.getParcelableExtra(KEY_ENTRY) - mEntry = currentDatabase.getEntryById(keyEntry) - } catch (e: ClassCastException) { - Log.e(TAG, "Unable to retrieve the entry key") - } - - if (mEntry == null) { - Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show() - finish() - return - } - - // Update last access time. - mEntry?.touch(modified = false, touchParents = false) - // Retrieve the textColor to tint the icon val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) iconColor = taIconColor.getColor(0, Color.BLACK) @@ -108,8 +100,10 @@ class EntryActivity : LockingHideActivity() { // Get views collapsingToolbarLayout = findViewById(R.id.toolbar_layout) titleIconView = findViewById(R.id.entry_icon) + historyView = findViewById(R.id.history_container) entryContentsView = findViewById(R.id.entry_contents) entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this)) + entryProgress = findViewById(R.id.entry_progress) // Init the clipboard helper clipboardHelper = ClipboardHelper(this) @@ -119,6 +113,29 @@ class EntryActivity : LockingHideActivity() { override fun onResume() { super.onResume() + // Get Entry from UUID + try { + val keyEntry: PwNodeId = intent.getParcelableExtra(KEY_ENTRY) + mEntry = mDatabase?.getEntryById(keyEntry) + } catch (e: ClassCastException) { + Log.e(TAG, "Unable to retrieve the entry key") + } + + val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1) + if (historyPosition >= 0) { + mIsHistory = true + mEntry = mEntry?.getHistory()?.get(historyPosition) + } + + if (mEntry == null) { + Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show() + finish() + return + } + + // Update last access time. + mEntry?.touch(modified = false, touchParents = false) + mEntry?.let { entry -> // Fill data in resume to update from EntryEditActivity fillEntryDataInContentsView(entry) @@ -206,6 +223,17 @@ class EntryActivity : LockingHideActivity() { } } + //Assign OTP field + entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress, + View.OnClickListener { + entry.getOtpElement()?.let { otpElement -> + clipboardHelper?.timeoutCopyToClipboard( + otpElement.token, + getString(R.string.copy_field, getString(R.string.entry_otp)) + ) + } + }) + entryContentsView?.assignURL(entry.url) entryContentsView?.assignComment(entry.notes) @@ -238,18 +266,12 @@ class EntryActivity : LockingHideActivity() { entryContentsView?.setHiddenPasswordStyle(!mShowPassword) // Assign dates - entry.creationTime.date?.let { - entryContentsView?.assignCreationDate(it) - } - entry.lastModificationTime.date?.let { - entryContentsView?.assignModificationDate(it) - } - entry.lastAccessTime.date?.let { - entryContentsView?.assignLastAccessDate(it) - } - val expires = entry.expiryTime.date - if (entry.isExpires && expires != null) { - entryContentsView?.assignExpiresDate(expires) + entryContentsView?.assignCreationDate(entry.creationTime) + entryContentsView?.assignModificationDate(entry.lastModificationTime) + entryContentsView?.assignLastAccessDate(entry.lastAccessTime) + entryContentsView?.setExpires(entry.isCurrentlyExpires) + if (entry.expires) { + entryContentsView?.assignExpiresDate(entry.expiryTime) } else { entryContentsView?.assignExpiresDate(getString(R.string.never)) } @@ -257,6 +279,24 @@ class EntryActivity : LockingHideActivity() { // Assign special data entryContentsView?.assignUUID(entry.nodeId.id) + // Manage history + historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE + if (mIsHistory) { + val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) + collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK)) + taColorAccent.recycle() + } + val entryHistory = entry.getHistory() + // isMainEntry = not an history + val showHistoryView = entryHistory.isNotEmpty() + entryContentsView?.showHistory(showHistoryView) + if (showHistoryView) { + entryContentsView?.assignHistory(entryHistory) + entryContentsView?.onHistoryClick { historyItem, position -> + launch(this, historyItem, true, position) + } + } + database.stopManageEntry(entry) } @@ -404,7 +444,7 @@ class EntryActivity : LockingHideActivity() { TODO Slowdown when add entry as result Intent intent = new Intent(); intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry); - setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent); + onFinish(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent); */ super.finish() } @@ -412,13 +452,16 @@ class EntryActivity : LockingHideActivity() { companion object { private val TAG = EntryActivity::class.java.name - const val KEY_ENTRY = "entry" + const val KEY_ENTRY = "KEY_ENTRY" + const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION" - fun launch(activity: Activity, pw: EntryVersioned, readOnly: Boolean) { + fun launch(activity: Activity, entry: EntryVersioned, readOnly: Boolean, historyPosition: Int? = null) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { val intent = Intent(activity, EntryActivity::class.java) - intent.putExtra(KEY_ENTRY, pw.nodeId) + intent.putExtra(KEY_ENTRY, entry.nodeId) ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly) + if (historyPosition != null) + intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition) activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index 234cef43b..0ad978870 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -22,32 +22,36 @@ import android.app.Activity import android.content.Intent import android.os.Bundle import android.os.Handler -import androidx.appcompat.widget.Toolbar import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.ScrollView +import androidx.appcompat.widget.Toolbar import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment import com.kunzisoft.keepass.activities.lock.LockingHideActivity -import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread -import com.kunzisoft.keepass.database.action.node.ActionNodeValues -import com.kunzisoft.keepass.database.action.node.AddEntryRunnable -import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable -import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable +import com.kunzisoft.keepass.database.action.ProgressDialogThread import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.education.EntryEditActivityEducation +import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK +import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService +import com.kunzisoft.keepass.otp.OtpElement +import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.view.EntryEditContentsView +import java.util.* class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPickerListener, - GeneratePasswordDialogFragment.GeneratePasswordListener { + GeneratePasswordDialogFragment.GeneratePasswordListener, + SetOTPDialogFragment.CreateOtpListener { private var mDatabase: Database? = null @@ -60,11 +64,12 @@ class EntryEditActivity : LockingHideActivity(), // Views private var scrollView: ScrollView? = null - private var entryEditContentsView: EntryEditContentsView? = null - private var saveView: View? = null + // Dialog thread + private var progressDialogThread: ProgressDialogThread? = null + // Education private var entryEditActivityEducation: EntryEditActivityEducation? = null @@ -86,11 +91,14 @@ class EntryEditActivity : LockingHideActivity(), // Focus view to reinitialize timeout resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView) + stopService(Intent(this, ClipboardEntryNotificationService::class.java)) + stopService(Intent(this, KeyboardEntryNotificationService::class.java)) + // Likely the app has been killed exit the activity mDatabase = Database.getInstance() // Entry is retrieve, it's an entry to update - intent.getParcelableExtra>(KEY_ENTRY)?.let { + intent.getParcelableExtra>(KEY_ENTRY)?.let { mIsNew = false // Create an Entry copy to modify from the database entry mEntry = mDatabase?.getEntryById(it) @@ -105,16 +113,14 @@ class EntryEditActivity : LockingHideActivity(), } } - // Retrieve the icon after an orientation change - if (savedInstanceState != null && savedInstanceState.containsKey(KEY_NEW_ENTRY)) { - mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY) as EntryVersioned - } else { + // Create the new entry from the current one + if (savedInstanceState == null + || !savedInstanceState.containsKey(KEY_NEW_ENTRY)) { mEntry?.let { entry -> // Create a copy to modify mNewEntry = EntryVersioned(entry).also { newEntry -> - // WARNING Remove the parent to keep memory with parcelable - newEntry.parent = null + newEntry.removeParent() } } } @@ -123,7 +129,11 @@ class EntryEditActivity : LockingHideActivity(), // Parent is retrieve, it's a new entry to create intent.getParcelableExtra>(KEY_PARENT)?.let { mIsNew = true - mNewEntry = mDatabase?.createEntry() + // Create an empty new entry + if (savedInstanceState == null + || !savedInstanceState.containsKey(KEY_NEW_ENTRY)) { + mNewEntry = mDatabase?.createEntry() + } mParent = mDatabase?.getGroupById(it) // Add the default icon mDatabase?.drawFactory?.let { iconFactory -> @@ -131,6 +141,12 @@ class EntryEditActivity : LockingHideActivity(), } } + // Retrieve the new entry after an orientation change + if (savedInstanceState != null + && savedInstanceState.containsKey(KEY_NEW_ENTRY)) { + mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY) + } + // Close the activity if entry or parent can't be retrieve if (mNewEntry == null || mParent == null) { finish() @@ -152,10 +168,23 @@ class EntryEditActivity : LockingHideActivity(), saveView = findViewById(R.id.entry_edit_save) saveView?.setOnClickListener { saveEntry() } - entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) { addNewCustomField() } + entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) { + addNewCustomField() + } // Verify the education views entryEditActivityEducation = EntryEditActivityEducation(this) + + // Create progress dialog + progressDialogThread = ProgressDialogThread(this) { actionTask, result -> + when (actionTask) { + ACTION_DATABASE_CREATE_ENTRY_TASK, + ACTION_DATABASE_UPDATE_ENTRY_TASK -> { + if (result.isSuccess) + finish() + } + } + } } private fun populateViewsWithEntry(newEntry: EntryVersioned) { @@ -168,12 +197,14 @@ class EntryEditActivity : LockingHideActivity(), // Set info in view entryEditContentsView?.apply { title = newEntry.title - username = newEntry.username + username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username url = newEntry.url password = newEntry.password notes = newEntry.notes for (entry in newEntry.customFields.entries) { - addNewCustomField(entry.key, entry.value) + post { + putCustomField(entry.key, entry.value) + } } } } @@ -185,13 +216,14 @@ class EntryEditActivity : LockingHideActivity(), newEntry.apply { // Build info from view entryEditContentsView?.let { entryView -> + removeAllFields() title = entryView.title username = entryView.username url = entryView.url password = entryView.password notes = entryView.notes entryView.customFields.forEach { customField -> - addExtraField(customField.name, customField.protectedValue) + putExtraField(customField.name, customField.protectedValue) } } } @@ -217,9 +249,7 @@ class EntryEditActivity : LockingHideActivity(), * Add a new customized field view and scroll to bottom */ private fun addNewCustomField() { - entryEditContentsView?.addNewCustomField() - // Scroll bottom - scrollView?.post { scrollView?.fullScroll(ScrollView.FOCUS_DOWN) } + entryEditContentsView?.addEmptyCustomField() } /** @@ -230,59 +260,57 @@ class EntryEditActivity : LockingHideActivity(), // Launch a validation and show the error if present if (entryEditContentsView?.isValid() == true) { // Clone the entry - mDatabase?.let { database -> - mNewEntry?.let { newEntry -> - - // WARNING Add the parent previously deleted - newEntry.parent = mEntry?.parent - // Build info - newEntry.lastAccessTime = PwDate() - newEntry.lastModificationTime = PwDate() - - populateEntryWithViews(newEntry) - - // Open a progress dialog and save entry - var actionRunnable: ActionRunnable? = null - val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() { - override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) { - if (actionNodeValues.result.isSuccess) - finish() - } - } - if (mIsNew) { - mParent?.let { parent -> - actionRunnable = AddEntryRunnable(this@EntryEditActivity, - database, - newEntry, - parent, - afterActionNodeFinishRunnable, - !mReadOnly) - } - - } else { - mEntry?.let { oldEntry -> - actionRunnable = UpdateEntryRunnable(this@EntryEditActivity, - database, - oldEntry, - newEntry, - afterActionNodeFinishRunnable, - !mReadOnly) - } + mNewEntry?.let { newEntry -> + + // WARNING Add the parent previously deleted + newEntry.parent = mEntry?.parent + // Build info + newEntry.lastAccessTime = PwDate() + newEntry.lastModificationTime = PwDate() + + populateEntryWithViews(newEntry) + + // Open a progress dialog and save entry + if (mIsNew) { + mParent?.let { parent -> + progressDialogThread?.startDatabaseCreateEntry( + newEntry, + parent, + !mReadOnly + ) } - actionRunnable?.let { runnable -> - ProgressDialogSaveDatabaseThread(this@EntryEditActivity) { runnable }.start() + } else { + mEntry?.let { oldEntry -> + progressDialogThread?.startDatabaseUpdateEntry( + oldEntry, + newEntry, + !mReadOnly + ) } } } } } + override fun onResume() { + super.onResume() + + progressDialogThread?.registerProgressTask() + } + + override fun onPause() { + progressDialogThread?.unregisterProgressTask() + + super.onPause() + } + override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) val inflater = menuInflater inflater.inflate(R.menu.database_lock, menu) MenuUtil.contributionMenuInflater(inflater, menu) + inflater.inflate(R.menu.edit_entry, menu) entryEditActivityEducation?.let { Handler().post { performedNextEducation(it) } @@ -293,7 +321,7 @@ class EntryEditActivity : LockingHideActivity(), private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) { val passwordView = entryEditContentsView?.generatePasswordView - val addNewFieldView = entryEditContentsView?.addNewFieldView + val addNewFieldView = entryEditContentsView?.addNewFieldButton val generatePasswordEducationPerformed = passwordView != null && entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( @@ -329,12 +357,28 @@ class EntryEditActivity : LockingHideActivity(), return true } + R.id.menu_add_otp -> { + // Retrieve the current otpElement if exists + // and open the dialog to set up the OTP + SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel) + .show(supportFragmentManager, "addOTPDialog") + return true + } + android.R.id.home -> finish() } return super.onOptionsItemSelected(item) } + override fun onOtpCreated(otpElement: OtpElement) { + // Update the otp field with otpauth:// url + val otpField = OtpEntryFields.buildOtpField(otpElement, + mEntry?.title, mEntry?.username) + entryEditContentsView?.putCustomField(otpField.name, otpField.protectedValue) + mEntry?.putExtraField(otpField.name, otpField.protectedValue) + } + override fun iconPicked(bundle: Bundle) { IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon -> temporarilySaveAndShowSelectedIcon(icon) @@ -342,7 +386,10 @@ class EntryEditActivity : LockingHideActivity(), } override fun onSaveInstanceState(outState: Bundle) { - outState.putParcelable(KEY_NEW_ENTRY, mNewEntry) + mNewEntry?.let { + populateEntryWithViews(it) + outState.putParcelable(KEY_NEW_ENTRY, it) + } super.onSaveInstanceState(outState) } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt index 70ef64046..78ac5f64a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -29,18 +29,17 @@ import android.os.Bundle import android.os.Environment import android.os.Handler import android.preference.PreferenceManager -import androidx.annotation.RequiresApi -import com.google.android.material.snackbar.Snackbar -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.SimpleItemAnimator -import androidx.appcompat.widget.Toolbar import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View -import android.widget.EditText import android.widget.TextView +import androidx.annotation.RequiresApi +import androidx.appcompat.widget.Toolbar +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment @@ -50,17 +49,14 @@ import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.autofill.AutofillHelper -import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable import com.kunzisoft.keepass.database.action.ProgressDialogThread import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation -import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.asError import kotlinx.android.synthetic.main.activity_file_selection.* -import net.cachapa.expandablelayout.ExpandableLayout import java.io.FileNotFoundException class FileDatabaseSelectActivity : StylishActivity(), @@ -69,11 +65,7 @@ class FileDatabaseSelectActivity : StylishActivity(), // Views private var fileListContainer: View? = null private var createButtonView: View? = null - private var browseButtonView: View? = null - private var openButtonView: View? = null - private var fileSelectExpandableButtonView: View? = null - private var fileSelectExpandableLayout: ExpandableLayout? = null - private var openFileNameView: EditText? = null + private var openDatabaseButtonView: View? = null // Adapter to manage database history list private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null @@ -84,7 +76,7 @@ class FileDatabaseSelectActivity : StylishActivity(), private var mOpenFileHelper: OpenFileHelper? = null - private var mDefaultPath: String? = null + private var progressDialogThread: ProgressDialogThread? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -98,44 +90,8 @@ class FileDatabaseSelectActivity : StylishActivity(), toolbar.title = "" setSupportActionBar(toolbar) - openFileNameView = findViewById(R.id.file_filename) - - // Set the initial value of the filename - mDefaultPath = (Environment.getExternalStorageDirectory().absolutePath - + getString(R.string.database_file_path_default) - + getString(R.string.database_file_name_default) - + getString(R.string.database_file_extension_default)) - openFileNameView?.setHint(R.string.open_link_database) - - // Button to expand file selection - fileSelectExpandableButtonView = findViewById(R.id.file_select_expandable_button) - fileSelectExpandableLayout = findViewById(R.id.file_select_expandable) - fileSelectExpandableButtonView?.setOnClickListener { _ -> - if (fileSelectExpandableLayout?.isExpanded == true) - fileSelectExpandableLayout?.collapse() - else - fileSelectExpandableLayout?.expand() - } - - // Open button - openButtonView = findViewById(R.id.open_database) - openButtonView?.setOnClickListener { _ -> - var fileName = openFileNameView?.text?.toString() ?: "" - mDefaultPath?.let { - if (fileName.isEmpty()) - fileName = it - } - UriUtil.parse(fileName)?.let { fileNameUri -> - launchPasswordActivityWithPath(fileNameUri) - } ?: run { - Log.e(TAG, "Unable to open the database link") - Snackbar.make(activity_file_selection_coordinator_layout, getString(R.string.error_can_not_handle_uri), Snackbar.LENGTH_LONG).asError().show() - null - } - } - // Create button - createButtonView = findViewById(R.id.create_database) + createButtonView = findViewById(R.id.create_database_button) if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/x-keepass" @@ -151,10 +107,8 @@ class FileDatabaseSelectActivity : StylishActivity(), createButtonView?.setOnClickListener { createNewFile() } mOpenFileHelper = OpenFileHelper(this) - browseButtonView = findViewById(R.id.browse_button) - browseButtonView?.setOnClickListener(mOpenFileHelper!!.getOpenFileOnClickViewListener { - UriUtil.parse(openFileNameView?.text?.toString()) - }) + openDatabaseButtonView = findViewById(R.id.open_database_button) + openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener) // History list val fileDatabaseHistoryRecyclerView = findViewById(R.id.file_list) @@ -207,6 +161,18 @@ class FileDatabaseSelectActivity : StylishActivity(), && savedInstanceState.containsKey(EXTRA_DATABASE_URI)) { mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI) } + + // Attach the dialog thread to this activity + progressDialogThread = ProgressDialogThread(this) { actionTask, _ -> + when (actionTask) { + ACTION_DATABASE_CREATE_TASK -> { + // TODO Check + // mAdapterDatabaseHistory?.notifyDataSetChanged() + // updateFileListVisibility() + GroupActivity.launch(this) + } + } + } } /** @@ -267,6 +233,23 @@ class FileDatabaseSelectActivity : StylishActivity(), }) } + private fun launchGroupActivity(readOnly: Boolean) { + EntrySelectionHelper.doEntrySelectionAction(intent, + { + GroupActivity.launch(this@FileDatabaseSelectActivity, readOnly) + }, + { + GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity, readOnly) + // Do not keep history + finish() + }, + { assistStructure -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity, assistStructure, readOnly) + } + }) + } + private fun launchPasswordActivityWithPath(databaseUri: Uri) { launchPasswordActivity(databaseUri, null) // Delete flickering for kitkat <= @@ -294,6 +277,11 @@ class FileDatabaseSelectActivity : StylishActivity(), } override fun onResume() { + val database = Database.getInstance() + if (database.loaded) { + launchGroupActivity(database.isReadOnly) + } + super.onResume() updateExternalStorageWarning() @@ -306,6 +294,16 @@ class FileDatabaseSelectActivity : StylishActivity(), mAdapterDatabaseHistory?.notifyDataSetChanged() } } + + // Register progress task + progressDialogThread?.registerProgressTask() + } + + override fun onPause() { + // Unregister progress task + progressDialogThread?.unregisterProgressTask() + + super.onPause() } override fun onSaveInstanceState(outState: Bundle) { @@ -331,21 +329,13 @@ class FileDatabaseSelectActivity : StylishActivity(), mDatabaseFileUri?.let { databaseUri -> // Create the new database - ProgressDialogThread(this@FileDatabaseSelectActivity, - { - CreateDatabaseRunnable(this@FileDatabaseSelectActivity, - databaseUri, - Database.getInstance(), - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile, - true, // TODO get readonly - LaunchGroupActivityFinish(databaseUri, keyFile) - ) - }, - R.string.progress_create) - .start() + progressDialogThread?.startDatabaseCreate( + databaseUri, + masterPasswordChecked, + masterPassword, + keyFileChecked, + keyFile + ) } } catch (e: Exception) { val error = getString(R.string.error_create_database_file) @@ -354,28 +344,6 @@ class FileDatabaseSelectActivity : StylishActivity(), } } - private inner class LaunchGroupActivityFinish(private val databaseFileUri: Uri, - private val keyFileUri: Uri?) : ActionRunnable() { - - override fun run() { - finishRun(true, null) - } - - override fun onFinishRun(result: Result) { - runOnUiThread { - if (result.isSuccess) { - // Add database to recent files - mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseFileUri, keyFileUri) - mAdapterDatabaseHistory?.notifyDataSetChanged() - updateFileListVisibility() - GroupActivity.launch(this@FileDatabaseSelectActivity) - } else { - Log.e(TAG, "Unable to open the database") - } - } - } - } - override fun onAssignKeyDialogNegativeClick( masterPasswordChecked: Boolean, masterPassword: String?, keyFileChecked: Boolean, keyFile: Uri?) { @@ -392,12 +360,7 @@ class FileDatabaseSelectActivity : StylishActivity(), mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data ) { uri -> if (uri != null) { - if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) { - launchPasswordActivityWithPath(uri) - } else { - fileSelectExpandableLayout?.expand(false) - openFileNameView?.setText(uri.toString()) - } + launchPasswordActivityWithPath(uri) } } @@ -405,7 +368,8 @@ class FileDatabaseSelectActivity : StylishActivity(), if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) { mDatabaseFileUri = data?.data if (mDatabaseFileUri != null) { - AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog") + AssignMasterKeyDialogFragment.getInstance(true) + .show(supportFragmentManager, "passwordDialog") } // else { // TODO Show error @@ -438,20 +402,15 @@ class FileDatabaseSelectActivity : StylishActivity(), }) if (!createDatabaseEducationPerformed) { // selectDatabaseEducationPerformed - browseButtonView != null + openDatabaseButtonView != null && fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation( - browseButtonView!!, + openDatabaseButtonView!!, {tapTargetView -> tapTargetView?.let { mOpenFileHelper?.openFileOnClickViewListener?.onClick(it) } }, - { - fileSelectExpandableButtonView?.let { - fileDatabaseSelectActivityEducation - .checkAndPerformedOpenLinkDatabaseEducation(it) - } - } + {} ) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index 1b5a29882..1cf9d7771 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -18,7 +18,6 @@ */ package com.kunzisoft.keepass.activities -import android.annotation.SuppressLint import android.app.Activity import android.app.SearchManager import android.app.assist.AssistStructure @@ -29,16 +28,19 @@ import android.graphics.Color import android.os.Build import android.os.Bundle import android.os.Handler -import androidx.annotation.RequiresApi -import androidx.fragment.app.FragmentManager -import androidx.appcompat.widget.SearchView -import androidx.appcompat.widget.Toolbar import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.ImageView import android.widget.TextView +import androidx.annotation.RequiresApi +import androidx.appcompat.view.ActionMode +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.Toolbar +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.fragment.app.FragmentManager +import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment @@ -47,37 +49,44 @@ import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.lock.LockingActivity -import com.kunzisoft.keepass.adapters.NodeAdapter import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.SortNodeEnum -import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread -import com.kunzisoft.keepass.database.action.node.* -import com.kunzisoft.keepass.database.action.node.ActionNodeDatabaseRunnable.Companion.NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY +import com.kunzisoft.keepass.database.action.ProgressDialogThread import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.magikeyboard.MagikIME +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.view.AddNodeButtonView -import net.cachapa.expandablelayout.ExpandableLayout +import com.kunzisoft.keepass.view.ToolbarAction +import com.kunzisoft.keepass.view.asError class GroupActivity : LockingActivity(), GroupEditDialogFragment.EditGroupListener, IconPickerDialogFragment.IconPickerListener, - NodeAdapter.NodeMenuListener, + ListNodesFragment.NodeClickListener, + ListNodesFragment.NodesActionMenuListener, ListNodesFragment.OnScrollListener, - NodeAdapter.NodeClickCallback, SortDialogFragment.SortSelectionListener { // Views + private var coordinatorLayout: CoordinatorLayout? = null private var toolbar: Toolbar? = null private var searchTitleView: View? = null - private var toolbarPasteExpandableLayout: ExpandableLayout? = null - private var toolbarPaste: Toolbar? = null + private var toolbarAction: ToolbarAction? = null private var iconView: ImageView? = null + private var numberChildrenView: TextView? = null private var modeTitleView: TextView? = null private var addNodeButtonView: AddNodeButtonView? = null private var groupNameView: TextView? = null @@ -87,12 +96,14 @@ class GroupActivity : LockingActivity(), private var mListNodesFragment: ListNodesFragment? = null private var mCurrentGroupIsASearch: Boolean = false + private var progressDialogThread: ProgressDialogThread? = null + // Nodes private var mRootGroup: GroupVersioned? = null private var mCurrentGroup: GroupVersioned? = null private var mOldGroupToUpdate: GroupVersioned? = null - private var mNodeToCopy: NodeVersioned? = null - private var mNodeToMove: NodeVersioned? = null + // TODO private var mNodeToCopy: NodeVersioned? = null + // TODO private var mNodeToMove: NodeVersioned? = null private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null @@ -110,15 +121,27 @@ class GroupActivity : LockingActivity(), setContentView(layoutInflater.inflate(R.layout.activity_group, null)) // Initialize views - iconView = findViewById(R.id.icon) + coordinatorLayout = findViewById(R.id.group_coordinator) + iconView = findViewById(R.id.group_icon) + numberChildrenView = findViewById(R.id.group_numbers) addNodeButtonView = findViewById(R.id.add_node_button) toolbar = findViewById(R.id.toolbar) searchTitleView = findViewById(R.id.search_title) groupNameView = findViewById(R.id.group_name) - toolbarPasteExpandableLayout = findViewById(R.id.expandable_toolbar_paste_layout) - toolbarPaste = findViewById(R.id.toolbar_paste) + toolbarAction = findViewById(R.id.toolbar_action) modeTitleView = findViewById(R.id.mode_title_view) + toolbar?.title = "" + setSupportActionBar(toolbar) + + /* + toolbarAction?.setNavigationOnClickListener { + toolbarAction?.collapse() + mNodeToCopy = null + mNodeToMove = null + } + */ + // Focus view to reinitialize timeout resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView) @@ -126,13 +149,6 @@ class GroupActivity : LockingActivity(), if (savedInstanceState != null) { if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY)) mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY) - if (savedInstanceState.containsKey(NODE_TO_COPY_KEY)) { - mNodeToCopy = savedInstanceState.getParcelable(NODE_TO_COPY_KEY) - toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener()) - } else if (savedInstanceState.containsKey(NODE_TO_MOVE_KEY)) { - mNodeToMove = savedInstanceState.getParcelable(NODE_TO_MOVE_KEY) - toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener()) - } } try { @@ -153,17 +169,6 @@ class GroupActivity : LockingActivity(), // Update last access time. mCurrentGroup?.touch(modified = false, touchParents = false) - toolbar?.title = "" - setSupportActionBar(toolbar) - - toolbarPaste?.inflateMenu(R.menu.node_paste_menu) - toolbarPaste?.setNavigationIcon(R.drawable.ic_arrow_left_white_24dp) - toolbarPaste?.setNavigationOnClickListener { - toolbarPasteExpandableLayout?.collapse() - mNodeToCopy = null - mNodeToMove = null - } - // Retrieve the textColor to tint the icon val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) mIconColor = taTextColor.getColor(0, Color.WHITE) @@ -197,9 +202,74 @@ class GroupActivity : LockingActivity(), } }) - // Search suggestion mDatabase?.let { database -> + // Search suggestion mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database) + + // Init dialog thread + progressDialogThread = ProgressDialogThread(this) { actionTask, result -> + + var oldNodes: List = ArrayList() + result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle -> + oldNodes = getListNodesFromBundle(database, oldNodesBundle) + } + var newNodes: List = ArrayList() + result.data?.getBundle(NEW_NODES_KEY)?.let { newNodesBundle -> + newNodes = getListNodesFromBundle(database, newNodesBundle) + } + + when (actionTask) { + ACTION_DATABASE_UPDATE_GROUP_TASK -> { + if (result.isSuccess) { + mListNodesFragment?.updateNodes(oldNodes, newNodes) + } + } + ACTION_DATABASE_CREATE_GROUP_TASK, + ACTION_DATABASE_COPY_NODES_TASK, + ACTION_DATABASE_MOVE_NODES_TASK -> { + if (result.isSuccess) { + mListNodesFragment?.addNodes(newNodes) + } + } + ACTION_DATABASE_DELETE_NODES_TASK -> { + if (result.isSuccess) { + + // Rebuild all the list the avoid bug when delete node from db sort + if (PreferencesUtil.getListSort(this@GroupActivity) == SortNodeEnum.DB) { + mListNodesFragment?.rebuildList() + } else { + // Use the old Nodes / entries unchanged with the old parent + mListNodesFragment?.removeNodes(oldNodes) + } + + // Add trash in views list if it doesn't exists + if (database.isRecycleBinEnabled) { + val recycleBin = database.recycleBin + if (mCurrentGroup != null && recycleBin != null + && mCurrentGroup!!.parent == null + && mCurrentGroup != recycleBin) { + if (mListNodesFragment?.contains(recycleBin) == true) + mListNodesFragment?.updateNode(recycleBin) + else + mListNodesFragment?.addNode(recycleBin) + } + } + } + } + } + + if (!result.isSuccess) { + result.exception?.errorId?.let { errorId -> + coordinatorLayout?.let { coordinatorLayout -> + Snackbar.make(coordinatorLayout, errorId, Snackbar.LENGTH_LONG).asError().show() + } + } + } + + finishNodeAction() + + refreshNumberOfChildren() + } } Log.i(TAG, "Finished creating tree") @@ -274,12 +344,6 @@ class GroupActivity : LockingActivity(), mOldGroupToUpdate?.let { outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it) } - mNodeToCopy?.let { - outState.putParcelable(NODE_TO_COPY_KEY, it) - } - mNodeToMove?.let { - outState.putParcelable(NODE_TO_MOVE_KEY, it) - } super.onSaveInstanceState(outState) } @@ -359,6 +423,9 @@ class GroupActivity : LockingActivity(), } } + // Assign number of children + refreshNumberOfChildren() + // Show selection mode message if needed if (mSelectionMode) { modeTitleView?.visibility = View.VISIBLE @@ -388,6 +455,17 @@ class GroupActivity : LockingActivity(), } } + private fun refreshNumberOfChildren() { + numberChildrenView?.apply { + if (PreferencesUtil.showNumberEntries(context)) { + text = mCurrentGroup?.getChildEntries(true)?.size?.toString() ?: "" + visibility = View.VISIBLE + } else { + visibility = View.GONE + } + } + } + override fun onScrolled(dy: Int) { addNodeButtonView?.hideButtonOnScrollListener(dy) } @@ -419,8 +497,10 @@ class GroupActivity : LockingActivity(), { // Build response with the entry selected if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) { - AutofillHelper.buildResponseWhenEntrySelected(this@GroupActivity, - entryVersioned.getEntryInfo(mDatabase!!)) + mDatabase?.let { database -> + AutofillHelper.buildResponseWhenEntrySelected(this@GroupActivity, + entryVersioned.getEntryInfo(database)) + } } finish() }) @@ -430,12 +510,36 @@ class GroupActivity : LockingActivity(), } } + private var actionNodeMode: ActionMode? = null + + private fun finishNodeAction() { + actionNodeMode?.finish() + actionNodeMode = null + } + + override fun onNodeSelected(nodes: List): Boolean { + if (nodes.isNotEmpty()) { + if (actionNodeMode == null || toolbarAction?.getSupportActionModeCallback() == null) { + mListNodesFragment?.actionNodesCallback(nodes, this)?.let { + actionNodeMode = toolbarAction?.startSupportActionMode(it) + } + } else { + actionNodeMode?.invalidate() + } + } else { + finishNodeAction() + } + return true + } + override fun onOpenMenuClick(node: NodeVersioned): Boolean { + finishNodeAction() onNodeClick(node) return true } override fun onEditMenuClick(node: NodeVersioned): Boolean { + finishNodeAction() when (node.type) { Type.GROUP -> { mOldGroupToUpdate = node as GroupVersioned @@ -448,132 +552,56 @@ class GroupActivity : LockingActivity(), return true } - override fun onCopyMenuClick(node: NodeVersioned): Boolean { - toolbarPasteExpandableLayout?.expand() - mNodeToCopy = node - toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener()) - return false - } - - private inner class OnCopyMenuItemClickListener : Toolbar.OnMenuItemClickListener { - override fun onMenuItemClick(item: MenuItem): Boolean { - toolbarPasteExpandableLayout?.collapse() + override fun onCopyMenuClick(nodes: List): Boolean { + actionNodeMode?.invalidate() - when (item.itemId) { - R.id.menu_paste -> { - when (mNodeToCopy?.type) { - Type.GROUP -> Log.e(TAG, "Copy not allowed for group") - Type.ENTRY -> { - mCurrentGroup?.let { currentGroup -> - copyEntry(mNodeToCopy as EntryVersioned, currentGroup) - } - } - } - mNodeToCopy = null - return true - } - } - return true - } + // Nothing here fragment calls onPasteMenuClick internally + return true } - private fun copyEntry(entryToCopy: EntryVersioned, newParent: GroupVersioned) { - ProgressDialogSaveDatabaseThread(this) { - CopyEntryRunnable(this, - Database.getInstance(), - entryToCopy, - newParent, - AfterAddNodeRunnable(), - !mReadOnly) - }.start() - } + override fun onMoveMenuClick(nodes: List): Boolean { + actionNodeMode?.invalidate() - override fun onMoveMenuClick(node: NodeVersioned): Boolean { - toolbarPasteExpandableLayout?.expand() - mNodeToMove = node - toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener()) - return false + // Nothing here fragment calls onPasteMenuClick internally + return true } - private inner class OnMoveMenuItemClickListener : Toolbar.OnMenuItemClickListener { - override fun onMenuItemClick(item: MenuItem): Boolean { - toolbarPasteExpandableLayout?.collapse() - - when (item.itemId) { - R.id.menu_paste -> { - when (mNodeToMove?.type) { - Type.GROUP -> { - mCurrentGroup?.let { currentGroup -> - moveGroup(mNodeToMove as GroupVersioned, currentGroup) - } - } - Type.ENTRY -> { - mCurrentGroup?.let { currentGroup -> - moveEntry(mNodeToMove as EntryVersioned, currentGroup) - } - } - } - mNodeToMove = null - return true + override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?, + nodes: List): Boolean { + when (pasteMode) { + ListNodesFragment.PasteMode.PASTE_FROM_COPY -> { + // Copy + mCurrentGroup?.let { newParent -> + progressDialogThread?.startDatabaseCopyNodes( + nodes, + newParent, + !mReadOnly + ) } } - return true - } - } - - private fun moveGroup(groupToMove: GroupVersioned, newParent: GroupVersioned) { - ProgressDialogSaveDatabaseThread(this) { - MoveGroupRunnable( - this, - Database.getInstance(), - groupToMove, - newParent, - AfterAddNodeRunnable(), - !mReadOnly) - }.start() - } - - private fun moveEntry(entryToMove: EntryVersioned, newParent: GroupVersioned) { - ProgressDialogSaveDatabaseThread(this) { - MoveEntryRunnable( - this, - Database.getInstance(), - entryToMove, - newParent, - AfterAddNodeRunnable(), - !mReadOnly) - }.start() - } - - override fun onDeleteMenuClick(node: NodeVersioned): Boolean { - when (node.type) { - Type.GROUP -> deleteGroup(node as GroupVersioned) - Type.ENTRY -> deleteEntry(node as EntryVersioned) + ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> { + // Move + mCurrentGroup?.let { newParent -> + progressDialogThread?.startDatabaseMoveNodes( + nodes, + newParent, + !mReadOnly + ) + } + } + else -> {} } + finishNodeAction() return true } - private fun deleteGroup(group: GroupVersioned) { - //TODO Verify trash recycle bin - ProgressDialogSaveDatabaseThread(this) { - DeleteGroupRunnable( - this, - Database.getInstance(), - group, - AfterDeleteNodeRunnable(), - !mReadOnly) - }.start() - } - - private fun deleteEntry(entry: EntryVersioned) { - ProgressDialogSaveDatabaseThread(this) { - DeleteEntryRunnable( - this, - Database.getInstance(), - entry, - AfterDeleteNodeRunnable(), - !mReadOnly) - }.start() + override fun onDeleteMenuClick(nodes: List): Boolean { + progressDialogThread?.startDatabaseDeleteNodes( + nodes, + !mReadOnly + ) + finishNodeAction() + return true } override fun onResume() { @@ -582,6 +610,16 @@ class GroupActivity : LockingActivity(), assignGroupViewElements() // Refresh suggestions to change preferences mSearchSuggestionAdapter?.reInit(this) + + progressDialogThread?.registerProgressTask() + } + + override fun onPause() { + progressDialogThread?.unregisterProgressTask() + + super.onPause() + + finishNodeAction() } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -713,7 +751,6 @@ class GroupActivity : LockingActivity(), override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?, name: String?, icon: PwIcon?) { - val database = Database.getInstance() if (name != null && name.isNotEmpty() && icon != null) { when (action) { @@ -721,104 +758,37 @@ class GroupActivity : LockingActivity(), // If group creation mCurrentGroup?.let { currentGroup -> // Build the group - database.createGroup()?.let { newGroup -> + mDatabase?.createGroup()?.let { newGroup -> newGroup.title = name newGroup.icon = icon // Not really needed here because added in runnable but safe newGroup.parent = currentGroup - // If group created save it in the database - ProgressDialogSaveDatabaseThread(this) { - AddGroupRunnable(this, - Database.getInstance(), - newGroup, - currentGroup, - AfterAddNodeRunnable(), - !mReadOnly) - }.start() + progressDialogThread?.startDatabaseCreateGroup( + newGroup, currentGroup, !mReadOnly) } } } - GroupEditDialogFragment.EditGroupDialogAction.UPDATE -> + GroupEditDialogFragment.EditGroupDialogAction.UPDATE -> { // If update add new elements mOldGroupToUpdate?.let { oldGroupToUpdate -> GroupVersioned(oldGroupToUpdate).let { updateGroup -> - updateGroup.title = name - // TODO custom icon - updateGroup.icon = icon + updateGroup.apply { + // WARNING remove parent and children to keep memory + removeParent() + removeChildren() // TODO concurrent exception - mListNodesFragment?.removeNode(oldGroupToUpdate) + title = name + this.icon = icon // TODO custom icon + } // If group updated save it in the database - ProgressDialogSaveDatabaseThread(this) { - UpdateGroupRunnable(this, - Database.getInstance(), - oldGroupToUpdate, - updateGroup, - AfterUpdateNodeRunnable(), - !mReadOnly) - }.start() - } - } - else -> { - } - } - } - } - - internal inner class AfterAddNodeRunnable : AfterActionNodeFinishRunnable() { - override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) { - runOnUiThread { - if (actionNodeValues.result.isSuccess) { - if (actionNodeValues.newNode != null) - mListNodesFragment?.addNode(actionNodeValues.newNode) - } - } - } - } - - internal inner class AfterUpdateNodeRunnable : AfterActionNodeFinishRunnable() { - override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) { - runOnUiThread { - if (actionNodeValues.result.isSuccess) { - if (actionNodeValues.oldNode!= null && actionNodeValues.newNode != null) - mListNodesFragment?.updateNode(actionNodeValues.oldNode, actionNodeValues.newNode) - } - } - } - } - - internal inner class AfterDeleteNodeRunnable : AfterActionNodeFinishRunnable() { - override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) { - runOnUiThread { - if (actionNodeValues.result.isSuccess) { - - // If the action register the position, use it to remove the entry view - val positionNode = actionNodeValues.result.data?.getInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY) - if (PreferencesUtil.getListSort(this@GroupActivity) == SortNodeEnum.DB - && positionNode != null) { - mListNodesFragment?.removeNodeAt(positionNode) - } else { - // else use the old Node that was the entry unchanged with the old parent - actionNodeValues.oldNode?.let { oldNode -> - mListNodesFragment?.removeNode(oldNode) - } - } - - // Add trash in views list if it doesn't exists - val database = Database.getInstance() - if (database.isRecycleBinEnabled) { - val recycleBin = database.recycleBin - if (mCurrentGroup != null && recycleBin != null - && mCurrentGroup!!.parent == null - && mCurrentGroup != recycleBin) { - if (mListNodesFragment?.contains(recycleBin) == true) - mListNodesFragment?.updateNode(recycleBin) - else - mListNodesFragment?.addNode(recycleBin) + progressDialogThread?.startDatabaseUpdateGroup( + oldGroupToUpdate, updateGroup, !mReadOnly) } } } + else -> {} } } } @@ -902,25 +872,29 @@ class GroupActivity : LockingActivity(), } override fun onBackPressed() { - // Normal way when we are not in root - if (mRootGroup != null && mRootGroup != mCurrentGroup) - super.onBackPressed() - // Else lock if needed - else { - if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) { - lockAndExit() + if (mListNodesFragment?.nodeActionSelectionMode == true) { + finishNodeAction() + } else { + // Normal way when we are not in root + if (mRootGroup != null && mRootGroup != mCurrentGroup) super.onBackPressed() - } else { - moveTaskToBack(true) + // Else lock if needed + else { + if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) { + lockAndExit() + super.onBackPressed() + } else { + moveTaskToBack(true) + } } - } - mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment - // to refresh fragment - mListNodesFragment?.rebuildList() - mCurrentGroup = mListNodesFragment?.mainGroup - removeSearchInIntent(intent) - assignGroupViewElements() + mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment + // to refresh fragment + mListNodesFragment?.rebuildList() + mCurrentGroup = mListNodesFragment?.mainGroup + removeSearchInIntent(intent) + assignGroupViewElements() + } } companion object { @@ -931,13 +905,15 @@ class GroupActivity : LockingActivity(), private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG" private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG" private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY" - private const val NODE_TO_COPY_KEY = "NODE_TO_COPY_KEY" - private const val NODE_TO_MOVE_KEY = "NODE_TO_MOVE_KEY" - private fun buildAndLaunchIntent(activity: Activity, group: GroupVersioned?, readOnly: Boolean, + private fun buildAndLaunchIntent(context: Context, group: GroupVersioned?, readOnly: Boolean, intentBuildLauncher: (Intent) -> Unit) { - if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { - val intent = Intent(activity, GroupActivity::class.java) + val checkTime = if (context is Activity) + TimeoutHelper.checkTimeAndLockIfTimeout(context) + else + TimeoutHelper.checkTime(context) + if (checkTime) { + val intent = Intent(context, GroupActivity::class.java) if (group != null) { intent.putExtra(GROUP_ID_KEY, group.nodeId) } @@ -953,10 +929,10 @@ class GroupActivity : LockingActivity(), */ @JvmOverloads - fun launch(activity: Activity, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) { - TimeoutHelper.recordTime(activity) - buildAndLaunchIntent(activity, null, readOnly) { intent -> - activity.startActivity(intent) + fun launch(context: Context, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) { + TimeoutHelper.recordTime(context) + buildAndLaunchIntent(context, null, readOnly) { intent -> + context.startActivity(intent) } } @@ -967,10 +943,10 @@ class GroupActivity : LockingActivity(), */ // TODO implement pre search to directly open the direct group - fun launchForKeyboarSelection(activity: Activity, readOnly: Boolean) { - TimeoutHelper.recordTime(activity) - buildAndLaunchIntent(activity, null, readOnly) { intent -> - EntrySelectionHelper.startActivityForEntrySelection(activity, intent) + fun launchForKeyboardSelection(context: Context, readOnly: Boolean) { + TimeoutHelper.recordTime(context) + buildAndLaunchIntent(context, null, readOnly) { intent -> + EntrySelectionHelper.startActivityForEntrySelection(context, intent) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt index 3346deb1f..470034783 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt @@ -14,6 +14,7 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.appcompat.view.ActionMode import com.kunzisoft.keepass.R import com.kunzisoft.keepass.adapters.NodeAdapter @@ -26,11 +27,12 @@ import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.Type +import java.util.* class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener { - private var nodeClickCallback: NodeAdapter.NodeClickCallback? = null - private var nodeMenuListener: NodeAdapter.NodeMenuListener? = null + private var nodeClickListener: NodeClickListener? = null private var onScrollListener: OnScrollListener? = null private var listView: RecyclerView? = null @@ -38,6 +40,13 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis private set private var mAdapter: NodeAdapter? = null + var nodeActionSelectionMode = false + private set + var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED + private set + private val listActionNodes = LinkedList() + private val listPasteNodes = LinkedList() + private var notFoundView: View? = null private var isASearchResult: Boolean = false @@ -56,22 +65,13 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis override fun onAttach(context: Context) { super.onAttach(context) try { - nodeClickCallback = context as NodeAdapter.NodeClickCallback + nodeClickListener = context as NodeClickListener } catch (e: ClassCastException) { // The activity doesn't implement the interface, throw exception throw ClassCastException(context.toString() + " must implement " + NodeAdapter.NodeClickCallback::class.java.name) } - try { - nodeMenuListener = context as NodeAdapter.NodeMenuListener - } catch (e: ClassCastException) { - nodeMenuListener = null - // Context menu can be omit - Log.w(TAG, context.toString() - + " must implement " + NodeAdapter.NodeMenuListener::class.java.name) - } - try { onScrollListener = context as OnScrollListener } catch (e: ClassCastException) { @@ -85,33 +85,58 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - activity?.let { currentActivity -> - setHasOptionsMenu(true) + setHasOptionsMenu(true) - readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments) + readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments) - arguments?.let { args -> - // Contains all the group in element - if (args.containsKey(GROUP_KEY)) { - mainGroup = args.getParcelable(GROUP_KEY) - } - if (args.containsKey(IS_SEARCH)) { - isASearchResult = args.getBoolean(IS_SEARCH) - } + arguments?.let { args -> + // Contains all the group in element + if (args.containsKey(GROUP_KEY)) { + mainGroup = args.getParcelable(GROUP_KEY) + } + if (args.containsKey(IS_SEARCH)) { + isASearchResult = args.getBoolean(IS_SEARCH) } + } - contextThemed?.let { context -> - mAdapter = NodeAdapter(context, currentActivity.menuInflater) - mAdapter?.apply { - setReadOnly(readOnly) - setIsASearchResult(isASearchResult) - setOnNodeClickListener(nodeClickCallback) - setActivateContextMenu(true) - setNodeMenuListener(nodeMenuListener) - } + contextThemed?.let { context -> + mAdapter = NodeAdapter(context) + mAdapter?.apply { + setOnNodeClickListener(object : NodeAdapter.NodeClickCallback { + override fun onNodeClick(node: NodeVersioned) { + if (nodeActionSelectionMode) { + if (listActionNodes.contains(node)) { + // Remove selected item if already selected + listActionNodes.remove(node) + } else { + // Add selected item if not already selected + listActionNodes.add(node) + } + nodeClickListener?.onNodeSelected(listActionNodes) + setActionNodes(listActionNodes) + notifyNodeChanged(node) + } else { + nodeClickListener?.onNodeClick(node) + } + } + + override fun onNodeLongClick(node: NodeVersioned): Boolean { + if (nodeActionPasteMode == PasteMode.UNDEFINED) { + // Select the first item after a long click + if (!listActionNodes.contains(node)) + listActionNodes.add(node) + + nodeClickListener?.onNodeSelected(listActionNodes) + + setActionNodes(listActionNodes) + notifyNodeChanged(node) + } + return true + } + }) } - prefs = PreferenceManager.getDefaultSharedPreferences(context) } + prefs = PreferenceManager.getDefaultSharedPreferences(context) } override fun onSaveInstanceState(outState: Bundle) { @@ -148,10 +173,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis activity?.intent?.let { selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it) } - // Force read only mode if selection mode - mAdapter?.apply { - setReadOnly(readOnly) - } // Refresh data mAdapter?.notifyDataSetChanged() @@ -207,7 +228,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis R.id.menu_sort -> { context?.let { context -> val sortDialogFragment: SortDialogFragment = - if (Database.getInstance().isRecycleBinAvailable + if (Database.getInstance().allowRecycleBin && Database.getInstance().isRecycleBinEnabled) { SortDialogFragment.getInstance( PreferencesUtil.getListSort(context), @@ -230,6 +251,102 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis } } + fun actionNodesCallback(nodes: List, + menuListener: NodesActionMenuListener?) : ActionMode.Callback { + + return object : ActionMode.Callback { + + override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + nodeActionSelectionMode = false + nodeActionPasteMode = PasteMode.UNDEFINED + return true + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + menu?.clear() + + if (nodeActionPasteMode != PasteMode.UNDEFINED) { + mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu) + } else { + nodeActionSelectionMode = true + mode?.menuInflater?.inflate(R.menu.node_menu, menu) + + val database = Database.getInstance() + + // Open and Edit for a single item + if (nodes.size == 1) { + // Edition + if (readOnly || nodes[0] == database.recycleBin) { + menu?.removeItem(R.id.menu_edit) + } + } else { + menu?.removeItem(R.id.menu_open) + menu?.removeItem(R.id.menu_edit) + } + + // Copy and Move (not for groups) + if (readOnly + || isASearchResult + || nodes.any { it == database.recycleBin } + || nodes.any { it.type == Type.GROUP }) { + // TODO COPY For Group + menu?.removeItem(R.id.menu_copy) + menu?.removeItem(R.id.menu_move) + } + + // Deletion + if (readOnly || nodes.any { it == database.recycleBin }) { + menu?.removeItem(R.id.menu_delete) + } + } + + // Add the number of items selected in title + mode?.title = nodes.size.toString() + + return true + } + + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { + if (menuListener == null) + return false + return when (item?.itemId) { + R.id.menu_open -> menuListener.onOpenMenuClick(nodes[0]) + R.id.menu_edit -> menuListener.onEditMenuClick(nodes[0]) + R.id.menu_copy -> { + nodeActionPasteMode = PasteMode.PASTE_FROM_COPY + mAdapter?.unselectActionNodes() + val returnValue = menuListener.onCopyMenuClick(nodes) + nodeActionSelectionMode = false + returnValue + } + R.id.menu_move -> { + nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE + mAdapter?.unselectActionNodes() + val returnValue = menuListener.onMoveMenuClick(nodes) + nodeActionSelectionMode = false + returnValue + } + R.id.menu_delete -> menuListener.onDeleteMenuClick(nodes) + R.id.menu_paste -> { + val returnValue = menuListener.onPasteMenuClick(nodeActionPasteMode, nodes) + nodeActionPasteMode = PasteMode.UNDEFINED + nodeActionSelectionMode = false + returnValue + } + else -> false + } + } + + override fun onDestroyActionMode(mode: ActionMode?) { + listActionNodes.clear() + listPasteNodes.clear() + mAdapter?.unselectActionNodes() + nodeActionPasteMode = PasteMode.UNDEFINED + nodeActionSelectionMode = false + } + } + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) @@ -260,18 +377,58 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis mAdapter?.addNode(newNode) } + fun addNodes(newNodes: List) { + mAdapter?.addNodes(newNodes) + } + fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned? = null) { mAdapter?.updateNode(oldNode, newNode ?: oldNode) } + fun updateNodes(oldNodes: List, newNodes: List) { + mAdapter?.updateNodes(oldNodes, newNodes) + } + fun removeNode(pwNode: NodeVersioned) { mAdapter?.removeNode(pwNode) } + fun removeNodes(nodes: List) { + mAdapter?.removeNodes(nodes) + } + fun removeNodeAt(position: Int) { mAdapter?.removeNodeAt(position) } + fun removeNodesAt(positions: IntArray) { + mAdapter?.removeNodesAt(positions) + } + + /** + * Callback listener to redefine to do an action when a node is click + */ + interface NodeClickListener { + fun onNodeClick(node: NodeVersioned) + fun onNodeSelected(nodes: List): Boolean + } + + /** + * Menu listener to redefine to do an action in menu + */ + interface NodesActionMenuListener { + fun onOpenMenuClick(node: NodeVersioned): Boolean + fun onEditMenuClick(node: NodeVersioned): Boolean + fun onCopyMenuClick(nodes: List): Boolean + fun onMoveMenuClick(nodes: List): Boolean + fun onDeleteMenuClick(nodes: List): Boolean + fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List): Boolean + } + + enum class PasteMode { + UNDEFINED, PASTE_FROM_COPY, PASTE_FROM_MOVE + } + interface OnScrollListener { /** diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt index 1df82893d..8340b7eff 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities import android.app.Activity import android.app.assist.AssistStructure import android.app.backup.BackupManager -import android.content.DialogInterface import android.content.Intent import android.content.SharedPreferences import android.net.Uri @@ -30,9 +29,6 @@ import android.os.Build import android.os.Bundle import android.os.Handler import android.preference.PreferenceManager -import androidx.annotation.RequiresApi -import com.google.android.material.snackbar.Snackbar -import androidx.appcompat.widget.Toolbar import android.text.Editable import android.text.TextWatcher import android.util.Log @@ -41,41 +37,50 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.inputmethod.EditorInfo.IME_ACTION_DONE -import android.widget.* +import android.widget.Button +import android.widget.CompoundButton +import android.widget.EditText +import android.widget.TextView +import androidx.annotation.RequiresApi +import androidx.appcompat.widget.Toolbar import androidx.biometric.BiometricManager +import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.dialogs.FingerPrintExplanationDialog -import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment +import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.OpenFileHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.stylish.StylishActivity -import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseEntity -import com.kunzisoft.keepass.utils.FileDatabaseInfo import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.autofill.AutofillHelper -import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable +import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager import com.kunzisoft.keepass.database.action.ProgressDialogThread import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException import com.kunzisoft.keepass.education.PasswordActivityEducation -import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_KEY +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.utils.FileDatabaseInfo import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.asError import kotlinx.android.synthetic.main.activity_password.* import java.io.FileNotFoundException -import java.lang.ref.WeakReference class PasswordActivity : StylishActivity() { // Views private var toolbar: Toolbar? = null + private var containerView: View? = null private var filenameView: TextView? = null private var passwordView: EditText? = null private var keyFileView: EditText? = null @@ -87,6 +92,8 @@ class PasswordActivity : StylishActivity() { private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null private var mDatabaseFileUri: Uri? = null + private var mDatabaseKeyFileUri: Uri? = null + private var prefs: SharedPreferences? = null private var mRememberKeyFile: Boolean = false @@ -94,6 +101,8 @@ class PasswordActivity : StylishActivity() { private var readOnly: Boolean = false + private var progressDialogThread: ProgressDialogThread? = null + private var advancedUnlockedManager: AdvancedUnlockedManager? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -101,8 +110,7 @@ class PasswordActivity : StylishActivity() { prefs = PreferenceManager.getDefaultSharedPreferences(this) - mRememberKeyFile = prefs!!.getBoolean(getString(R.string.keyfile_key), - resources.getBoolean(R.bool.keyfile_default)) + mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this) setContentView(R.layout.activity_password) @@ -112,6 +120,7 @@ class PasswordActivity : StylishActivity() { supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true) + containerView = findViewById(R.id.container) confirmButtonView = findViewById(R.id.pass_ok) filenameView = findViewById(R.id.filename) passwordView = findViewById(R.id.password) @@ -119,11 +128,11 @@ class PasswordActivity : StylishActivity() { checkboxPasswordView = findViewById(R.id.password_checkbox) checkboxKeyFileView = findViewById(R.id.keyfile_checkox) checkboxDefaultDatabaseView = findViewById(R.id.default_database) - advancedUnlockInfoView = findViewById(R.id.fingerprint_info) + advancedUnlockInfoView = findViewById(R.id.biometric_info) readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState) - val browseView = findViewById(R.id.browse_button) + val browseView = findViewById(R.id.open_database_button) mOpenFileHelper = OpenFileHelper(this@PasswordActivity) browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener) @@ -153,6 +162,91 @@ class PasswordActivity : StylishActivity() { enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ -> enableOrNotTheConfirmationButton() } + + progressDialogThread = ProgressDialogThread(this) { actionTask, result -> + when (actionTask) { + ACTION_DATABASE_LOAD_TASK -> { + // Recheck biometric if error + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) { + // Stay with the same mode and init it + advancedUnlockedManager?.initBiometricMode() + } + } + + // Remove the password in view in all cases + removePassword() + + if (result.isSuccess) { + launchGroupActivity() + } else { + var resultError = "" + val resultException = result.exception + val resultMessage = result.message + + if (resultException != null) { + resultError = resultException.getLocalizedMessage(resources) + + // Relaunch loading if we need to fix UUID + if (resultException is LoadDatabaseDuplicateUuidException) { + showLoadDatabaseDuplicateUuidMessage { + + var databaseUri: Uri? = null + var masterPassword: String? = null + var keyFileUri: Uri? = null + var readOnly = true + var cipherEntity: CipherDatabaseEntity? = null + + result.data?.let { resultData -> + databaseUri = resultData.getParcelable(DATABASE_URI_KEY) + masterPassword = resultData.getString(MASTER_PASSWORD_KEY) + keyFileUri = resultData.getParcelable(KEY_FILE_KEY) + readOnly = resultData.getBoolean(READ_ONLY_KEY) + cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY) + } + + databaseUri?.let { databaseFileUri -> + showProgressDialogAndLoadDatabase( + databaseFileUri, + masterPassword, + keyFileUri, + readOnly, + cipherEntity, + true) + } + } + } + } + + // Show error message + if (resultMessage != null && resultMessage.isNotEmpty()) { + resultError = "$resultError $resultMessage" + } + Log.e(TAG, resultError, resultException) + Snackbar.make(activity_password_coordinator_layout, + resultError, + Snackbar.LENGTH_LONG).asError().show() + } + } + } + } + } + + private fun launchGroupActivity() { + EntrySelectionHelper.doEntrySelectionAction(intent, + { + GroupActivity.launch(this@PasswordActivity, readOnly) + }, + { + GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly) + // Do not keep history + finish() + }, + { assistStructure -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly) + } + }) } private val onEditorActionListener = object : TextView.OnEditorActionListener { @@ -166,6 +260,9 @@ class PasswordActivity : StylishActivity() { } override fun onResume() { + if (Database.getInstance().loaded) + launchGroupActivity() + // If the database isn't accessible make sure to clear the password field, if it // was saved in the instance state if (Database.getInstance().loaded) { @@ -175,6 +272,8 @@ class PasswordActivity : StylishActivity() { // For check shutdown super.onResume() + progressDialogThread?.registerProgressTask() + initUriFromIntent() } @@ -190,17 +289,10 @@ class PasswordActivity : StylishActivity() { // If is a view intent val action = intent.action - if (action != null && action == VIEW_INTENT) { - - val databaseUriRetrieve = intent.data - // Stop activity here if we can't verify database URI - if (!UriUtil.verifyFileUri(databaseUriRetrieve)) { - Log.e(TAG, "File URI not validate") - finish() - } - databaseUri = databaseUriRetrieve + if (action != null + && action == VIEW_INTENT) { + databaseUri = intent.data keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE) - } else { databaseUri = intent.getParcelableExtra(KEY_FILENAME) keyFileUri = intent.getParcelableExtra(KEY_KEYFILE) @@ -222,6 +314,7 @@ class PasswordActivity : StylishActivity() { private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) { mDatabaseFileUri = databaseFileUri + mDatabaseKeyFileUri = keyFileUri // Define title databaseFileUri?.let { @@ -243,11 +336,13 @@ class PasswordActivity : StylishActivity() { newDefaultFileName = databaseFileUri ?: newDefaultFileName } - newDefaultFileName?.let { - prefs?.edit()?.apply { + prefs?.edit()?.apply { + newDefaultFileName?.let { putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString()) - apply() + } ?: kotlin.run { + remove(KEY_DEFAULT_DATABASE_PATH) } + apply() } val backupManager = BackupManager(this@PasswordActivity) @@ -273,15 +368,11 @@ class PasswordActivity : StylishActivity() { if (launchImmediately) { verifyCheckboxesAndLoadDatabase(password, keyFileUri) } else { - // Init FingerPrint elements - var fingerPrintInit = false + // Init Biometric elements + var biometricInitialize = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (PreferencesUtil.isBiometricUnlockEnable(this)) { - advancedUnlockInfoView?.setOnClickListener { - FingerPrintExplanationDialog().show(supportFragmentManager, "fingerPrintExplanationDialog") - } - if (advancedUnlockedManager == null && databaseFileUri != null) { advancedUnlockedManager = AdvancedUnlockedManager(this, databaseFileUri, @@ -303,18 +394,18 @@ class PasswordActivity : StylishActivity() { { passwordDecrypted -> // Load the database if password is retrieve from biometric passwordDecrypted?.let { - // Retrieve from fingerprint + // Retrieve from biometric verifyKeyFileCheckboxAndLoadDatabase(it) } }) } advancedUnlockedManager?.initBiometric() - fingerPrintInit = true + biometricInitialize = true } else { advancedUnlockedManager?.destroy() } } - if (!fingerPrintInit) { + if (!biometricInitialize) { checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener) } checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener) @@ -368,9 +459,8 @@ class PasswordActivity : StylishActivity() { } override fun onPause() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - advancedUnlockedManager?.pause() - } + progressDialogThread?.unregisterProgressTask() + super.onPause() } @@ -391,14 +481,18 @@ class PasswordActivity : StylishActivity() { keyFile: Uri?, cipherDatabaseEntity: CipherDatabaseEntity? = null) { val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password - val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile - loadDatabase(keyPassword, keyFileUri, cipherDatabaseEntity) + verifyKeyFileCheckbox(keyFile) + loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity) } private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) { val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString()) - val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile - loadDatabase(password, keyFileUri) + verifyKeyFileCheckbox(keyFile) + loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri) + } + + private fun verifyKeyFileCheckbox(keyFile: Uri?) { + mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile } private fun removePassword() { @@ -406,104 +500,51 @@ class PasswordActivity : StylishActivity() { checkboxPasswordView?.isChecked = false } - private fun loadDatabase(password: String?, keyFile: Uri?, cipherDatabaseEntity: CipherDatabaseEntity? = null) { + private fun loadDatabase(databaseFileUri: Uri?, + password: String?, + keyFileUri: Uri?, + cipherDatabaseEntity: CipherDatabaseEntity? = null) { - runOnUiThread { - if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) { - removePassword() - } + if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) { + removePassword() } - // Clear before we load - val database = Database.getInstance() - database.closeAndClear(applicationContext.filesDir) - - mDatabaseFileUri?.let { databaseUri -> + databaseFileUri?.let { databaseUri -> // Show the progress dialog and load the database - ProgressDialogThread(this, - { progressTaskUpdater -> - LoadDatabaseRunnable( - WeakReference(this@PasswordActivity), - database, - databaseUri, - password, - keyFile, - progressTaskUpdater, - AfterLoadingDatabase(database, password, cipherDatabaseEntity)) - }, - R.string.loading_database).start() + showProgressDialogAndLoadDatabase( + databaseUri, + password, + keyFileUri, + readOnly, + cipherDatabaseEntity, + false) } } - /** - * Called after verify and try to opening the database - */ - private inner class AfterLoadingDatabase(val database: Database, val password: String?, - val cipherDatabaseEntity: CipherDatabaseEntity? = null) - : ActionRunnable() { - - override fun onFinishRun(result: Result) { - runOnUiThread { - // Recheck fingerprint if error - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) { - // Stay with the same mode and init it - advancedUnlockedManager?.initBiometricMode() - } - } - - if (result.isSuccess) { - // Remove the password in view in all cases - removePassword() - - // Register the biometric - if (cipherDatabaseEntity != null) { - CipherDatabaseAction.getInstance(this@PasswordActivity) - .addOrUpdateCipherDatabase(cipherDatabaseEntity) { - checkAndLaunchGroupActivity(database, password) - } - } else { - checkAndLaunchGroupActivity(database, password) - } - - } else { - if (result.message != null && result.message!!.isNotEmpty()) { - Snackbar.make(activity_password_coordinator_layout, result.message!!, Snackbar.LENGTH_LONG).asError().show() - } - } - } - } + private fun showProgressDialogAndLoadDatabase(databaseUri: Uri, + password: String?, + keyFile: Uri?, + readOnly: Boolean, + cipherDatabaseEntity: CipherDatabaseEntity?, + fixDuplicateUUID: Boolean) { + progressDialogThread?.startDatabaseLoad( + databaseUri, + password, + keyFile, + readOnly, + cipherDatabaseEntity, + fixDuplicateUUID + ) } - private fun checkAndLaunchGroupActivity(database: Database, password: String?) { - if (database.validatePasswordEncoding(password)) { - launchGroupActivity() - } else { - PasswordEncodingDialogFragment().apply { - positiveButtonClickListener = DialogInterface.OnClickListener { _, _ -> - launchGroupActivity() - } - show(supportFragmentManager, "passwordEncodingTag") - } - } + private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) { + DuplicateUuidDialog().apply { + positiveAction = loadDatabaseWithFix + }.show(supportFragmentManager, "duplicateUUIDDialog") } - private fun launchGroupActivity() { - EntrySelectionHelper.doEntrySelectionAction(intent, - { - GroupActivity.launch(this@PasswordActivity, readOnly) - }, - { - GroupActivity.launchForKeyboarSelection(this@PasswordActivity, readOnly) - // Do not keep history - finish() - }, - { assistStructure -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly) - } - }) - } + // To fix multiple view education + private var performedEductionInProgress = false override fun onCreateOptionsMenu(menu: Menu): Boolean { val inflater = menuInflater @@ -514,23 +555,27 @@ class PasswordActivity : StylishActivity() { MenuUtil.defaultMenuInflater(inflater, menu) if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // Fingerprint menu + // biometric menu advancedUnlockedManager?.inflateOptionsMenu(inflater, menu) } super.onCreateOptionsMenu(menu) - // Show education views - Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) } + if (!performedEductionInProgress) { + performedEductionInProgress = true + // Show education views + Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) } + } return true } private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation, menu: Menu) { - val unlockEducationPerformed = toolbar != null + val educationContainerView = containerView + val unlockEducationPerformed = educationContainerView != null && passwordActivityEducation.checkAndPerformedUnlockEducation( - toolbar!!, + educationContainerView, { performedNextEducation(passwordActivityEducation, menu) }, @@ -538,11 +583,11 @@ class PasswordActivity : StylishActivity() { performedNextEducation(passwordActivityEducation, menu) }) if (!unlockEducationPerformed) { - - val readOnlyEducationPerformed = toolbar != null - && toolbar!!.findViewById(R.id.menu_open_file_read_mode_key) != null + val educationToolbar = toolbar + val readOnlyEducationPerformed = + educationToolbar?.findViewById(R.id.menu_open_file_read_mode_key) != null && passwordActivityEducation.checkAndPerformedReadOnlyEducation( - toolbar!!.findViewById(R.id.menu_open_file_read_mode_key), + educationToolbar.findViewById(R.id.menu_open_file_read_mode_key), { onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key)) performedNextEducation(passwordActivityEducation, menu) @@ -554,12 +599,12 @@ class PasswordActivity : StylishActivity() { if (!readOnlyEducationPerformed) { val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate() - // fingerprintEducationPerformed + // EducationPerformed Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && PreferencesUtil.isBiometricUnlockEnable(applicationContext) && (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) && advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null - && passwordActivityEducation.checkAndPerformedFingerprintEducation(advancedUnlockInfoView?.unlockIconImageView!!) + && passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!) } } @@ -583,7 +628,7 @@ class PasswordActivity : StylishActivity() { readOnly = !readOnly changeOpenFileReadIcon(item) } - R.id.menu_fingerprint_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + R.id.menu_biometric_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { advancedUnlockedManager?.deleteEntryKey() } else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt index a71a8f09b..f78a239ea 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt @@ -45,6 +45,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() { private var rootView: View? = null private var passwordCheckBox: CompoundButton? = null + + private var passwordTextInputLayout: TextInputLayout? = null private var passwordView: TextView? = null private var passwordRepeatTextInputLayout: TextInputLayout? = null private var passwordRepeatView: TextView? = null @@ -96,6 +98,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { activity?.let { activity -> + + var allowNoMasterKey = false + arguments?.apply { + if (containsKey(ALLOW_NO_MASTER_KEY_ARG)) + allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false) + } + val builder = AlertDialog.Builder(activity) val inflater = activity.layoutInflater @@ -104,9 +113,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() { .setTitle(R.string.assign_master_key) // Add action buttons .setPositiveButton(android.R.string.ok) { _, _ -> } - .setNegativeButton(R.string.cancel) { _, _ -> } + .setNegativeButton(android.R.string.cancel) { _, _ -> } passwordCheckBox = rootView?.findViewById(R.id.password_checkbox) + passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout) passwordView = rootView?.findViewById(R.id.pass_password) passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout) passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password) @@ -116,7 +126,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() { keyFileView = rootView?.findViewById(R.id.pass_keyfile) mOpenFileHelper = OpenFileHelper(this) - rootView?.findViewById(R.id.browse_button)?.setOnClickListener { view -> + rootView?.findViewById(R.id.open_database_button)?.setOnClickListener { view -> mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) } val dialog = builder.create() @@ -132,7 +142,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() { var error = verifyPassword() || verifyFile() if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) { error = true - showNoKeyConfirmationDialog() + if (allowNoMasterKey) + showNoKeyConfirmationDialog() + else { + passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials) + } } if (!error) { mListener?.onAssignKeyDialogPositiveClick( @@ -193,6 +207,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() { showEmptyPasswordConfirmationDialog() } } + return error } @@ -223,7 +238,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() { this@AssignMasterKeyDialogFragment.dismiss() } } - .setNegativeButton(R.string.cancel) { _, _ -> } + .setNegativeButton(android.R.string.cancel) { _, _ -> } builder.create().show() } } @@ -238,7 +253,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() { keyFileCheckBox!!.isChecked, mKeyFile) this@AssignMasterKeyDialogFragment.dismiss() } - .setNegativeButton(R.string.cancel) { _, _ -> } + .setNegativeButton(android.R.string.cancel) { _, _ -> } builder.create().show() } } @@ -255,4 +270,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() { } } } + + companion object { + + private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG" + + fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment { + val fragment = AssignMasterKeyDialogFragment() + val args = Bundle() + args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey) + fragment.arguments = args + return fragment + } + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt index a981e04d0..3dd5514ac 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt @@ -36,7 +36,7 @@ class BrowserDialogFragment : DialogFragment() { // Get the layout inflater val root = activity.layoutInflater.inflate(R.layout.fragment_browser_install, null) builder.setView(root) - .setNegativeButton(R.string.cancel) { _, _ -> } + .setNegativeButton(android.R.string.cancel) { _, _ -> } val textDescription = root.findViewById(R.id.file_manager_install_description) textDescription.text = getString(R.string.file_manager_install_description) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DuplicateUuidDialog.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DuplicateUuidDialog.kt new file mode 100644 index 000000000..219dd9c01 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DuplicateUuidDialog.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.activities.dialogs + +import android.app.Dialog +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import com.kunzisoft.keepass.R + +class DuplicateUuidDialog : DialogFragment() { + + var positiveAction: (() -> Unit)? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + activity?.let { activity -> + // Use the Builder class for convenient dialog construction + val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply { + val message = getString(R.string.contains_duplicate_uuid) + + "\n\n" + getString(R.string.contains_duplicate_uuid_procedure) + setMessage(message) + setPositiveButton(getString(android.R.string.ok)) { _, _ -> + positiveAction?.invoke() + dismiss() + } + setNegativeButton(getString(android.R.string.cancel)) { _, _ -> dismiss() } + } + // Create the AlertDialog object and return it + return builder.create() + } + return super.onCreateDialog(savedInstanceState) + } + + override fun onPause() { + super.onPause() + this.dismiss() + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/FingerPrintExplanationDialog.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/FingerPrintExplanationDialog.kt deleted file mode 100644 index d78a51162..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/FingerPrintExplanationDialog.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.activities.dialogs - -import android.app.Dialog -import android.content.Intent -import android.os.Build -import android.os.Bundle -import androidx.annotation.RequiresApi -import androidx.fragment.app.DialogFragment -import androidx.appcompat.app.AlertDialog -import android.view.View -import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.biometric.FingerPrintAnimatedVector -import com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity - -@RequiresApi(api = Build.VERSION_CODES.M) -class FingerPrintExplanationDialog : DialogFragment() { - - private var fingerPrintAnimatedVector: FingerPrintAnimatedVector? = null - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - activity?.let { activity -> - val builder = AlertDialog.Builder(activity) - val inflater = activity.layoutInflater - - val rootView = inflater.inflate(R.layout.fragment_fingerprint_explanation, null) - - rootView.findViewById(R.id.fingerprint_setting_link_text).setOnClickListener { - startActivity(Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS)) - } - - rootView.findViewById(R.id.auto_open_biometric_prompt_button).setOnClickListener { - startActivity(Intent(activity, SettingsAdvancedUnlockActivity::class.java)) - } - - fingerPrintAnimatedVector = FingerPrintAnimatedVector(activity, - rootView.findViewById(R.id.biometric_image)) - - builder.setView(rootView) - .setPositiveButton(android.R.string.ok) { _, _ -> } - return builder.create() - } - return super.onCreateDialog(savedInstanceState) - } - - override fun onResume() { - super.onResume() - fingerPrintAnimatedVector?.startScan() - } - - override fun onPause() { - super.onPause() - fingerPrintAnimatedVector?.stopScan() - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt index 350049fba..156a0be88 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt @@ -114,7 +114,7 @@ class GeneratePasswordDialogFragment : DialogFragment() { dismiss() } - .setNegativeButton(R.string.cancel) { _, _ -> + .setNegativeButton(android.R.string.cancel) { _, _ -> val bundle = Bundle() mListener?.cancelPassword(bundle) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt index 032f80cb8..8f126d083 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt @@ -122,7 +122,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP val builder = AlertDialog.Builder(activity) builder.setView(root) .setPositiveButton(android.R.string.ok, null) - .setNegativeButton(R.string.cancel) { _, _ -> + .setNegativeButton(android.R.string.cancel) { _, _ -> editGroupListener?.cancelEditGroup( editGroupDialogAction, nameTextView?.text?.toString(), diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt index 1e218ccff..7b0691ba9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt @@ -77,7 +77,7 @@ class IconPickerDialogFragment : DialogFragment() { dismiss() } - builder.setNegativeButton(R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() } + builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() } return builder.create() } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/KeyboardExplanationDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/KeyboardExplanationDialogFragment.kt deleted file mode 100644 index c00c80b7e..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/KeyboardExplanationDialogFragment.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.activities.dialogs - -import android.app.Dialog -import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.os.Bundle -import android.provider.Settings -import androidx.fragment.app.DialogFragment -import androidx.appcompat.app.AlertDialog -import android.view.View -import com.kunzisoft.keepass.BuildConfig -import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.utils.UriUtil - -class KeyboardExplanationDialogFragment : DialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - activity?.let { - val builder = AlertDialog.Builder(activity!!) - val inflater = activity!!.layoutInflater - - val rootView = inflater.inflate(R.layout.fragment_keyboard_explanation, null) - - rootView.findViewById(R.id.keyboards_activate_device_setting_button) - .setOnClickListener { launchActivateKeyboardSetting() } - - val containerKeyboardSwitcher = rootView.findViewById(R.id.container_keyboard_switcher) - if (BuildConfig.CLOSED_STORE) { - containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_play_store) } - } else { - containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_f_droid) } - } - - builder.setView(rootView) - .setPositiveButton(android.R.string.ok) { _, _ -> } - return builder.create() - } - return super.onCreateDialog(savedInstanceState) - } - - private fun launchActivateKeyboardSetting() { - val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS) - intent.addFlags(FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt index 87b1baa87..4cb8f1234 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt @@ -35,7 +35,7 @@ class PasswordEncodingDialogFragment : DialogFragment() { val builder = AlertDialog.Builder(activity) builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning) builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener) - builder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.cancel() } + builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() } return builder.create() } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt new file mode 100644 index 000000000..5cb840453 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt @@ -0,0 +1,381 @@ +package com.kunzisoft.keepass.activities.dialogs + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.Spinner +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.google.android.material.textfield.TextInputLayout +import com.kunzisoft.keepass.BuildConfig +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.model.OtpModel +import com.kunzisoft.keepass.otp.OtpElement +import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_HOTP_COUNTER +import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_OTP_DIGITS +import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD +import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER +import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS +import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD +import com.kunzisoft.keepass.otp.OtpTokenType +import com.kunzisoft.keepass.otp.OtpType +import com.kunzisoft.keepass.otp.TokenCalculator + +class SetOTPDialogFragment : DialogFragment() { + + private var mCreateOTPElementListener: CreateOtpListener? = null + + private var mOtpElement: OtpElement = OtpElement() + + private var otpTypeSpinner: Spinner? = null + private var otpTokenTypeSpinner: Spinner? = null + private var otpSecretContainer: TextInputLayout? = null + private var otpSecretTextView: EditText? = null + private var otpPeriodContainer: TextInputLayout? = null + private var otpPeriodTextView: EditText? = null + private var otpCounterContainer: TextInputLayout? = null + private var otpCounterTextView: EditText? = null + private var otpDigitsContainer: TextInputLayout? = null + private var otpDigitsTextView: EditText? = null + private var otpAlgorithmSpinner: Spinner? = null + + private var otpTypeAdapter: ArrayAdapter? = null + private var otpTokenTypeAdapter: ArrayAdapter? = null + private var totpTokenTypeAdapter: ArrayAdapter? = null + private var hotpTokenTypeAdapter: ArrayAdapter? = null + private var otpAlgorithmAdapter: ArrayAdapter? = null + + private var mManualEvent = false + private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus -> + if (!isFocus) + mManualEvent = true + } + private var mOnTouchListener = View.OnTouchListener { _, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + mManualEvent = true + } + } + false + } + + private var mSecretWellFormed = false + private var mCounterWellFormed = true + private var mPeriodWellFormed = true + private var mDigitsWellFormed = true + + override fun onAttach(context: Context) { + super.onAttach(context) + // Verify that the host activity implements the callback interface + try { + // Instantiate the NoticeDialogListener so we can send events to the host + mCreateOTPElementListener = context as CreateOtpListener + } catch (e: ClassCastException) { + // The activity doesn't implement the interface, throw exception + throw ClassCastException(context.toString() + + " must implement " + CreateOtpListener::class.java.name) + } + } + + @SuppressLint("ClickableViewAccessibility") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + + // Retrieve OTP model from instance state + if (savedInstanceState != null) { + if (savedInstanceState.containsKey(KEY_OTP)) { + savedInstanceState.getParcelable(KEY_OTP)?.let { otpModel -> + mOtpElement = OtpElement(otpModel) + } + } + } else { + arguments?.apply { + if (containsKey(KEY_OTP)) { + getParcelable(KEY_OTP)?.let { otpModel -> + mOtpElement = OtpElement(otpModel) + } + } + } + } + + activity?.let { activity -> + val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup? + otpTypeSpinner = root?.findViewById(R.id.setup_otp_type) + otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type) + otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label) + otpSecretTextView = root?.findViewById(R.id.setup_otp_secret) + otpAlgorithmSpinner = root?.findViewById(R.id.setup_otp_algorithm) + otpPeriodContainer= root?.findViewById(R.id.setup_otp_period_label) + otpPeriodTextView = root?.findViewById(R.id.setup_otp_period) + otpCounterContainer= root?.findViewById(R.id.setup_otp_counter_label) + otpCounterTextView = root?.findViewById(R.id.setup_otp_counter) + otpDigitsContainer = root?.findViewById(R.id.setup_otp_digits_label) + otpDigitsTextView = root?.findViewById(R.id.setup_otp_digits) + + // To fix init element + // With tab keyboard selection + otpSecretTextView?.onFocusChangeListener = mOnFocusChangeListener + // With finger selection + otpTypeSpinner?.setOnTouchListener(mOnTouchListener) + otpTokenTypeSpinner?.setOnTouchListener(mOnTouchListener) + otpSecretTextView?.setOnTouchListener(mOnTouchListener) + otpAlgorithmSpinner?.setOnTouchListener(mOnTouchListener) + otpPeriodTextView?.setOnTouchListener(mOnTouchListener) + otpCounterTextView?.setOnTouchListener(mOnTouchListener) + otpDigitsTextView?.setOnTouchListener(mOnTouchListener) + + + // HOTP / TOTP Type selection + val otpTypeArray = OtpType.values() + otpTypeAdapter = ArrayAdapter(activity, + android.R.layout.simple_spinner_item, otpTypeArray).apply { + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + } + otpTypeSpinner?.adapter = otpTypeAdapter + + // Otp Token type selection + val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues() + hotpTokenTypeAdapter = ArrayAdapter(activity, + android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply { + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + } + // Proprietary only on closed and full version + val totpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues( + BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION) + totpTokenTypeAdapter = ArrayAdapter(activity, + android.R.layout.simple_spinner_item, totpTokenTypeArray).apply { + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + } + otpTokenTypeAdapter = hotpTokenTypeAdapter + otpTokenTypeSpinner?.adapter = otpTokenTypeAdapter + + // OTP Algorithm + val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values() + otpAlgorithmAdapter = ArrayAdapter(activity, + android.R.layout.simple_spinner_item, otpAlgorithmArray).apply { + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + } + otpAlgorithmSpinner?.adapter = otpAlgorithmAdapter + + // Set the default value of OTP element + upgradeType() + upgradeTokenType() + upgradeParameters() + + attachListeners() + + val builder = AlertDialog.Builder(activity) + builder.apply { + setTitle(R.string.entry_setup_otp) + setView(root) + .setPositiveButton(android.R.string.ok) {_, _ -> } + .setNegativeButton(android.R.string.cancel) { _, _ -> + } + } + + return builder.create() + } + return super.onCreateDialog(savedInstanceState) + } + + override fun onResume() { + super.onResume() + (dialog as AlertDialog).getButton(Dialog.BUTTON_POSITIVE).setOnClickListener { + if (mSecretWellFormed + && mCounterWellFormed + && mPeriodWellFormed + && mDigitsWellFormed) { + mCreateOTPElementListener?.onOtpCreated(mOtpElement) + dismiss() + } + } + } + + private fun attachListeners() { + // Set Type listener + otpTypeSpinner?.onItemSelectedListener = object: AdapterView.OnItemSelectedListener { + override fun onNothingSelected(parent: AdapterView<*>?) {} + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + if (mManualEvent) { + (parent?.selectedItem as OtpType?)?.let { + mOtpElement.type = it + upgradeTokenType() + } + } + } + } + + // Set type token listener + otpTokenTypeSpinner?.onItemSelectedListener = object: AdapterView.OnItemSelectedListener { + override fun onNothingSelected(parent: AdapterView<*>?) {} + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + if (mManualEvent) { + (parent?.selectedItem as OtpTokenType?)?.let { + mOtpElement.tokenType = it + upgradeParameters() + } + } + } + } + + // Set algorithm spinner + otpAlgorithmSpinner?.onItemSelectedListener = object: AdapterView.OnItemSelectedListener { + override fun onNothingSelected(parent: AdapterView<*>?) {} + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + if (mManualEvent) { + (parent?.selectedItem as TokenCalculator.HashAlgorithm?)?.let { + mOtpElement.algorithm = it + } + } + } + } + + // Set secret in OtpElement + otpSecretTextView?.addTextChangedListener(object: TextWatcher { + override fun afterTextChanged(s: Editable?) { + s?.toString()?.let { userString -> + try { + mOtpElement.setBase32Secret(userString) + otpSecretContainer?.error = null + } catch (exception: Exception) { + otpSecretContainer?.error = getString(R.string.error_otp_secret_key) + } + mSecretWellFormed = otpSecretContainer?.error == null + } + } + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + + // Set counter in OtpElement + otpCounterTextView?.addTextChangedListener(object: TextWatcher { + override fun afterTextChanged(s: Editable?) { + if (mManualEvent) { + s?.toString()?.toLongOrNull()?.let { + try { + mOtpElement.counter = it + otpCounterContainer?.error = null + } catch (exception: Exception) { + otpCounterContainer?.error = getString(R.string.error_otp_counter, + MIN_HOTP_COUNTER, MAX_HOTP_COUNTER) + } + mCounterWellFormed = otpCounterContainer?.error == null + } + } + } + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + + // Set period in OtpElement + otpPeriodTextView?.addTextChangedListener(object: TextWatcher { + override fun afterTextChanged(s: Editable?) { + if (mManualEvent) { + s?.toString()?.toIntOrNull()?.let { + try { + mOtpElement.period = it + otpPeriodContainer?.error = null + } catch (exception: Exception) { + otpPeriodContainer?.error = getString(R.string.error_otp_period, + MIN_TOTP_PERIOD, MAX_TOTP_PERIOD) + } + mPeriodWellFormed = otpPeriodContainer?.error == null + } + } + } + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + + // Set digits in OtpElement + otpDigitsTextView?.addTextChangedListener(object: TextWatcher { + override fun afterTextChanged(s: Editable?) { + if (mManualEvent) { + s?.toString()?.toIntOrNull()?.let { + try { + mOtpElement.digits = it + otpDigitsContainer?.error = null + } catch (exception: Exception) { + otpDigitsContainer?.error = getString(R.string.error_otp_digits, + MIN_OTP_DIGITS, MAX_OTP_DIGITS) + } + mDigitsWellFormed = otpDigitsContainer?.error == null + } + } + } + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + } + + private fun upgradeType() { + otpTypeSpinner?.setSelection(OtpType.values().indexOf(mOtpElement.type)) + } + + private fun upgradeTokenType() { + when (mOtpElement.type) { + OtpType.HOTP -> { + otpPeriodContainer?.visibility = View.GONE + otpCounterContainer?.visibility = View.VISIBLE + otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter + otpTokenTypeSpinner?.setSelection(OtpTokenType + .getHotpTokenTypeValues().indexOf(mOtpElement.tokenType)) + } + OtpType.TOTP -> { + otpPeriodContainer?.visibility = View.VISIBLE + otpCounterContainer?.visibility = View.GONE + otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter + otpTokenTypeSpinner?.setSelection(OtpTokenType + .getTotpTokenTypeValues().indexOf(mOtpElement.tokenType)) + } + } + } + + private fun upgradeParameters() { + otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values() + .indexOf(mOtpElement.algorithm)) + otpSecretTextView?.apply { + setText(mOtpElement.getBase32Secret()) + // Cursor at end + setSelection(this.text.length) + } + otpCounterTextView?.setText(mOtpElement.counter.toString()) + otpPeriodTextView?.setText(mOtpElement.period.toString()) + otpDigitsTextView?.setText(mOtpElement.digits.toString()) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putParcelable(KEY_OTP, mOtpElement.otpModel) + } + + interface CreateOtpListener { + fun onOtpCreated(otpElement: OtpElement) + } + + companion object { + + private const val KEY_OTP = "KEY_OTP" + + fun build(otpModel: OtpModel? = null): SetOTPDialogFragment { + return SetOTPDialogFragment().apply { + if (otpModel != null) { + arguments = Bundle().apply { + putParcelable(KEY_OTP, otpModel) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt index 7f67091ab..9c17defbb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt @@ -83,7 +83,7 @@ class SortDialogFragment : DialogFragment() { // Add action buttons .setPositiveButton(android.R.string.ok ) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) } - .setNegativeButton(R.string.cancel) { _, _ -> } + .setNegativeButton(android.R.string.cancel) { _, _ -> } val ascendingView = rootView.findViewById(R.id.sort_selection_ascending) // Check if is ascending or descending diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt index e046223f6..dc04e3291 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt @@ -1,7 +1,7 @@ package com.kunzisoft.keepass.activities.helpers -import android.app.Activity import android.app.assist.AssistStructure +import android.content.Context import android.content.Intent import android.os.Build import com.kunzisoft.keepass.autofill.AutofillHelper @@ -11,10 +11,10 @@ object EntrySelectionHelper { private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE" private const val DEFAULT_ENTRY_SELECTION_MODE = false - fun startActivityForEntrySelection(activity: Activity, intent: Intent) { + fun startActivityForEntrySelection(context: Context, intent: Intent) { addEntrySelectionModeExtraInIntent(intent) // only to avoid visible flickering when redirecting - activity.startActivity(intent) + context.startActivity(intent) } fun addEntrySelectionModeExtraInIntent(intent: Intent) { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/OpenFileHelper.kt b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/OpenFileHelper.kt index 52ebf4bb5..c9f09f0a4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/OpenFileHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/OpenFileHelper.kt @@ -19,6 +19,7 @@ */ package com.kunzisoft.keepass.activities.helpers +import android.annotation.SuppressLint import android.app.Activity import android.app.Activity.RESULT_OK import android.content.Context @@ -26,10 +27,10 @@ import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity import android.util.Log import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment import com.kunzisoft.keepass.utils.UriUtil @@ -39,7 +40,7 @@ class OpenFileHelper { private var fragment: Fragment? = null val openFileOnClickViewListener: OpenFileOnClickViewListener - get() = OpenFileOnClickViewListener(null) + get() = OpenFileOnClickViewListener() constructor(context: Activity) { this.activity = context @@ -51,7 +52,7 @@ class OpenFileHelper { this.fragment = context } - inner class OpenFileOnClickViewListener(private val dataUri: (() -> Uri?)?) : View.OnClickListener { + inner class OpenFileOnClickViewListener : View.OnClickListener { override fun onClick(v: View) { try { @@ -62,58 +63,50 @@ class OpenFileHelper { } } catch (e: Exception) { Log.e(TAG, "Enable to start the file picker activity", e) - - // Open File picker if can't open activity - if (lookForOpenIntentsFilePicker(dataUri?.invoke())) + // Open browser dialog + if (lookForOpenIntentsFilePicker()) showBrowserDialog() } } } + @SuppressLint("InlinedApi") private fun openActivityWithActionOpenDocument() { - val i = Intent(ACTION_OPEN_DOCUMENT) - i.addCategory(Intent.CATEGORY_OPENABLE) - i.type = "*/*" - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION or - Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - } else { - i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" + flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION } if (fragment != null) - fragment?.startActivityForResult(i, OPEN_DOC) + fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC) else - activity?.startActivityForResult(i, OPEN_DOC) + activity?.startActivityForResult(intentOpenDocument, OPEN_DOC) } + @SuppressLint("InlinedApi") private fun openActivityWithActionGetContent() { - val i = Intent(Intent.ACTION_GET_CONTENT) - i.addCategory(Intent.CATEGORY_OPENABLE) - i.type = "*/*" + val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" + flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + } if (fragment != null) - fragment?.startActivityForResult(i, GET_CONTENT) + fragment?.startActivityForResult(intentGetContent, GET_CONTENT) else - activity?.startActivityForResult(i, GET_CONTENT) - } - - fun getOpenFileOnClickViewListener(dataUri: () -> Uri?): OpenFileOnClickViewListener { - return OpenFileOnClickViewListener(dataUri) + activity?.startActivityForResult(intentGetContent, GET_CONTENT) } - private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean { + private fun lookForOpenIntentsFilePicker(): Boolean { var showBrowser = false try { if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) { val intent = Intent(OPEN_INTENTS_FILE_BROWSE) - // Get file path parent if possible - if (dataUri != null - && dataUri.toString().isNotEmpty() - && dataUri.scheme == "file") { - intent.data = dataUri - } else { - Log.w(javaClass.name, "Unable to read the URI") - } if (fragment != null) fragment?.startActivityForResult(intent, FILE_BROWSE) else @@ -190,22 +183,19 @@ class OpenFileHelper { GET_CONTENT, OPEN_DOC -> { if (resultCode == RESULT_OK) { if (data != null) { - var uri = data.data + val uri = data.data if (uri != null) { try { // try to persist read and write permissions if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { activity?.contentResolver?.apply { - takePersistableUriPermission(uri!!, Intent.FLAG_GRANT_READ_URI_PERMISSION) - takePersistableUriPermission(uri!!, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) } } } catch (e: Exception) { // nop } - if (requestCode == GET_CONTENT) { - uri = UriUtil.translateUri(activity!!, uri) - } keyFileCallback?.invoke(uri) } } @@ -220,15 +210,10 @@ class OpenFileHelper { private const val TAG = "OpenFileHelper" - private var ACTION_OPEN_DOCUMENT: String - - init { - ACTION_OPEN_DOCUMENT = try { - val openDocument = Intent::class.java.getField("ACTION_OPEN_DOCUMENT") - openDocument.get(null) as String - } catch (e: Exception) { - "android.intent.action.OPEN_DOCUMENT" - } + private var APP_ACTION_OPEN_DOCUMENT: String = try { + Intent::class.java.getField("ACTION_OPEN_DOCUMENT").get(null) as String + } catch (e: Exception) { + "android.intent.action.OPEN_DOCUMENT" } const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE" diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt index 46b760641..574ad9f95 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt @@ -35,6 +35,7 @@ import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService import com.kunzisoft.keepass.magikeyboard.MagikIME +import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.LOCK_ACTION @@ -199,6 +200,9 @@ fun Activity.lock() { stopService(Intent(this, KeyboardEntryNotificationService::class.java)) MagikIME.removeEntry(this) + // Stop the notification service + stopService(Intent(this, ClipboardEntryNotificationService::class.java)) + Log.i(Activity::class.java.name, "Shutdown " + localClassName + " after inactivity or manual lock") (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply { diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryHistoryAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryHistoryAdapter.kt new file mode 100644 index 000000000..bd6744cf2 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryHistoryAdapter.kt @@ -0,0 +1,50 @@ +package com.kunzisoft.keepass.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.EntryVersioned + +class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter() { + + private val inflater: LayoutInflater = LayoutInflater.from(context) + var entryHistoryList: MutableList = ArrayList() + var onItemClickListener: ((item: EntryVersioned, position: Int)->Unit)? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder { + return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false)) + } + + override fun onBindViewHolder(holder: EntryHistoryViewHolder, position: Int) { + val entryHistory = entryHistoryList[position] + + holder.lastModifiedView.text = entryHistory.lastModificationTime.getDateTimeString(context.resources) + holder.titleView.text = entryHistory.title + holder.usernameView.text = entryHistory.username + holder.urlView.text = entryHistory.url + + holder.itemView.setOnClickListener { + onItemClickListener?.invoke(entryHistory, position) + } + } + + override fun getItemCount(): Int { + return entryHistoryList.size + } + + fun clear() { + entryHistoryList.clear() + } + + inner class EntryHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + var lastModifiedView: TextView = itemView.findViewById(R.id.entry_history_last_modified) + var titleView: TextView = itemView.findViewById(R.id.entry_history_title) + var usernameView: TextView = itemView.findViewById(R.id.entry_history_username) + var urlView: TextView = itemView.findViewById(R.id.entry_history_url) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/FieldsAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/FieldsAdapter.kt index 2354675d6..bdd16ae11 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/FieldsAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/FieldsAdapter.kt @@ -18,7 +18,6 @@ class FieldsAdapter(context: Context) : RecyclerView.Adapter = ArrayList() var onItemClickListener: OnItemClickListener? = null - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldViewHolder { val view = inflater.inflate(R.layout.keyboard_popup_fields_item, parent, false) return FieldViewHolder(view) diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt index 32e280e27..f22e4cb88 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt @@ -21,27 +21,31 @@ package com.kunzisoft.keepass.adapters import android.content.Context import android.graphics.Color -import androidx.recyclerview.widget.SortedList -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.SortedListAdapterCallback +import android.graphics.Paint import android.util.Log import android.util.TypedValue -import android.view.* +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import android.widget.Toast +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SortedList +import androidx.recyclerview.widget.SortedListAdapterCallback import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.SortNodeEnum import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.settings.PreferencesUtil +import java.util.* class NodeAdapter /** * Create node list adapter with contextMenu or not * @param context Context to use */ -(private val context: Context, private val menuInflater: MenuInflater) +(private val context: Context) : RecyclerView.Adapter() { private val nodeSortedList: SortedList @@ -61,11 +65,8 @@ class NodeAdapter private var showUserNames: Boolean = true private var showNumberEntries: Boolean = true + private var actionNodesList = LinkedList() private var nodeClickCallback: NodeClickCallback? = null - private var nodeMenuListener: NodeMenuListener? = null - private var activateContextMenu: Boolean = false - private var readOnly: Boolean = false - private var isASearchResult: Boolean = false private val mDatabase: Database @@ -81,9 +82,6 @@ class NodeAdapter init { assignPreferences() - this.activateContextMenu = false - this.readOnly = false - this.isASearchResult = false this.nodeSortedList = SortedList(NodeVersioned::class.java, object : SortedListAdapterCallback(this) { override fun compare(item1: NodeVersioned, item2: NodeVersioned): Int { @@ -114,18 +112,6 @@ class NodeAdapter taTextColor.recycle() } - fun setReadOnly(readOnly: Boolean) { - this.readOnly = readOnly - } - - fun setIsASearchResult(isASearchResult: Boolean) { - this.isASearchResult = isASearchResult - } - - fun setActivateContextMenu(activate: Boolean) { - this.activateContextMenu = activate - } - private fun assignPreferences() { this.prefTextSize = PreferencesUtil.getListTextSize(context) this.infoTextSize = context.resources.getDimension(R.dimen.list_medium_size_default) * prefTextSize @@ -156,6 +142,7 @@ class NodeAdapter Log.e(TAG, "Can't add node elements to the list", e) Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show() } + notifyDataSetChanged() } fun contains(node: NodeVersioned): Boolean { @@ -170,6 +157,14 @@ class NodeAdapter nodeSortedList.add(node) } + /** + * Add nodes to the list + * @param nodes Nodes to add + */ + fun addNodes(nodes: List) { + nodeSortedList.addAll(nodes) + } + /** * Remove a node in the list * @param node Node to delete @@ -178,11 +173,35 @@ class NodeAdapter nodeSortedList.remove(node) } + /** + * Remove nodes in the list + * @param nodes Nodes to delete + */ + fun removeNodes(nodes: List) { + nodes.forEach { node -> + nodeSortedList.remove(node) + } + } + /** * Remove a node at [position] in the list */ fun removeNodeAt(position: Int) { nodeSortedList.removeItemAt(position) + // Refresh all the next items + notifyItemRangeChanged(position, nodeSortedList.size() - position) + } + + /** + * Remove nodes in the list by [positions] + * Note : algorithm remove the higher position at each iteration + */ + fun removeNodesAt(positions: IntArray) { + val positionsSortDescending = positions.toMutableList() + positionsSortDescending.sortDescending() + positionsSortDescending.forEach { + removeNodeAt(it) + } } /** @@ -197,6 +216,40 @@ class NodeAdapter nodeSortedList.endBatchedUpdates() } + /** + * Update nodes in the list + * @param oldNodes Nodes before the update + * @param newNodes Node after the update + */ + fun updateNodes(oldNodes: List, newNodes: List) { + nodeSortedList.beginBatchedUpdates() + oldNodes.forEach { oldNode -> + nodeSortedList.remove(oldNode) + } + nodeSortedList.addAll(newNodes) + nodeSortedList.endBatchedUpdates() + } + + fun notifyNodeChanged(node: NodeVersioned) { + notifyItemChanged(nodeSortedList.indexOf(node)) + } + + fun setActionNodes(actionNodes: List) { + this.actionNodesList.apply { + clear() + addAll(actionNodes) + } + } + + fun unselectActionNodes() { + actionNodesList.forEach { + notifyItemChanged(nodeSortedList.indexOf(it)) + } + this.actionNodesList.apply { + clear() + } + } + /** * Notify a change sort of the list */ @@ -238,18 +291,28 @@ class NodeAdapter holder.text.apply { text = subNode.title setTextSize(textSizeUnit, infoTextSize) + paintFlags = if (subNode.isCurrentlyExpires) + paintFlags or Paint.STRIKE_THRU_TEXT_FLAG + else + paintFlags and Paint.STRIKE_THRU_TEXT_FLAG } // Assign click - holder.container.setOnClickListener { nodeClickCallback?.onNodeClick(subNode) } - // Context menu - if (activateContextMenu) { - holder.container.setOnCreateContextMenuListener( - ContextMenuBuilder(menuInflater, subNode, readOnly, isASearchResult, nodeMenuListener)) + holder.container.setOnClickListener { + nodeClickCallback?.onNodeClick(subNode) } + holder.container.setOnLongClickListener { + nodeClickCallback?.onNodeLongClick(subNode) ?: false + } + + holder.container.isSelected = actionNodesList.contains(subNode) // Add subText with username holder.subText.apply { text = "" + paintFlags = if (subNode.isCurrentlyExpires) + paintFlags or Paint.STRIKE_THRU_TEXT_FLAG + else + paintFlags and Paint.STRIKE_THRU_TEXT_FLAG visibility = View.GONE if (subNode.type == Type.ENTRY) { val entry = subNode as EntryVersioned @@ -294,103 +357,12 @@ class NodeAdapter this.nodeClickCallback = nodeClickCallback } - /** - * Assign a listener when an element of menu is clicked - */ - fun setNodeMenuListener(nodeMenuListener: NodeMenuListener?) { - this.nodeMenuListener = nodeMenuListener - } - /** * Callback listener to redefine to do an action when a node is click */ interface NodeClickCallback { fun onNodeClick(node: NodeVersioned) - } - - /** - * Menu listener to redefine to do an action in menu - */ - interface NodeMenuListener { - fun onOpenMenuClick(node: NodeVersioned): Boolean - fun onEditMenuClick(node: NodeVersioned): Boolean - fun onCopyMenuClick(node: NodeVersioned): Boolean - fun onMoveMenuClick(node: NodeVersioned): Boolean - fun onDeleteMenuClick(node: NodeVersioned): Boolean - } - - /** - * Utility class for menu listener - */ - private class ContextMenuBuilder(val menuInflater: MenuInflater, - val node: NodeVersioned, - val readOnly: Boolean, - val isASearchResult: Boolean, - val menuListener: NodeMenuListener?) - : View.OnCreateContextMenuListener { - - private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item -> - if (menuListener == null) - return@OnMenuItemClickListener false - when (item.itemId) { - R.id.menu_open -> menuListener.onOpenMenuClick(node) - R.id.menu_edit -> menuListener.onEditMenuClick(node) - R.id.menu_copy -> menuListener.onCopyMenuClick(node) - R.id.menu_move -> menuListener.onMoveMenuClick(node) - R.id.menu_delete -> menuListener.onDeleteMenuClick(node) - else -> false - } - } - - override fun onCreateContextMenu(contextMenu: ContextMenu?, - view: View?, - contextMenuInfo: ContextMenu.ContextMenuInfo?) { - menuInflater.inflate(R.menu.node_menu, contextMenu) - - // Opening - var menuItem = contextMenu?.findItem(R.id.menu_open) - menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener) - - val database = Database.getInstance() - - // Edition - if (readOnly || node == database.recycleBin) { - contextMenu?.removeItem(R.id.menu_edit) - } else { - menuItem = contextMenu?.findItem(R.id.menu_edit) - menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener) - } - - // Copy (not for group) - if (readOnly - || isASearchResult - || node == database.recycleBin - || node.type == Type.GROUP) { - // TODO COPY For Group - contextMenu?.removeItem(R.id.menu_copy) - } else { - menuItem = contextMenu?.findItem(R.id.menu_copy) - menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener) - } - - // Move - if (readOnly - || isASearchResult - || node == database.recycleBin) { - contextMenu?.removeItem(R.id.menu_move) - } else { - menuItem = contextMenu?.findItem(R.id.menu_move) - menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener) - } - - // Deletion - if (readOnly || node == database.recycleBin) { - contextMenu?.removeItem(R.id.menu_delete) - } else { - menuItem = contextMenu?.findItem(R.id.menu_delete) - menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener) - } - } + fun onNodeLongClick(node: NodeVersioned): Boolean } class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseEntity.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseEntity.kt index 140b2bf2c..304517552 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseEntity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseEntity.kt @@ -1,5 +1,7 @@ package com.kunzisoft.keepass.app.database +import android.os.Parcel +import android.os.Parcelable import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @@ -15,7 +17,33 @@ data class CipherDatabaseEntity( @ColumnInfo(name = "specs_parameters") var specParameters: String -) { +): Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readString()!!, + parcel.readString()!!, + parcel.readString()!!) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(databaseUri) + parcel.writeString(encryptedValue) + parcel.writeString(specParameters) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): CipherDatabaseEntity { + return CipherDatabaseEntity(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryDao.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryDao.kt index 62dc2642c..1268238da 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryDao.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryDao.kt @@ -19,7 +19,7 @@ interface FileDatabaseHistoryDao { @Delete fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int - @Query("REPLACE INTO file_database_history(keyfile_uri) VALUES(null)") + @Query("UPDATE file_database_history SET keyfile_uri=null") fun deleteAllKeyFiles() @Query("DELETE FROM file_database_history") diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt index 2698d32cd..6d8e14ae0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt @@ -1,7 +1,9 @@ package com.kunzisoft.keepass.biometric +import android.content.Intent import android.net.Uri import android.os.Build +import android.provider.Settings import android.util.Log import android.view.Menu import android.view.MenuInflater @@ -19,12 +21,12 @@ import com.kunzisoft.keepass.view.AdvancedUnlockInfoView @RequiresApi(api = Build.VERSION_CODES.M) class AdvancedUnlockedManager(var context: FragmentActivity, var databaseFileUri: Uri, - var advancedUnlockInfoView: AdvancedUnlockInfoView?, - var checkboxPasswordView: CompoundButton?, - var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null, + private var advancedUnlockInfoView: AdvancedUnlockInfoView?, + private var checkboxPasswordView: CompoundButton?, + private var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null, var passwordView: TextView?, - var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit, - var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit) + private var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit, + private var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit) : BiometricUnlockDatabaseHelper.BiometricUnlockCallback { private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null @@ -39,11 +41,11 @@ class AdvancedUnlockedManager(var context: FragmentActivity, // Check if fingerprint well init (be called the first time the fingerprint is configured // and the activity still active) - if (biometricUnlockDatabaseHelper == null || !biometricUnlockDatabaseHelper!!.isFingerprintInitialized) { - - biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context, this) + if (biometricUnlockDatabaseHelper == null || !biometricUnlockDatabaseHelper!!.isBiometricInitialized) { + biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context) // callback for fingerprint findings - biometricUnlockDatabaseHelper?.setAuthenticationCallback(biometricCallback) + biometricUnlockDatabaseHelper?.biometricUnlockCallback = this + biometricUnlockDatabaseHelper?.authenticationCallback = biometricAuthenticationCallback } // Add a check listener to change fingerprint mode @@ -59,7 +61,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity, } @Synchronized - fun checkBiometricAvailability() { + private fun checkBiometricAvailability() { // fingerprint not supported (by API level or hardware) so keep option hidden // or manually disable @@ -83,10 +85,10 @@ class AdvancedUnlockedManager(var context: FragmentActivity, // listen for encryption toggleMode(Mode.STORE) } else { - cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { + cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher -> // fingerprint available but no stored password found yet for this DB so show info don't listen - toggleMode( if (it) { + toggleMode( if (containsCipher) { // listen for decryption Mode.OPEN } else { @@ -106,35 +108,43 @@ class AdvancedUnlockedManager(var context: FragmentActivity, } } - private val biometricCallback = object : BiometricPrompt.AuthenticationCallback () { + private val biometricAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback () { override fun onAuthenticationError( errorCode: Int, errString: CharSequence) { - Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") - setAdvancedUnlockedMessageView(errString.toString()) + context.runOnUiThread { + Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") + setAdvancedUnlockedMessageView(errString.toString()) + } } override fun onAuthenticationFailed() { - Log.e(TAG, "Biometric authentication failed, biometric not recognized") - setAdvancedUnlockedMessageView(R.string.biometric_not_recognized) + context.runOnUiThread { + Log.e(TAG, "Biometric authentication failed, biometric not recognized") + setAdvancedUnlockedMessageView(R.string.biometric_not_recognized) + } } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - when (biometricMode) { - Mode.UNAVAILABLE -> {} - Mode.PAUSE -> {} - Mode.NOT_CONFIGURED -> {} - Mode.WAIT_CREDENTIAL -> {} - Mode.STORE -> { - // newly store the entered password in encrypted way - biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString()) - } - Mode.OPEN -> { - // retrieve the encrypted value from preferences - cipherDatabaseAction.getCipherDatabase(databaseFileUri) { - it?.encryptedValue?.let { value -> - biometricUnlockDatabaseHelper?.decryptData(value) + context.runOnUiThread { + when (biometricMode) { + Mode.UNAVAILABLE -> { + } + Mode.NOT_CONFIGURED -> { + } + Mode.WAIT_CREDENTIAL -> { + } + Mode.STORE -> { + // newly store the entered password in encrypted way + biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString()) + } + Mode.OPEN -> { + // retrieve the encrypted value from preferences + cipherDatabaseAction.getCipherDatabase(databaseFileUri) { + it?.encryptedValue?.let { value -> + biometricUnlockDatabaseHelper?.decryptData(value) + } } } } @@ -148,16 +158,14 @@ class AdvancedUnlockedManager(var context: FragmentActivity, advancedUnlockInfoView?.setIconViewClickListener(null) } - private fun initPause() { - advancedUnlockInfoView?.setIconViewClickListener(null) - } - private fun initNotConfigured() { showFingerPrintViews(true) setAdvancedUnlockedTitleView(R.string.configure_biometric) setAdvancedUnlockedMessageView("") - advancedUnlockInfoView?.setIconViewClickListener(null) + advancedUnlockInfoView?.setIconViewClickListener { + context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS)) + } } private fun initWaitData() { @@ -168,6 +176,14 @@ class AdvancedUnlockedManager(var context: FragmentActivity, advancedUnlockInfoView?.setIconViewClickListener(null) } + private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?, + cryptoObject: BiometricPrompt.CryptoObject, + promptInfo: BiometricPrompt.PromptInfo) { + context.runOnUiThread { + biometricPrompt?.authenticate(promptInfo, cryptoObject) + } + } + private fun initEncryptData() { showFingerPrintViews(true) setAdvancedUnlockedTitleView(R.string.open_biometric_prompt_store_credential) @@ -178,9 +194,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity, cryptoObject?.let { crypto -> // Set listener to open the biometric dialog and save credential advancedUnlockInfoView?.setIconViewClickListener { _ -> - context.runOnUiThread { - biometricPrompt?.authenticate(promptInfo, crypto) - } + openBiometricPrompt(biometricPrompt, crypto, promptInfo) } } @@ -201,17 +215,13 @@ class AdvancedUnlockedManager(var context: FragmentActivity, cryptoObject?.let { crypto -> // Set listener to open the biometric dialog and check credential advancedUnlockInfoView?.setIconViewClickListener { _ -> - context.runOnUiThread { - biometricPrompt?.authenticate(promptInfo, crypto) - } + openBiometricPrompt(biometricPrompt, crypto, promptInfo) } // Auto open the biometric prompt if (isBiometricPromptAutoOpenEnable) { isBiometricPromptAutoOpenEnable = false - context.runOnUiThread { - biometricPrompt?.authenticate(promptInfo, crypto) - } + openBiometricPrompt(biometricPrompt, crypto, promptInfo) } } @@ -225,7 +235,6 @@ class AdvancedUnlockedManager(var context: FragmentActivity, fun initBiometricMode() { when (biometricMode) { Mode.UNAVAILABLE -> initNotAvailable() - Mode.PAUSE -> initPause() Mode.NOT_CONFIGURED -> initNotConfigured() Mode.WAIT_CREDENTIAL -> initWaitData() Mode.STORE -> initEncryptData() @@ -235,25 +244,23 @@ class AdvancedUnlockedManager(var context: FragmentActivity, context.invalidateOptionsMenu() } - fun pause() { - biometricMode = Mode.PAUSE - initBiometricMode() - } - fun destroy() { // Restore the checked listener checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener) - - biometricMode = Mode.UNAVAILABLE - initBiometricMode() - biometricUnlockDatabaseHelper = null } + // Only to fix multiple fingerprint menu #332 + private var addBiometricMenuInProgress = false fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) { - cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { - if ((biometricMode != Mode.UNAVAILABLE - && biometricMode != Mode.NOT_CONFIGURED) && it) - menuInflater.inflate(R.menu.advanced_unlock, menu) + if (!addBiometricMenuInProgress) { + addBiometricMenuInProgress = true + cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { + if ((biometricMode != Mode.UNAVAILABLE && biometricMode != Mode.NOT_CONFIGURED) + && it) { + menuInflater.inflate(R.menu.advanced_unlock, menu) + addBiometricMenuInProgress = false + } + } } } @@ -306,7 +313,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity, } enum class Mode { - UNAVAILABLE, PAUSE, NOT_CONFIGURED, WAIT_CREDENTIAL, STORE, OPEN + UNAVAILABLE, NOT_CONFIGURED, WAIT_CREDENTIAL, STORE, OPEN } companion object { diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt index 63a4e53ad..ad869885b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt @@ -42,8 +42,7 @@ import javax.crypto.SecretKey import javax.crypto.spec.IvParameterSpec @RequiresApi(api = Build.VERSION_CODES.M) -class BiometricUnlockDatabaseHelper(private val context: FragmentActivity, - private val biometricUnlockCallback: BiometricUnlockCallback?) { +class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) { private var biometricPrompt: BiometricPrompt? = null @@ -54,26 +53,37 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity, private var cryptoObject: BiometricPrompt.CryptoObject? = null private var isBiometricInit = false - private var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null - - private val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder() - .setTitle(context.getString(R.string.biometric_prompt_store_credential_title)) - .setDescription(context.getString(R.string.biometric_prompt_store_credential_message)) - //.setDeviceCredentialAllowed(true) TODO device credential - .setNegativeButtonText(context.getString(android.R.string.cancel)) - .build() - - private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder() - .setTitle(context.getString(R.string.biometric_prompt_extract_credential_title)) - .setDescription(context.getString(R.string.biometric_prompt_extract_credential_message)) - //.setDeviceCredentialAllowed(true) - .setNegativeButtonText(context.getString(android.R.string.cancel)) - .build() - - val isFingerprintInitialized: Boolean + var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null + var biometricUnlockCallback: BiometricUnlockCallback? = null + + private val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder().apply { + setTitle(context.getString(R.string.biometric_prompt_store_credential_title)) + setDescription(context.getString(R.string.biometric_prompt_store_credential_message)) + // TODO device credential + /* + if (keyguardManager?.isDeviceSecure == true) + setDeviceCredentialAllowed(true) + else + */ + setNegativeButtonText(context.getString(android.R.string.cancel)) + }.build() + + private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply { + setTitle(context.getString(R.string.biometric_prompt_extract_credential_title)) + setDescription(context.getString(R.string.biometric_prompt_extract_credential_message)) + // TODO device credential + /* + if (keyguardManager?.isDeviceSecure == true) + setDeviceCredentialAllowed(true) + else + */ + setNegativeButtonText(context.getString(android.R.string.cancel)) + }.build() + + val isBiometricInitialized: Boolean get() { - if (!isBiometricInit && biometricUnlockCallback != null) { - biometricUnlockCallback.onBiometricException(Exception("FingerPrint not initialized")) + if (!isBiometricInit) { + biometricUnlockCallback?.onBiometricException(Exception("Biometric not initialized")) } return isBiometricInit } @@ -103,7 +113,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity, } private fun getSecretKey(): SecretKey? { - if (!isFingerprintInitialized) { + if (!isBiometricInitialized) { return null } try { @@ -145,7 +155,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity, : (biometricPrompt: BiometricPrompt?, cryptoObject: BiometricPrompt.CryptoObject?, promptInfo: BiometricPrompt.PromptInfo)->Unit) { - if (!isFingerprintInitialized) { + if (!isBiometricInitialized) { return } try { @@ -158,7 +168,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity, } catch (unrecoverableKeyException: UnrecoverableKeyException) { Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException) - deleteEntryKey() + biometricUnlockCallback?.onInvalidKeyException(unrecoverableKeyException) } catch (invalidKeyException: KeyPermanentlyInvalidatedException) { Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException) biometricUnlockCallback?.onInvalidKeyException(invalidKeyException) @@ -170,7 +180,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity, } fun encryptData(value: String) { - if (!isFingerprintInitialized) { + if (!isBiometricInitialized) { return } try { @@ -194,7 +204,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity, : (biometricPrompt: BiometricPrompt?, cryptoObject: BiometricPrompt.CryptoObject?, promptInfo: BiometricPrompt.PromptInfo)->Unit) { - if (!isFingerprintInitialized) { + if (!isBiometricInitialized) { return } try { @@ -223,7 +233,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity, } fun decryptData(encryptedValue: String) { - if (!isFingerprintInitialized) { + if (!isBiometricInitialized) { return } try { @@ -252,10 +262,6 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity, } } - fun setAuthenticationCallback(authenticationCallback: BiometricPrompt.AuthenticationCallback) { - this.authenticationCallback = authenticationCallback - } - @Synchronized fun initBiometricPrompt() { if (biometricPrompt == null) { @@ -289,22 +295,24 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity, * Remove entry key in keystore */ fun deleteEntryKeyInKeystoreForBiometric(context: FragmentActivity, - biometricUnlockCallback: BiometricUnlockErrorCallback) { - val fingerPrintHelper = BiometricUnlockDatabaseHelper(context, object : BiometricUnlockCallback { + biometricCallback: BiometricUnlockErrorCallback) { + BiometricUnlockDatabaseHelper(context).apply { + biometricUnlockCallback = object : BiometricUnlockCallback { - override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {} + override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {} - override fun handleDecryptedResult(decryptedValue: String) {} + override fun handleDecryptedResult(decryptedValue: String) {} - override fun onInvalidKeyException(e: Exception) { - biometricUnlockCallback.onInvalidKeyException(e) - } + override fun onInvalidKeyException(e: Exception) { + biometricCallback.onInvalidKeyException(e) + } - override fun onBiometricException(e: Exception) { - biometricUnlockCallback.onBiometricException(e) + override fun onBiometricException(e: Exception) { + biometricCallback.onBiometricException(e) + } } - }) - fingerPrintHelper.deleteEntryKey() + deleteEntryKey() + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/AesKdf.kt b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/AesKdf.kt index a56827df1..7d7060272 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/AesKdf.kt +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/AesKdf.kt @@ -32,12 +32,10 @@ class AesKdf internal constructor() : KdfEngine() { override val defaultParameters: KdfParameters get() { - val p = KdfParameters(uuid) - - p.setParamUUID() - p.setUInt32(ParamRounds, DEFAULT_ROUNDS.toLong()) - - return p + return KdfParameters(uuid).apply { + setParamUUID() + setUInt32(PARAM_ROUNDS, DEFAULT_ROUNDS.toLong()) + } } override val defaultKeyRounds: Long @@ -54,8 +52,8 @@ class AesKdf internal constructor() : KdfEngine() { @Throws(IOException::class) override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray { var currentMasterKey = masterKey - val rounds = p.getUInt64(ParamRounds) - var seed = p.getByteArray(ParamSeed) + val rounds = p.getUInt64(PARAM_ROUNDS) + var seed = p.getByteArray(PARAM_SEED) if (currentMasterKey.size != 32) { currentMasterKey = CryptoUtil.hashSha256(currentMasterKey) @@ -75,15 +73,15 @@ class AesKdf internal constructor() : KdfEngine() { val seed = ByteArray(32) random.nextBytes(seed) - p.setByteArray(ParamSeed, seed) + p.setByteArray(PARAM_SEED, seed) } override fun getKeyRounds(p: KdfParameters): Long { - return p.getUInt64(ParamRounds) + return p.getUInt64(PARAM_ROUNDS) } override fun setKeyRounds(p: KdfParameters, keyRounds: Long) { - p.setUInt64(ParamRounds, keyRounds) + p.setUInt64(PARAM_ROUNDS, keyRounds) } companion object { @@ -91,9 +89,24 @@ class AesKdf internal constructor() : KdfEngine() { private const val DEFAULT_ROUNDS = 6000 val CIPHER_UUID: UUID = Types.bytestoUUID( - byteArrayOf(0xC9.toByte(), 0xD9.toByte(), 0xF3.toByte(), 0x9A.toByte(), 0x62.toByte(), 0x8A.toByte(), 0x44.toByte(), 0x60.toByte(), 0xBF.toByte(), 0x74.toByte(), 0x0D.toByte(), 0x08.toByte(), 0xC1.toByte(), 0x8A.toByte(), 0x4F.toByte(), 0xEA.toByte())) - - const val ParamRounds = "R" - const val ParamSeed = "S" + byteArrayOf(0xC9.toByte(), + 0xD9.toByte(), + 0xF3.toByte(), + 0x9A.toByte(), + 0x62.toByte(), + 0x8A.toByte(), + 0x44.toByte(), + 0x60.toByte(), + 0xBF.toByte(), + 0x74.toByte(), + 0x0D.toByte(), + 0x08.toByte(), + 0xC1.toByte(), + 0x8A.toByte(), + 0x4F.toByte(), + 0xEA.toByte())) + + const val PARAM_ROUNDS = "R" + const val PARAM_SEED = "S" } } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Kdf.kt b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Kdf.kt index f246f627a..a2d485ccb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Kdf.kt +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Kdf.kt @@ -33,16 +33,16 @@ class Argon2Kdf internal constructor() : KdfEngine() { val p = KdfParameters(uuid) p.setParamUUID() - p.setUInt32(ParamParallelism, DefaultParallelism) - p.setUInt64(ParamMemory, DefaultMemory) - p.setUInt64(ParamIterations, DefaultIterations) - p.setUInt32(ParamVersion, MaxVersion) + p.setUInt32(PARAM_PARALLELISM, DEFAULT_PARALLELISM) + p.setUInt64(PARAM_MEMORY, DEFAULT_MEMORY) + p.setUInt64(PARAM_ITERATIONS, DEFAULT_ITERATIONS) + p.setUInt32(PARAM_VERSION, MAX_VERSION) return p } override val defaultKeyRounds: Long - get() = DefaultIterations + get() = DEFAULT_ITERATIONS init { uuid = CIPHER_UUID @@ -55,13 +55,13 @@ class Argon2Kdf internal constructor() : KdfEngine() { @Throws(IOException::class) override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray { - val salt = p.getByteArray(ParamSalt) - val parallelism = p.getUInt32(ParamParallelism).toInt() - val memory = p.getUInt64(ParamMemory) - val iterations = p.getUInt64(ParamIterations) - val version = p.getUInt32(ParamVersion) - val secretKey = p.getByteArray(ParamSecretKey) - val assocData = p.getByteArray(ParamAssocData) + val salt = p.getByteArray(PARAM_SALT) + val parallelism = p.getUInt32(PARAM_PARALLELISM).toInt() + val memory = p.getUInt64(PARAM_MEMORY) + val iterations = p.getUInt64(PARAM_ITERATIONS) + val version = p.getUInt32(PARAM_VERSION) + val secretKey = p.getByteArray(PARAM_SECRET_KEY) + val assocData = p.getByteArray(PARAM_ASSOC_DATA) return Argon2Native.transformKey(masterKey, salt, parallelism, memory, iterations, secretKey, assocData, version) @@ -73,71 +73,102 @@ class Argon2Kdf internal constructor() : KdfEngine() { val salt = ByteArray(32) random.nextBytes(salt) - p.setByteArray(ParamSalt, salt) + p.setByteArray(PARAM_SALT, salt) } override fun getKeyRounds(p: KdfParameters): Long { - return p.getUInt64(ParamIterations) + return p.getUInt64(PARAM_ITERATIONS) } override fun setKeyRounds(p: KdfParameters, keyRounds: Long) { - p.setUInt64(ParamIterations, keyRounds) + p.setUInt64(PARAM_ITERATIONS, keyRounds) } + override val minKeyRounds: Long + get() = MIN_ITERATIONS + + override val maxKeyRounds: Long + get() = MAX_ITERATIONS + override fun getMemoryUsage(p: KdfParameters): Long { - return p.getUInt64(ParamMemory) + return p.getUInt64(PARAM_MEMORY) } override fun setMemoryUsage(p: KdfParameters, memory: Long) { - p.setUInt64(ParamMemory, memory) + p.setUInt64(PARAM_MEMORY, memory) } - override fun getDefaultMemoryUsage(): Long { - return DefaultMemory - } + override val defaultMemoryUsage: Long + get() = DEFAULT_MEMORY + + override val minMemoryUsage: Long + get() = MIN_MEMORY + + override val maxMemoryUsage: Long + get() = MAX_MEMORY override fun getParallelism(p: KdfParameters): Int { - return p.getUInt32(ParamParallelism).toInt() // TODO Verify + return p.getUInt32(PARAM_PARALLELISM).toInt() // TODO Verify } override fun setParallelism(p: KdfParameters, parallelism: Int) { - p.setUInt32(ParamParallelism, parallelism.toLong()) + p.setUInt32(PARAM_PARALLELISM, parallelism.toLong()) } - override fun getDefaultParallelism(): Int { - return DefaultParallelism.toInt() // TODO Verify - } + override val defaultParallelism: Int + get() = DEFAULT_PARALLELISM.toInt() - companion object { + override val minParallelism: Int + get() = MIN_PARALLELISM - val CIPHER_UUID: UUID = Types.bytestoUUID( - byteArrayOf(0xEF.toByte(), 0x63.toByte(), 0x6D.toByte(), 0xDF.toByte(), 0x8C.toByte(), 0x29.toByte(), 0x44.toByte(), 0x4B.toByte(), 0x91.toByte(), 0xF7.toByte(), 0xA9.toByte(), 0xA4.toByte(), 0x03.toByte(), 0xE3.toByte(), 0x0A.toByte(), 0x0C.toByte())) + override val maxParallelism: Int + get() = MAX_PARALLELISM - private const val ParamSalt = "S" // byte[] - private const val ParamParallelism = "P" // UInt32 - private const val ParamMemory = "M" // UInt64 - private const val ParamIterations = "I" // UInt64 - private const val ParamVersion = "V" // UInt32 - private const val ParamSecretKey = "K" // byte[] - private const val ParamAssocData = "A" // byte[] - - private const val MinVersion: Long = 0x10 - private const val MaxVersion: Long = 0x13 - - private const val MinSalt = 8 - private const val MaxSalt = Integer.MAX_VALUE - - private const val MinIterations: Long = 1 - private const val MaxIterations = 4294967295L - - private const val MinMemory = (1024 * 8).toLong() - private const val MaxMemory = Integer.MAX_VALUE.toLong() - - private const val MinParallelism = 1 - private const val MaxParallelism = (1 shl 24) - 1 + companion object { - private const val DefaultIterations: Long = 2 - private const val DefaultMemory = (1024 * 1024).toLong() - private const val DefaultParallelism: Long = 2 + val CIPHER_UUID: UUID = Types.bytestoUUID( + byteArrayOf(0xEF.toByte(), + 0x63.toByte(), + 0x6D.toByte(), + 0xDF.toByte(), + 0x8C.toByte(), + 0x29.toByte(), + 0x44.toByte(), + 0x4B.toByte(), + 0x91.toByte(), + 0xF7.toByte(), + 0xA9.toByte(), + 0xA4.toByte(), + 0x03.toByte(), + 0xE3.toByte(), + 0x0A.toByte(), + 0x0C.toByte())) + + private const val PARAM_SALT = "S" // byte[] + private const val PARAM_PARALLELISM = "P" // UInt32 + private const val PARAM_MEMORY = "M" // UInt64 + private const val PARAM_ITERATIONS = "I" // UInt64 + private const val PARAM_VERSION = "V" // UInt32 + private const val PARAM_SECRET_KEY = "K" // byte[] + private const val PARAM_ASSOC_DATA = "A" // byte[] + + private const val MIN_VERSION: Long = 0x10 + private const val MAX_VERSION: Long = 0x13 + + private const val MIN_SALT = 8 + private const val MAX_SALT = Integer.MAX_VALUE + + private const val MIN_ITERATIONS: Long = 1 + private const val MAX_ITERATIONS = 4294967295L + + private const val MIN_MEMORY = (1024 * 8).toLong() + private const val MAX_MEMORY = Integer.MAX_VALUE.toLong() + + private const val MIN_PARALLELISM = 1 + private const val MAX_PARALLELISM = (1 shl 24) - 1 + + private const val DEFAULT_ITERATIONS: Long = 2 + private const val DEFAULT_MEMORY = (1024 * 1024).toLong() + private const val DEFAULT_PARALLELISM: Long = 2 } } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfEngine.kt b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfEngine.kt index ce123626e..fbd22c191 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfEngine.kt +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfEngine.kt @@ -19,28 +19,44 @@ */ package com.kunzisoft.keepass.crypto.keyDerivation -import com.kunzisoft.keepass.database.ObjectNameResource +import com.kunzisoft.keepass.utils.ObjectNameResource import java.io.IOException +import java.io.Serializable import java.util.UUID -abstract class KdfEngine : ObjectNameResource { +// TODO Parcelable +abstract class KdfEngine : ObjectNameResource, Serializable { var uuid: UUID? = null abstract val defaultParameters: KdfParameters - abstract val defaultKeyRounds: Long - @Throws(IOException::class) abstract fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray abstract fun randomize(p: KdfParameters) + /* + * ITERATIONS + */ + abstract fun getKeyRounds(p: KdfParameters): Long abstract fun setKeyRounds(p: KdfParameters, keyRounds: Long) + abstract val defaultKeyRounds: Long + + open val minKeyRounds: Long + get() = 1 + + open val maxKeyRounds: Long + get() = Int.MAX_VALUE.toLong() + + /* + * MEMORY + */ + open fun getMemoryUsage(p: KdfParameters): Long { return UNKNOWN_VALUE.toLong() } @@ -49,9 +65,18 @@ abstract class KdfEngine : ObjectNameResource { // Do nothing by default } - open fun getDefaultMemoryUsage(): Long { - return UNKNOWN_VALUE.toLong() - } + open val defaultMemoryUsage: Long + get() = UNKNOWN_VALUE.toLong() + + open val minMemoryUsage: Long + get() = 1 + + open val maxMemoryUsage: Long + get() = Int.MAX_VALUE.toLong() + + /* + * PARALLELISM + */ open fun getParallelism(p: KdfParameters): Int { return UNKNOWN_VALUE @@ -61,13 +86,16 @@ abstract class KdfEngine : ObjectNameResource { // Do nothing by default } - open fun getDefaultParallelism(): Int { - return UNKNOWN_VALUE - } + open val defaultParallelism: Int + get() = UNKNOWN_VALUE - companion object { + open val minParallelism: Int + get() = 1 + open val maxParallelism: Int + get() = Int.MAX_VALUE + + companion object { const val UNKNOWN_VALUE = -1 - const val UNKNOWN_VALUE_STRING = (-1).toString() } } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfFactory.kt b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfFactory.kt index 5db965de2..d2cfa0994 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfFactory.kt +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfFactory.kt @@ -19,37 +19,7 @@ */ package com.kunzisoft.keepass.crypto.keyDerivation -import com.kunzisoft.keepass.database.exception.UnknownKDF - -import java.util.ArrayList - object KdfFactory { - var aesKdf = AesKdf() var argon2Kdf = Argon2Kdf() - - var kdfListV3: MutableList = ArrayList() - var kdfListV4: MutableList = ArrayList() - - init { - kdfListV3.add(aesKdf) - - kdfListV4.add(aesKdf) - kdfListV4.add(argon2Kdf) - } - - @Throws(UnknownKDF::class) - fun getEngineV4(kdfParameters: KdfParameters?): KdfEngine { - val unknownKDFException = UnknownKDF() - if (kdfParameters == null) { - throw unknownKDFException - } - for (engine in kdfListV4) { - if (engine.uuid == kdfParameters.uuid) { - return engine - } - } - throw unknownKDFException - } - } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt b/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt index 87d2832c2..7caddbe8e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/SortNodeEnum.kt @@ -140,7 +140,7 @@ enum class SortNodeEnum { override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int { return object1.creationTime.date - ?.compareTo(object2.creationTime.date) ?: 0 + .compareTo(object2.creationTime.date) } } @@ -152,7 +152,7 @@ enum class SortNodeEnum { override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int { return object1.lastModificationTime.date - ?.compareTo(object2.lastModificationTime.date) ?: 0 + .compareTo(object2.lastModificationTime.date) } } @@ -164,7 +164,7 @@ enum class SortNodeEnum { override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int { return object1.lastAccessTime.date - ?.compareTo(object2.lastAccessTime.date) ?: 0 + .compareTo(object2.lastAccessTime.date) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt index a72f1317c..e2499b24a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt @@ -21,25 +21,23 @@ package com.kunzisoft.keepass.database.action import android.content.Context import android.net.Uri +import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.exception.InvalidKeyFileException -import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.utils.UriUtil -import java.io.IOException -open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor( +open class AssignPasswordInDatabaseRunnable ( context: Context, database: Database, + protected val mDatabaseUri: Uri, withMasterPassword: Boolean, masterPassword: String?, withKeyFile: Boolean, keyFile: Uri?, - save: Boolean, - actionRunnable: ActionRunnable? = null) - : SaveDatabaseRunnable(context, database, save, actionRunnable) { + save: Boolean) + : SaveDatabaseRunnable(context, database, save) { private var mMasterPassword: String? = null - private var mKeyFile: Uri? = null + protected var mKeyFile: Uri? = null private var mBackupKey: ByteArray? = null @@ -50,7 +48,7 @@ open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor( this.mKeyFile = keyFile } - override fun run() { + override fun onStartRun() { // Set key try { // TODO move master key methods @@ -59,20 +57,21 @@ open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor( val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFile) database.retrieveMasterKey(mMasterPassword, uriInputStream) - - // To save the database - super.run() - finishRun(true) - } catch (e: InvalidKeyFileException) { - erase(mBackupKey) - finishRun(false, e.message) - } catch (e: IOException) { + } catch (e: Exception) { erase(mBackupKey) - finishRun(false, e.message) + setError(e.message) } + + super.onStartRun() } - override fun onFinishRun(result: Result) { + override fun onFinishRun() { + super.onFinishRun() + + // Erase the biometric + CipherDatabaseAction.getInstance(context) + .deleteByDatabaseUri(mDatabaseUri) + if (!result.isSuccess) { // Erase the current master key erase(database.masterKey) @@ -80,8 +79,6 @@ open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor( database.masterKey = it } } - - super.onFinishRun(result) } /** diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt index a9b6b565f..188ba639c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt @@ -21,38 +21,45 @@ package com.kunzisoft.keepass.database.action import android.content.Context import android.net.Uri +import android.util.Log +import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.tasks.ActionRunnable class CreateDatabaseRunnable(context: Context, - private val mDatabaseUri: Uri, private val mDatabase: Database, + databaseUri: Uri, withMasterPassword: Boolean, masterPassword: String?, withKeyFile: Boolean, keyFile: Uri?, - save: Boolean, - actionRunnable: ActionRunnable? = null) - : AssignPasswordInDatabaseRunnable(context, mDatabase, withMasterPassword, masterPassword, withKeyFile, keyFile, save, actionRunnable) { + save: Boolean) + : AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile, save) { - override fun run() { + override fun onStartRun() { try { // Create new database record mDatabase.apply { createData(mDatabaseUri) // Set Database state loaded = true - // Commit changes - super.run() } - - finishRun(true) } catch (e: Exception) { - mDatabase.closeAndClear() - finishRun(false, e.message) + setError(e.message) } + + super.onStartRun() } - override fun onFinishRun(result: Result) {} + override fun onFinishRun() { + super.onFinishRun() + + if (result.isSuccess) { + // Add database to recent files + FileDatabaseHistoryAction.getInstance(context.applicationContext) + .addOrUpdateDatabaseUri(mDatabaseUri, mKeyFile) + } else { + Log.e("CreateDatabaseRunnable", "Unable to create the database") + } + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt index 1b31b3274..8404fd650 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt @@ -21,116 +21,79 @@ package com.kunzisoft.keepass.database.action import android.content.Context import android.net.Uri -import android.preference.PreferenceManager -import androidx.annotation.StringRes -import android.util.Log -import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.exception.* +import com.kunzisoft.keepass.app.database.CipherDatabaseAction +import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException +import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService +import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater -import java.io.FileNotFoundException -import java.io.IOException -import java.lang.ref.WeakReference -class LoadDatabaseRunnable(private val mWeakContext: WeakReference, +class LoadDatabaseRunnable(private val context: Context, private val mDatabase: Database, private val mUri: Uri, private val mPass: String?, private val mKey: Uri?, + private val mReadonly: Boolean, + private val mCipherEntity: CipherDatabaseEntity?, + private val mOmitBackup: Boolean, + private val mFixDuplicateUUID: Boolean, private val progressTaskUpdater: ProgressTaskUpdater?, - nestedAction: ActionRunnable) - : ActionRunnable(nestedAction, executeNestedActionIfResultFalse = true) { - - private val mRememberKeyFile: Boolean - get() { - return mWeakContext.get()?.let { - PreferenceManager.getDefaultSharedPreferences(it) - .getBoolean(it.getString(R.string.keyfile_key), - it.resources.getBoolean(R.bool.keyfile_default)) - } ?: true - } + private val mDuplicateUuidAction: ((Result) -> Unit)?) + : ActionRunnable() { - override fun run() { - try { - mWeakContext.get()?.let { - mDatabase.loadData(it, mUri, mPass, mKey, progressTaskUpdater) - saveFileData(mUri, mKey) - finishRun(true) - } ?: finishRun(false, "Context null") - } catch (e: ArcFourException) { - catchError(e, R.string.error_arc4) - return - } catch (e: InvalidPasswordException) { - catchError(e, R.string.invalid_password) - return - } catch (e: ContentFileNotFoundException) { - catchError(e, R.string.file_not_found_content) - return - } catch (e: FileNotFoundException) { - catchError(e, R.string.file_not_found) - return - } catch (e: IOException) { - var messageId = R.string.error_load_database - e.message?.let { - if (it.contains("Hash failed with code")) - messageId = R.string.error_load_database_KDF_memory - } - catchError(e, messageId, true) - return - } catch (e: KeyFileEmptyException) { - catchError(e, R.string.keyfile_is_empty) - return - } catch (e: InvalidAlgorithmException) { - catchError(e, R.string.invalid_algorithm) - return - } catch (e: InvalidKeyFileException) { - catchError(e, R.string.keyfile_does_not_exist) - return - } catch (e: InvalidDBSignatureException) { - catchError(e, R.string.invalid_db_sig) - return - } catch (e: InvalidDBVersionException) { - catchError(e, R.string.unsupported_db_version) - return - } catch (e: InvalidDBException) { - catchError(e, R.string.error_invalid_db) - return - } catch (e: OutOfMemoryError) { - catchError(e, R.string.error_out_of_memory) - return - } catch (e: Exception) { - catchError(e, R.string.error_load_database, true) - return - } - } + private val cacheDirectory = context.applicationContext.filesDir - private fun catchError(e: Throwable, @StringRes messageId: Int, addThrowableMessage: Boolean = false) { - var errorMessage = mWeakContext.get()?.getString(messageId) - Log.e(TAG, errorMessage, e) - if (addThrowableMessage) - errorMessage = errorMessage + " " + e.localizedMessage - finishRun(false, errorMessage) + override fun onStartRun() { + // Clear before we load + mDatabase.closeAndClear(cacheDirectory) } - private fun saveFileData(uri: Uri, key: Uri?) { - var keyFileUri = key - if (!mRememberKeyFile) { - keyFileUri = null + override fun onActionRun() { + try { + mDatabase.loadData(mUri, mPass, mKey, + mReadonly, + context.contentResolver, + cacheDirectory, + mOmitBackup, + mFixDuplicateUUID, + progressTaskUpdater) } - mWeakContext.get()?.let { - FileDatabaseHistoryAction.getInstance(it).addOrUpdateDatabaseUri(uri, keyFileUri) + catch (e: LoadDatabaseDuplicateUuidException) { + mDuplicateUuidAction?.invoke(result) + setError(e) } - } - - override fun onFinishRun(result: Result) { - if (!result.isSuccess) { - mDatabase.closeAndClear(mWeakContext.get()?.filesDir) + catch (e: LoadDatabaseException) { + setError(e) } } - companion object { - private val TAG = LoadDatabaseRunnable::class.java.name + override fun onFinishRun() { + if (result.isSuccess) { + // Save keyFile in app database + val rememberKeyFile = PreferencesUtil.rememberKeyFiles(context) + if (rememberKeyFile) { + var keyUri = mKey + if (!rememberKeyFile) { + keyUri = null + } + FileDatabaseHistoryAction.getInstance(context) + .addOrUpdateDatabaseUri(mUri, keyUri) + } + + // Register the biometric + mCipherEntity?.let { cipherDatabaseEntity -> + CipherDatabaseAction.getInstance(context) + .addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called + } + + // Start the opening notification + DatabaseOpenNotificationService.startIfAllowed(context) + } else { + mDatabase.closeAndClear(cacheDirectory) + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogSaveDatabaseThread.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogSaveDatabaseThread.kt deleted file mode 100644 index 33f97d9dd..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogSaveDatabaseThread.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.kunzisoft.keepass.database.action - -import androidx.fragment.app.FragmentActivity -import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.tasks.ActionRunnable -import com.kunzisoft.keepass.tasks.ProgressTaskUpdater - -class ProgressDialogSaveDatabaseThread(activity: FragmentActivity, - actionRunnable: (ProgressTaskUpdater?)-> ActionRunnable) - : ProgressDialogThread(activity, - actionRunnable, - R.string.saving_database, - null, - R.string.do_not_kill_app) \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogThread.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogThread.kt index 38ff4ef09..8e4beb7e5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogThread.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDialogThread.kt @@ -1,86 +1,464 @@ package com.kunzisoft.keepass.database.action -import android.content.Intent -import android.os.AsyncTask +import android.content.* +import android.content.Context.BIND_ABOVE_CLIENT +import android.content.Context.BIND_NOT_FOREGROUND +import android.net.Uri import android.os.Build -import androidx.annotation.StringRes +import android.os.Bundle +import android.os.IBinder import androidx.fragment.app.FragmentActivity +import com.kunzisoft.keepass.app.database.CipherDatabaseEntity +import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine +import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_TASK_TITLE_KEY +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_COLOR_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_COMPRESSION_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_DEFAULT_USERNAME_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_DESCRIPTION_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_ENCRYPTION_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_ITERATIONS_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_KEY_DERIVATION_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_MAX_HISTORY_ITEMS_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_MAX_HISTORY_SIZE_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_NAME_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_PARALLELISM_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment -import com.kunzisoft.keepass.tasks.ProgressTaskUpdater +import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.retrieveProgressDialog import com.kunzisoft.keepass.timeout.TimeoutHelper +import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION +import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION +import java.util.* +import kotlin.collections.ArrayList -open class ProgressDialogThread(private val activity: FragmentActivity, - private val actionRunnable: (ProgressTaskUpdater?)-> ActionRunnable, - @StringRes private val titleId: Int, - @StringRes private val messageId: Int? = null, - @StringRes private val warningId: Int? = null) { - - private val progressTaskDialogFragment = ProgressTaskDialogFragment.build( - titleId, - messageId, - warningId) - private var actionRunnableAsyncTask: ActionRunnableAsyncTask? = null - var actionFinishInUIThread: ActionRunnable? = null - - private var intentDatabaseTask:Intent = Intent(activity, DatabaseTaskNotificationService::class.java) - - init { - actionRunnableAsyncTask = ActionRunnableAsyncTask(progressTaskDialogFragment, - { - activity.runOnUiThread { - intentDatabaseTask.putExtra(DATABASE_TASK_TITLE_KEY, titleId) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activity.startForegroundService(intentDatabaseTask) - } else { - activity.startService(intentDatabaseTask) - } - TimeoutHelper.temporarilyDisableTimeout() - // Show the dialog - ProgressTaskDialogFragment.start(activity, progressTaskDialogFragment) + +class ProgressDialogThread(private val activity: FragmentActivity, + var onActionFinish: (actionTask: String, + result: ActionRunnable.Result) -> Unit) { + + private var intentDatabaseTask = Intent(activity, DatabaseTaskNotificationService::class.java) + + private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null + private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null + + private var serviceConnection: ServiceConnection? = null + + private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener { + override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) { + TimeoutHelper.temporarilyDisableTimeout(activity) + startOrUpdateDialog(titleId, messageId, warningId) + } + + override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) { + TimeoutHelper.temporarilyDisableTimeout(activity) + startOrUpdateDialog(titleId, messageId, warningId) + } + + override fun onStopAction(actionTask: String, result: ActionRunnable.Result) { + onActionFinish.invoke(actionTask, result) + // Remove the progress task + ProgressTaskDialogFragment.stop(activity) + TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity) + } + } + + private fun startOrUpdateDialog(titleId: Int?, messageId: Int?, warningId: Int?) { + var progressTaskDialogFragment = retrieveProgressDialog(activity) + if (progressTaskDialogFragment == null) { + progressTaskDialogFragment = ProgressTaskDialogFragment.build() + ProgressTaskDialogFragment.start(activity, progressTaskDialogFragment) + } + progressTaskDialogFragment.apply { + titleId?.let { + updateTitle(it) + } + messageId?.let { + updateMessage(it) + } + warningId?.let { + updateWarning(it) + } + } + } + + @Synchronized + private fun initServiceConnection() { + if (serviceConnection == null) { + serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) { + mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder).apply { + addActionTaskListener(actionTaskListener) + getService().checkAction() } - }, { result -> - activity.runOnUiThread { - actionFinishInUIThread?.onFinishRun(result) - // Remove the progress task - ProgressTaskDialogFragment.stop(activity) - TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity) - activity.stopService(intentDatabaseTask) + } + + override fun onServiceDisconnected(name: ComponentName?) { + mBinder?.removeActionTaskListener(actionTaskListener) + mBinder = null + } + } + } + } + + @Synchronized + private fun bindService() { + initServiceConnection() + serviceConnection?.let { + activity.bindService(intentDatabaseTask, it, BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT) + } + } + + /** + * Unbind the service and assign null to the service connection to check if already unbind or not + */ + @Synchronized + private fun unBindService() { + serviceConnection?.let { + activity.unbindService(it) + } + serviceConnection = null + } + + @Synchronized + fun registerProgressTask() { + ProgressTaskDialogFragment.stop(activity) + + // Register a database task receiver to stop loading dialog when service finish the task + databaseTaskBroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + activity.runOnUiThread { + when (intent?.action) { + DATABASE_START_TASK_ACTION -> { + // Bind to the service when is starting + bindService() + } + DATABASE_STOP_TASK_ACTION -> { + unBindService() + } } - }) + } + } + } + activity.registerReceiver(databaseTaskBroadcastReceiver, + IntentFilter().apply { + addAction(DATABASE_START_TASK_ACTION) + addAction(DATABASE_STOP_TASK_ACTION) + } + ) + + // Check if a service is currently running else do nothing + bindService() + } + + @Synchronized + fun unregisterProgressTask() { + ProgressTaskDialogFragment.stop(activity) + + mBinder?.removeActionTaskListener(actionTaskListener) + mBinder = null + + unBindService() + + activity.unregisterReceiver(databaseTaskBroadcastReceiver) + } + + @Synchronized + private fun start(bundle: Bundle? = null, actionTask: String) { + activity.stopService(intentDatabaseTask) + if (bundle != null) + intentDatabaseTask.putExtras(bundle) + activity.runOnUiThread { + intentDatabaseTask.action = actionTask + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + activity.startForegroundService(intentDatabaseTask) + } else { + activity.startService(intentDatabaseTask) + } + } + } + + /* + ---- + Main methods + ---- + */ + + fun startDatabaseCreate(databaseUri: Uri, + masterPasswordChecked: Boolean, + masterPassword: String?, + keyFileChecked: Boolean, + keyFile: Uri?) { + start(Bundle().apply { + putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) + putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked) + putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword) + putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked) + putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile) + } + , ACTION_DATABASE_CREATE_TASK) + } + + fun startDatabaseLoad(databaseUri: Uri, + masterPassword: String?, + keyFile: Uri?, + readOnly: Boolean, + cipherEntity: CipherDatabaseEntity?, + fixDuplicateUuid: Boolean) { + start(Bundle().apply { + putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) + putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword) + putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile) + putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly) + putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity) + putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) + } + , ACTION_DATABASE_LOAD_TASK) } - fun start() { - actionRunnableAsyncTask?.execute(actionRunnable) + fun startDatabaseAssignPassword(databaseUri: Uri, + masterPasswordChecked: Boolean, + masterPassword: String?, + keyFileChecked: Boolean, + keyFile: Uri?) { + + start(Bundle().apply { + putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) + putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked) + putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword) + putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked) + putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile) + } + , ACTION_DATABASE_ASSIGN_PASSWORD_TASK) } + /* + ---- + Nodes Actions + ---- + */ + + fun startDatabaseCreateGroup(newGroup: GroupVersioned, + parent: GroupVersioned, + save: Boolean) { + start(Bundle().apply { + putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup) + putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId) + putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) + } + , ACTION_DATABASE_CREATE_GROUP_TASK) + } - private class ActionRunnableAsyncTask(private val progressTaskUpdater: ProgressTaskUpdater, - private val onPreExecute: () -> Unit, - private val onPostExecute: (result: ActionRunnable.Result) -> Unit) - : AsyncTask<((ProgressTaskUpdater?)-> ActionRunnable), Void, ActionRunnable.Result>() { + fun startDatabaseUpdateGroup(oldGroup: GroupVersioned, + groupToUpdate: GroupVersioned, + save: Boolean) { + start(Bundle().apply { + putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId) + putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate) + putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) + } + , ACTION_DATABASE_UPDATE_GROUP_TASK) + } - override fun onPreExecute() { - super.onPreExecute() - onPreExecute.invoke() + fun startDatabaseCreateEntry(newEntry: EntryVersioned, + parent: GroupVersioned, + save: Boolean) { + start(Bundle().apply { + putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry) + putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId) + putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) } + , ACTION_DATABASE_CREATE_ENTRY_TASK) + } - override fun doInBackground(vararg actionRunnables: ((ProgressTaskUpdater?)-> ActionRunnable)?): ActionRunnable.Result { - var resultTask = ActionRunnable.Result(false) - actionRunnables.forEach { - it?.invoke(progressTaskUpdater)?.apply { - run() - resultTask = result + fun startDatabaseUpdateEntry(oldEntry: EntryVersioned, + entryToUpdate: EntryVersioned, + save: Boolean) { + start(Bundle().apply { + putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId) + putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate) + putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) + } + , ACTION_DATABASE_UPDATE_ENTRY_TASK) + } + + private fun startDatabaseActionListNodes(actionTask: String, + nodesPaste: List, + newParent: GroupVersioned?, + save: Boolean) { + val groupsIdToCopy = ArrayList>() + val entriesIdToCopy = ArrayList>() + nodesPaste.forEach { nodeVersioned -> + when (nodeVersioned.type) { + Type.GROUP -> { + (nodeVersioned as GroupVersioned).nodeId?.let { groupId -> + groupsIdToCopy.add(groupId) + } + } + Type.ENTRY -> { + entriesIdToCopy.add((nodeVersioned as EntryVersioned).nodeId) } } - return resultTask } + val newParentId = newParent?.nodeId + + start(Bundle().apply { + putAll(getBundleFromListNodes(nodesPaste)) + putParcelableArrayList(DatabaseTaskNotificationService.GROUPS_ID_KEY, groupsIdToCopy) + putParcelableArrayList(DatabaseTaskNotificationService.ENTRIES_ID_KEY, entriesIdToCopy) + if (newParentId != null) + putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId) + putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) + } + , actionTask) + } + + fun startDatabaseCopyNodes(nodesToCopy: List, + newParent: GroupVersioned, + save: Boolean) { + startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save) + } + + fun startDatabaseMoveNodes(nodesToMove: List, + newParent: GroupVersioned, + save: Boolean) { + startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save) + } + + fun startDatabaseDeleteNodes(nodesToDelete: List, + save: Boolean) { + startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save) + } + + /* + ----------------- + Main Settings + ----------------- + */ + + fun startDatabaseSaveName(oldName: String, + newName: String) { + start(Bundle().apply { + putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName) + putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName) + } + , ACTION_DATABASE_SAVE_NAME_TASK) + } + + fun startDatabaseSaveDescription(oldDescription: String, + newDescription: String) { + start(Bundle().apply { + putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription) + putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription) + } + , ACTION_DATABASE_SAVE_DESCRIPTION_TASK) + } + + fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String, + newDefaultUsername: String) { + start(Bundle().apply { + putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername) + putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername) + } + , ACTION_DATABASE_SAVE_DEFAULT_USERNAME_TASK) + } + + fun startDatabaseSaveColor(oldColor: String, + newColor: String) { + start(Bundle().apply { + putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor) + putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor) + } + , ACTION_DATABASE_SAVE_COLOR_TASK) + } + + fun startDatabaseSaveCompression(oldCompression: PwCompressionAlgorithm, + newCompression: PwCompressionAlgorithm) { + start(Bundle().apply { + putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression) + putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression) + } + , ACTION_DATABASE_SAVE_COMPRESSION_TASK) + } + + fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int, + newMaxHistoryItems: Int) { + start(Bundle().apply { + putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems) + putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems) + } + , ACTION_DATABASE_SAVE_MAX_HISTORY_ITEMS_TASK) + } + + fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long, + newMaxHistorySize: Long) { + start(Bundle().apply { + putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize) + putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize) + } + , ACTION_DATABASE_SAVE_MAX_HISTORY_SIZE_TASK) + } + + /* + ------------------- + Security Settings + ------------------- + */ + + fun startDatabaseSaveEncryption(oldEncryption: PwEncryptionAlgorithm, + newEncryption: PwEncryptionAlgorithm) { + start(Bundle().apply { + putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption) + putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption) + } + , ACTION_DATABASE_SAVE_ENCRYPTION_TASK) + } + + fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine, + newKeyDerivation: KdfEngine) { + start(Bundle().apply { + putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation) + putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation) + } + , ACTION_DATABASE_SAVE_KEY_DERIVATION_TASK) + } + + fun startDatabaseSaveIterations(oldIterations: Long, + newIterations: Long) { + start(Bundle().apply { + putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations) + putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations) + } + , ACTION_DATABASE_SAVE_ITERATIONS_TASK) + } + + fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long, + newMemoryUsage: Long) { + start(Bundle().apply { + putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage) + putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage) + } + , ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK) + } - override fun onPostExecute(result: ActionRunnable.Result) { - super.onPostExecute(result) - onPostExecute.invoke(result) + fun startDatabaseSaveParallelism(oldParallelism: Int, + newParallelism: Int) { + start(Bundle().apply { + putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism) + putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism) } + , ACTION_DATABASE_SAVE_PARALLELISM_TASK) } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt index bba0a529f..092744fb8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt @@ -21,43 +21,33 @@ package com.kunzisoft.keepass.database.action import android.content.Context import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.exception.PwDbOutputException +import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.tasks.ActionRunnable import java.io.IOException -abstract class SaveDatabaseRunnable(protected var context: Context, +open class SaveDatabaseRunnable(protected var context: Context, protected var database: Database, - private val save: Boolean, - nestedAction: ActionRunnable? = null) : ActionRunnable(nestedAction) { + private var saveDatabase: Boolean) + : ActionRunnable() { - // TODO Service to prevent background thread kill - override fun run() { - if (save) { + var mAfterSaveDatabase: ((Result) -> Unit)? = null + + override fun onStartRun() {} + + override fun onActionRun() { + if (saveDatabase && result.isSuccess) { try { database.saveData(context.contentResolver) } catch (e: IOException) { - finishRun(false, e.message) - } catch (e: PwDbOutputException) { - finishRun(false, e.message) + setError(e.message) + } catch (e: DatabaseOutputException) { + setError(e.message) } } - - // Need to call super.run() in child class } - override fun onFinishRun(result: Result) { - // Need to call super.onFinishRun(result) in child class - } -} - -class SaveDatabaseActionRunnable(context: Context, - database: Database, - save: Boolean, - nestedAction: ActionRunnable? = null) - : SaveDatabaseRunnable(context, database, save, nestedAction) { - - override fun run() { - super.run() - finishRun(true) + override fun onFinishRun() { + // Need to call super.onFinishRun() in child class + mAfterSaveDatabase?.invoke(result) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt index cce1386f7..e8c8c2fee 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt @@ -1,52 +1,35 @@ package com.kunzisoft.keepass.database.action.node -import androidx.fragment.app.FragmentActivity -import android.util.Log +import android.content.Context import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable import com.kunzisoft.keepass.database.element.Database abstract class ActionNodeDatabaseRunnable( - context: FragmentActivity, + context: Context, database: Database, - private val callbackRunnable: AfterActionNodeFinishRunnable?, + private val afterActionNodesFinish: AfterActionNodesFinish?, save: Boolean) : SaveDatabaseRunnable(context, database, save) { /** - * Function do to a node action, don't implements run() if used this + * Function do to a node action */ abstract fun nodeAction() - override fun run() { - try { - nodeAction() - // To save the database - super.run() - finishRun(true) - } catch (e: Exception) { - Log.e("ActionNodeDBRunnable", e.message) - finishRun(false, e.message) - } + override fun onStartRun() { + nodeAction() + super.onStartRun() } /** - * Function do get the finish node action, don't implements onFinishRun() if used this + * Function do get the finish node action */ - abstract fun nodeFinish(result: Result): ActionNodeValues - - override fun onFinishRun(result: Result) { - callbackRunnable?.apply { - onActionNodeFinish(nodeFinish(result)) - } + abstract fun nodeFinish(): ActionNodesValues - if (!result.isSuccess) { - displayMessage(context) + override fun onFinishRun() { + super.onFinishRun() + afterActionNodesFinish?.apply { + onActionNodesFinish(result, nodeFinish()) } - - super.onFinishRun(result) - } - - companion object { - const val NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY = "NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY" } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddEntryRunnable.kt index 63a555c66..991b91be2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddEntryRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddEntryRunnable.kt @@ -19,19 +19,20 @@ */ package com.kunzisoft.keepass.database.action.node -import androidx.fragment.app.FragmentActivity +import android.content.Context import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.EntryVersioned import com.kunzisoft.keepass.database.element.GroupVersioned +import com.kunzisoft.keepass.database.element.NodeVersioned class AddEntryRunnable constructor( - context: FragmentActivity, + context: Context, database: Database, private val mNewEntry: EntryVersioned, private val mParent: GroupVersioned, - finishRunnable: AfterActionNodeFinishRunnable?, - save: Boolean) - : ActionNodeDatabaseRunnable(context, database, finishRunnable, save) { + save: Boolean, + afterActionNodesFinish: AfterActionNodesFinish?) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { override fun nodeAction() { mNewEntry.touch(modified = true, touchParents = true) @@ -39,12 +40,16 @@ class AddEntryRunnable constructor( database.addEntryTo(mNewEntry, mParent) } - override fun nodeFinish(result: Result): ActionNodeValues { + override fun nodeFinish(): ActionNodesValues { if (!result.isSuccess) { mNewEntry.parent?.let { database.removeEntryFrom(mNewEntry, it) } } - return ActionNodeValues(result, null, mNewEntry) + + val oldNodesReturn = ArrayList() + val newNodesReturn = ArrayList() + newNodesReturn.add(mNewEntry) + return ActionNodesValues(oldNodesReturn, newNodesReturn) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddGroupRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddGroupRunnable.kt index bf3c7fd0c..ebbce2abf 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddGroupRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddGroupRunnable.kt @@ -19,18 +19,19 @@ */ package com.kunzisoft.keepass.database.action.node -import androidx.fragment.app.FragmentActivity +import android.content.Context import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.GroupVersioned +import com.kunzisoft.keepass.database.element.NodeVersioned class AddGroupRunnable constructor( - context: FragmentActivity, + context: Context, database: Database, private val mNewGroup: GroupVersioned, private val mParent: GroupVersioned, - afterAddNodeRunnable: AfterActionNodeFinishRunnable?, - save: Boolean) - : ActionNodeDatabaseRunnable(context, database, afterAddNodeRunnable, save) { + save: Boolean, + afterActionNodesFinish: AfterActionNodesFinish?) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { override fun nodeAction() { mNewGroup.touch(modified = true, touchParents = true) @@ -38,10 +39,14 @@ class AddGroupRunnable constructor( database.addGroupTo(mNewGroup, mParent) } - override fun nodeFinish(result: Result): ActionNodeValues { + override fun nodeFinish(): ActionNodesValues { if (!result.isSuccess) { database.removeGroupFrom(mNewGroup, mParent) } - return ActionNodeValues(result, null, mNewGroup) + + val oldNodesReturn = ArrayList() + val newNodesReturn = ArrayList() + newNodesReturn.add(mNewGroup) + return ActionNodesValues(oldNodesReturn, newNodesReturn) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AfterActionNodeFinishRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AfterActionNodesFinish.kt similarity index 63% rename from app/src/main/java/com/kunzisoft/keepass/database/action/node/AfterActionNodeFinishRunnable.kt rename to app/src/main/java/com/kunzisoft/keepass/database/action/node/AfterActionNodesFinish.kt index f3d9e1bdb..c509a3223 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AfterActionNodeFinishRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AfterActionNodesFinish.kt @@ -24,14 +24,14 @@ import com.kunzisoft.keepass.tasks.ActionRunnable /** * Callback method who return the node(s) modified after an action - * - Add : @param oldNode NULL, @param newNode CreatedNode - * - Copy : @param oldNode NodeToCopy, @param newNode NodeCopied - * - Delete : @param oldNode NodeToDelete, @param NULL - * - Move : @param oldNode NULL, @param NodeToMove - * - Update : @param oldNode NodeToUpdate, @param NodeUpdated + * - Add : @param oldNodes empty, @param newNodes CreatedNodes + * - Copy : @param oldNodes NodesToCopy, @param newNodes NodesCopied + * - Delete : @param oldNodes NodesToDelete, @param newNodes empty + * - Move : @param oldNodes empty, @param newNodes NodesToMove + * - Update : @param oldNodes NodesToUpdate, @param newNodes NodesUpdated */ -data class ActionNodeValues(val result: ActionRunnable.Result, val oldNode: NodeVersioned?, val newNode: NodeVersioned?) +class ActionNodesValues(val oldNodes: List, val newNodes: List) -abstract class AfterActionNodeFinishRunnable { - abstract fun onActionNodeFinish(actionNodeValues: ActionNodeValues) +abstract class AfterActionNodesFinish { + abstract fun onActionNodesFinish(result: ActionRunnable.Result, actionNodesValues: ActionNodesValues) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyEntryRunnable.kt deleted file mode 100644 index aae0a530c..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyEntryRunnable.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.action.node - -import androidx.fragment.app.FragmentActivity -import android.util.Log -import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.element.EntryVersioned -import com.kunzisoft.keepass.database.element.GroupVersioned - -class CopyEntryRunnable constructor( - context: FragmentActivity, - database: Database, - private val mEntryToCopy: EntryVersioned, - private val mNewParent: GroupVersioned, - afterAddNodeRunnable: AfterActionNodeFinishRunnable?, - save: Boolean) - : ActionNodeDatabaseRunnable(context, database, afterAddNodeRunnable, save) { - - private var mEntryCopied: EntryVersioned? = null - - override fun nodeAction() { - // Condition - var conditionAccepted = true - if(mNewParent == database.rootGroup && !database.rootCanContainsEntry()) - conditionAccepted = false - if (conditionAccepted) { - // Update entry with new values - mNewParent.touch(modified = false, touchParents = true) - mEntryCopied = database.copyEntryTo(mEntryToCopy, mNewParent) - } else { - // Only finish thread - throw Exception(context.getString(R.string.error_copy_entry_here)) - } - - mEntryCopied?.apply { - touch(modified = true, touchParents = true) - } ?: Log.e(TAG, "Unable to create a copy of the entry") - } - - override fun nodeFinish(result: Result): ActionNodeValues { - if (!result.isSuccess) { - // If we fail to save, try to delete the copy - try { - mEntryCopied?.let { - database.deleteEntry(it) - } - } catch (e: Exception) { - Log.i(TAG, "Unable to delete the copied entry") - } - } - return ActionNodeValues(result, mEntryToCopy, mEntryCopied) - } - - companion object { - private val TAG = CopyEntryRunnable::class.java.name - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt new file mode 100644 index 000000000..e76479992 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.database.action.node + +import android.content.Context +import android.util.Log +import com.kunzisoft.keepass.database.element.* +import com.kunzisoft.keepass.database.exception.CopyDatabaseEntryException +import com.kunzisoft.keepass.database.exception.CopyDatabaseGroupException + +class CopyNodesRunnable constructor( + context: Context, + database: Database, + private val mNodesToCopy: List, + private val mNewParent: GroupVersioned, + save: Boolean, + afterActionNodesFinish: AfterActionNodesFinish?) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { + + private var mEntriesCopied = ArrayList() + + override fun nodeAction() { + + foreachNode@ for(currentNode in mNodesToCopy) { + when (currentNode.type) { + Type.GROUP -> { + Log.e(TAG, "Copy not allowed for group")// Only finish thread + setError(CopyDatabaseGroupException()) + break@foreachNode + } + Type.ENTRY -> { + // Root can contains entry + if (mNewParent != database.rootGroup || database.rootCanContainsEntry()) { + // Update entry with new values + mNewParent.touch(modified = false, touchParents = true) + + val entryCopied = database.copyEntryTo(currentNode as EntryVersioned, mNewParent) + if (entryCopied != null) { + entryCopied.touch(modified = true, touchParents = true) + mEntriesCopied.add(entryCopied) + } else { + Log.e(TAG, "Unable to create a copy of the entry") + setError(CopyDatabaseEntryException()) + break@foreachNode + } + } else { + // Only finish thread + setError(CopyDatabaseEntryException()) + break@foreachNode + } + } + } + } + } + + override fun nodeFinish(): ActionNodesValues { + if (!result.isSuccess) { + // If we fail to save, try to delete the copy + mEntriesCopied.forEach { + try { + database.deleteEntry(it) + } catch (e: Exception) { + Log.i(TAG, "Unable to delete the copied entry") + } + } + } + return ActionNodesValues(mNodesToCopy, mEntriesCopied) + } + + companion object { + private val TAG = CopyNodesRunnable::class.java.name + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteEntryRunnable.kt deleted file mode 100644 index 7111bb00a..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteEntryRunnable.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.action.node - -import android.os.Bundle -import androidx.fragment.app.FragmentActivity -import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.element.EntryVersioned -import com.kunzisoft.keepass.database.element.GroupVersioned - -class DeleteEntryRunnable constructor( - context: FragmentActivity, - database: Database, - private val mEntryToDelete: EntryVersioned, - finishRunnable: AfterActionNodeFinishRunnable?, - save: Boolean) - : ActionNodeDatabaseRunnable(context, database, finishRunnable, save) { - - private var mParent: GroupVersioned? = null - private var mCanRecycle: Boolean = false - - private var mEntryToDeleteBackup: EntryVersioned? = null - private var mNodePosition: Int? = null - - override fun nodeAction() { - mParent = mEntryToDelete.parent - mParent?.touch(modified = false, touchParents = true) - - // Get the node position - mNodePosition = mEntryToDelete.nodePositionInParent - - // Create a copy to keep the old ref and remove it visually - mEntryToDeleteBackup = EntryVersioned(mEntryToDelete) - - // Remove Entry from parent - mCanRecycle = database.canRecycle(mEntryToDelete) - if (mCanRecycle) { - database.recycle(mEntryToDelete, context.resources) - } else { - database.deleteEntry(mEntryToDelete) - } - } - - override fun nodeFinish(result: Result): ActionNodeValues { - if (!result.isSuccess) { - mParent?.let { - if (mCanRecycle) { - database.undoRecycle(mEntryToDelete, it) - } else { - database.undoDeleteEntry(mEntryToDelete, it) - } - } - } - - // Add position in bundle to delete the node in view - mNodePosition?.let { position -> - result.data = Bundle().apply { - putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position ) - } - } - - // Return a copy of unchanged entry as old param - // and entry deleted or moved in recycle bin as new param - return ActionNodeValues(result, mEntryToDeleteBackup, mEntryToDelete) - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteGroupRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteGroupRunnable.kt deleted file mode 100644 index 023988453..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteGroupRunnable.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.action.node - -import android.os.Bundle -import androidx.fragment.app.FragmentActivity - -import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.element.GroupVersioned - -class DeleteGroupRunnable(context: FragmentActivity, - database: Database, - private val mGroupToDelete: GroupVersioned, - finish: AfterActionNodeFinishRunnable, - save: Boolean) : ActionNodeDatabaseRunnable(context, database, finish, save) { - private var mParent: GroupVersioned? = null - private var mRecycle: Boolean = false - - private var mGroupToDeleteBackup: GroupVersioned? = null - private var mNodePosition: Int? = null - - override fun nodeAction() { - mParent = mGroupToDelete.parent - mParent?.touch(modified = false, touchParents = true) - - // Get the node position - mNodePosition = mGroupToDelete.nodePositionInParent - - // Create a copy to keep the old ref and remove it visually - mGroupToDeleteBackup = GroupVersioned(mGroupToDelete) - - // Remove Group from parent - mRecycle = database.canRecycle(mGroupToDelete) - if (mRecycle) { - database.recycle(mGroupToDelete, context.resources) - } else { - database.deleteGroup(mGroupToDelete) - } - } - - override fun nodeFinish(result: Result): ActionNodeValues { - if (!result.isSuccess) { - if (mRecycle) { - mParent?.let { - database.undoRecycle(mGroupToDelete, it) - } - } - // else { - // Let's not bother recovering from a failure to save a deleted tree. It is too much work. - // TODO database.undoDeleteGroupFrom(mGroup, mParent); - // } - } - - // Add position in bundle to delete the node in view - mNodePosition?.let { position -> - result.data = Bundle().apply { - putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position ) - } - } - - // Return a copy of unchanged group as old param - // and group deleted or moved in recycle bin as new param - return ActionNodeValues(result, mGroupToDeleteBackup, mGroupToDelete) - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt new file mode 100644 index 000000000..e24619d46 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.database.action.node + +import android.content.Context +import com.kunzisoft.keepass.database.element.* + +class DeleteNodesRunnable(context: Context, + database: Database, + private val mNodesToDelete: List, + save: Boolean, + afterActionNodesFinish: AfterActionNodesFinish) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { + + private var mParent: GroupVersioned? = null + private var mCanRecycle: Boolean = false + + private var mNodesToDeleteBackup = ArrayList() + + override fun nodeAction() { + + foreachNode@ for(currentNode in mNodesToDelete) { + mParent = currentNode.parent + mParent?.touch(modified = false, touchParents = true) + + when (currentNode.type) { + Type.GROUP -> { + // Create a copy to keep the old ref and remove it visually + mNodesToDeleteBackup.add(GroupVersioned(currentNode as GroupVersioned)) + // Remove Node from parent + mCanRecycle = database.canRecycle(currentNode) + if (mCanRecycle) { + database.recycle(currentNode, context.resources) + } else { + database.deleteGroup(currentNode) + } + } + Type.ENTRY -> { + // Create a copy to keep the old ref and remove it visually + mNodesToDeleteBackup.add(EntryVersioned(currentNode as EntryVersioned)) + // Remove Node from parent + mCanRecycle = database.canRecycle(currentNode) + if (mCanRecycle) { + database.recycle(currentNode, context.resources) + } else { + database.deleteEntry(currentNode) + } + } + } + } + } + + override fun nodeFinish(): ActionNodesValues { + if (!result.isSuccess) { + if (mCanRecycle) { + mParent?.let { + mNodesToDeleteBackup.forEach { backupNode -> + when (backupNode.type) { + Type.GROUP -> { + database.undoRecycle(backupNode as GroupVersioned, it) + } + Type.ENTRY -> { + database.undoRecycle(backupNode as EntryVersioned, it) + } + } + } + } + } + // else { + // Let's not bother recovering from a failure to save a deleted tree. It is too much work. + // TODO database.undoDeleteGroupFrom(mGroup, mParent); + // } + } + + // Return a copy of unchanged nodes as old param + // and nodes deleted or moved in recycle bin as new param + return ActionNodesValues(mNodesToDeleteBackup, mNodesToDelete) + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveEntryRunnable.kt deleted file mode 100644 index 0ca58c81e..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveEntryRunnable.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.action.node - -import androidx.fragment.app.FragmentActivity -import android.util.Log -import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.element.EntryVersioned -import com.kunzisoft.keepass.database.element.GroupVersioned - -class MoveEntryRunnable constructor( - context: FragmentActivity, - database: Database, - private val mEntryToMove: EntryVersioned?, - private val mNewParent: GroupVersioned, - afterAddNodeRunnable: AfterActionNodeFinishRunnable?, - save: Boolean) - : ActionNodeDatabaseRunnable(context, database, afterAddNodeRunnable, save) { - - private var mOldParent: GroupVersioned? = null - - override fun nodeAction() { - // Move entry in new parent - mEntryToMove?.let { - mOldParent = it.parent - - // Condition - var conditionAccepted = true - if(mNewParent == database.rootGroup && !database.rootCanContainsEntry()) - conditionAccepted = false - // Move only if the parent change - if (mOldParent != mNewParent && conditionAccepted) { - database.moveEntryTo(it, mNewParent) - } else { - // Only finish thread - throw Exception(context.getString(R.string.error_move_entry_here)) - } - it.touch(modified = true, touchParents = true) - } ?: Log.e(TAG, "Unable to create a copy of the entry") - } - - override fun nodeFinish(result: Result): ActionNodeValues { - if (!result.isSuccess) { - // If we fail to save, try to remove in the first place - try { - if (mEntryToMove != null && mOldParent != null) - database.moveEntryTo(mEntryToMove, mOldParent!!) - } catch (e: Exception) { - Log.i(TAG, "Unable to replace the entry") - } - - } - return ActionNodeValues(result, null, mEntryToMove) - } - - companion object { - private val TAG = MoveEntryRunnable::class.java.name - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveGroupRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveGroupRunnable.kt deleted file mode 100644 index 23dc80fdd..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveGroupRunnable.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.action.node - -import androidx.fragment.app.FragmentActivity -import android.util.Log -import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.element.GroupVersioned - -class MoveGroupRunnable constructor( - context: FragmentActivity, - database: Database, - private val mGroupToMove: GroupVersioned?, - private val mNewParent: GroupVersioned, - afterAddNodeRunnable: AfterActionNodeFinishRunnable?, - save: Boolean) - : ActionNodeDatabaseRunnable(context, database, afterAddNodeRunnable, save) { - - private var mOldParent: GroupVersioned? = null - - override fun nodeAction() { - mGroupToMove?.let { - mOldParent = it.parent - // Move group in new parent if not in the current group - if (mGroupToMove != mNewParent && !mNewParent.isContainedIn(mGroupToMove)) { - database.moveGroupTo(mGroupToMove, mNewParent) - mGroupToMove.touch(modified = true, touchParents = true) - finishRun(true) - } else { - // Only finish thread - throw Exception(context.getString(R.string.error_move_folder_in_itself)) - } - } ?: Log.e(TAG, "Unable to create a copy of the group") - } - - override fun nodeFinish(result: Result): ActionNodeValues { - if (!result.isSuccess) { - // If we fail to save, try to move in the first place - try { - if (mGroupToMove != null && mOldParent != null) - database.moveGroupTo(mGroupToMove, mOldParent!!) - } catch (e: Exception) { - Log.i(TAG, "Unable to replace the group") - } - - } - return ActionNodeValues(result, null, mGroupToMove) - } - - companion object { - private val TAG = MoveGroupRunnable::class.java.name - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt new file mode 100644 index 000000000..ae8335f6b --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.database.action.node + +import android.content.Context +import android.util.Log +import com.kunzisoft.keepass.database.element.* +import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.database.exception.MoveDatabaseEntryException +import com.kunzisoft.keepass.database.exception.MoveDatabaseGroupException + +class MoveNodesRunnable constructor( + context: Context, + database: Database, + private val mNodesToMove: List, + private val mNewParent: GroupVersioned, + save: Boolean, + afterActionNodesFinish: AfterActionNodesFinish?) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { + + private var mOldParent: GroupVersioned? = null + + override fun nodeAction() { + + foreachNode@ for(nodeToMove in mNodesToMove) { + // Move node in new parent + mOldParent = nodeToMove.parent + + when (nodeToMove.type) { + Type.GROUP -> { + val groupToMove = nodeToMove as GroupVersioned + // Move group in new parent if not in the current group + if (groupToMove != mNewParent + && !mNewParent.isContainedIn(groupToMove)) { + nodeToMove.touch(modified = true, touchParents = true) + database.moveGroupTo(groupToMove, mNewParent) + } else { + // Only finish thread + setError(MoveDatabaseGroupException()) + break@foreachNode + } + } + Type.ENTRY -> { + val entryToMove = nodeToMove as EntryVersioned + // Move only if the parent change + if (mOldParent != mNewParent + // and root can contains entry + && (mNewParent != database.rootGroup || database.rootCanContainsEntry())) { + nodeToMove.touch(modified = true, touchParents = true) + database.moveEntryTo(entryToMove, mNewParent) + } else { + // Only finish thread + setError(MoveDatabaseEntryException()) + break@foreachNode + } + } + } + } + } + + override fun nodeFinish(): ActionNodesValues { + if (!result.isSuccess) { + try { + mNodesToMove.forEach { nodeToMove -> + // If we fail to save, try to move in the first place + if (mOldParent != null && + mOldParent != nodeToMove.parent) { + when (nodeToMove.type) { + Type.GROUP -> database.moveGroupTo(nodeToMove as GroupVersioned, mOldParent!!) + Type.ENTRY -> database.moveEntryTo(nodeToMove as EntryVersioned, mOldParent!!) + } + } + } + } catch (e: Exception) { + Log.i(TAG, "Unable to replace the node") + } + } + return ActionNodesValues(ArrayList(), mNodesToMove) + } + + companion object { + private val TAG = MoveNodesRunnable::class.java.name + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt index 4c5be2905..14e1689f5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt @@ -19,36 +19,50 @@ */ package com.kunzisoft.keepass.database.action.node -import androidx.fragment.app.FragmentActivity +import android.content.Context import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.EntryVersioned +import com.kunzisoft.keepass.database.element.NodeVersioned class UpdateEntryRunnable constructor( - context: FragmentActivity, + context: Context, database: Database, private val mOldEntry: EntryVersioned, private val mNewEntry: EntryVersioned, - finishRunnable: AfterActionNodeFinishRunnable?, - save: Boolean) - : ActionNodeDatabaseRunnable(context, database, finishRunnable, save) { + save: Boolean, + afterActionNodesFinish: AfterActionNodesFinish?) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { // Keep backup of original values in case save fails - private var mBackupEntry: EntryVersioned? = null + private var mBackupEntryHistory: EntryVersioned = EntryVersioned(mOldEntry) override fun nodeAction() { - mBackupEntry = database.addHistoryBackupTo(mOldEntry) - mOldEntry.touch(modified = true, touchParents = true) + // WARNING : Re attribute parent removed in entry edit activity to save memory + mNewEntry.addParentFrom(mOldEntry) + // Update entry with new values mOldEntry.updateWith(mNewEntry) + mNewEntry.touch(modified = true, touchParents = true) + + // Create an entry history (an entry history don't have history) + mOldEntry.addEntryToHistory(EntryVersioned(mBackupEntryHistory, copyHistory = false)) + database.removeOldestHistory(mOldEntry) + + // Only change data in index + database.updateEntry(mOldEntry) } - override fun nodeFinish(result: Result): ActionNodeValues { + override fun nodeFinish(): ActionNodesValues { if (!result.isSuccess) { + mOldEntry.updateWith(mBackupEntryHistory) // If we fail to save, back out changes to global structure - mBackupEntry?.let { - mOldEntry.updateWith(it) - } + database.updateEntry(mOldEntry) } - return ActionNodeValues(result, mOldEntry, mNewEntry) + + val oldNodesReturn = ArrayList() + oldNodesReturn.add(mBackupEntryHistory) + val newNodesReturn = ArrayList() + newNodesReturn.add(mOldEntry) + return ActionNodesValues(oldNodesReturn, newNodesReturn) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateGroupRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateGroupRunnable.kt index 40d3104c0..273bb7227 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateGroupRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateGroupRunnable.kt @@ -19,33 +19,47 @@ */ package com.kunzisoft.keepass.database.action.node -import androidx.fragment.app.FragmentActivity +import android.content.Context import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.GroupVersioned +import com.kunzisoft.keepass.database.element.NodeVersioned class UpdateGroupRunnable constructor( - context: FragmentActivity, + context: Context, database: Database, private val mOldGroup: GroupVersioned, private val mNewGroup: GroupVersioned, - finishRunnable: AfterActionNodeFinishRunnable?, - save: Boolean) - : ActionNodeDatabaseRunnable(context, database, finishRunnable, save) { + save: Boolean, + afterActionNodesFinish: AfterActionNodesFinish?) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { // Keep backup of original values in case save fails private val mBackupGroup: GroupVersioned = GroupVersioned(mOldGroup) override fun nodeAction() { + // WARNING : Re attribute parent and children removed in group activity to save memory + mNewGroup.addParentFrom(mOldGroup) + mNewGroup.addChildrenFrom(mOldGroup) + // Update group with new values - mOldGroup.touch(modified = true, touchParents = true) mOldGroup.updateWith(mNewGroup) + mOldGroup.touch(modified = true, touchParents = true) + + // Only change data in index + database.updateGroup(mOldGroup) } - override fun nodeFinish(result: Result): ActionNodeValues { + override fun nodeFinish(): ActionNodesValues { if (!result.isSuccess) { // If we fail to save, back out changes to global structure mOldGroup.updateWith(mBackupGroup) + database.updateGroup(mOldGroup) } - return ActionNodeValues(result, mOldGroup, mNewGroup) + + val oldNodesReturn = ArrayList() + oldNodesReturn.add(mBackupGroup) + val newNodesReturn = ArrayList() + newNodesReturn.add(mOldGroup) + return ActionNodesValues(oldNodesReturn, newNodesReturn) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursor.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursor.kt index 67f89fd51..e9f59624b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursor.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursor.kt @@ -2,12 +2,11 @@ package com.kunzisoft.keepass.database.cursor import android.database.MatrixCursor import android.provider.BaseColumns +import com.kunzisoft.keepass.database.element.PwEntry +import com.kunzisoft.keepass.database.element.PwIconFactory +import com.kunzisoft.keepass.database.element.PwNodeId -import com.kunzisoft.keepass.database.element.* - -import java.util.UUID - -abstract class EntryCursor> : MatrixCursor(arrayOf( +abstract class EntryCursor> : MatrixCursor(arrayOf( _ID, COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS, COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS, @@ -25,10 +24,10 @@ abstract class EntryCursor> : MatrixCursor(arrayOf( abstract fun addEntry(entry: PwEntryV) + abstract fun getPwNodeId(): PwNodeId + open fun populateEntry(pwEntry: PwEntryV, iconFactory: PwIconFactory) { - pwEntry.nodeId = PwNodeIdUUID( - UUID(getLong(getColumnIndex(COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)), - getLong(getColumnIndex(COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS)))) + pwEntry.nodeId = getPwNodeId() pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE)) val iconStandard = iconFactory.getIcon(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD))) @@ -53,5 +52,4 @@ abstract class EntryCursor> : MatrixCursor(arrayOf( const val COLUMN_INDEX_URL = "URL" const val COLUMN_INDEX_NOTES = "notes" } - } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorUUID.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorUUID.kt new file mode 100644 index 000000000..95be574a9 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorUUID.kt @@ -0,0 +1,15 @@ +package com.kunzisoft.keepass.database.cursor + +import com.kunzisoft.keepass.database.element.PwEntry +import com.kunzisoft.keepass.database.element.PwNodeId +import com.kunzisoft.keepass.database.element.PwNodeIdUUID +import java.util.* + +abstract class EntryCursorUUID>: EntryCursor() { + + override fun getPwNodeId(): PwNodeId { + return PwNodeIdUUID( + UUID(getLong(getColumnIndex(COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)), + getLong(getColumnIndex(COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS)))) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV3.kt index 9ef52fbe5..f50b65388 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV3.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV3.kt @@ -3,7 +3,7 @@ package com.kunzisoft.keepass.database.cursor import com.kunzisoft.keepass.database.element.PwDatabase import com.kunzisoft.keepass.database.element.PwEntryV3 -class EntryCursorV3 : EntryCursor() { +class EntryCursorV3 : EntryCursorUUID() { override fun addEntry(entry: PwEntryV3) { addRow(arrayOf( diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV4.kt index d4b1c02dd..f4fcbb3a1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/EntryCursorV4.kt @@ -5,7 +5,7 @@ import com.kunzisoft.keepass.database.element.PwIconFactory import java.util.UUID -class EntryCursorV4 : EntryCursor() { +class EntryCursorV4 : EntryCursorUUID() { private val extraFieldCursor: ExtraFieldCursor = ExtraFieldCursor() diff --git a/app/src/main/java/com/kunzisoft/keepass/database/cursor/ExtraFieldCursor.kt b/app/src/main/java/com/kunzisoft/keepass/database/cursor/ExtraFieldCursor.kt index 33913ab08..f86a27b82 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/cursor/ExtraFieldCursor.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/cursor/ExtraFieldCursor.kt @@ -23,7 +23,7 @@ class ExtraFieldCursor : MatrixCursor(arrayOf( } fun populateExtraFieldInEntry(pwEntry: PwEntryV4) { - pwEntry.addExtraField(getString(getColumnIndex(COLUMN_LABEL)), + pwEntry.putExtraField(getString(getColumnIndex(COLUMN_LABEL)), ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0, getString(getColumnIndex(COLUMN_VALUE)))) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt index 8954abf88..1cb8c505f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt @@ -20,14 +20,12 @@ package com.kunzisoft.keepass.database.element import android.content.ContentResolver -import android.content.Context import android.content.res.Resources import android.database.Cursor import android.net.Uri import android.util.Log import android.webkit.URLUtil import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine -import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.database.NodeHandler import com.kunzisoft.keepass.database.cursor.EntryCursorV3 import com.kunzisoft.keepass.database.cursor.EntryCursorV4 @@ -40,7 +38,6 @@ import com.kunzisoft.keepass.database.file.save.PwDbV3Output import com.kunzisoft.keepass.database.file.save.PwDbV4Output import com.kunzisoft.keepass.database.search.SearchDbHelper import com.kunzisoft.keepass.icons.IconDrawableFactory -import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.stream.LEDataInputStream import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.utils.SingletonHolder @@ -56,8 +53,11 @@ class Database { private var pwDatabaseV3: PwDatabaseV3? = null private var pwDatabaseV4: PwDatabaseV4? = null - private var mUri: Uri? = null - private var searchHelper: SearchDbHelper? = null + var fileUri: Uri? = null + private set + + private var mSearchHelper: SearchDbHelper? = null + var isReadOnly = false val drawFactory = IconDrawableFactory() @@ -69,51 +69,120 @@ class Database { return pwDatabaseV3?.iconFactory ?: pwDatabaseV4?.iconFactory ?: PwIconFactory() } - val name: String + val allowName: Boolean + get() = pwDatabaseV4 != null + + var name: String get() { return pwDatabaseV4?.name ?: "" } + set(name) { + pwDatabaseV4?.name = name + pwDatabaseV4?.nameChanged = PwDate() + } + + val allowDescription: Boolean + get() = pwDatabaseV4 != null - val description: String + var description: String get() { return pwDatabaseV4?.description ?: "" } + set(description) { + pwDatabaseV4?.description = description + pwDatabaseV4?.descriptionChanged = PwDate() + } + + val allowDefaultUsername: Boolean + get() = pwDatabaseV4 != null + // TODO get() = pwDatabaseV3 != null || pwDatabaseV4 != null var defaultUsername: String get() { - return pwDatabaseV4?.defaultUserName ?: "" + return pwDatabaseV4?.defaultUserName ?: "" // TODO pwDatabaseV3 default username } set(username) { pwDatabaseV4?.defaultUserName = username pwDatabaseV4?.defaultUserNameChanged = PwDate() } - val encryptionAlgorithm: PwEncryptionAlgorithm? + val allowCustomColor: Boolean + get() = pwDatabaseV4 != null + // TODO get() = pwDatabaseV3 != null || pwDatabaseV4 != null + + // with format "#000000" + var customColor: String get() { - return pwDatabaseV4?.encryptionAlgorithm + return pwDatabaseV4?.color ?: "" // TODO pwDatabaseV3 color } + set(value) { + // TODO Check color string + pwDatabaseV4?.color = value + } + + val version: String + get() = pwDatabaseV3?.version ?: pwDatabaseV4?.version ?: "-" + + val allowDataCompression: Boolean + get() = pwDatabaseV4 != null + + val availableCompressionAlgorithms: List + get() = pwDatabaseV4?.availableCompressionAlgorithms ?: ArrayList() + + var compressionAlgorithm: PwCompressionAlgorithm? + get() = pwDatabaseV4?.compressionAlgorithm + set(value) { + value?.let { + pwDatabaseV4?.compressionAlgorithm = it + } + } + + val allowNoMasterKey: Boolean + get() = pwDatabaseV4 != null + + val allowEncryptionAlgorithmModification: Boolean + get() = availableEncryptionAlgorithms.size > 1 + + fun getEncryptionAlgorithmName(resources: Resources): String { + return pwDatabaseV3?.encryptionAlgorithm?.getName(resources) + ?: pwDatabaseV4?.encryptionAlgorithm?.getName(resources) + ?: "" + } val availableEncryptionAlgorithms: List get() = pwDatabaseV3?.availableEncryptionAlgorithms ?: pwDatabaseV4?.availableEncryptionAlgorithms ?: ArrayList() - val availableKdfEngines: List - get() { - if (pwDatabaseV3 != null) { - return KdfFactory.kdfListV3 - } - if (pwDatabaseV4 != null) { - return KdfFactory.kdfListV4 + var encryptionAlgorithm: PwEncryptionAlgorithm? + get() = pwDatabaseV3?.encryptionAlgorithm ?: pwDatabaseV4?.encryptionAlgorithm + set(algorithm) { + algorithm?.let { + pwDatabaseV4?.encryptionAlgorithm = algorithm + pwDatabaseV4?.setDataEngine(algorithm.cipherEngine) + pwDatabaseV4?.dataCipher = algorithm.dataCipher } - return ArrayList() } - val kdfEngine: KdfEngine - get() { - return pwDatabaseV4?.kdfEngine ?: return KdfFactory.aesKdf + val availableKdfEngines: List + get() = pwDatabaseV3?.kdfAvailableList ?: pwDatabaseV4?.kdfAvailableList ?: ArrayList() + + val allowKdfModification: Boolean + get() = availableKdfEngines.size > 1 + + var kdfEngine: KdfEngine? + get() = pwDatabaseV3?.kdfEngine ?: pwDatabaseV4?.kdfEngine + set(kdfEngine) { + kdfEngine?.let { + if (pwDatabaseV4?.kdfParameters?.uuid != kdfEngine.defaultParameters.uuid) + pwDatabaseV4?.kdfParameters = kdfEngine.defaultParameters + numberKeyEncryptionRounds = kdfEngine.defaultKeyRounds + memoryUsage = kdfEngine.defaultMemoryUsage + parallelism = kdfEngine.defaultParallelism + } } - val numberKeyEncryptionRoundsAsString: String - get() = numberKeyEncryptionRounds.toString() + fun getKeyDerivationName(resources: Resources): String { + return kdfEngine?.getName(resources) ?: "" + } var numberKeyEncryptionRounds: Long get() = pwDatabaseV3?.numberKeyEncryptionRounds ?: pwDatabaseV4?.numberKeyEncryptionRounds ?: 0 @@ -123,9 +192,6 @@ class Database { pwDatabaseV4?.numberKeyEncryptionRounds = numberRounds } - val memoryUsageAsString: String - get() = memoryUsage.toString() - var memoryUsage: Long get() { return pwDatabaseV4?.memoryUsage ?: return KdfEngine.UNKNOWN_VALUE.toLong() @@ -134,9 +200,6 @@ class Database { pwDatabaseV4?.memoryUsage = memory } - val parallelismAsString: String - get() = parallelism.toString() - var parallelism: Int get() = pwDatabaseV4?.parallelism ?: KdfEngine.UNKNOWN_VALUE set(parallelism) { @@ -161,11 +224,30 @@ class Database { return null } + val manageHistory: Boolean + get() = pwDatabaseV4 != null + + var historyMaxItems: Int + get() { + return pwDatabaseV4?.historyMaxItems ?: 0 + } + set(value) { + pwDatabaseV4?.historyMaxItems = value + } + + var historyMaxSize: Long + get() { + return pwDatabaseV4?.historyMaxSize ?: 0 + } + set(value) { + pwDatabaseV4?.historyMaxSize = value + } + /** * Determine if RecycleBin is available or not for this version of database * @return true if RecycleBin available */ - val isRecycleBinAvailable: Boolean + val allowRecycleBin: Boolean get() = pwDatabaseV4 != null val isRecycleBinEnabled: Boolean @@ -203,14 +285,20 @@ class Database { fun createData(databaseUri: Uri) { // Always create a new database with the last version setDatabaseV4(PwDatabaseV4(dbNameFromUri(databaseUri))) - this.mUri = databaseUri + this.fileUri = databaseUri } - @Throws(IOException::class, InvalidDBException::class) - fun loadData(ctx: Context, uri: Uri, password: String?, keyfile: Uri?, progressTaskUpdater: ProgressTaskUpdater?) { + @Throws(LoadDatabaseException::class) + fun loadData(uri: Uri, password: String?, keyfile: Uri?, + readOnly: Boolean, + contentResolver: ContentResolver, + cacheDirectory: File, + omitBackup: Boolean, + fixDuplicateUUID: Boolean, + progressTaskUpdater: ProgressTaskUpdater?) { - mUri = uri - isReadOnly = false + this.fileUri = uri + isReadOnly = readOnly if (uri.scheme == "file") { val file = File(uri.path!!) isReadOnly = !file.canWrite() @@ -219,20 +307,20 @@ class Database { // Pass Uris as InputStreams val inputStream: InputStream? try { - inputStream = UriUtil.getUriInputStream(ctx.contentResolver, uri) + inputStream = UriUtil.getUriInputStream(contentResolver, uri) } catch (e: Exception) { Log.e("KPD", "Database::loadData", e) - throw ContentFileNotFoundException.getInstance(uri) + throw LoadDatabaseFileNotFoundException() } // Pass KeyFile Uri as InputStreams var keyFileInputStream: InputStream? = null keyfile?.let { try { - keyFileInputStream = UriUtil.getUriInputStream(ctx.contentResolver, keyfile) + keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile) } catch (e: Exception) { Log.e("KPD", "Database::loadData", e) - throw ContentFileNotFoundException.getInstance(keyfile) + throw LoadDatabaseFileNotFoundException() } } @@ -257,28 +345,25 @@ class Database { // Header of database V3 PwDbHeaderV3.matchesHeader(sig1, sig2) -> setDatabaseV3(ImporterV3() .openDatabase(bufferedInputStream, - password, - keyFileInputStream, - progressTaskUpdater)) + password, + keyFileInputStream, + progressTaskUpdater)) // Header of database V4 - PwDbHeaderV4.matchesHeader(sig1, sig2) -> setDatabaseV4(ImporterV4(ctx.filesDir) + PwDbHeaderV4.matchesHeader(sig1, sig2) -> setDatabaseV4(ImporterV4( + cacheDirectory, + fixDuplicateUUID) .openDatabase(bufferedInputStream, - password, - keyFileInputStream, - progressTaskUpdater)) + password, + keyFileInputStream, + progressTaskUpdater)) // Header not recognized - else -> throw InvalidDBSignatureException() + else -> throw LoadDatabaseSignatureException() } - try { - searchHelper = SearchDbHelper(PreferencesUtil.omitBackup(ctx)) - loaded = true - } catch (e: Exception) { - Log.e(TAG, "Load can't be performed with this Database version", e) - loaded = false - } + this.mSearchHelper = SearchDbHelper(omitBackup) + loaded = true } fun isGroupSearchable(group: GroupVersioned, isOmitBackup: Boolean): Boolean { @@ -289,7 +374,7 @@ class Database { @JvmOverloads fun search(str: String, max: Int = Integer.MAX_VALUE): GroupVersioned? { - return searchHelper?.search(this, str, max) + return mSearchHelper?.search(this, str, max) } fun searchEntries(query: String): Cursor? { @@ -340,14 +425,14 @@ class Database { return entry } - @Throws(IOException::class, PwDbOutputException::class) + @Throws(IOException::class, DatabaseOutputException::class) fun saveData(contentResolver: ContentResolver) { - mUri?.let { + this.fileUri?.let { saveData(contentResolver, it) } } - @Throws(IOException::class, PwDbOutputException::class) + @Throws(IOException::class, DatabaseOutputException::class) private fun saveData(contentResolver: ContentResolver, uri: Uri) { val errorMessage = "Failed to store database." @@ -394,7 +479,7 @@ class Database { outputStream?.close() } } - mUri = uri + this.fileUri = uri } // TODO Clear database when lock broadcast is receive in backstage @@ -406,77 +491,23 @@ class Database { // In all cases, delete all the files in the temp dir try { FileUtils.cleanDirectory(filesDirectory) - } catch (e: IOException) { + } catch (e: Exception) { Log.e(TAG, "Unable to clear the directory cache.", e) } - pwDatabaseV3 = null - pwDatabaseV4 = null - mUri = null - loaded = false - } - - fun getVersion(): String { - return pwDatabaseV3?.version ?: pwDatabaseV4?.version ?: "unknown" - } - - fun containsName(): Boolean { - pwDatabaseV4?.let { return true } - return false - } - - fun assignName(name: String) { - pwDatabaseV4?.name = name - pwDatabaseV4?.nameChanged = PwDate() - } - - fun containsDescription(): Boolean { - pwDatabaseV4?.let { return true } - return false - } - - fun assignDescription(description: String) { - pwDatabaseV4?.description = description - pwDatabaseV4?.descriptionChanged = PwDate() - } - - fun allowEncryptionAlgorithmModification(): Boolean { - return availableEncryptionAlgorithms.size > 1 - } - - fun assignEncryptionAlgorithm(algorithm: PwEncryptionAlgorithm) { - pwDatabaseV4?.encryptionAlgorithm = algorithm - pwDatabaseV4?.setDataEngine(algorithm.cipherEngine) - pwDatabaseV4?.dataCipher = algorithm.dataCipher - } - - fun getEncryptionAlgorithmName(resources: Resources): String { - return pwDatabaseV3?.encryptionAlgorithm?.getName(resources) ?: pwDatabaseV4?.encryptionAlgorithm?.getName(resources) ?: "" - } - - fun allowKdfModification(): Boolean { - return availableKdfEngines.size > 1 - } - - fun assignKdfEngine(kdfEngine: KdfEngine) { - if (pwDatabaseV4?.kdfParameters?.uuid != kdfEngine.defaultParameters.uuid) - pwDatabaseV4?.kdfParameters = kdfEngine.defaultParameters - numberKeyEncryptionRounds = kdfEngine.defaultKeyRounds - memoryUsage = kdfEngine.getDefaultMemoryUsage() - parallelism = kdfEngine.getDefaultParallelism() - } - - fun getKeyDerivationName(resources: Resources): String { - return kdfEngine.getName(resources) + this.pwDatabaseV3 = null + this.pwDatabaseV4 = null + this.fileUri = null + this.loaded = false } - fun validatePasswordEncoding(key: String?): Boolean { - return pwDatabaseV3?.validatePasswordEncoding(key) - ?: pwDatabaseV4?.validatePasswordEncoding(key) + fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean { + return pwDatabaseV3?.validatePasswordEncoding(password, containsKeyFile) + ?: pwDatabaseV4?.validatePasswordEncoding(password, containsKeyFile) ?: false } - @Throws(InvalidKeyFileException::class, IOException::class) + @Throws(IOException::class) fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) { pwDatabaseV3?.retrieveMasterKey(key, keyInputStream) pwDatabaseV4?.retrieveMasterKey(key, keyInputStream) @@ -516,7 +547,7 @@ class Database { return null } - fun getEntryById(id: PwNodeId<*>): EntryVersioned? { + fun getEntryById(id: PwNodeId): EntryVersioned? { pwDatabaseV3?.getEntryById(id)?.let { return EntryVersioned(it) } @@ -527,12 +558,14 @@ class Database { } fun getGroupById(id: PwNodeId<*>): GroupVersioned? { - pwDatabaseV3?.getGroupById(id)?.let { - return GroupVersioned(it) - } - pwDatabaseV4?.getGroupById(id)?.let { - return GroupVersioned(it) - } + if (id is PwNodeIdInt) + pwDatabaseV3?.getGroupById(id)?.let { + return GroupVersioned(it) + } + else if (id is PwNodeIdUUID) + pwDatabaseV4?.getGroupById(id)?.let { + return GroupVersioned(it) + } return null } @@ -546,6 +579,15 @@ class Database { entry.afterAssignNewParent() } + fun updateEntry(entry: EntryVersioned) { + entry.pwEntryV3?.let { entryV3 -> + pwDatabaseV3?.updateEntry(entryV3) + } + entry.pwEntryV4?.let { entryV4 -> + pwDatabaseV4?.updateEntry(entryV4) + } + } + fun removeEntryFrom(entry: EntryVersioned, parent: GroupVersioned) { entry.pwEntryV3?.let { entryV3 -> pwDatabaseV3?.removeEntryFrom(entryV3, parent.pwGroupV3) @@ -566,6 +608,15 @@ class Database { group.afterAssignNewParent() } + fun updateGroup(group: GroupVersioned) { + group.pwGroupV3?.let { groupV3 -> + pwDatabaseV3?.updateGroup(groupV3) + } + group.pwGroupV4?.let { groupV4 -> + pwDatabaseV4?.updateGroup(groupV4) + } + } + fun removeGroupFrom(group: GroupVersioned, parent: GroupVersioned) { group.pwGroupV3?.let { groupV3 -> pwDatabaseV3?.removeGroupFrom(groupV3, parent.pwGroupV3) @@ -582,7 +633,7 @@ class Database { * @param newParent */ fun copyEntryTo(entryToCopy: EntryVersioned, newParent: GroupVersioned): EntryVersioned? { - val entryCopied = EntryVersioned(entryToCopy) + val entryCopied = EntryVersioned(entryToCopy, false) entryCopied.nodeId = pwDatabaseV3?.newEntryId() ?: pwDatabaseV4?.newEntryId() ?: PwNodeIdUUID() entryCopied.parent = newParent entryCopied.title += " (~)" @@ -702,31 +753,28 @@ class Database { } } - fun addHistoryBackupTo(entry: EntryVersioned): EntryVersioned { - val backupEntry = EntryVersioned(entry) - - entry.addBackupToHistory() + fun removeOldestHistory(entry: EntryVersioned) { - // Remove oldest backup if more than max items or max memory + // Remove oldest history if more than max items or max memory pwDatabaseV4?.let { val history = entry.getHistory() - val maxItems = it.historyMaxItems + val maxItems = historyMaxItems if (maxItems >= 0) { while (history.size > maxItems) { entry.removeOldestEntryFromHistory() } } - val maxSize = it.historyMaxSize + val maxSize = historyMaxSize if (maxSize >= 0) { while (true) { - var histSize: Long = 0 - for (backup in history) { - histSize += backup.size + var historySize: Long = 0 + for (entryHistory in history) { + historySize += entryHistory.getSize() } - if (histSize > maxSize) { + if (historySize > maxSize) { entry.removeOldestEntryFromHistory() } else { break @@ -734,8 +782,6 @@ class Database { } } } - - return backupEntry } companion object : SingletonHolder(::Database) { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/EntryVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/EntryVersioned.kt index 676f2ceb0..900778505 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/EntryVersioned.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/EntryVersioned.kt @@ -5,6 +5,8 @@ import android.os.Parcelable import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.Field +import com.kunzisoft.keepass.otp.OtpElement +import com.kunzisoft.keepass.otp.OtpEntryFields import java.util.* import kotlin.collections.ArrayList @@ -15,26 +17,26 @@ class EntryVersioned : NodeVersioned, PwEntryInterface { var pwEntryV4: PwEntryV4? = null private set - fun updateWith(entry: EntryVersioned) { + fun updateWith(entry: EntryVersioned, copyHistory: Boolean = true) { entry.pwEntryV3?.let { this.pwEntryV3?.updateWith(it) } entry.pwEntryV4?.let { - this.pwEntryV4?.updateWith(it) + this.pwEntryV4?.updateWith(it, copyHistory) } } /** * Use this constructor to copy an Entry with exact same values */ - constructor(entry: EntryVersioned) { + constructor(entry: EntryVersioned, copyHistory: Boolean = true) { if (entry.pwEntryV3 != null) { this.pwEntryV3 = PwEntryV3() } if (entry.pwEntryV4 != null) { this.pwEntryV4 = PwEntryV4() } - updateWith(entry) + updateWith(entry, copyHistory) } constructor(entry: PwEntryV3) { @@ -61,7 +63,7 @@ class EntryVersioned : NodeVersioned, PwEntryInterface { dest.writeParcelable(pwEntryV4, flags) } - var nodeId: PwNodeId + override var nodeId: PwNodeId get() = pwEntryV4?.nodeId ?: pwEntryV3?.nodeId ?: PwNodeIdUUID() set(value) { pwEntryV3?.nodeId = value @@ -154,13 +156,16 @@ class EntryVersioned : NodeVersioned, PwEntryInterface { pwEntryV4?.expiryTime = value } - override var isExpires: Boolean - get() =pwEntryV3?.isExpires ?: pwEntryV4?.isExpires ?: false + override var expires: Boolean + get() = pwEntryV3?.expires ?: pwEntryV4?.expires ?: false set(value) { - pwEntryV3?.isExpires = value - pwEntryV4?.isExpires = value + pwEntryV3?.expires = value + pwEntryV4?.expires = value } + override val isCurrentlyExpires: Boolean + get() = pwEntryV3?.isCurrentlyExpires ?: pwEntryV4?.isCurrentlyExpires ?: false + override var username: String get() = pwEntryV3?.username ?: pwEntryV4?.username ?: "" set(value) { @@ -241,13 +246,23 @@ class EntryVersioned : NodeVersioned, PwEntryInterface { return pwEntryV4?.allowCustomFields() ?: false } + fun removeAllFields() { + pwEntryV4?.removeAllFields() + } + /** - * Add an extra field to the list (standard or custom) + * Update or add an extra field to the list (standard or custom) * @param label Label of field, must be unique * @param value Value of field */ - fun addExtraField(label: String, value: ProtectedString) { - pwEntryV4?.addExtraField(label, value) + fun putExtraField(label: String, value: ProtectedString) { + pwEntryV4?.putExtraField(label, value) + } + + fun getOtpElement(): OtpElement? { + return OtpEntryFields.parseFields { key -> + customFields[key]?.toString() + } } fun startToManageFieldReferences(db: PwDatabaseV4) { @@ -258,20 +273,31 @@ class EntryVersioned : NodeVersioned, PwEntryInterface { pwEntryV4?.stopToManageFieldReferences() } - fun addBackupToHistory() { - pwEntryV4?.let { - val entryHistory = PwEntryV4() - entryHistory.updateWith(it) - it.addEntryToHistory(entryHistory) + fun getHistory(): ArrayList { + val history = ArrayList() + val entryV4History = pwEntryV4?.history ?: ArrayList() + for (entryHistory in entryV4History) { + history.add(EntryVersioned(entryHistory)) + } + return history + } + + fun addEntryToHistory(entry: EntryVersioned) { + entry.pwEntryV4?.let { + pwEntryV4?.addEntryToHistory(it) } } + fun removeAllHistory() { + pwEntryV4?.removeAllHistory() + } + fun removeOldestEntryFromHistory() { pwEntryV4?.removeOldestEntryFromHistory() } - fun getHistory(): ArrayList { - return pwEntryV4?.history ?: ArrayList() + fun getSize(): Long { + return pwEntryV4?.size ?: 0L } fun containsCustomData(): Boolean { @@ -284,6 +310,10 @@ class EntryVersioned : NodeVersioned, PwEntryInterface { ------------ */ + /** + * Retrieve generated entry info, + * Remove parameter fields and add auto generated elements in auto custom fields + */ fun getEntryInfo(database: Database?, raw: Boolean = false): EntryInfo { val entryInfo = EntryInfo() if (raw) @@ -300,6 +330,10 @@ class EntryVersioned : NodeVersioned, PwEntryInterface { entryInfo.customFields.add( Field(entry.key, entry.value)) } + // Add otpElement to generate token + entryInfo.otpModel = getOtpElement()?.otpModel + // Replace parameter fields by generated OTP fields + entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields) if (!raw) database?.stopManageEntry(this) return entryInfo diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/GroupVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/GroupVersioned.kt index 3673e9cfb..5737738a4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/GroupVersioned.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/GroupVersioned.kt @@ -70,7 +70,7 @@ class GroupVersioned : NodeVersioned, PwGroupInterface? + override val nodeId: PwNodeId<*>? get() = pwGroupV4?.nodeId ?: pwGroupV3?.nodeId override var title: String @@ -114,6 +114,38 @@ class GroupVersioned : NodeVersioned, PwGroupInterface + pwGroupV3?.addChildEntry(entryToAdd) + } + group.pwGroupV3?.getChildGroups()?.forEach { groupToAdd -> + pwGroupV3?.addChildGroup(groupToAdd) + } + + group.pwGroupV4?.getChildEntries()?.forEach { entryToAdd -> + pwGroupV4?.addChildEntry(entryToAdd) + } + group.pwGroupV4?.getChildGroups()?.forEach { groupToAdd -> + pwGroupV4?.addChildGroup(groupToAdd) + } + } + + fun removeChildren() { + pwGroupV3?.getChildEntries()?.forEach { entryToRemove -> + pwGroupV3?.removeChildEntry(entryToRemove) + } + pwGroupV3?.getChildGroups()?.forEach { groupToRemove -> + pwGroupV3?.removeChildGroup(groupToRemove) + } + + pwGroupV4?.getChildEntries()?.forEach { entryToRemove -> + pwGroupV4?.removeChildEntry(entryToRemove) + } + pwGroupV4?.getChildGroups()?.forEach { groupToRemove -> + pwGroupV4?.removeChildGroup(groupToRemove) + } + } + override fun touch(modified: Boolean, touchParents: Boolean) { pwGroupV3?.touch(modified, touchParents) pwGroupV4?.touch(modified, touchParents) @@ -158,13 +190,16 @@ class GroupVersioned : NodeVersioned, PwGroupInterface { val children = ArrayList() diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/NodeTimeInterface.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/NodeTimeInterface.kt index 44223b6fa..7decbd1c0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/NodeTimeInterface.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/NodeTimeInterface.kt @@ -29,5 +29,7 @@ interface NodeTimeInterface { var expiryTime: PwDate - var isExpires: Boolean + var expires: Boolean + + val isCurrentlyExpires: Boolean } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/NodeVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/NodeVersioned.kt index 8d6759f15..f231245f8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/NodeVersioned.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/NodeVersioned.kt @@ -2,16 +2,26 @@ package com.kunzisoft.keepass.database.element interface NodeVersioned: PwNodeInterface { + val nodeId: PwNodeId<*>? + val nodePositionInParent: Int get() { parent?.getChildren(true)?.let { children -> - for ((i, child) in children.withIndex()) { - if (child == this) - return i + children.forEachIndexed { index, nodeVersioned -> + if (nodeVersioned.nodeId == this.nodeId) + return index } } return -1 } + + fun addParentFrom(node: NodeVersioned) { + parent = node.parent + } + + fun removeParent() { + parent = null + } } /** diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/PwCompressionAlgorithm.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwCompressionAlgorithm.kt similarity index 67% rename from app/src/main/java/com/kunzisoft/keepass/database/file/PwCompressionAlgorithm.kt rename to app/src/main/java/com/kunzisoft/keepass/database/element/PwCompressionAlgorithm.kt index 32d7295e9..c4bd02208 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/PwCompressionAlgorithm.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwCompressionAlgorithm.kt @@ -17,25 +17,24 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.database.file +package com.kunzisoft.keepass.database.element + +import android.content.res.Resources +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.utils.ObjectNameResource // Note: We can get away with using int's to store unsigned 32-bit ints // since we won't do arithmetic on these values (also unlikely to // reach negative ids). -enum class PwCompressionAlgorithm constructor(val id: Int) { - - None(0), - Gzip(1); +enum class PwCompressionAlgorithm : ObjectNameResource { - companion object { + None, + GZip; - fun fromId(num: Int): PwCompressionAlgorithm? { - for (e in values()) { - if (e.id == num) { - return e - } - } - return null + override fun getName(resources: Resources): String { + return when (this) { + None -> resources.getString(R.string.compression_none) + GZip -> resources.getString(R.string.compression_gzip) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt index 61e7b0a50..d15b1fe41 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt @@ -19,26 +19,29 @@ */ package com.kunzisoft.keepass.database.element -import android.util.Log -import com.kunzisoft.keepass.database.exception.InvalidKeyFileException -import com.kunzisoft.keepass.database.exception.KeyFileEmptyException +import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine +import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException +import com.kunzisoft.keepass.database.exception.LoadDatabaseKeyFileEmptyException import com.kunzisoft.keepass.utils.MemoryUtil - -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.IOException -import java.io.InputStream -import java.io.UnsupportedEncodingException +import java.io.* import java.security.MessageDigest import java.security.NoSuchAlgorithmException -import java.util.LinkedHashMap -import java.util.UUID +import java.util.* -abstract class PwDatabase, Entry : PwEntry> { +abstract class PwDatabase< + GroupId, + EntryId, + Group : PwGroup, + Entry : PwEntry + > { // Algorithm used to encrypt the database protected var algorithm: PwEncryptionAlgorithm? = null + abstract val kdfEngine: KdfEngine? + + abstract val kdfAvailableList: List + var masterKey = ByteArray(32) var finalKey: ByteArray? = null protected set @@ -46,8 +49,10 @@ abstract class PwDatabase, Entry : PwEntry, Group>() - private var entryIndexes = LinkedHashMap, Entry>() + var changeDuplicateId = false + + private var groupIndexes = LinkedHashMap, Group>() + private var entryIndexes = LinkedHashMap, Entry>() abstract val version: String @@ -67,15 +72,15 @@ abstract class PwDatabase, Entry : PwEntry, Entry : PwEntry, Entry : PwEntry throw KeyFileEmptyException() + 0L -> throw LoadDatabaseKeyFileEmptyException() 32L -> return keyData 64L -> try { return hexStringToByteArray(String(keyData)) @@ -156,15 +161,18 @@ abstract class PwDatabase, Entry : PwEntry, Entry : PwEntry, Entry : PwEntry + abstract fun newGroupId(): PwNodeId - abstract fun newEntryId(): PwNodeId<*> + abstract fun newEntryId(): PwNodeId abstract fun createGroup(): Group @@ -211,7 +219,7 @@ abstract class PwDatabase, Entry : PwEntry): Boolean { + fun isGroupIdUsed(id: PwNodeId): Boolean { return groupIndexes.containsKey(id) } @@ -226,19 +234,33 @@ abstract class PwDatabase, Entry : PwEntry): Group? { + fun getGroupById(id: PwNodeId): Group? { return this.groupIndexes[id] } fun addGroupIndex(group: Group) { val groupId = group.nodeId if (groupIndexes.containsKey(groupId)) { - Log.e(TAG, "Error, a group with the same UUID $groupId already exists") + if (changeDuplicateId) { + val newGroupId = newGroupId() + group.nodeId = newGroupId + group.parent?.addChildGroup(group) + this.groupIndexes[newGroupId] = group + } else { + throw LoadDatabaseDuplicateUuidException(Type.GROUP, groupId) + } } else { this.groupIndexes[groupId] = group } } + fun updateGroupIndex(group: Group) { + val groupId = group.nodeId + if (groupIndexes.containsKey(groupId)) { + groupIndexes[groupId] = group + } + } + fun removeGroupIndex(group: Group) { this.groupIndexes.remove(group.nodeId) } @@ -253,7 +275,7 @@ abstract class PwDatabase, Entry : PwEntry): Boolean { + fun isEntryIdUsed(id: PwNodeId): Boolean { return entryIndexes.containsKey(id) } @@ -261,20 +283,33 @@ abstract class PwDatabase, Entry : PwEntry): Entry? { + fun getEntryById(id: PwNodeId): Entry? { return this.entryIndexes[id] } fun addEntryIndex(entry: Entry) { val entryId = entry.nodeId if (entryIndexes.containsKey(entryId)) { - // TODO History - Log.e(TAG, "Error, a group with the same UUID $entryId already exists, change the UUID") + if (changeDuplicateId) { + val newEntryId = newEntryId() + entry.nodeId = newEntryId + entry.parent?.addChildEntry(entry) + this.entryIndexes[newEntryId] = entry + } else { + throw LoadDatabaseDuplicateUuidException(Type.ENTRY, entryId) + } } else { this.entryIndexes[entryId] = entry } } + fun updateEntryIndex(entry: Entry) { + val entryId = entry.nodeId + if (entryIndexes.containsKey(entryId)) { + entryIndexes[entryId] = entry + } + } + fun removeEntryIndex(entry: Entry) { this.entryIndexes.remove(entry.nodeId) } @@ -305,6 +340,10 @@ abstract class PwDatabase, Entry : PwEntry, Entry : PwEntry@phoneid.org> - * @author Bill Zwicky @pobox.com> - * @author Dominik Reichl @t-online.de> - */ -class PwDatabaseV3 : PwDatabase() { +class PwDatabaseV3 : PwDatabase() { private var numKeyEncRounds: Int = 0 + private var kdfListV3: MutableList = ArrayList() + override val version: String get() = "KeePass 1" + init { + kdfListV3.add(KdfFactory.aesKdf) + } + + override val kdfEngine: KdfEngine? + get() = kdfListV3[0] + + override val kdfAvailableList: List + get() = kdfListV3 + override val availableEncryptionAlgorithms: List get() { val list = ArrayList() @@ -103,7 +113,7 @@ class PwDatabaseV3 : PwDatabase() { return newId } - @Throws(InvalidKeyFileException::class, IOException::class) + @Throws(IOException::class) override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray { return if (key != null && keyInputStream != null) { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt index d4580bae2..31b11f8e1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt @@ -26,12 +26,8 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.crypto.CryptoUtil import com.kunzisoft.keepass.crypto.engine.AesEngine import com.kunzisoft.keepass.crypto.engine.CipherEngine -import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine -import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory -import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters -import com.kunzisoft.keepass.database.exception.InvalidKeyFileException +import com.kunzisoft.keepass.crypto.keyDerivation.* import com.kunzisoft.keepass.database.exception.UnknownKDF -import com.kunzisoft.keepass.database.file.PwCompressionAlgorithm import com.kunzisoft.keepass.utils.VariantDictionary import org.w3c.dom.Node import org.w3c.dom.Text @@ -40,17 +36,20 @@ import java.io.InputStream import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.util.* +import javax.xml.XMLConstants import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.parsers.ParserConfigurationException -class PwDatabaseV4 : PwDatabase { +class PwDatabaseV4 : PwDatabase { var hmacKey: ByteArray? = null private set var dataCipher = AesEngine.CIPHER_UUID private var dataEngine: CipherEngine = AesEngine() - var compressionAlgorithm = PwCompressionAlgorithm.Gzip + var compressionAlgorithm = PwCompressionAlgorithm.GZip var kdfParameters: KdfParameters? = null + private var kdfV4List: MutableList = ArrayList() private var numKeyEncRounds: Long = 0 var publicCustomData = VariantDictionary() @@ -93,6 +92,11 @@ class PwDatabaseV4 : PwDatabase { var localizedAppName = "KeePassDX" // TODO resource + init { + kdfV4List.add(KdfFactory.aesKdf) + kdfV4List.add(KdfFactory.argon2Kdf) + } + constructor() constructor(databaseName: String) { @@ -107,6 +111,39 @@ class PwDatabaseV4 : PwDatabase { override val version: String get() = "KeePass 2" + override val kdfEngine: KdfEngine? + get() = try { + getEngineV4(kdfParameters) + } catch (unknownKDF: UnknownKDF) { + Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF) + null + } + + override val kdfAvailableList: List + get() = kdfV4List + + @Throws(UnknownKDF::class) + fun getEngineV4(kdfParameters: KdfParameters?): KdfEngine { + val unknownKDFException = UnknownKDF() + if (kdfParameters == null) { + throw unknownKDFException + } + for (engine in kdfV4List) { + if (engine.uuid == kdfParameters.uuid) { + return engine + } + } + throw unknownKDFException + } + + val availableCompressionAlgorithms: List + get() { + val list = ArrayList() + list.add(PwCompressionAlgorithm.None) + list.add(PwCompressionAlgorithm.GZip) + return list + } + override val availableEncryptionAlgorithms: List get() { val list = ArrayList() @@ -116,45 +153,45 @@ class PwDatabaseV4 : PwDatabase { return list } - val kdfEngine: KdfEngine? - get() { - return try { - KdfFactory.getEngineV4(kdfParameters) - } catch (unknownKDF: UnknownKDF) { - Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF) - null - } - } - override var numberKeyEncryptionRounds: Long get() { + val kdfEngine = kdfEngine if (kdfEngine != null && kdfParameters != null) - numKeyEncRounds = kdfEngine!!.getKeyRounds(kdfParameters!!) + numKeyEncRounds = kdfEngine.getKeyRounds(kdfParameters!!) return numKeyEncRounds } @Throws(NumberFormatException::class) set(rounds) { + val kdfEngine = kdfEngine if (kdfEngine != null && kdfParameters != null) - kdfEngine!!.setKeyRounds(kdfParameters!!, rounds) + kdfEngine.setKeyRounds(kdfParameters!!, rounds) numKeyEncRounds = rounds } var memoryUsage: Long - get() = if (kdfEngine != null && kdfParameters != null) { - kdfEngine!!.getMemoryUsage(kdfParameters!!) - } else KdfEngine.UNKNOWN_VALUE.toLong() + get() { + val kdfEngine = kdfEngine + return if (kdfEngine != null && kdfParameters != null) { + kdfEngine.getMemoryUsage(kdfParameters!!) + } else KdfEngine.UNKNOWN_VALUE.toLong() + } set(memory) { + val kdfEngine = kdfEngine if (kdfEngine != null && kdfParameters != null) - kdfEngine!!.setMemoryUsage(kdfParameters!!, memory) + kdfEngine.setMemoryUsage(kdfParameters!!, memory) } var parallelism: Int - get() = if (kdfEngine != null && kdfParameters != null) { - kdfEngine!!.getParallelism(kdfParameters!!) - } else KdfEngine.UNKNOWN_VALUE + get() { + val kdfEngine = kdfEngine + return if (kdfEngine != null && kdfParameters != null) { + kdfEngine.getParallelism(kdfParameters!!) + } else KdfEngine.UNKNOWN_VALUE + } set(parallelism) { + val kdfEngine = kdfEngine if (kdfEngine != null && kdfParameters != null) - kdfEngine!!.setParallelism(kdfParameters!!, parallelism) + kdfEngine.setParallelism(kdfParameters!!, parallelism) } override val passwordEncoding: String @@ -200,7 +237,7 @@ class PwDatabaseV4 : PwDatabase { return getCustomData().isNotEmpty() } - @Throws(InvalidKeyFileException::class, IOException::class) + @Throws(IOException::class) public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray { var masterKey = byteArrayOf() @@ -227,7 +264,7 @@ class PwDatabaseV4 : PwDatabase { fun makeFinalKey(masterSeed: ByteArray) { kdfParameters?.let { keyDerivationFunctionParameters -> - val kdfEngine = KdfFactory.getEngineV4(keyDerivationFunctionParameters) + val kdfEngine = getEngineV4(keyDerivationFunctionParameters) var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters) if (transformedMasterKey.size != 32) { @@ -254,16 +291,24 @@ class PwDatabaseV4 : PwDatabase { override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { try { - val dbf = DocumentBuilderFactory.newInstance() - val db = dbf.newDocumentBuilder() - val doc = db.parse(keyInputStream) + val documentBuilderFactory = DocumentBuilderFactory.newInstance() - val el = doc.documentElement - if (el == null || !el.nodeName.equals(RootElementName, ignoreCase = true)) { + // Disable certain unsecure XML-Parsing DocumentBuilderFactory features + try { + documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) + } catch (e : ParserConfigurationException) { + Log.e(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)", e) + } + + val documentBuilder = documentBuilderFactory.newDocumentBuilder() + val doc = documentBuilder.parse(keyInputStream) + + val docElement = doc.documentElement + if (docElement == null || !docElement.nodeName.equals(RootElementName, ignoreCase = true)) { return null } - val children = el.childNodes + val children = docElement.childNodes if (children.length < 2) { return null } @@ -360,9 +405,7 @@ class PwDatabaseV4 : PwDatabase { } addGroupTo(recycleBinGroup, rootGroup) recycleBinUUID = recycleBinGroup.id - recycleBinGroup.lastModificationTime.date?.let { - recycleBinChanged = it - } + recycleBinChanged = recycleBinGroup.lastModificationTime.date } } @@ -427,10 +470,10 @@ class PwDatabaseV4 : PwDatabase { return publicCustomData.size() > 0 } - override fun validatePasswordEncoding(key: String?): Boolean { - if (key == null) + override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean { + if (password == null) return true - return super.validatePasswordEncoding(key) + return super.validatePasswordEncoding(password, containsKeyFile) } override fun clearCache() { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.kt index 1c2ec3c47..1ce08df7c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDate.kt @@ -19,14 +19,12 @@ */ package com.kunzisoft.keepass.database.element +import android.content.res.Resources import android.os.Parcel import android.os.Parcelable - +import androidx.core.os.ConfigurationCompat import com.kunzisoft.keepass.utils.Types - -import java.util.Arrays -import java.util.Calendar -import java.util.Date +import java.util.* /** * Converting from the C Date format to the Java data format is @@ -34,14 +32,14 @@ import java.util.Date */ class PwDate : Parcelable { - private var jDate: Date? = null + private var jDate: Date = Date() private var jDateBuilt = false @Transient private var cDate: ByteArray? = null @Transient private var cDateBuilt = false - val date: Date? + val date: Date get() { if (!jDateBuilt) { jDate = readTime(cDate, 0, calendar) @@ -68,9 +66,7 @@ class PwDate : Parcelable { } constructor(source: PwDate) { - if (source.jDate != null) { - this.jDate = Date(source.jDate!!.time) - } + this.jDate = Date(source.jDate.time) this.jDateBuilt = source.jDateBuilt if (source.cDate != null) { @@ -106,6 +102,10 @@ class PwDate : Parcelable { return 0 } + fun getDateTimeString(resources: Resources): String { + return Companion.getDateTimeString(resources, this.date) + } + override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeSerializable(date) dest.writeByte((if (jDateBuilt) 1 else 0).toByte()) @@ -135,7 +135,7 @@ class PwDate : Parcelable { } override fun hashCode(): Int { - var result = jDate?.hashCode() ?: 0 + var result = jDate.hashCode() result = 31 * result + jDateBuilt.hashCode() result = 31 * result + (cDate?.contentHashCode() ?: 0) result = 31 * result + cDateBuilt.hashCode() @@ -149,10 +149,6 @@ class PwDate : Parcelable { private var mCalendar: Calendar? = null val NEVER_EXPIRE = neverExpire - val DEFAULT_DATE = defaultDate - - val PW_NEVER_EXPIRE = PwDate(NEVER_EXPIRE) - val DEFAULT_PWDATE = PwDate(DEFAULT_DATE) private val calendar: Calendar? get() { @@ -162,20 +158,7 @@ class PwDate : Parcelable { return mCalendar } - private val defaultDate: Date - get() { - val cal = Calendar.getInstance() - cal.set(Calendar.YEAR, 2004) - cal.set(Calendar.MONTH, Calendar.JANUARY) - cal.set(Calendar.DAY_OF_MONTH, 1) - cal.set(Calendar.HOUR, 0) - cal.set(Calendar.MINUTE, 0) - cal.set(Calendar.SECOND, 0) - - return cal.time - } - - private val neverExpire: Date + private val neverExpire: PwDate get() { val cal = Calendar.getInstance() cal.set(Calendar.YEAR, 2999) @@ -185,7 +168,7 @@ class PwDate : Parcelable { cal.set(Calendar.MINUTE, 59) cal.set(Calendar.SECOND, 59) - return cal.time + return PwDate(cal.time) } @JvmField @@ -280,5 +263,13 @@ class PwDate : Parcelable { cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND) } + + fun getDateTimeString(resources: Resources, date: Date): String { + return java.text.DateFormat.getDateTimeInstance( + java.text.DateFormat.MEDIUM, + java.text.DateFormat.MEDIUM, + ConfigurationCompat.getLocales(resources.configuration)[0]) + .format(date) + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEncryptionAlgorithm.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEncryptionAlgorithm.kt index 72ce3cffb..d214bd9cd 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEncryptionAlgorithm.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEncryptionAlgorithm.kt @@ -26,7 +26,7 @@ import com.kunzisoft.keepass.crypto.engine.AesEngine import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine import com.kunzisoft.keepass.crypto.engine.CipherEngine import com.kunzisoft.keepass.crypto.engine.TwofishEngine -import com.kunzisoft.keepass.database.ObjectNameResource +import com.kunzisoft.keepass.utils.ObjectNameResource import java.util.UUID diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntry.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntry.kt index 1b27ce15d..e5a1067ae 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntry.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntry.kt @@ -5,10 +5,12 @@ import java.util.* abstract class PwEntry < - ParentGroup: PwGroupInterface, - Entry: PwEntryInterface + GroupId, + EntryId, + ParentGroup: PwGroup, + Entry: PwEntry > - : PwNode, PwEntryInterface { + : PwNode, PwEntryInterface { constructor() : super() diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV3.kt index 41e723047..9c69dfd6c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV3.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV3.kt @@ -48,7 +48,7 @@ import java.util.UUID * @author Dominik Reichl @t-online.de> * @author Jeremy Jamet @kunzisoft.com> */ -class PwEntryV3 : PwEntry { +class PwEntryV3 : PwEntry, PwNodeV3Interface { /** A string describing what is in pBinaryData */ var binaryDesc = "" diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt index cb355f064..465e72b16 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwEntryV4.kt @@ -26,7 +26,7 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.utils.MemoryUtil import java.util.* -class PwEntryV4 : PwEntry, PwNodeV4Interface { +class PwEntryV4 : PwEntry, PwNodeV4Interface { // To decode each field not parcelable @Transient @@ -88,6 +88,8 @@ class PwEntryV4 : PwEntry, PwNodeV4Interface { return size } + override var expires: Boolean = false + constructor() : super() constructor(parcel: Parcel) : super(parcel) { @@ -129,7 +131,7 @@ class PwEntryV4 : PwEntry, PwNodeV4Interface { * Update with deep copy of each entry element * @param source */ - fun updateWith(source: PwEntryV4) { + fun updateWith(source: PwEntryV4, copyHistory: Boolean = true) { super.updateWith(source) iconCustom = PwIconCustom(source.iconCustom) usageCount = source.usageCount @@ -146,7 +148,8 @@ class PwEntryV4 : PwEntry, PwNodeV4Interface { overrideURL = source.overrideURL autoType = AutoType(source.autoType) history.clear() - history.addAll(source.history) + if (copyHistory) + history.addAll(source.history) url = source.url additional = source.additional tags = source.tags @@ -263,7 +266,11 @@ class PwEntryV4 : PwEntry, PwNodeV4Interface { return true } - fun addExtraField(label: String, value: ProtectedString) { + fun removeAllFields() { + fields.clear() + } + + fun putExtraField(label: String, value: ProtectedString) { fields[label] = value } @@ -287,6 +294,10 @@ class PwEntryV4 : PwEntry, PwNodeV4Interface { history.add(entry) } + fun removeAllHistory() { + history.clear() + } + fun removeOldestEntryFromHistory() { var min: Date? = null var index = -1 @@ -294,7 +305,7 @@ class PwEntryV4 : PwEntry, PwNodeV4Interface { for (i in history.indices) { val entry = history[i] val lastMod = entry.lastModificationTime.date - if (min == null || lastMod == null || lastMod.before(min)) { + if (min == null || lastMod.before(min)) { index = i min = lastMod } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroup.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroup.kt index b696a145e..50d94dca0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroup.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroup.kt @@ -4,11 +4,12 @@ import android.os.Parcel abstract class PwGroup < - Id, - Group: PwGroupInterface, - Entry: PwEntryInterface + GroupId, + EntryId, + Group: PwGroup, + Entry: PwEntry > - : PwNode, PwGroupInterface { + : PwNode, PwGroupInterface { private var titleGroup = "" @Transient @@ -27,10 +28,12 @@ abstract class PwGroup dest.writeString(titleGroup) } - protected fun updateWith(source: PwGroup) { + protected fun updateWith(source: PwGroup) { super.updateWith(source) titleGroup = source.titleGroup + childGroups.clear() childGroups.addAll(source.childGroups) + childEntries.clear() childEntries.addAll(source.childEntries) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV3.kt index cad871a94..ed2aaec3d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV3.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV3.kt @@ -22,8 +22,9 @@ package com.kunzisoft.keepass.database.element import android.os.Parcel import android.os.Parcelable +import java.util.* -class PwGroupV3 : PwGroup { +class PwGroupV3 : PwGroup, PwNodeV3Interface { var level = 0 // short /** Used by KeePass internally, don't use */ diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV4.kt index ed0ef76f7..e7eee8abc 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwGroupV4.kt @@ -25,7 +25,7 @@ import android.os.Parcelable import java.util.HashMap import java.util.UUID -class PwGroupV4 : PwGroup, PwNodeV4Interface { +class PwGroupV4 : PwGroup, PwNodeV4Interface { // TODO Encapsulate override var icon: PwIcon @@ -43,12 +43,15 @@ class PwGroupV4 : PwGroup, PwNodeV4Interface { var iconCustom = PwIconCustom.UNKNOWN_ICON private val customData = HashMap() var notes = "" + var isExpanded = true var defaultAutoTypeSequence = "" var enableAutoType: Boolean? = null var enableSearching: Boolean? = null var lastTopVisibleEntry: UUID = PwDatabase.UUID_ZERO + override var expires: Boolean = false + override val type: Type get() = Type.GROUP diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwNode.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNode.kt index 3cf796319..60e1a6028 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwNode.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNode.kt @@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.element import android.os.Parcel import android.os.Parcelable -import org.joda.time.LocalDate +import org.joda.time.LocalDateTime /** * Abstract class who manage Groups and Entries @@ -44,6 +44,7 @@ abstract class PwNode, Entry : this.lastModificationTime = parcel.readParcelable(PwDate::class.java.classLoader) ?: lastModificationTime this.lastAccessTime = parcel.readParcelable(PwDate::class.java.classLoader) ?: lastAccessTime this.expiryTime = parcel.readParcelable(PwDate::class.java.classLoader) ?: expiryTime + this.expires = parcel.readByte().toInt() != 0 } override fun writeToParcel(dest: Parcel, flags: Int) { @@ -54,6 +55,7 @@ abstract class PwNode, Entry : dest.writeParcelable(lastModificationTime, flags) dest.writeParcelable(lastAccessTime, flags) dest.writeParcelable(expiryTime, flags) + dest.writeByte((if (expires) 1 else 0).toByte()) } override fun describeContents(): Int { @@ -68,6 +70,7 @@ abstract class PwNode, Entry : this.lastModificationTime = PwDate(source.lastModificationTime) this.lastAccessTime = PwDate(source.lastAccessTime) this.expiryTime = PwDate(source.expiryTime) + this.expires = source.expires } protected abstract fun initNodeId(): PwNodeId @@ -85,17 +88,11 @@ abstract class PwNode, Entry : final override var lastAccessTime: PwDate = PwDate() - final override var expiryTime: PwDate = PwDate.PW_NEVER_EXPIRE + final override var expiryTime: PwDate = PwDate() - final override var isExpires: Boolean - // If expireDate is before NEVER_EXPIRE date less 1 month (to be sure) - get() = expiryTime.date - ?.before(LocalDate.fromDateFields(PwDate.NEVER_EXPIRE).minusMonths(1).toDate()) ?: true - set(value) { - if (!value) { - expiryTime = PwDate.PW_NEVER_EXPIRE - } - } + final override val isCurrentlyExpires: Boolean + get() = expires + && LocalDateTime.fromDateFields(expiryTime.date).isBefore(LocalDateTime.now()) /** * @return true if parent is present (false if not present, can be a root or a detach element) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeId.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeId.kt index 9cfa914ec..38bc119b5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeId.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeId.kt @@ -31,4 +31,17 @@ abstract class PwNodeId : Parcelable { override fun describeContents(): Int { return 0 } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is PwNodeId<*>) return false + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id?.hashCode() ?: 0 + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeV3Interface.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeV3Interface.kt new file mode 100644 index 000000000..ee39cc0e6 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwNodeV3Interface.kt @@ -0,0 +1,18 @@ +package com.kunzisoft.keepass.database.element + +import org.joda.time.LocalDateTime + +interface PwNodeV3Interface : NodeTimeInterface { + + override var expires: Boolean + // If expireDate is before NEVER_EXPIRE date less 1 month (to be sure) + // it is not expires + get() = LocalDateTime(expiryTime.date) + .isBefore(LocalDateTime.fromDateFields(PwDate.NEVER_EXPIRE.date) + .minusMonths(1)) + set(value) { + if (!value) + expiryTime = PwDate.NEVER_EXPIRE + } +} + diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/ArcFourException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/ArcFourException.kt deleted file mode 100644 index 9ad91f6ca..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/ArcFourException.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -class ArcFourException : InvalidDBException() { - companion object { - private const val serialVersionUID = 2103983626687861237L - } - -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/ContentFileNotFoundException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/ContentFileNotFoundException.kt deleted file mode 100644 index 2c9f4c3c9..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/ContentFileNotFoundException.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -import android.net.Uri -import java.io.FileNotFoundException - -class ContentFileNotFoundException : FileNotFoundException() { - companion object { - fun getInstance(uri: Uri?): FileNotFoundException { - if (uri == null) { - return FileNotFoundException() - } - - val scheme = uri.scheme - return if (scheme != null - && scheme.isNotEmpty() - && scheme.equals("content", ignoreCase = true)) { - ContentFileNotFoundException() - } else FileNotFoundException() - } - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt new file mode 100644 index 000000000..e16dadb92 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt @@ -0,0 +1,161 @@ +package com.kunzisoft.keepass.database.exception + +import android.content.res.Resources +import androidx.annotation.StringRes +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.PwNodeId +import com.kunzisoft.keepass.database.element.Type + +abstract class DatabaseException : Exception { + + abstract var errorId: Int + var parameters: (Array)? = null + + constructor() : super() + + constructor(throwable: Throwable) : super(throwable) + + fun getLocalizedMessage(resources: Resources): String { + parameters?.let { + return resources.getString(errorId, *it) + } ?: return resources.getString(errorId) + } +} + +open class LoadDatabaseException : DatabaseException { + + @StringRes + override var errorId: Int = R.string.error_load_database + + constructor() : super() + + constructor(vararg params: String) : super() { + parameters = params + } + + constructor(throwable: Throwable) : super(throwable) +} + +class LoadDatabaseArcFourException : LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.error_arc4 + + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class LoadDatabaseFileNotFoundException : LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.file_not_found_content + + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class LoadDatabaseInvalidAlgorithmException : LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.invalid_algorithm + + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class LoadDatabaseDuplicateUuidException: LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.invalid_db_same_uuid + + constructor(type: Type, uuid: PwNodeId<*>) : super() { + parameters = arrayOf(type.name, uuid.toString()) + } + constructor(exception: Throwable) : super(exception) +} + +class LoadDatabaseIOException : LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.error_load_database + + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class LoadDatabaseKDFMemoryException : LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.error_load_database_KDF_memory + + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class LoadDatabaseSignatureException : LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.invalid_db_sig + + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class LoadDatabaseVersionException : LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.unsupported_db_version + + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class LoadDatabaseInvalidCredentialsException : LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.invalid_credentials + + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class LoadDatabaseKeyFileEmptyException : LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.keyfile_is_empty + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class LoadDatabaseNoMemoryException: LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.error_out_of_memory + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class MoveDatabaseEntryException: LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.error_move_entry_here + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class MoveDatabaseGroupException: LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.error_move_folder_in_itself + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class CopyDatabaseEntryException: LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.error_copy_entry_here + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class CopyDatabaseGroupException: LoadDatabaseException { + @StringRes + override var errorId: Int = R.string.error_copy_group_here + constructor() : super() + constructor(exception: Throwable) : super(exception) +} + +class DatabaseOutputException : Exception { + constructor(string: String) : super(string) + + constructor(string: String, e: Exception) : super(string, e) + + constructor(e: Exception) : super(e) +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidAlgorithmException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidAlgorithmException.kt deleted file mode 100644 index 99cc71f2c..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidAlgorithmException.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -class InvalidAlgorithmException : InvalidDBException() { - companion object { - private const val serialVersionUID = 3062682891863487208L - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBException.kt deleted file mode 100644 index d50721f39..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBException.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -open class InvalidDBException : Exception { - - constructor(str: String) : super(str) - - constructor() : super() - - companion object { - private const val serialVersionUID = 5191964825154190923L - } - -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBSignatureException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBSignatureException.kt deleted file mode 100644 index 9445755b0..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBSignatureException.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -class InvalidDBSignatureException : InvalidDBException() { - companion object { - private const val serialVersionUID = -5358923878743513758L - } - -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBVersionException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBVersionException.kt deleted file mode 100644 index 577bc0052..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBVersionException.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -class InvalidDBVersionException : InvalidDBException() { - companion object { - private const val serialVersionUID = -4260650987856400586L - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidKeyFileException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidKeyFileException.kt deleted file mode 100644 index d7f54fcec..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidKeyFileException.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */package com.kunzisoft.keepass.database.exception - -open class InvalidKeyFileException : InvalidDBException() { - companion object { - private const val serialVersionUID = 5540694419562294464L - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidPasswordException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidPasswordException.kt deleted file mode 100644 index dcd51b94d..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidPasswordException.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -class InvalidPasswordException : InvalidDBException() { - companion object { - private const val serialVersionUID = -8729476180242058319L - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/KeyFileEmptyException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/KeyFileEmptyException.kt deleted file mode 100644 index 474d2dfea..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/KeyFileEmptyException.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -class KeyFileEmptyException : InvalidKeyFileException() { - companion object { - private const val serialVersionUID = -1630780661204212325L - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/PwDbOutputException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/PwDbOutputException.kt deleted file mode 100644 index d892cdee2..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/PwDbOutputException.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -class PwDbOutputException : Exception { - constructor(string: String) : super(string) - - constructor(string: String, e: Exception) : super(string, e) - - constructor(e: Exception) : super(e) - - companion object { - private const val serialVersionUID = 3321212743159473368L - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/SamsungClipboardException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/SamsungClipboardException.kt index 8db01ab9c..0c3f13a8d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/SamsungClipboardException.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/exception/SamsungClipboardException.kt @@ -19,9 +19,4 @@ */ package com.kunzisoft.keepass.database.exception -class SamsungClipboardException(e: Exception) : Exception(e) { - companion object { - private const val serialVersionUID = -3168837280393843509L - } - -} +class SamsungClipboardException(e: Exception) : Exception(e) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt index 2897148c5..435bf4b48 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt @@ -2,8 +2,4 @@ package com.kunzisoft.keepass.database.exception import java.io.IOException -class UnknownKDF : IOException(message) { - companion object { - private const val message = "Unknown key derivation function" - } -} +class UnknownKDF : IOException("Unknown key derivation function") diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt index a6d089c1e..9cff24a2a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt @@ -24,11 +24,8 @@ import com.kunzisoft.keepass.crypto.keyDerivation.AesKdf import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters import com.kunzisoft.keepass.database.NodeHandler -import com.kunzisoft.keepass.database.element.PwNodeV4Interface -import com.kunzisoft.keepass.database.element.PwDatabaseV4 -import com.kunzisoft.keepass.database.element.PwEntryV4 -import com.kunzisoft.keepass.database.element.PwGroupV4 -import com.kunzisoft.keepass.database.exception.InvalidDBVersionException +import com.kunzisoft.keepass.database.element.* +import com.kunzisoft.keepass.database.exception.LoadDatabaseVersionException import com.kunzisoft.keepass.stream.CopyInputStream import com.kunzisoft.keepass.stream.HmacBlockStream import com.kunzisoft.keepass.stream.LEDataInputStream @@ -51,10 +48,10 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() { // version < FILE_VERSION_32_4) var transformSeed: ByteArray? - get() = databaseV4.kdfParameters?.getByteArray(AesKdf.ParamSeed) + get() = databaseV4.kdfParameters?.getByteArray(AesKdf.PARAM_SEED) private set(seed) { assignAesKdfEngineIfNotExists() - databaseV4.kdfParameters?.setByteArray(AesKdf.ParamSeed, seed) + databaseV4.kdfParameters?.setByteArray(AesKdf.PARAM_SEED, seed) } object PwDbHeaderV4Fields { @@ -133,9 +130,9 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() { /** Assumes the input stream is at the beginning of the .kdbx file * @param inputStream * @throws IOException - * @throws InvalidDBVersionException + * @throws LoadDatabaseVersionException */ - @Throws(IOException::class, InvalidDBVersionException::class) + @Throws(IOException::class, LoadDatabaseVersionException::class) fun loadFromFile(inputStream: InputStream): HeaderAndHash { val messageDigest: MessageDigest try { @@ -153,12 +150,12 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() { val sig2 = littleEndianDataInputStream.readInt() if (!matchesHeader(sig1, sig2)) { - throw InvalidDBVersionException() + throw LoadDatabaseVersionException() } version = littleEndianDataInputStream.readUInt() // Erase previous value if (!validVersion(version)) { - throw InvalidDBVersionException() + throw LoadDatabaseVersionException() } var done = false @@ -229,7 +226,9 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() { } private fun assignAesKdfEngineIfNotExists() { - if (databaseV4.kdfParameters == null || databaseV4.kdfParameters!!.uuid != KdfFactory.aesKdf.uuid) { + val kdfParams = databaseV4.kdfParameters + if (kdfParams == null + || kdfParams.uuid != KdfFactory.aesKdf.uuid) { databaseV4.kdfParameters = KdfFactory.aesKdf.defaultParameters } } @@ -246,7 +245,7 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() { private fun setTransformRound(roundsByte: ByteArray?) { assignAesKdfEngineIfNotExists() val rounds = LEDataInputStream.readLong(roundsByte!!, 0) - databaseV4.kdfParameters?.setUInt64(AesKdf.ParamRounds, rounds) + databaseV4.kdfParameters?.setUInt64(AesKdf.PARAM_ROUNDS, rounds) databaseV4.numberKeyEncryptionRounds = rounds } @@ -261,7 +260,7 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() { throw IOException("Unrecognized compression flag.") } - PwCompressionAlgorithm.fromId(flag)?.let { compression -> + getCompressionFromFlag(flag)?.let { compression -> databaseV4.compressionAlgorithm = compression } } @@ -299,6 +298,21 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() { const val FILE_VERSION_32_3: Long = 0x00030001 const val FILE_VERSION_32_4: Long = 0x00040000 + fun getCompressionFromFlag(flag: Int): PwCompressionAlgorithm? { + return when (flag) { + 0 -> PwCompressionAlgorithm.None + 1 -> PwCompressionAlgorithm.GZip + else -> null + } + } + + fun getFlagFromCompression(compression: PwCompressionAlgorithm): Int { + return when (compression) { + PwCompressionAlgorithm.GZip -> 1 + else -> 0 + } + } + fun matchesHeader(sig1: Int, sig2: Int): Boolean { return sig1 == PWM_DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/load/Importer.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/load/Importer.kt index 1eec9700e..8743eb855 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/load/Importer.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/load/Importer.kt @@ -20,13 +20,11 @@ package com.kunzisoft.keepass.database.file.load import com.kunzisoft.keepass.database.element.PwDatabase -import com.kunzisoft.keepass.database.exception.InvalidDBException +import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.tasks.ProgressTaskUpdater - -import java.io.IOException import java.io.InputStream -abstract class Importer> { +abstract class Importer> { /** * Load a versioned database file, return contents in a new PwDatabase. @@ -35,10 +33,9 @@ abstract class Importer> { * @param password Pass phrase for infile. * @return new PwDatabase container. * - * @throws IOException on any file error. - * @throws InvalidDBException on database error. + * @throws LoadDatabaseException on database error (contains IO exceptions) */ - @Throws(IOException::class, InvalidDBException::class) + @Throws(LoadDatabaseException::class) abstract fun openDatabase(databaseInputStream: InputStream, password: String?, keyInputStream: InputStream?, diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt index 3771f2d09..be42a451a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt @@ -73,155 +73,163 @@ class ImporterV3 : Importer() { private lateinit var mDatabaseToOpen: PwDatabaseV3 - @Throws(IOException::class, InvalidDBException::class) + @Throws(LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, password: String?, keyInputStream: InputStream?, progressTaskUpdater: ProgressTaskUpdater?): PwDatabaseV3 { - // Load entire file, most of it's encrypted. - val fileSize = databaseInputStream.available() - val filebuf = ByteArray(fileSize + 16) // Pad with a blocksize (Twofish uses 128 bits), since Android 4.3 tries to write more to the buffer - databaseInputStream.read(filebuf, 0, fileSize) // TODO remove - databaseInputStream.close() + try { + // Load entire file, most of it's encrypted. + val fileSize = databaseInputStream.available() + val filebuf = ByteArray(fileSize + 16) // Pad with a blocksize (Twofish uses 128 bits), since Android 4.3 tries to write more to the buffer + databaseInputStream.read(filebuf, 0, fileSize) // TODO remove + databaseInputStream.close() + + // Parse header (unencrypted) + if (fileSize < PwDbHeaderV3.BUF_SIZE) + throw IOException("File too short for header") + val hdr = PwDbHeaderV3() + hdr.loadFromFile(filebuf, 0) + + if (hdr.signature1 != PwDbHeader.PWM_DBSIG_1 || hdr.signature2 != PwDbHeaderV3.DBSIG_2) { + throw LoadDatabaseSignatureException() + } - // Parse header (unencrypted) - if (fileSize < PwDbHeaderV3.BUF_SIZE) - throw IOException("File too short for header") - val hdr = PwDbHeaderV3() - hdr.loadFromFile(filebuf, 0) + if (!hdr.matchesVersion()) { + throw LoadDatabaseVersionException() + } - if (hdr.signature1 != PwDbHeader.PWM_DBSIG_1 || hdr.signature2 != PwDbHeaderV3.DBSIG_2) { - throw InvalidDBSignatureException() - } + progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) + mDatabaseToOpen = PwDatabaseV3() + mDatabaseToOpen.retrieveMasterKey(password, keyInputStream) - if (!hdr.matchesVersion()) { - throw InvalidDBVersionException() - } + // Select algorithm + when { + hdr.flags and PwDbHeaderV3.FLAG_RIJNDAEL != 0 -> mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.AESRijndael + hdr.flags and PwDbHeaderV3.FLAG_TWOFISH != 0 -> mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.Twofish + else -> throw LoadDatabaseInvalidAlgorithmException() + } - progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) - mDatabaseToOpen = PwDatabaseV3() - mDatabaseToOpen.retrieveMasterKey(password, keyInputStream) - - // Select algorithm - if (hdr.flags and PwDbHeaderV3.FLAG_RIJNDAEL != 0) { - mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.AESRijndael - } else if (hdr.flags and PwDbHeaderV3.FLAG_TWOFISH != 0) { - mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.Twofish - } else { - throw InvalidAlgorithmException() - } + mDatabaseToOpen.numberKeyEncryptionRounds = hdr.numKeyEncRounds.toLong() - mDatabaseToOpen.numberKeyEncryptionRounds = hdr.numKeyEncRounds.toLong() + // Generate transformedMasterKey from masterKey + mDatabaseToOpen.makeFinalKey(hdr.masterSeed, hdr.transformSeed, mDatabaseToOpen.numberKeyEncryptionRounds) - // Generate transformedMasterKey from masterKey - mDatabaseToOpen.makeFinalKey(hdr.masterSeed, hdr.transformSeed, mDatabaseToOpen.numberKeyEncryptionRounds) + progressTaskUpdater?.updateMessage(R.string.decrypting_db) + // Initialize Rijndael algorithm + val cipher: Cipher + try { + if (mDatabaseToOpen.encryptionAlgorithm === PwEncryptionAlgorithm.AESRijndael) { + cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding") + } else if (mDatabaseToOpen.encryptionAlgorithm === PwEncryptionAlgorithm.Twofish) { + cipher = CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING") + } else { + throw IOException("Encryption algorithm is not supported") + } - progressTaskUpdater?.updateMessage(R.string.decrypting_db) - // Initialize Rijndael algorithm - val cipher: Cipher - try { - if (mDatabaseToOpen.encryptionAlgorithm === PwEncryptionAlgorithm.AESRijndael) { - cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding") - } else if (mDatabaseToOpen.encryptionAlgorithm === PwEncryptionAlgorithm.Twofish) { - cipher = CipherFactory.getInstance("Twofish/CBC/PKCS7PADDING") - } else { - throw IOException("Encryption algorithm is not supported") + } catch (e1: NoSuchAlgorithmException) { + throw IOException("No such algorithm") + } catch (e1: NoSuchPaddingException) { + throw IOException("No such pdading") } - } catch (e1: NoSuchAlgorithmException) { - throw IOException("No such algorithm") - } catch (e1: NoSuchPaddingException) { - throw IOException("No such pdading") - } - - try { - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(mDatabaseToOpen.finalKey, "AES"), IvParameterSpec(hdr.encryptionIV)) - } catch (e1: InvalidKeyException) { - throw IOException("Invalid key") - } catch (e1: InvalidAlgorithmParameterException) { - throw IOException("Invalid algorithm parameter.") - } + try { + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(mDatabaseToOpen.finalKey, "AES"), IvParameterSpec(hdr.encryptionIV)) + } catch (e1: InvalidKeyException) { + throw IOException("Invalid key") + } catch (e1: InvalidAlgorithmParameterException) { + throw IOException("Invalid algorithm parameter.") + } - // Decrypt! The first bytes aren't encrypted (that's the header) - val encryptedPartSize: Int - try { - encryptedPartSize = cipher.doFinal(filebuf, PwDbHeaderV3.BUF_SIZE, fileSize - PwDbHeaderV3.BUF_SIZE, filebuf, PwDbHeaderV3.BUF_SIZE) - } catch (e1: ShortBufferException) { - throw IOException("Buffer too short") - } catch (e1: IllegalBlockSizeException) { - throw IOException("Invalid block size") - } catch (e1: BadPaddingException) { - throw InvalidPasswordException() - } + // Decrypt! The first bytes aren't encrypted (that's the header) + val encryptedPartSize: Int + try { + encryptedPartSize = cipher.doFinal(filebuf, PwDbHeaderV3.BUF_SIZE, fileSize - PwDbHeaderV3.BUF_SIZE, filebuf, PwDbHeaderV3.BUF_SIZE) + } catch (e1: ShortBufferException) { + throw IOException("Buffer too short") + } catch (e1: IllegalBlockSizeException) { + throw IOException("Invalid block size") + } catch (e1: BadPaddingException) { + throw LoadDatabaseInvalidCredentialsException() + } - val md: MessageDigest - try { - md = MessageDigest.getInstance("SHA-256") - } catch (e: NoSuchAlgorithmException) { - throw IOException("No SHA-256 algorithm") - } + val md: MessageDigest + try { + md = MessageDigest.getInstance("SHA-256") + } catch (e: NoSuchAlgorithmException) { + throw IOException("No SHA-256 algorithm") + } - val nos = NullOutputStream() - val dos = DigestOutputStream(nos, md) - dos.write(filebuf, PwDbHeaderV3.BUF_SIZE, encryptedPartSize) - dos.close() - val hash = md.digest() + val nos = NullOutputStream() + val dos = DigestOutputStream(nos, md) + dos.write(filebuf, PwDbHeaderV3.BUF_SIZE, encryptedPartSize) + dos.close() + val hash = md.digest() - if (!Arrays.equals(hash, hdr.contentsHash)) { + if (!Arrays.equals(hash, hdr.contentsHash)) { - Log.w(TAG, "Database file did not decrypt correctly. (checksum code is broken)") - throw InvalidPasswordException() - } + Log.w(TAG, "Database file did not decrypt correctly. (checksum code is broken)") + throw LoadDatabaseInvalidCredentialsException() + } - // New manual root because V3 contains multiple root groups (here available with getRootGroups()) - val newRoot = mDatabaseToOpen.createGroup() - newRoot.level = -1 - mDatabaseToOpen.rootGroup = newRoot + // New manual root because V3 contains multiple root groups (here available with getRootGroups()) + val newRoot = mDatabaseToOpen.createGroup() + newRoot.level = -1 + mDatabaseToOpen.rootGroup = newRoot + + // Import all groups + var pos = PwDbHeaderV3.BUF_SIZE + var newGrp = mDatabaseToOpen.createGroup() + run { + var i = 0 + while (i < hdr.numGroups) { + val fieldType = LEDataInputStream.readUShort(filebuf, pos) + pos += 2 + val fieldSize = LEDataInputStream.readInt(filebuf, pos) + pos += 4 + + if (fieldType == 0xFFFF) { + // End-Group record. Save group and count it. + mDatabaseToOpen.addGroupIndex(newGrp) + newGrp = mDatabaseToOpen.createGroup() + i++ + } else { + readGroupField(mDatabaseToOpen, newGrp, fieldType, filebuf, pos) + } + pos += fieldSize + } + } - // Import all groups - var pos = PwDbHeaderV3.BUF_SIZE - var newGrp = mDatabaseToOpen.createGroup() - run { + // Import all entries + var newEnt = mDatabaseToOpen.createEntry() var i = 0 - while (i < hdr.numGroups) { + while (i < hdr.numEntries) { val fieldType = LEDataInputStream.readUShort(filebuf, pos) - pos += 2 - val fieldSize = LEDataInputStream.readInt(filebuf, pos) - pos += 4 + val fieldSize = LEDataInputStream.readInt(filebuf, pos + 2) if (fieldType == 0xFFFF) { // End-Group record. Save group and count it. - mDatabaseToOpen.addGroupIndex(newGrp) - newGrp = mDatabaseToOpen.createGroup() + mDatabaseToOpen.addEntryIndex(newEnt) + newEnt = mDatabaseToOpen.createEntry() i++ } else { - readGroupField(mDatabaseToOpen, newGrp, fieldType, filebuf, pos) + readEntryField(mDatabaseToOpen, newEnt, filebuf, pos) } - pos += fieldSize + pos += 2 + 4 + fieldSize } - } - // Import all entries - var newEnt = mDatabaseToOpen.createEntry() - var i = 0 - while (i < hdr.numEntries) { - val fieldType = LEDataInputStream.readUShort(filebuf, pos) - val fieldSize = LEDataInputStream.readInt(filebuf, pos + 2) - - if (fieldType == 0xFFFF) { - // End-Group record. Save group and count it. - mDatabaseToOpen.addEntryIndex(newEnt) - newEnt = mDatabaseToOpen.createEntry() - i++ - } else { - readEntryField(mDatabaseToOpen, newEnt, filebuf, pos) - } - pos += 2 + 4 + fieldSize + constructTreeFromIndex() + } catch (e: LoadDatabaseException) { + throw e + } catch (e: IOException) { + throw LoadDatabaseIOException(e) + } catch (e: OutOfMemoryError) { + throw LoadDatabaseNoMemoryException(e) + } catch (e: Exception) { + throw LoadDatabaseException(e) } - constructTreeFromIndex() - return mDatabaseToOpen } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt index 546456b80..09adaba71 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt @@ -24,20 +24,17 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.crypto.CipherFactory import com.kunzisoft.keepass.crypto.StreamCipherFactory import com.kunzisoft.keepass.crypto.engine.CipherEngine -import com.kunzisoft.keepass.database.file.PwCompressionAlgorithm import com.kunzisoft.keepass.database.element.* -import com.kunzisoft.keepass.database.exception.ArcFourException -import com.kunzisoft.keepass.database.exception.InvalidDBException -import com.kunzisoft.keepass.database.exception.InvalidPasswordException -import com.kunzisoft.keepass.database.file.PwDbHeaderV4 import com.kunzisoft.keepass.database.element.security.ProtectedBinary import com.kunzisoft.keepass.database.element.security.ProtectedString +import com.kunzisoft.keepass.database.exception.* +import com.kunzisoft.keepass.database.file.KDBX4DateUtil +import com.kunzisoft.keepass.database.file.PwDbHeaderV4 import com.kunzisoft.keepass.stream.BetterCipherInputStream import com.kunzisoft.keepass.stream.HashedBlockInputStream import com.kunzisoft.keepass.stream.HmacBlockInputStream import com.kunzisoft.keepass.stream.LEDataInputStream import com.kunzisoft.keepass.tasks.ProgressTaskUpdater -import com.kunzisoft.keepass.database.file.KDBX4DateUtil import com.kunzisoft.keepass.utils.MemoryUtil import com.kunzisoft.keepass.utils.Types import org.spongycastle.crypto.StreamCipher @@ -46,17 +43,14 @@ import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserFactory import java.io.* import java.nio.charset.Charset -import java.security.InvalidAlgorithmParameterException -import java.security.InvalidKeyException -import java.security.NoSuchAlgorithmException import java.text.ParseException import java.util.* import java.util.zip.GZIPInputStream import javax.crypto.Cipher -import javax.crypto.NoSuchPaddingException import kotlin.math.min -class ImporterV4(private val streamDir: File) : Importer() { +class ImporterV4(private val streamDir: File, + private val fixDuplicateUUID: Boolean = false) : Importer() { private var randomStream: StreamCipher? = null private lateinit var mDatabase: PwDatabaseV4 @@ -89,108 +83,120 @@ class ImporterV4(private val streamDir: File) : Importer() { private var entryCustomDataKey: String? = null private var entryCustomDataValue: String? = null - @Throws(IOException::class, InvalidDBException::class) + @Throws(LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, password: String?, keyInputStream: InputStream?, progressTaskUpdater: ProgressTaskUpdater?): PwDatabaseV4 { - // TODO performance - progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) + try { + // TODO performance + progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) - mDatabase = PwDatabaseV4() - val header = PwDbHeaderV4(mDatabase) + mDatabase = PwDatabaseV4() - val headerAndHash = header.loadFromFile(databaseInputStream) - version = header.version + mDatabase.changeDuplicateId = fixDuplicateUUID - hashOfHeader = headerAndHash.hash - val pbHeader = headerAndHash.header + val header = PwDbHeaderV4(mDatabase) - mDatabase.retrieveMasterKey(password, keyInputStream) - mDatabase.makeFinalKey(header.masterSeed) - // TODO performance + val headerAndHash = header.loadFromFile(databaseInputStream) + version = header.version - progressTaskUpdater?.updateMessage(R.string.decrypting_db) - val engine: CipherEngine - val cipher: Cipher - try { - engine = CipherFactory.getInstance(mDatabase.dataCipher) - mDatabase.setDataEngine(engine) - mDatabase.encryptionAlgorithm = engine.getPwEncryptionAlgorithm() - cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV) - } catch (e: NoSuchAlgorithmException) { - throw IOException("Invalid algorithm.", e) - } catch (e: NoSuchPaddingException) { - throw IOException("Invalid algorithm.", e) - } catch (e: InvalidKeyException) { - throw IOException("Invalid algorithm.", e) - } catch (e: InvalidAlgorithmParameterException) { - throw IOException("Invalid algorithm.", e) - } + hashOfHeader = headerAndHash.hash + val pbHeader = headerAndHash.header - val isPlain: InputStream - if (version < PwDbHeaderV4.FILE_VERSION_32_4) { + mDatabase.retrieveMasterKey(password, keyInputStream) + mDatabase.makeFinalKey(header.masterSeed) + // TODO performance - val decrypted = attachCipherStream(databaseInputStream, cipher) - val dataDecrypted = LEDataInputStream(decrypted) - val storedStartBytes: ByteArray? + progressTaskUpdater?.updateMessage(R.string.decrypting_db) + val engine: CipherEngine + val cipher: Cipher try { - storedStartBytes = dataDecrypted.readBytes(32) - if (storedStartBytes == null || storedStartBytes.size != 32) { - throw InvalidPasswordException() - } - } catch (e: IOException) { - throw InvalidPasswordException() + engine = CipherFactory.getInstance(mDatabase.dataCipher) + mDatabase.setDataEngine(engine) + mDatabase.encryptionAlgorithm = engine.getPwEncryptionAlgorithm() + cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV) + } catch (e: Exception) { + throw LoadDatabaseInvalidAlgorithmException(e) } - if (!Arrays.equals(storedStartBytes, header.streamStartBytes)) { - throw InvalidPasswordException() - } + val isPlain: InputStream + if (version < PwDbHeaderV4.FILE_VERSION_32_4) { - isPlain = HashedBlockInputStream(dataDecrypted) - } else { // KDBX 4 - val isData = LEDataInputStream(databaseInputStream) - val storedHash = isData.readBytes(32) - if (!Arrays.equals(storedHash, hashOfHeader)) { - throw InvalidDBException() - } + val decrypted = attachCipherStream(databaseInputStream, cipher) + val dataDecrypted = LEDataInputStream(decrypted) + val storedStartBytes: ByteArray? + try { + storedStartBytes = dataDecrypted.readBytes(32) + if (storedStartBytes == null || storedStartBytes.size != 32) { + throw LoadDatabaseInvalidCredentialsException() + } + } catch (e: IOException) { + throw LoadDatabaseInvalidCredentialsException() + } - val hmacKey = mDatabase.hmacKey ?: throw InvalidDBException() - val headerHmac = PwDbHeaderV4.computeHeaderHmac(pbHeader, hmacKey) - val storedHmac = isData.readBytes(32) - if (storedHmac == null || storedHmac.size != 32) { - throw InvalidDBException() - } - // Mac doesn't match - if (!Arrays.equals(headerHmac, storedHmac)) { - throw InvalidDBException() + if (!Arrays.equals(storedStartBytes, header.streamStartBytes)) { + throw LoadDatabaseInvalidCredentialsException() + } + + isPlain = HashedBlockInputStream(dataDecrypted) + } else { // KDBX 4 + val isData = LEDataInputStream(databaseInputStream) + val storedHash = isData.readBytes(32) + if (!Arrays.equals(storedHash, hashOfHeader)) { + throw LoadDatabaseInvalidCredentialsException() + } + + val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException() + val headerHmac = PwDbHeaderV4.computeHeaderHmac(pbHeader, hmacKey) + val storedHmac = isData.readBytes(32) + if (storedHmac == null || storedHmac.size != 32) { + throw LoadDatabaseInvalidCredentialsException() + } + // Mac doesn't match + if (!Arrays.equals(headerHmac, storedHmac)) { + throw LoadDatabaseInvalidCredentialsException() + } + + val hmIs = HmacBlockInputStream(isData, true, hmacKey) + + isPlain = attachCipherStream(hmIs, cipher) } - val hmIs = HmacBlockInputStream(isData, true, hmacKey) + val inputStreamXml: InputStream + inputStreamXml = when (mDatabase.compressionAlgorithm) { + PwCompressionAlgorithm.GZip -> GZIPInputStream(isPlain) + else -> isPlain + } - isPlain = attachCipherStream(hmIs, cipher) - } + if (version >= PwDbHeaderV4.FILE_VERSION_32_4) { + loadInnerHeader(inputStreamXml, header) + } - val isXml: InputStream - if (mDatabase.compressionAlgorithm === PwCompressionAlgorithm.Gzip) { - isXml = GZIPInputStream(isPlain) - } else { - isXml = isPlain - } + randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey) - if (version >= PwDbHeaderV4.FILE_VERSION_32_4) { - loadInnerHeader(isXml, header) - } + if (randomStream == null) { + throw LoadDatabaseArcFourException() + } - randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey) + readDocumentStreamed(createPullParser(inputStreamXml)) - if (randomStream == null) { - throw ArcFourException() + } catch (e: LoadDatabaseException) { + throw e + } catch (e: XmlPullParserException) { + throw LoadDatabaseIOException(e) + } catch (e: IOException) { + if (e.message?.contains("Hash failed with code") == true) + throw LoadDatabaseKDFMemoryException(e) + else + throw LoadDatabaseIOException(e) + } catch (e: OutOfMemoryError) { + throw LoadDatabaseNoMemoryException(e) + } catch (e: Exception) { + throw LoadDatabaseException(e) } - readXmlStreamed(isXml) - return mDatabase } @@ -271,18 +277,7 @@ class ImporterV4(private val streamDir: File) : Importer() { Binaries } - @Throws(IOException::class, InvalidDBException::class) - private fun readXmlStreamed(readerStream: InputStream) { - try { - readDocumentStreamed(createPullParser(readerStream)) - } catch (e: XmlPullParserException) { - e.printStackTrace() - throw IOException(e.localizedMessage) - } - - } - - @Throws(XmlPullParserException::class, IOException::class, InvalidDBException::class) + @Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class) private fun readDocumentStreamed(xpp: XmlPullParser) { ctxGroups.clear() @@ -313,7 +308,7 @@ class ImporterV4(private val streamDir: File) : Importer() { if (ctxGroups.size != 0) throw IOException("Malformed") } - @Throws(XmlPullParserException::class, IOException::class, InvalidDBException::class) + @Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class) private fun readXmlElement(ctx: KdbContext, xpp: XmlPullParser): KdbContext { val name = xpp.name when (ctx) { @@ -337,7 +332,7 @@ class ImporterV4(private val streamDir: File) : Importer() { if (encodedHash.isNotEmpty() && hashOfHeader != null) { val hash = Base64Coder.decode(encodedHash) if (!Arrays.equals(hash, hashOfHeader)) { - throw InvalidDBException() + throw LoadDatabaseException() } } } else if (name.equals(PwDatabaseV4XML.ElemSettingsChanged, ignoreCase = true)) { @@ -355,7 +350,6 @@ class ImporterV4(private val streamDir: File) : Importer() { } else if (name.equals(PwDatabaseV4XML.ElemDbDefaultUserChanged, ignoreCase = true)) { mDatabase.defaultUserNameChanged = readPwTime(xpp) } else if (name.equals(PwDatabaseV4XML.ElemDbColor, ignoreCase = true)) { - // TODO: Add support to interpret the color if we want to allow changing the database color mDatabase.color = readString(xpp) } else if (name.equals(PwDatabaseV4XML.ElemDbMntncHistoryDays, ignoreCase = true)) { mDatabase.maintenanceHistoryDays = readUInt(xpp, DEFAULT_HISTORY_DAYS) @@ -589,7 +583,7 @@ class ImporterV4(private val streamDir: File) : Importer() { name.equals(PwDatabaseV4XML.ElemCreationTime, ignoreCase = true) -> tl?.creationTime = readPwTime(xpp) name.equals(PwDatabaseV4XML.ElemLastAccessTime, ignoreCase = true) -> tl?.lastAccessTime = readPwTime(xpp) name.equals(PwDatabaseV4XML.ElemExpiryTime, ignoreCase = true) -> tl?.expiryTime = readPwTime(xpp) - name.equals(PwDatabaseV4XML.ElemExpires, ignoreCase = true) -> tl?.isExpires = readBool(xpp, false) + name.equals(PwDatabaseV4XML.ElemExpires, ignoreCase = true) -> tl?.expires = readBool(xpp, false) name.equals(PwDatabaseV4XML.ElemUsageCount, ignoreCase = true) -> tl?.usageCount = readULong(xpp, 0) name.equals(PwDatabaseV4XML.ElemLocationChanged, ignoreCase = true) -> tl?.locationChanged = readPwTime(xpp) else -> readUnknown(xpp) @@ -748,7 +742,7 @@ class ImporterV4(private val streamDir: File) : Importer() { return KdbContext.Entry } else if (ctx == KdbContext.EntryString && name.equals(PwDatabaseV4XML.ElemString, ignoreCase = true)) { if (ctxStringName != null && ctxStringValue != null) - ctxEntry?.addExtraField(ctxStringName!!, ctxStringValue!!) + ctxEntry?.putExtraField(ctxStringName!!, ctxStringValue!!) ctxStringName = null ctxStringValue = null @@ -1001,13 +995,12 @@ class ImporterV4(private val streamDir: File) : Importer() { try { return String(buf, Charset.forName("UTF-8")) } catch (e: UnsupportedEncodingException) { - throw IOException(e.localizedMessage) + throw IOException(e) } } - //readNextNode = false; - return xpp.nextText() + return xpp.safeNextText() } @@ -1015,7 +1008,7 @@ class ImporterV4(private val streamDir: File) : Importer() { private fun readBase64String(xpp: XmlPullParser): ByteArray { //readNextNode = false; - Base64Coder.decode(xpp.nextText())?.let { buffer -> + Base64Coder.decode(xpp.safeNextText())?.let { buffer -> val plainText = ByteArray(buffer.size) randomStream?.processBytes(buffer, 0, buffer.size, plainText, 0) return plainText @@ -1066,3 +1059,12 @@ class ImporterV4(private val streamDir: File) : Importer() { private const val MAX_UINT = 4294967296L // 2^32 } } + +@Throws(IOException::class, XmlPullParserException::class) +fun XmlPullParser.safeNextText(): String { + val result = nextText() + if (eventType != XmlPullParser.END_TAG) { + nextTag() + } + return result +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt index 8d81f5dbc..6a5720825 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt @@ -24,7 +24,7 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters import com.kunzisoft.keepass.database.element.PwDatabaseV4 import com.kunzisoft.keepass.database.file.PwDbHeader import com.kunzisoft.keepass.database.file.PwDbHeaderV4 -import com.kunzisoft.keepass.database.exception.PwDbOutputException +import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.stream.HmacBlockStream import com.kunzisoft.keepass.stream.LEDataOutputStream import com.kunzisoft.keepass.stream.MacOutputStream @@ -41,7 +41,7 @@ import java.security.NoSuchAlgorithmException import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec -class PwDbHeaderOutputV4 @Throws(PwDbOutputException::class) +class PwDbHeaderOutputV4 @Throws(DatabaseOutputException::class) constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os: OutputStream) : PwDbHeaderOutput() { private val los: LEDataOutputStream private val mos: MacOutputStream @@ -54,13 +54,13 @@ constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os: try { md = MessageDigest.getInstance("SHA-256") } catch (e: NoSuchAlgorithmException) { - throw PwDbOutputException("SHA-256 not implemented here.") + throw DatabaseOutputException("SHA-256 not implemented here.") } try { db.makeFinalKey(header.masterSeed) } catch (e: IOException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } val hmac: Mac @@ -69,9 +69,9 @@ constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os: val signingKey = SecretKeySpec(HmacBlockStream.GetHmacKey64(db.hmacKey, Types.ULONG_MAX_VALUE), "HmacSHA256") hmac.init(signingKey) } catch (e: NoSuchAlgorithmException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } catch (e: InvalidKeyException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } dos = DigestOutputStream(os, md) @@ -86,9 +86,8 @@ constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os: los.writeUInt(PwDbHeaderV4.DBSIG_2.toLong()) los.writeUInt(header.version) - writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.CipherID, Types.UUIDtoBytes(db.dataCipher)) - writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.CompressionFlags, LEDataOutputStream.writeIntBuf(db.compressionAlgorithm.id)) + writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.CompressionFlags, LEDataOutputStream.writeIntBuf(PwDbHeaderV4.getFlagFromCompression(db.compressionAlgorithm))) writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.MasterSeed, header.masterSeed) if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt index e9a486af6..a670182ca 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt @@ -20,7 +20,7 @@ package com.kunzisoft.keepass.database.file.save import com.kunzisoft.keepass.database.file.PwDbHeader -import com.kunzisoft.keepass.database.exception.PwDbOutputException +import com.kunzisoft.keepass.database.exception.DatabaseOutputException import java.io.OutputStream import java.security.NoSuchAlgorithmException @@ -28,13 +28,13 @@ import java.security.SecureRandom abstract class PwDbOutput
protected constructor(protected var mOS: OutputStream) { - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) protected open fun setIVs(header: Header): SecureRandom { val random: SecureRandom try { random = SecureRandom.getInstance("SHA1PRNG") } catch (e: NoSuchAlgorithmException) { - throw PwDbOutputException("Does not support secure random number generation.") + throw DatabaseOutputException("Does not support secure random number generation.") } random.nextBytes(header.encryptionIV) @@ -43,10 +43,10 @@ abstract class PwDbOutput
protected constructor(protected v return random } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) abstract fun output() - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) abstract fun outputHeader(outputStream: OutputStream): Header } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV3Output.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV3Output.kt index 8a546c298..5a2710cbe 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV3Output.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV3Output.kt @@ -21,7 +21,7 @@ package com.kunzisoft.keepass.database.file.save import com.kunzisoft.keepass.crypto.CipherFactory import com.kunzisoft.keepass.database.element.* -import com.kunzisoft.keepass.database.exception.PwDbOutputException +import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.file.PwDbHeader import com.kunzisoft.keepass.database.file.PwDbHeaderV3 import com.kunzisoft.keepass.stream.LEDataOutputStream @@ -42,18 +42,18 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw private var headerHashBlock: ByteArray? = null - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) fun getFinalKey(header: PwDbHeader): ByteArray? { try { val h3 = header as PwDbHeaderV3 mDatabaseV3.makeFinalKey(h3.masterSeed, h3.transformSeed, mDatabaseV3.numberKeyEncryptionRounds) return mDatabaseV3.finalKey } catch (e: IOException) { - throw PwDbOutputException("Key creation failed.", e) + throw DatabaseOutputException("Key creation failed.", e) } } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) override fun output() { // Before we output the header, we should sort our list of groups // and remove any orphaned nodes that are no longer part of the tree hierarchy @@ -74,7 +74,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw throw Exception() } } catch (e: Exception) { - throw PwDbOutputException("Algorithm not supported.", e) + throw DatabaseOutputException("Algorithm not supported.", e) } try { @@ -86,23 +86,23 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw bos.close() } catch (e: InvalidKeyException) { - throw PwDbOutputException("Invalid key", e) + throw DatabaseOutputException("Invalid key", e) } catch (e: InvalidAlgorithmParameterException) { - throw PwDbOutputException("Invalid algorithm parameter.", e) + throw DatabaseOutputException("Invalid algorithm parameter.", e) } catch (e: IOException) { - throw PwDbOutputException("Failed to output final encrypted part.", e) + throw DatabaseOutputException("Failed to output final encrypted part.", e) } } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) override fun setIVs(header: PwDbHeaderV3): SecureRandom { val random = super.setIVs(header) random.nextBytes(header.transformSeed) return random } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) override fun outputHeader(outputStream: OutputStream): PwDbHeaderV3 { // Build header val header = PwDbHeaderV3() @@ -115,7 +115,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw } else if (mDatabaseV3.encryptionAlgorithm === PwEncryptionAlgorithm.Twofish) { header.flags = header.flags or PwDbHeaderV3.FLAG_TWOFISH } else { - throw PwDbOutputException("Unsupported algorithm.") + throw DatabaseOutputException("Unsupported algorithm.") } header.version = PwDbHeaderV3.DBVER_DW @@ -130,7 +130,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw try { messageDigest = MessageDigest.getInstance("SHA-256") } catch (e: NoSuchAlgorithmException) { - throw PwDbOutputException("SHA-256 not implemented here.", e) + throw DatabaseOutputException("SHA-256 not implemented here.", e) } // Header checksum @@ -138,7 +138,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw try { headerDigest = MessageDigest.getInstance("SHA-256") } catch (e: NoSuchAlgorithmException) { - throw PwDbOutputException("SHA-256 not implemented here.", e) + throw DatabaseOutputException("SHA-256 not implemented here.", e) } var nos = NullOutputStream() @@ -151,7 +151,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw pho.outputEnd() headerDos.flush() } catch (e: IOException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } val headerHash = headerDigest.digest() @@ -166,7 +166,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw bos.flush() bos.close() } catch (e: IOException) { - throw PwDbOutputException("Failed to generate checksum.", e) + throw DatabaseOutputException("Failed to generate checksum.", e) } header.contentsHash = messageDigest!!.digest() @@ -181,14 +181,14 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw pho.outputEnd() dos.flush() } catch (e: IOException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } return header } @Suppress("CAST_NEVER_SUCCEEDS") - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) fun outputPlanGroupAndEntries(os: OutputStream) { val los = LEDataOutputStream(os) @@ -199,7 +199,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw los.writeInt(headerHashBlock!!.size) los.write(headerHashBlock!!) } catch (e: IOException) { - throw PwDbOutputException("Failed to output header hash.", e) + throw DatabaseOutputException("Failed to output header hash.", e) } } @@ -209,7 +209,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw try { pgo.output() } catch (e: IOException) { - throw PwDbOutputException("Failed to output a tree", e) + throw DatabaseOutputException("Failed to output a tree", e) } } mDatabaseV3.doForEachEntryInIndex { entry -> @@ -217,7 +217,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw try { peo.output() } catch (e: IOException) { - throw PwDbOutputException("Failed to output an entry.", e) + throw DatabaseOutputException("Failed to output an entry.", e) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt index 5ec421bce..20227dded 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt @@ -29,9 +29,9 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.database.* import com.kunzisoft.keepass.database.element.* -import com.kunzisoft.keepass.database.exception.PwDbOutputException +import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.UnknownKDF -import com.kunzisoft.keepass.database.file.PwCompressionAlgorithm +import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm import com.kunzisoft.keepass.database.file.PwDbHeaderV4 import com.kunzisoft.keepass.database.element.security.ProtectedBinary import com.kunzisoft.keepass.database.element.security.ProtectedString @@ -63,14 +63,14 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt private var headerHmac: ByteArray? = null private var engine: CipherEngine? = null - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) override fun output() { try { try { engine = CipherFactory.getInstance(mDatabaseV4.dataCipher) } catch (e: NoSuchAlgorithmException) { - throw PwDbOutputException("No such cipher", e) + throw DatabaseOutputException("No such cipher", e) } header = outputHeader(mOS) @@ -91,10 +91,9 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt val osXml: OutputStream try { - if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.Gzip) { - osXml = GZIPOutputStream(osPlain) - } else { - osXml = osPlain + osXml = when(mDatabaseV4.compressionAlgorithm) { + PwCompressionAlgorithm.GZip -> GZIPOutputStream(osPlain) + else -> osPlain } if (header!!.version >= PwDbHeaderV4.FILE_VERSION_32_4) { @@ -105,13 +104,13 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt outputDatabase(osXml) osXml.close() } catch (e: IllegalArgumentException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } catch (e: IllegalStateException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } } catch (e: IOException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } } @@ -229,7 +228,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt xml.endTag(null, PwDatabaseV4XML.ElemMeta) } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) private fun attachStreamEncryptor(header: PwDbHeaderV4, os: OutputStream): CipherOutputStream { val cipher: Cipher try { @@ -237,13 +236,13 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt cipher = engine!!.getCipher(Cipher.ENCRYPT_MODE, mDatabaseV4.finalKey!!, header.encryptionIV) } catch (e: Exception) { - throw PwDbOutputException("Invalid algorithm.", e) + throw DatabaseOutputException("Invalid algorithm.", e) } return CipherOutputStream(os, cipher) } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) override fun setIVs(header: PwDbHeaderV4): SecureRandom { val random = super.setIVs(header) random.nextBytes(header.masterSeed) @@ -259,7 +258,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt } try { - val kdf = KdfFactory.getEngineV4(mDatabaseV4.kdfParameters) + val kdf = mDatabaseV4.getEngineV4(mDatabaseV4.kdfParameters) kdf.randomize(mDatabaseV4.kdfParameters!!) } catch (unknownKDF: UnknownKDF) { Log.e(TAG, "Unable to retrieve header", unknownKDF) @@ -276,7 +275,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey) if (randomStream == null) { - throw PwDbOutputException("Invalid random cipher") + throw DatabaseOutputException("Invalid random cipher") } if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) { @@ -286,7 +285,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt return random } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) override fun outputHeader(outputStream: OutputStream): PwDbHeaderV4 { val header = PwDbHeaderV4(mDatabaseV4) @@ -296,7 +295,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt try { pho.output() } catch (e: IOException) { - throw PwDbOutputException("Failed to output the header.", e) + throw DatabaseOutputException("Failed to output the header.", e) } hashOfHeader = pho.hashOfHeader @@ -403,7 +402,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt } } else { - if (mDatabaseV4.getCompressionAlgorithm() == PwCompressionAlgorithm.Gzip) { + if (mDatabaseV4.getCompressionAlgorithm() == PwCompressionAlgorithm.GZip) { xml.attribute(null, PwDatabaseV4XML.AttrCompressed, PwDatabaseV4XML.ValTrue); @@ -445,7 +444,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt xml.text(String(Base64Coder.encode(encoded))) } else { - if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.Gzip) { + if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.GZip) { xml.attribute(null, PwDatabaseV4XML.AttrCompressed, PwDatabaseV4XML.ValTrue) val compressData = MemoryUtil.compress(buffer) @@ -662,7 +661,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt writeObject(PwDatabaseV4XML.ElemCreationTime, it.creationTime.date) writeObject(PwDatabaseV4XML.ElemLastAccessTime, it.lastAccessTime.date) writeObject(PwDatabaseV4XML.ElemExpiryTime, it.expiryTime.date) - writeObject(PwDatabaseV4XML.ElemExpires, it.isExpires) + writeObject(PwDatabaseV4XML.ElemExpires, it.expires) writeObject(PwDatabaseV4XML.ElemUsageCount, it.usageCount) writeObject(PwDatabaseV4XML.ElemLocationChanged, it.locationChanged.date) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/search/EntrySearchHandlerV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/search/EntrySearchHandlerV4.kt index a62a28dd4..a14670220 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/search/EntrySearchHandlerV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/search/EntrySearchHandlerV4.kt @@ -24,16 +24,14 @@ import com.kunzisoft.keepass.database.element.PwEntryV4 import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorV4 import com.kunzisoft.keepass.utils.StringUtil -import java.util.Date import java.util.Locale class EntrySearchHandlerV4(private val mSearchParametersV4: SearchParametersV4, private val mListStorage: MutableList) : NodeHandler() { - private var now: Date = Date() - override fun operate(node: PwEntryV4): Boolean { - if (mSearchParametersV4.excludeExpired && node.isExpires && now.after(node.expiryTime.date)) { + if (mSearchParametersV4.excludeExpired + && node.isCurrentlyExpires) { return true } diff --git a/app/src/main/java/com/kunzisoft/keepass/education/Education.kt b/app/src/main/java/com/kunzisoft/keepass/education/Education.kt index d39783e2b..ba4f3d35b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/Education.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/Education.kt @@ -33,11 +33,10 @@ open class Education(val activity: Activity) { return doEducation } - /** * Define if educations screens are enabled */ - fun isEducationScreensEnabled(): Boolean { + private fun isEducationScreensEnabled(): Boolean { return isEducationScreensEnabled(activity) } @@ -47,7 +46,7 @@ open class Education(val activity: Activity) { * @param context The context to retrieve the key string in XML * @param educationKeys Keys to save as boolean 'true' */ - fun saveEducationPreference(context: Context, vararg educationKeys: Int) { + private fun saveEducationPreference(context: Context, vararg educationKeys: Int) { val sharedPreferences = getEducationSharedPreferences(context) val editor = sharedPreferences.edit() for (key in educationKeys) { @@ -66,10 +65,9 @@ open class Education(val activity: Activity) { val educationResourcesKeys = intArrayOf( R.string.education_create_db_key, R.string.education_select_db_key, - R.string.education_open_link_db_key, R.string.education_unlock_key, R.string.education_read_only_key, - R.string.education_fingerprint_key, + R.string.education_biometric_key, R.string.education_search_key, R.string.education_new_node_key, R.string.education_sort_key, @@ -122,18 +120,6 @@ open class Education(val activity: Activity) { context.resources.getBoolean(R.bool.education_select_db_default)) } - /** - * Determines whether the explanatory view of the database selection has already been displayed. - * - * @param context The context to open the SharedPreferences - * @return boolean value of education_select_db_key key - */ - fun isEducationOpenLinkDatabasePerformed(context: Context): Boolean { - val prefs = getEducationSharedPreferences(context) - return prefs.getBoolean(context.getString(R.string.education_open_link_db_key), - context.resources.getBoolean(R.bool.education_open_link_db_default)) - } - /** * Determines whether the explanatory view of the database unlock has already been displayed. * @@ -159,15 +145,15 @@ open class Education(val activity: Activity) { } /** - * Determines whether the explanatory view of the fingerprint unlock has already been displayed. + * Determines whether the explanatory view of the biometric unlock has already been displayed. * * @param context The context to open the SharedPreferences - * @return boolean value of education_fingerprint_key key + * @return boolean value of education_biometric_key key */ - fun isEducationFingerprintPerformed(context: Context): Boolean { + fun isEducationBiometricPerformed(context: Context): Boolean { val prefs = getEducationSharedPreferences(context) - return prefs.getBoolean(context.getString(R.string.education_fingerprint_key), - context.resources.getBoolean(R.bool.education_fingerprint_default)) + return prefs.getBoolean(context.getString(R.string.education_biometric_key), + context.resources.getBoolean(R.bool.education_biometric_default)) } /** diff --git a/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt index 12f61384f..00d759d72 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/FileDatabaseSelectActivityEducation.kt @@ -72,31 +72,4 @@ class FileDatabaseSelectActivityEducation(activity: Activity) }, R.string.education_select_db_key) } - - - fun checkAndPerformedOpenLinkDatabaseEducation(educationView: View, - onEducationViewClick: ((TapTargetView?) -> Unit)? = null, - onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationOpenLinkDatabasePerformed(activity), - TapTarget.forView(educationView, - activity.getString(R.string.education_open_link_database_title), - activity.getString(R.string.education_open_link_database_summary)) - .icon(ContextCompat.getDrawable(activity, R.drawable.ic_link_white_24dp)) - .textColorInt(Color.WHITE) - .tintTarget(true) - .cancelable(true), - object : TapTargetView.Listener() { - override fun onTargetClick(view: TapTargetView) { - super.onTargetClick(view) - onEducationViewClick?.invoke(view) - } - - override fun onOuterCircleClick(view: TapTargetView?) { - super.onOuterCircleClick(view) - view?.dismiss(false) - onOuterViewClick?.invoke(view) - } - }, - R.string.education_open_link_db_key) - } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt index ffd404c43..2b1fdcd6f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt @@ -18,7 +18,6 @@ class PasswordActivityEducation(activity: Activity) TapTarget.forView(educationView, activity.getString(R.string.education_unlock_title), activity.getString(R.string.education_unlock_summary)) - .dimColor(R.color.green) .icon(ContextCompat.getDrawable(activity, R.mipmap.ic_launcher_round)) .textColorInt(Color.WHITE) .tintTarget(false) @@ -63,13 +62,13 @@ class PasswordActivityEducation(activity: Activity) R.string.education_read_only_key) } - fun checkAndPerformedFingerprintEducation(educationView: View, - onEducationViewClick: ((TapTargetView?) -> Unit)? = null, - onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { - return checkAndPerformedEducation(!isEducationFingerprintPerformed(activity), + fun checkAndPerformedBiometricEducation(educationView: View, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { + return checkAndPerformedEducation(!isEducationBiometricPerformed(activity), TapTarget.forView(educationView, - activity.getString(R.string.education_fingerprint_title), - activity.getString(R.string.education_fingerprint_summary)) + activity.getString(R.string.education_biometric_title), + activity.getString(R.string.education_biometric_summary)) .textColorInt(Color.WHITE) .tintTarget(false) .cancelable(true), @@ -85,6 +84,6 @@ class PasswordActivityEducation(activity: Activity) onOuterViewClick?.invoke(view) } }, - R.string.education_fingerprint_key) + R.string.education_biometric_key) } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt index efe0269ee..2828960a2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardLauncherActivity.kt @@ -12,7 +12,7 @@ class KeyboardLauncherActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { if (Database.getInstance().loaded && TimeoutHelper.checkTime(this)) - GroupActivity.launchForKeyboarSelection(this, PreferencesUtil.enableReadOnlyDatabase(this)) + GroupActivity.launchForKeyboardSelection(this, PreferencesUtil.enableReadOnlyDatabase(this)) else { // Pass extra to get entry FileDatabaseSelectActivity.launchForKeyboardSelection(this) diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt index b740c987d..83690fdf9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt +++ b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt @@ -100,20 +100,20 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener { val popupFieldsView = LayoutInflater.from(context) .inflate(R.layout.keyboard_popup_fields, FrameLayout(context)) - popupCustomKeys?.dismiss() - - popupCustomKeys = PopupWindow(context) - popupCustomKeys?.width = WindowManager.LayoutParams.WRAP_CONTENT - popupCustomKeys?.height = WindowManager.LayoutParams.WRAP_CONTENT - popupCustomKeys?.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE - popupCustomKeys?.inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED - popupCustomKeys?.contentView = popupFieldsView + dismissCustomKeys() + popupCustomKeys = PopupWindow(context).apply { + width = WindowManager.LayoutParams.WRAP_CONTENT + height = WindowManager.LayoutParams.WRAP_CONTENT + softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED + contentView = popupFieldsView + } val recyclerView = popupFieldsView.findViewById(R.id.keyboard_popup_fields_list) fieldsAdapter = FieldsAdapter(this) fieldsAdapter?.onItemClickListener = object : FieldsAdapter.OnItemClickListener { override fun onItemClick(item: Field) { - currentInputConnection.commitText(item.protectedValue.toString(), 1) + currentInputConnection.commitText(entryInfoKey?.getGeneratedFieldValue(item.name) , 1) } } recyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this, androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL, true) @@ -129,6 +129,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener { } private fun assignKeyboardView() { + dismissCustomKeys() if (keyboardView != null) { if (entryInfoKey != null) { if (keyboardEntry != null) { @@ -138,7 +139,6 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener { } else { if (keyboard != null) { hideEntryInfo() - dismissCustomKeys() keyboardView?.keyboard = keyboard } } @@ -251,6 +251,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener { KEY_FIELDS -> { if (entryInfoKey != null) { fieldsAdapter?.fields = entryInfoKey!!.customFields + fieldsAdapter?.notifyDataSetChanged() } popupCustomKeys?.showAtLocation(keyboardView, Gravity.END or Gravity.TOP, 0, 0) } diff --git a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt index 142dcdf16..26866973e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt @@ -2,8 +2,9 @@ package com.kunzisoft.keepass.model import android.os.Parcel import android.os.Parcelable - -import java.util.ArrayList +import com.kunzisoft.keepass.otp.OtpElement +import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD +import java.util.* class EntryInfo : Parcelable { @@ -14,6 +15,7 @@ class EntryInfo : Parcelable { var url: String = "" var notes: String = "" var customFields: MutableList = ArrayList() + var otpModel: OtpModel? = null constructor() @@ -25,6 +27,7 @@ class EntryInfo : Parcelable { url = parcel.readString() ?: url notes = parcel.readString() ?: notes parcel.readList(customFields, Field::class.java.classLoader) + otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel } override fun describeContents(): Int { @@ -39,6 +42,7 @@ class EntryInfo : Parcelable { parcel.writeString(url) parcel.writeString(notes) parcel.writeArray(customFields.toTypedArray()) + parcel.writeParcelable(otpModel, flags) } fun containsCustomFieldsProtected(): Boolean { @@ -49,6 +53,19 @@ class EntryInfo : Parcelable { return customFields.any { !it.protectedValue.isProtected } } + fun isAutoGeneratedField(field: Field): Boolean { + return field.name == OTP_TOKEN_FIELD + } + + fun getGeneratedFieldValue(label: String): String { + otpModel?.let { + if (label == OTP_TOKEN_FIELD) { + return OtpElement(it).token + } + } + return customFields.lastOrNull { it.name == label }?.protectedValue?.toString() ?: "" + } + companion object { @JvmField diff --git a/app/src/main/java/com/kunzisoft/keepass/model/Field.kt b/app/src/main/java/com/kunzisoft/keepass/model/Field.kt index d689684b5..9695db421 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/Field.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/Field.kt @@ -9,7 +9,7 @@ class Field : Parcelable { var name: String = "" var protectedValue: ProtectedString = ProtectedString() - constructor(name: String, value: ProtectedString) { + constructor(name: String, value: ProtectedString = ProtectedString()) { this.name = name this.protectedValue = value } @@ -28,6 +28,21 @@ class Field : Parcelable { dest.writeParcelable(protectedValue, flags) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Field + + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + return name.hashCode() + } + companion object { @JvmField diff --git a/app/src/main/java/com/kunzisoft/keepass/model/OtpModel.kt b/app/src/main/java/com/kunzisoft/keepass/model/OtpModel.kt new file mode 100644 index 000000000..b5cf65274 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/model/OtpModel.kt @@ -0,0 +1,95 @@ +package com.kunzisoft.keepass.model + +import android.os.Parcel +import android.os.Parcelable +import com.kunzisoft.keepass.otp.OtpElement +import com.kunzisoft.keepass.otp.OtpTokenType +import com.kunzisoft.keepass.otp.OtpType +import com.kunzisoft.keepass.otp.TokenCalculator +import com.kunzisoft.keepass.otp.TokenCalculator.OTP_DEFAULT_ALGORITHM + +class OtpModel() : Parcelable { + + var type: OtpType = OtpType.TOTP // ie : HOTP or TOTP + var tokenType: OtpTokenType = OtpTokenType.RFC6238 + var name: String = "OTP" // ie : user@email.com + var issuer: String = "None" // ie : Gitlab + var secret: ByteArray? = null // Seed + var counter: Long = TokenCalculator.HOTP_INITIAL_COUNTER // ie : 5 - only for HOTP + var period: Int = TokenCalculator.TOTP_DEFAULT_PERIOD // ie : 30 seconds - only for TOTP + var digits: Int = TokenCalculator.OTP_DEFAULT_DIGITS + var algorithm: TokenCalculator.HashAlgorithm = OTP_DEFAULT_ALGORITHM + + constructor(parcel: Parcel) : this() { + val typeRead = parcel.readInt() + type = OtpType.values()[typeRead] + tokenType = OtpTokenType.values()[parcel.readInt()] + name = parcel.readString() ?: name + issuer = parcel.readString() ?: issuer + secret = parcel.createByteArray() ?: secret + counter = parcel.readLong() + period = parcel.readInt() + digits = parcel.readInt() + algorithm = TokenCalculator.HashAlgorithm.values()[parcel.readInt()] + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OtpElement + + if (type != other.type) return false + // Token type is important only if it's a TOTP + if (type == OtpType.TOTP && tokenType != other.tokenType) return false + if (secret == null || other.secret == null) return false + if (!secret!!.contentEquals(other.secret!!)) return false + // Counter only for HOTP + if (type == OtpType.HOTP && counter != other.counter) return false + // Step only for TOTP + if (type == OtpType.TOTP && period != other.period) return false + if (digits != other.digits) return false + if (algorithm != other.algorithm) return false + + return true + } + + override fun describeContents(): Int { + return 0 + } + + override fun hashCode(): Int { + var result = type.hashCode() + result = 31 * result + tokenType.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + issuer.hashCode() + result = 31 * result + (secret?.contentHashCode() ?: 0) + result = 31 * result + counter.hashCode() + result = 31 * result + period + result = 31 * result + digits + result = 31 * result + algorithm.hashCode() + return result + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeInt(type.ordinal) + parcel.writeInt(tokenType.ordinal) + parcel.writeString(name) + parcel.writeString(issuer) + parcel.writeByteArray(secret) + parcel.writeLong(counter) + parcel.writeInt(period) + parcel.writeInt(digits) + parcel.writeInt(algorithm.ordinal) + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): OtpModel { + return OtpModel(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt index 5f16bc089..064c8b4a0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt +++ b/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt @@ -19,24 +19,19 @@ */ package com.kunzisoft.keepass.notifications -import android.content.res.Resources import android.os.Parcel import android.os.Parcelable import android.util.Log - -import com.kunzisoft.keepass.R - -import java.util.ArrayList +import com.kunzisoft.keepass.model.EntryInfo +import java.util.* /** * Utility class to manage fields in Notifications */ -open class ClipboardEntryNotificationField : Parcelable { +class ClipboardEntryNotificationField : Parcelable { private var id: NotificationFieldId = NotificationFieldId.UNKNOWN - var value: String = "" var label: String = "" - var copyText: String = "" val actionKey: String get() = getActionKey(id) @@ -44,32 +39,30 @@ open class ClipboardEntryNotificationField : Parcelable { val extraKey: String get() = getExtraKey(id) - constructor(id: NotificationFieldId, value: String, resources: Resources) { - this.id = id - this.value = value - this.label = getLabel(resources) - this.copyText = getCopyText(resources) - } - - constructor(id: NotificationFieldId, value: String, label: String, resources: Resources) { + constructor(id: NotificationFieldId, label: String) { this.id = id - this.value = value this.label = label - this.copyText = getCopyText(resources) } - protected constructor(parcel: Parcel) { + constructor(parcel: Parcel) { id = NotificationFieldId.values()[parcel.readInt()] - value = parcel.readString() ?: value label = parcel.readString() ?: label - copyText = parcel.readString() ?: copyText + } + + fun getGeneratedValue(entryInfo: EntryInfo?): String { + return when (id) { + NotificationFieldId.UNKNOWN -> "" + NotificationFieldId.USERNAME -> entryInfo?.username ?: "" + NotificationFieldId.PASSWORD -> entryInfo?.password ?: "" + NotificationFieldId.FIELD_A, + NotificationFieldId.FIELD_B, + NotificationFieldId.FIELD_C -> entryInfo?.getGeneratedFieldValue(label) ?: "" + } } override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeInt(id.ordinal) - dest.writeString(value) dest.writeString(label) - dest.writeString(copyText) } override fun describeContents(): Int { @@ -91,24 +84,11 @@ open class ClipboardEntryNotificationField : Parcelable { UNKNOWN, USERNAME, PASSWORD, FIELD_A, FIELD_B, FIELD_C; companion object { - val anonymousFieldId: Array get() = arrayOf(FIELD_A, FIELD_B, FIELD_C) } } - private fun getLabel(resources: Resources): String { - return when (id) { - NotificationFieldId.USERNAME -> resources.getString(R.string.entry_user_name) - NotificationFieldId.PASSWORD -> resources.getString(R.string.entry_password) - else -> id.name - } - } - - private fun getCopyText(resources: Resources): String { - return resources.getString(R.string.select_to_copy, label) - } - companion object { private val TAG = ClipboardEntryNotificationField::class.java.name diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt index 2831176b8..620819f9f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt @@ -29,17 +29,17 @@ import com.kunzisoft.keepass.database.exception.SamsungClipboardException import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.ClipboardHelper +import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER import com.kunzisoft.keepass.utils.LOCK_ACTION import java.util.* -class ClipboardEntryNotificationService : NotificationService() { - - private var notificationId = 485 - private var cleanNotificationTimerTask: Thread? = null - private var notificationTimeoutMilliSecs: Long = 0 +class ClipboardEntryNotificationService : LockNotificationService() { + override val notificationId = 485 + private var mEntryInfo: EntryInfo? = null private var clipboardHelper: ClipboardHelper? = null + private var notificationTimeoutMilliSecs: Long = 0 private var cleanCopyNotificationTimerTask: Thread? = null override fun onCreate() { @@ -53,28 +53,27 @@ class ClipboardEntryNotificationService : NotificationService() { if (PreferencesUtil.isClearClipboardNotificationEnable(this)) { sendBroadcast(Intent(LOCK_ACTION)) } + // Stop the service + stopSelf() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + // Get entry info from intent + mEntryInfo = intent?.getParcelableExtra(EXTRA_ENTRY_INFO) + //Get settings - val prefs = PreferenceManager.getDefaultSharedPreferences(this) - val timeoutClipboardClear = prefs.getString(getString(R.string.clipboard_timeout_key), - getString(R.string.clipboard_timeout_default)) ?: "6000" - notificationTimeoutMilliSecs = java.lang.Long.parseLong(timeoutClipboardClear) + notificationTimeoutMilliSecs = PreferenceManager.getDefaultSharedPreferences(this) + .getString(getString(R.string.clipboard_timeout_key), + getString(R.string.clipboard_timeout_default))?.toLong() ?: TimeoutHelper.DEFAULT_TIMEOUT when { intent == null -> Log.w(TAG, "null intent") ACTION_NEW_NOTIFICATION == intent.action -> { - val title = intent.getStringExtra(EXTRA_ENTRY_TITLE) - newNotification(title, constructListOfField(intent)) + newNotification(mEntryInfo?.title, constructListOfField(intent)) } ACTION_CLEAN_CLIPBOARD == intent.action -> { stopTask(cleanCopyNotificationTimerTask) - try { - clipboardHelper?.cleanClipboard() - } catch (e: SamsungClipboardException) { - Log.e(TAG, "Clipboard can't be cleaned", e) - } + cleanClipboard() stopNotificationAndSendLockIfNeeded() } else -> for (actionKey in ClipboardEntryNotificationField.allActionKeys) { @@ -94,18 +93,19 @@ class ClipboardEntryNotificationService : NotificationService() { private fun constructListOfField(intent: Intent?): ArrayList { var fieldList = ArrayList() if (intent != null && intent.extras != null) { - if (intent.extras!!.containsKey(EXTRA_FIELDS)) - fieldList = intent.getParcelableArrayListExtra(EXTRA_FIELDS) + if (intent.extras!!.containsKey(EXTRA_CLIPBOARD_FIELDS)) + fieldList = intent.getParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS) } return fieldList } private fun getCopyPendingIntent(fieldToCopy: ClipboardEntryNotificationField, fieldsToAdd: ArrayList): PendingIntent { - val copyIntent = Intent(this, ClipboardEntryNotificationService::class.java) - copyIntent.action = fieldToCopy.actionKey - copyIntent.putExtra(fieldToCopy.extraKey, fieldToCopy) - copyIntent.putParcelableArrayListExtra(EXTRA_FIELDS, fieldsToAdd) - + val copyIntent = Intent(this, ClipboardEntryNotificationService::class.java).apply { + action = fieldToCopy.actionKey + putExtra(EXTRA_ENTRY_INFO, mEntryInfo) + putExtra(fieldToCopy.extraKey, fieldToCopy) + putParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS, fieldsToAdd) + } return PendingIntent.getService( this, 0, copyIntent, PendingIntent.FLAG_UPDATE_CURRENT) } @@ -114,14 +114,14 @@ class ClipboardEntryNotificationService : NotificationService() { stopTask(cleanCopyNotificationTimerTask) val builder = buildNewNotification() - .setSmallIcon(R.drawable.ic_clipboard_key_white_24dp) + .setSmallIcon(R.drawable.notification_ic_clipboard_key_24dp) if (title != null) builder.setContentTitle(title) if (fieldsToAdd.size > 0) { val field = fieldsToAdd[0] - builder.setContentText(field.copyText) + builder.setContentText(getString(R.string.select_to_copy, field.label)) builder.setContentIntent(getCopyPendingIntent(field, fieldsToAdd)) // Add extra actions without 1st field @@ -129,45 +129,28 @@ class ClipboardEntryNotificationService : NotificationService() { fieldsWithoutFirstField.remove(field) // Add extra actions for (fieldToAdd in fieldsWithoutFirstField) { - builder.addAction(R.drawable.ic_clipboard_key_white_24dp, fieldToAdd.label, + builder.addAction(R.drawable.notification_ic_clipboard_key_24dp, fieldToAdd.label, getCopyPendingIntent(fieldToAdd, fieldsToAdd)) } } - - notificationManager?.cancel(notificationId) - notificationManager?.notify(++notificationId, builder.build()) - - val myNotificationId = notificationId - stopTask(cleanNotificationTimerTask) - // If timer - if (notificationTimeoutMilliSecs != NEVER) { - cleanNotificationTimerTask = Thread { - try { - Thread.sleep(notificationTimeoutMilliSecs) - } catch (e: InterruptedException) { - cleanNotificationTimerTask = null - } - notificationManager?.cancel(myNotificationId) - } - cleanNotificationTimerTask?.start() - } + notificationManager?.notify(notificationId, builder.build()) } private fun copyField(fieldToCopy: ClipboardEntryNotificationField, nextFields: ArrayList) { stopTask(cleanCopyNotificationTimerTask) - stopTask(cleanNotificationTimerTask) try { - clipboardHelper?.copyToClipboard(fieldToCopy.label, fieldToCopy.value) + var generatedValue = fieldToCopy.getGeneratedValue(mEntryInfo) + clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue) val builder = buildNewNotification() - .setSmallIcon(R.drawable.ic_clipboard_key_white_24dp) + .setSmallIcon(R.drawable.notification_ic_clipboard_key_24dp) .setContentTitle(fieldToCopy.label) // New action with next field if click if (nextFields.size > 0) { val nextField = nextFields[0] - builder.setContentText(nextField.copyText) + builder.setContentText(getString(R.string.select_to_copy, nextField.label)) builder.setContentIntent(getCopyPendingIntent(nextField, nextFields)) // Else tell to swipe for a clean } else { @@ -187,6 +170,12 @@ class ClipboardEntryNotificationService : NotificationService() { val maxPos = 100 val posDurationMills = notificationTimeoutMilliSecs / maxPos for (pos in maxPos downTo 0) { + val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo) + // New auto generated value + if (generatedValue != newGeneratedValue) { + generatedValue = newGeneratedValue + clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue) + } builder.setProgress(maxPos, pos, false) notificationManager?.notify(myNotificationId, builder.build()) try { @@ -202,11 +191,7 @@ class ClipboardEntryNotificationService : NotificationService() { notificationManager?.cancel(myNotificationId) // Clean password only if no next field if (nextFields.size <= 0) - try { - clipboardHelper?.cleanClipboard() - } catch (e: SamsungClipboardException) { - Log.e(TAG, "Clipboard can't be cleaned", e) - } + cleanClipboard() } cleanCopyNotificationTimerTask?.start() } else { @@ -220,9 +205,27 @@ class ClipboardEntryNotificationService : NotificationService() { } - private fun stopTask(task: Thread?) { - if (task != null && task.isAlive) - task.interrupt() + private fun cleanClipboard() { + try { + clipboardHelper?.cleanClipboard() + } catch (e: SamsungClipboardException) { + Log.e(TAG, "Clipboard can't be cleaned", e) + } + } + + override fun onTaskRemoved(rootIntent: Intent?) { + cleanClipboard() + + super.onTaskRemoved(rootIntent) + } + + override fun onDestroy() { + cleanClipboard() + + stopTask(cleanCopyNotificationTimerTask) + cleanCopyNotificationTimerTask = null + + super.onDestroy() } companion object { @@ -230,8 +233,8 @@ class ClipboardEntryNotificationService : NotificationService() { private val TAG = ClipboardEntryNotificationService::class.java.name const val ACTION_NEW_NOTIFICATION = "ACTION_NEW_NOTIFICATION" - const val EXTRA_ENTRY_TITLE = "EXTRA_ENTRY_TITLE" - const val EXTRA_FIELDS = "EXTRA_FIELDS" + const val EXTRA_ENTRY_INFO = "EXTRA_ENTRY_INFO" + const val EXTRA_CLIPBOARD_FIELDS = "EXTRA_CLIPBOARD_FIELDS" const val ACTION_CLEAN_CLIPBOARD = "ACTION_CLEAN_CLIPBOARD" fun launchNotificationIfAllowed(context: Context, entry: EntryInfo) { @@ -245,15 +248,17 @@ class ClipboardEntryNotificationService : NotificationService() { (entry.containsCustomFieldsProtected() && PreferencesUtil.allowCopyPasswordAndProtectedFields(context)) ) + var startService = false + val intent = Intent(context, ClipboardEntryNotificationService::class.java) + // If notifications enabled in settings // Don't if application timeout if (PreferencesUtil.isClipboardNotificationsEnable(context)) { if (containsUsernameToCopy || containsPasswordToCopy || containsExtraFieldToCopy) { // username already copied, waiting for user's action before copy password. - val intent = Intent(context, ClipboardEntryNotificationService::class.java) intent.action = ACTION_NEW_NOTIFICATION - intent.putExtra(EXTRA_ENTRY_TITLE, entry.title) + intent.putExtra(EXTRA_ENTRY_INFO, entry) // Construct notification fields val notificationFields = ArrayList() // Add username if exists to notifications @@ -261,15 +266,13 @@ class ClipboardEntryNotificationService : NotificationService() { notificationFields.add( ClipboardEntryNotificationField( ClipboardEntryNotificationField.NotificationFieldId.USERNAME, - entry.username, - context.resources)) + context.getString(R.string.entry_user_name))) // Add password to notifications if (containsPasswordToCopy) { notificationFields.add( ClipboardEntryNotificationField( ClipboardEntryNotificationField.NotificationFieldId.PASSWORD, - entry.password, - context.resources)) + context.getString(R.string.entry_password))) } // Add extra fields if (containsExtraFieldToCopy) { @@ -282,9 +285,7 @@ class ClipboardEntryNotificationService : NotificationService() { notificationFields.add( ClipboardEntryNotificationField( ClipboardEntryNotificationField.NotificationFieldId.anonymousFieldId[anonymousFieldNumber], - field.protectedValue.toString(), - field.name, - context.resources)) + field.name)) anonymousFieldNumber++ } } @@ -295,10 +296,14 @@ class ClipboardEntryNotificationService : NotificationService() { } // Add notifications - intent.putParcelableArrayListExtra(EXTRA_FIELDS, notificationFields) + startService = true + intent.putParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS, notificationFields) context.startService(intent) } } + + if (!startService) + context.stopService(intent) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseOpenNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseOpenNotificationService.kt new file mode 100644 index 000000000..13fd168d4 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseOpenNotificationService.kt @@ -0,0 +1,77 @@ +package com.kunzisoft.keepass.notifications + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.GroupActivity +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.utils.LOCK_ACTION + +class DatabaseOpenNotificationService: LockNotificationService() { + + override val notificationId: Int = 340 + + private fun stopNotificationAndSendLock() { + // Send lock action + sendBroadcast(Intent(LOCK_ACTION)) + // Stop the service + stopSelf() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + + when(intent?.action) { + ACTION_CLOSE_DATABASE -> { + stopNotificationAndSendLock() + } + else -> { + val databaseIntent = Intent(this, GroupActivity::class.java) + var pendingDatabaseFlag = 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + pendingDatabaseFlag = PendingIntent.FLAG_IMMUTABLE + } + val pendingDatabaseIntent = PendingIntent.getActivity(this, 0, databaseIntent, pendingDatabaseFlag) + val deleteIntent = Intent(this, DatabaseOpenNotificationService::class.java).apply { + action = ACTION_CLOSE_DATABASE + } + val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT) + + val database = Database.getInstance() + if (database.loaded) { + notificationManager?.notify(notificationId, buildNewNotification().apply { + setSmallIcon(R.drawable.notification_ic_database_open) + setContentTitle(getString(R.string.database_opened)) + setContentText(database.name + " (" + database.version + ")") + setAutoCancel(false) + setContentIntent(pendingDatabaseIntent) + setDeleteIntent(pendingDeleteIntent) + }.build()) + } else { + stopSelf() + } + } + } + + return START_STICKY + } + + companion object { + const val ACTION_CLOSE_DATABASE = "ACTION_CLOSE_DATABASE" + + fun startIfAllowed(context: Context) { + if (PreferencesUtil.isPersistentNotificationEnable(context)) { + // Start the opening notification + context.startService(Intent(context, DatabaseOpenNotificationService::class.java)) + } + } + + fun stop(context: Context) { + // Stop the opening notification + context.stopService(Intent(context, DatabaseOpenNotificationService::class.java)) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt index bf2e37132..8ca0dafb1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt @@ -1,44 +1,531 @@ package com.kunzisoft.keepass.notifications import android.content.Intent -import android.util.Log +import android.net.Uri +import android.os.AsyncTask +import android.os.Binder +import android.os.Bundle +import android.os.IBinder import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.app.database.CipherDatabaseEntity +import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable +import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable +import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable +import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable +import com.kunzisoft.keepass.database.action.node.* +import com.kunzisoft.keepass.database.element.* +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.tasks.ProgressTaskUpdater +import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION +import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION +import java.util.* +import kotlin.collections.ArrayList -class DatabaseTaskNotificationService : NotificationService() { +class DatabaseTaskNotificationService : NotificationService(), ProgressTaskUpdater { - private val notificationId = 532 + override val notificationId: Int = 575 + + private var actionRunnableAsyncTask: ActionRunnableAsyncTask? = null + + private var mActionTaskBinder = ActionTaskBinder() + private var mActionTaskListeners = LinkedList() + + private var mTitleId: Int? = null + private var mMessageId: Int? = null + private var mWarningId: Int? = null + + inner class ActionTaskBinder: Binder() { + + fun getService(): DatabaseTaskNotificationService = this@DatabaseTaskNotificationService + + fun addActionTaskListener(actionTaskListener: ActionTaskListener) { + mActionTaskListeners.add(actionTaskListener) + } + + fun removeActionTaskListener(actionTaskListener: ActionTaskListener) { + mActionTaskListeners.remove(actionTaskListener) + } + } + + interface ActionTaskListener { + fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) + fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) + fun onStopAction(actionTask: String, result: ActionRunnable.Result) + } + + fun checkAction() { + mActionTaskListeners.forEach { actionTaskListener -> + actionTaskListener.onUpdateAction(mTitleId, mMessageId, mWarningId) + } + } + + override fun onBind(intent: Intent): IBinder? { + return mActionTaskBinder + } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (intent == null) { - Log.w(TAG, "null intent") - } else { - newNotification(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, R.string.saving_database)) + + if (intent == null) return START_REDELIVER_INTENT + + val intentAction = intent.action + + val titleId: Int = when (intentAction) { + ACTION_DATABASE_CREATE_TASK -> R.string.creating_database + ACTION_DATABASE_LOAD_TASK -> R.string.loading_database + else -> R.string.saving_database + } + val messageId: Int? = when (intentAction) { + ACTION_DATABASE_LOAD_TASK -> null + else -> null } - return START_NOT_STICKY + val warningId: Int? = + if (intentAction == ACTION_DATABASE_LOAD_TASK) + null + else + R.string.do_not_kill_app + + val actionRunnable: ActionRunnable? = when (intentAction) { + ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent) + ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent) + ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent) + ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent) + ACTION_DATABASE_UPDATE_GROUP_TASK -> buildDatabaseUpdateGroupActionTask(intent) + ACTION_DATABASE_CREATE_ENTRY_TASK -> buildDatabaseCreateEntryActionTask(intent) + ACTION_DATABASE_UPDATE_ENTRY_TASK -> buildDatabaseUpdateEntryActionTask(intent) + ACTION_DATABASE_COPY_NODES_TASK -> buildDatabaseCopyNodesActionTask(intent) + ACTION_DATABASE_MOVE_NODES_TASK -> buildDatabaseMoveNodesActionTask(intent) + ACTION_DATABASE_DELETE_NODES_TASK -> buildDatabaseDeleteNodesActionTask(intent) + ACTION_DATABASE_SAVE_NAME_TASK, + ACTION_DATABASE_SAVE_DESCRIPTION_TASK, + ACTION_DATABASE_SAVE_DEFAULT_USERNAME_TASK, + ACTION_DATABASE_SAVE_COLOR_TASK, + ACTION_DATABASE_SAVE_COMPRESSION_TASK, + ACTION_DATABASE_SAVE_MAX_HISTORY_ITEMS_TASK, + ACTION_DATABASE_SAVE_MAX_HISTORY_SIZE_TASK, + ACTION_DATABASE_SAVE_ENCRYPTION_TASK, + ACTION_DATABASE_SAVE_KEY_DERIVATION_TASK, + ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK, + ACTION_DATABASE_SAVE_PARALLELISM_TASK, + ACTION_DATABASE_SAVE_ITERATIONS_TASK -> buildDatabaseSaveElementActionTask(intent) + else -> null + } + + actionRunnable?.let { actionRunnableNotNull -> + // Assign elements for updates + mTitleId = titleId + mMessageId = messageId + mWarningId = warningId + + // Create the notification + newNotification(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, titleId)) + + // Build and launch the action + actionRunnableAsyncTask = ActionRunnableAsyncTask(this, + { + sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply { + putExtra(DATABASE_TASK_TITLE_KEY, titleId) + putExtra(DATABASE_TASK_MESSAGE_KEY, messageId) + putExtra(DATABASE_TASK_WARNING_KEY, warningId) + }) + + mActionTaskListeners.forEach { actionTaskListener -> + actionTaskListener.onStartAction(titleId, messageId, warningId) + } + + }, { result -> + mActionTaskListeners.forEach { actionTaskListener -> + actionTaskListener.onStopAction(intentAction!!, result) + } + + sendBroadcast(Intent(DATABASE_STOP_TASK_ACTION)) + + stopSelf() + } + ) + actionRunnableAsyncTask?.execute({ actionRunnableNotNull }) + } + + return START_REDELIVER_INTENT } private fun newNotification(title: Int) { val builder = buildNewNotification() - .setSmallIcon(R.drawable.ic_data_usage_white_24dp) + .setSmallIcon(R.drawable.notification_ic_database_load) .setContentTitle(getString(title)) .setAutoCancel(false) .setContentIntent(null) startForeground(notificationId, builder.build()) } - override fun onDestroy() { + override fun updateMessage(resId: Int) { + mMessageId = resId + mActionTaskListeners.forEach { actionTaskListener -> + actionTaskListener.onUpdateAction(mTitleId, mMessageId, mWarningId) + } + } + + private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? { + + if (intent.hasExtra(DATABASE_URI_KEY) + && intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY) + && intent.hasExtra(MASTER_PASSWORD_KEY) + && intent.hasExtra(KEY_FILE_CHECKED_KEY) + && intent.hasExtra(KEY_FILE_KEY) + ) { + val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) + val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY) + return CreateDatabaseRunnable(this, + Database.getInstance(), + databaseUri, + intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false), + intent.getStringExtra(MASTER_PASSWORD_KEY), + intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false), + keyFileUri, + true // TODO get readonly + ) + } else { + return null + } + } + + private fun buildDatabaseLoadActionTask(intent: Intent): ActionRunnable? { + + if (intent.hasExtra(DATABASE_URI_KEY) + && intent.hasExtra(MASTER_PASSWORD_KEY) + && intent.hasExtra(KEY_FILE_KEY) + && intent.hasExtra(READ_ONLY_KEY) + && intent.hasExtra(CIPHER_ENTITY_KEY) + && intent.hasExtra(FIX_DUPLICATE_UUID_KEY) + ) { + val database = Database.getInstance() + val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) + val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY) + val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_KEY) + val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true) + val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY) + + return LoadDatabaseRunnable( + this, + database, + databaseUri, + masterPassword, + keyFileUri, + readOnly, + cipherEntity, + PreferencesUtil.omitBackup(this), + intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false), + this + ) { result -> + // Add each info to reload database after thrown duplicate UUID exception + result.data = Bundle().apply { + putParcelable(DATABASE_URI_KEY, databaseUri) + putString(MASTER_PASSWORD_KEY, masterPassword) + putParcelable(KEY_FILE_KEY, keyFileUri) + putBoolean(READ_ONLY_KEY, readOnly) + putParcelable(CIPHER_ENTITY_KEY, cipherEntity) + } + } + } else { + return null + } + } + + private fun buildDatabaseAssignPasswordActionTask(intent: Intent): ActionRunnable? { + return if (intent.hasExtra(DATABASE_URI_KEY) + && intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY) + && intent.hasExtra(MASTER_PASSWORD_KEY) + && intent.hasExtra(KEY_FILE_CHECKED_KEY) + && intent.hasExtra(KEY_FILE_KEY) + ) { + AssignPasswordInDatabaseRunnable(this, + Database.getInstance(), + intent.getParcelableExtra(DATABASE_URI_KEY), + intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false), + intent.getStringExtra(MASTER_PASSWORD_KEY), + intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false), + intent.getParcelableExtra(KEY_FILE_KEY), + true) + } else { + null + } + } + + private inner class AfterActionNodesRunnable : AfterActionNodesFinish() { + override fun onActionNodesFinish(result: ActionRunnable.Result, + actionNodesValues: ActionNodesValues) { + val bundle = result.data ?: Bundle() + bundle.putBundle(OLD_NODES_KEY, getBundleFromListNodes(actionNodesValues.oldNodes)) + bundle.putBundle(NEW_NODES_KEY, getBundleFromListNodes(actionNodesValues.newNodes)) + result.data = bundle + } + } - notificationManager?.cancel(notificationId) + private fun buildDatabaseCreateGroupActionTask(intent: Intent): ActionRunnable? { + return if (intent.hasExtra(GROUP_KEY) + && intent.hasExtra(PARENT_ID_KEY) + && intent.hasExtra(SAVE_DATABASE_KEY) + ) { + val database = Database.getInstance() + database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { parent -> + AddGroupRunnable(this, + database, + intent.getParcelableExtra(GROUP_KEY), + parent, + intent.getBooleanExtra(SAVE_DATABASE_KEY, false), + AfterActionNodesRunnable()) + } + } else { + null + } + } - super.onDestroy() + private fun buildDatabaseUpdateGroupActionTask(intent: Intent): ActionRunnable? { + return if (intent.hasExtra(GROUP_ID_KEY) + && intent.hasExtra(GROUP_KEY) + && intent.hasExtra(SAVE_DATABASE_KEY) + ) { + val database = Database.getInstance() + database.getGroupById(intent.getParcelableExtra(GROUP_ID_KEY))?.let { oldGroup -> + val newGroup: GroupVersioned = intent.getParcelableExtra(GROUP_KEY) + UpdateGroupRunnable(this, + database, + oldGroup, + newGroup, + intent.getBooleanExtra(SAVE_DATABASE_KEY, false), + AfterActionNodesRunnable()) + } + } else { + null + } + } + + private fun buildDatabaseCreateEntryActionTask(intent: Intent): ActionRunnable? { + return if (intent.hasExtra(ENTRY_KEY) + && intent.hasExtra(PARENT_ID_KEY) + && intent.hasExtra(SAVE_DATABASE_KEY) + ) { + val database = Database.getInstance() + database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { parent -> + AddEntryRunnable(this, + database, + intent.getParcelableExtra(ENTRY_KEY), + parent, + intent.getBooleanExtra(SAVE_DATABASE_KEY, false), + AfterActionNodesRunnable()) + } + } else { + null + } + } + + private fun buildDatabaseUpdateEntryActionTask(intent: Intent): ActionRunnable? { + return if (intent.hasExtra(ENTRY_ID_KEY) + && intent.hasExtra(ENTRY_KEY) + && intent.hasExtra(SAVE_DATABASE_KEY) + ) { + val database = Database.getInstance() + database.getEntryById(intent.getParcelableExtra(ENTRY_ID_KEY))?.let { oldEntry -> + val newEntry: EntryVersioned = intent.getParcelableExtra(ENTRY_KEY) + UpdateEntryRunnable(this, + database, + oldEntry, + newEntry, + intent.getBooleanExtra(SAVE_DATABASE_KEY, false), + AfterActionNodesRunnable()) + } + } else { + null + } + } + + private fun buildDatabaseCopyNodesActionTask(intent: Intent): ActionRunnable? { + return if (intent.hasExtra(GROUPS_ID_KEY) + && intent.hasExtra(ENTRIES_ID_KEY) + && intent.hasExtra(PARENT_ID_KEY) + && intent.hasExtra(SAVE_DATABASE_KEY) + ) { + val database = Database.getInstance() + database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { newParent -> + CopyNodesRunnable(this, + database, + getListNodesFromBundle(database, intent.extras!!), + newParent, + intent.getBooleanExtra(SAVE_DATABASE_KEY, false), + AfterActionNodesRunnable()) + } + + } else { + null + } + } + + private fun buildDatabaseMoveNodesActionTask(intent: Intent): ActionRunnable? { + return if (intent.hasExtra(GROUPS_ID_KEY) + && intent.hasExtra(ENTRIES_ID_KEY) + && intent.hasExtra(PARENT_ID_KEY) + && intent.hasExtra(SAVE_DATABASE_KEY) + ) { + val database = Database.getInstance() + database.getGroupById(intent.getParcelableExtra(PARENT_ID_KEY))?.let { newParent -> + MoveNodesRunnable(this, + database, + getListNodesFromBundle(database, intent.extras!!), + newParent, + intent.getBooleanExtra(SAVE_DATABASE_KEY, false), + AfterActionNodesRunnable()) + } + + } else { + null + } + } + + private fun buildDatabaseDeleteNodesActionTask(intent: Intent): ActionRunnable? { + return if (intent.hasExtra(GROUPS_ID_KEY) + && intent.hasExtra(ENTRIES_ID_KEY) + && intent.hasExtra(SAVE_DATABASE_KEY) + ) { + val database = Database.getInstance() + DeleteNodesRunnable(this, + database, + getListNodesFromBundle(database, intent.extras!!), + intent.getBooleanExtra(SAVE_DATABASE_KEY, false), + AfterActionNodesRunnable()) + + } else { + null + } + } + + private fun buildDatabaseSaveElementActionTask(intent: Intent): ActionRunnable? { + return SaveDatabaseRunnable(this, + Database.getInstance(), + true + ).apply { + mAfterSaveDatabase = { result -> + result.data = intent.extras + } + } + } + + private class ActionRunnableAsyncTask(private val progressTaskUpdater: ProgressTaskUpdater, + private val onPreExecute: () -> Unit, + private val onPostExecute: (result: ActionRunnable.Result) -> Unit) + : AsyncTask<((ProgressTaskUpdater?) -> ActionRunnable), Void, ActionRunnable.Result>() { + + override fun onPreExecute() { + super.onPreExecute() + onPreExecute.invoke() + } + + override fun doInBackground(vararg actionRunnables: ((ProgressTaskUpdater?)-> ActionRunnable)?): ActionRunnable.Result { + var resultTask = ActionRunnable.Result(false) + // Without that, bind listeners don't work properly (I don't know why?) + Thread.sleep(500) + actionRunnables.forEach { + it?.invoke(progressTaskUpdater)?.apply { + run() + resultTask = result + } + } + return resultTask + } + + override fun onPostExecute(result: ActionRunnable.Result) { + super.onPostExecute(result) + onPostExecute.invoke(result) + } } companion object { private val TAG = DatabaseTaskNotificationService::class.java.name - const val DATABASE_TASK_TITLE_KEY = "DatabaseTaskTitle" + const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY" + const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY" + const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY" + + const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK" + const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK" + const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK" + const val ACTION_DATABASE_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK" + const val ACTION_DATABASE_UPDATE_GROUP_TASK = "ACTION_DATABASE_UPDATE_GROUP_TASK" + const val ACTION_DATABASE_CREATE_ENTRY_TASK = "ACTION_DATABASE_CREATE_ENTRY_TASK" + const val ACTION_DATABASE_UPDATE_ENTRY_TASK = "ACTION_DATABASE_UPDATE_ENTRY_TASK" + const val ACTION_DATABASE_COPY_NODES_TASK = "ACTION_DATABASE_COPY_NODES_TASK" + const val ACTION_DATABASE_MOVE_NODES_TASK = "ACTION_DATABASE_MOVE_NODES_TASK" + const val ACTION_DATABASE_DELETE_NODES_TASK = "ACTION_DATABASE_DELETE_NODES_TASK" + const val ACTION_DATABASE_SAVE_NAME_TASK = "ACTION_DATABASE_SAVE_NAME_TASK" + const val ACTION_DATABASE_SAVE_DESCRIPTION_TASK = "ACTION_DATABASE_SAVE_DESCRIPTION_TASK" + const val ACTION_DATABASE_SAVE_DEFAULT_USERNAME_TASK = "ACTION_DATABASE_SAVE_DEFAULT_USERNAME_TASK" + const val ACTION_DATABASE_SAVE_COLOR_TASK = "ACTION_DATABASE_SAVE_COLOR_TASK" + const val ACTION_DATABASE_SAVE_COMPRESSION_TASK = "ACTION_DATABASE_SAVE_COMPRESSION_TASK" + const val ACTION_DATABASE_SAVE_MAX_HISTORY_ITEMS_TASK = "ACTION_DATABASE_SAVE_MAX_HISTORY_ITEMS_TASK" + const val ACTION_DATABASE_SAVE_MAX_HISTORY_SIZE_TASK = "ACTION_DATABASE_SAVE_MAX_HISTORY_SIZE_TASK" + const val ACTION_DATABASE_SAVE_ENCRYPTION_TASK = "ACTION_DATABASE_SAVE_ENCRYPTION_TASK" + const val ACTION_DATABASE_SAVE_KEY_DERIVATION_TASK = "ACTION_DATABASE_SAVE_KEY_DERIVATION_TASK" + const val ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK = "ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK" + const val ACTION_DATABASE_SAVE_PARALLELISM_TASK = "ACTION_DATABASE_SAVE_PARALLELISM_TASK" + const val ACTION_DATABASE_SAVE_ITERATIONS_TASK = "ACTION_DATABASE_SAVE_ITERATIONS_TASK" + + const val DATABASE_URI_KEY = "DATABASE_URI_KEY" + const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY" + const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY" + const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY" + const val KEY_FILE_KEY = "KEY_FILE_KEY" + const val READ_ONLY_KEY = "READ_ONLY_KEY" + const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY" + const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY" + const val GROUP_KEY = "GROUP_KEY" + const val ENTRY_KEY = "ENTRY_KEY" + const val GROUP_ID_KEY = "GROUP_ID_KEY" + const val ENTRY_ID_KEY = "ENTRY_ID_KEY" + const val GROUPS_ID_KEY = "GROUPS_ID_KEY" + const val ENTRIES_ID_KEY = "ENTRIES_ID_KEY" + const val PARENT_ID_KEY = "PARENT_ID_KEY" + const val SAVE_DATABASE_KEY = "SAVE_DATABASE_KEY" + const val OLD_NODES_KEY = "OLD_NODES_KEY" + const val NEW_NODES_KEY = "NEW_NODES_KEY" + const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time + const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time + + fun getListNodesFromBundle(database: Database, bundle: Bundle): List { + val nodesAction = ArrayList() + bundle.getParcelableArrayList>(GROUPS_ID_KEY)?.forEach { + database.getGroupById(it)?.let { groupRetrieve -> + nodesAction.add(groupRetrieve) + } + } + bundle.getParcelableArrayList>(ENTRIES_ID_KEY)?.forEach { + database.getEntryById(it)?.let { entryRetrieve -> + nodesAction.add(entryRetrieve) + } + } + return nodesAction + } + + fun getBundleFromListNodes(nodes: List): Bundle { + val groupsIdToCopy = ArrayList>() + val entriesIdToCopy = ArrayList>() + nodes.forEach { nodeVersioned -> + when (nodeVersioned.type) { + Type.GROUP -> { + (nodeVersioned as GroupVersioned).nodeId?.let { groupId -> + groupsIdToCopy.add(groupId) + } + } + Type.ENTRY -> { + entriesIdToCopy.add((nodeVersioned as EntryVersioned).nodeId) + } + } + } + return Bundle().apply { + putParcelableArrayList(GROUPS_ID_KEY, groupsIdToCopy) + putParcelableArrayList(ENTRIES_ID_KEY, entriesIdToCopy) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/KeyboardEntryNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/KeyboardEntryNotificationService.kt index c7d1c066a..ec4d87b03 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/KeyboardEntryNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/notifications/KeyboardEntryNotificationService.kt @@ -1,12 +1,10 @@ package com.kunzisoft.keepass.notifications import android.app.PendingIntent -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter -import androidx.preference.PreferenceManager import android.util.Log +import androidx.preference.PreferenceManager import com.kunzisoft.keepass.R import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.model.EntryInfo @@ -14,46 +12,29 @@ import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.LOCK_ACTION -class KeyboardEntryNotificationService : NotificationService() { +class KeyboardEntryNotificationService : LockNotificationService() { - private val notificationId = 486 + override val notificationId = 486 private var cleanNotificationTimerTask: Thread? = null private var notificationTimeoutMilliSecs: Long = 0 - private var lockBroadcastReceiver: BroadcastReceiver? = null private var pendingDeleteIntent: PendingIntent? = null - override fun onCreate() { - super.onCreate() - - // Register a lock receiver to stop notification service when lock on keyboard is performed - lockBroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - // Stop the service in all cases - stopSelf() - } - } - registerReceiver(lockBroadcastReceiver, - IntentFilter().apply { - addAction(LOCK_ACTION) - } - ) - } - private fun stopNotificationAndSendLockIfNeeded() { - // Remove the entry from the keyboard - MagikIME.removeEntry(this) // Clear the entry if define in preferences - val sharedPreferences = android.preference.PreferenceManager.getDefaultSharedPreferences(this) - if (sharedPreferences.getBoolean(getString(R.string.keyboard_notification_entry_clear_close_key), - resources.getBoolean(R.bool.keyboard_notification_entry_clear_close_default))) { + if (PreferencesUtil.isClearKeyboardNotificationEnable(this)) { sendBroadcast(Intent(LOCK_ACTION)) } - // Stop the notification + // Stop the service stopSelf() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + //Get settings + notificationTimeoutMilliSecs = PreferenceManager.getDefaultSharedPreferences(this) + .getString(getString(R.string.keyboard_entry_timeout_key), + getString(R.string.timeout_default))?.toLong() ?: TimeoutHelper.DEFAULT_TIMEOUT + when { intent == null -> Log.w(TAG, "null intent") ACTION_CLEAN_KEYBOARD_ENTRY == intent.action -> { @@ -84,7 +65,7 @@ class KeyboardEntryNotificationService : NotificationService() { pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT) val builder = buildNewNotification() - .setSmallIcon(R.drawable.ic_keyboard_key_white_24dp) + .setSmallIcon(R.drawable.notification_ic_keyboard_key_24dp) .setContentTitle(getString(R.string.keyboard_notification_entry_content_title, entryTitle)) .setContentText(getString(R.string.keyboard_notification_entry_content_text, entryUsername)) .setAutoCancel(false) @@ -94,22 +75,9 @@ class KeyboardEntryNotificationService : NotificationService() { notificationManager?.cancel(notificationId) notificationManager?.notify(notificationId, builder.build()) - stopTask(cleanNotificationTimerTask) // Timeout only if notification clear is available - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) - if (sharedPreferences.getBoolean(getString(R.string.keyboard_notification_entry_clear_close_key), - resources.getBoolean(R.bool.keyboard_notification_entry_clear_close_default))) { - val keyboardTimeout = sharedPreferences.getString(getString(R.string.keyboard_entry_timeout_key), - getString(R.string.timeout_default)) - notificationTimeoutMilliSecs = try { - keyboardTimeout?.let { - java.lang.Long.parseLong(keyboardTimeout) - } ?: 0 - } catch (e: NumberFormatException) { - TimeoutHelper.DEFAULT_TIMEOUT - } - + if (PreferencesUtil.isClearKeyboardNotificationEnable(this)) { if (notificationTimeoutMilliSecs != TimeoutHelper.NEVER) { cleanNotificationTimerTask = Thread { val maxPos = 100 @@ -132,23 +100,19 @@ class KeyboardEntryNotificationService : NotificationService() { } } - private fun stopTask(task: Thread?) { - if (task != null && task.isAlive) - task.interrupt() - } - - private fun destroyKeyboardNotification() { - stopTask(cleanNotificationTimerTask) - cleanNotificationTimerTask = null - unregisterReceiver(lockBroadcastReceiver) - pendingDeleteIntent?.cancel() + override fun onTaskRemoved(rootIntent: Intent?) { + MagikIME.removeEntry(this) - notificationManager?.cancel(notificationId) + super.onTaskRemoved(rootIntent) } override fun onDestroy() { + // Remove the entry from the keyboard + MagikIME.removeEntry(this) - destroyKeyboardNotification() + stopTask(cleanNotificationTimerTask) + cleanNotificationTimerTask = null + pendingDeleteIntent?.cancel() super.onDestroy() } @@ -162,12 +126,26 @@ class KeyboardEntryNotificationService : NotificationService() { const val ACTION_CLEAN_KEYBOARD_ENTRY = "ACTION_CLEAN_KEYBOARD_ENTRY" fun launchNotificationIfAllowed(context: Context, entry: EntryInfo) { + + val containsUsernameToCopy = entry.username.isNotEmpty() + val containsPasswordToCopy = entry.password.isNotEmpty() + val containsExtraFieldToCopy = entry.customFields.isNotEmpty() + + var startService = false + val intent = Intent(context, KeyboardEntryNotificationService::class.java) + // Show the notification if allowed in Preferences if (PreferencesUtil.isKeyboardNotificationEntryEnable(context)) { - context.startService(Intent(context, KeyboardEntryNotificationService::class.java).apply { - putExtra(ENTRY_INFO_KEY, entry) - }) + if (containsUsernameToCopy || containsPasswordToCopy || containsExtraFieldToCopy) { + startService = true + context.startService(intent.apply { + putExtra(ENTRY_INFO_KEY, entry) + }) + } } + + if (!startService) + context.stopService(intent) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/LockNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/LockNotificationService.kt new file mode 100644 index 000000000..99f6fdae3 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/notifications/LockNotificationService.kt @@ -0,0 +1,47 @@ +package com.kunzisoft.keepass.notifications + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import com.kunzisoft.keepass.utils.LOCK_ACTION + +abstract class LockNotificationService : NotificationService() { + + private var lockBroadcastReceiver: BroadcastReceiver? = null + + override fun onCreate() { + super.onCreate() + + // Register a lock receiver to stop notification service when lock on keyboard is performed + lockBroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + // Stop the service in all cases + stopSelf() + } + } + registerReceiver(lockBroadcastReceiver, + IntentFilter().apply { + addAction(LOCK_ACTION) + } + ) + } + + protected fun stopTask(task: Thread?) { + if (task != null && task.isAlive) + task.interrupt() + } + + override fun onTaskRemoved(rootIntent: Intent?) { + notificationManager?.cancel(notificationId) + + super.onTaskRemoved(rootIntent) + } + + override fun onDestroy() { + + unregisterReceiver(lockBroadcastReceiver) + + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationService.kt index c0fb5904f..a3a1c6e31 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationService.kt @@ -3,21 +3,23 @@ package com.kunzisoft.keepass.notifications import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service -import android.content.Context import android.content.Intent import android.os.Build import android.os.IBinder -import androidx.core.app.NotificationCompat import android.util.TypedValue +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.stylish.Stylish abstract class NotificationService : Service() { - protected var notificationManager: NotificationManager? = null + protected var notificationManager: NotificationManagerCompat? = null private var colorNotificationAccent: Int = 0 + protected abstract val notificationId: Int + override fun onBind(intent: Intent): IBinder? { return null } @@ -25,17 +27,19 @@ abstract class NotificationService : Service() { override fun onCreate() { super.onCreate() - notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager = NotificationManagerCompat.from(this) // Create notification channel for Oreo+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel(CHANNEL_ID_KEEPASS, - CHANNEL_NAME_KEEPASS, - NotificationManager.IMPORTANCE_DEFAULT).apply { - enableVibration(false) - setSound(null, null) + if (notificationManager?.getNotificationChannel(CHANNEL_ID_KEEPASS) == null) { + val channel = NotificationChannel(CHANNEL_ID_KEEPASS, + CHANNEL_NAME_KEEPASS, + NotificationManager.IMPORTANCE_DEFAULT).apply { + enableVibration(false) + setSound(null, null) + } + notificationManager?.createNotificationChannel(channel) } - notificationManager?.createNotificationChannel(channel) } // Get the color @@ -49,14 +53,29 @@ abstract class NotificationService : Service() { protected fun buildNewNotification(): NotificationCompat.Builder { return NotificationCompat.Builder(this, CHANNEL_ID_KEEPASS) .setColor(colorNotificationAccent) - .setGroup(GROUP_KEEPASS) + //TODO .setGroup(GROUP_KEEPASS) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setVisibility(NotificationCompat.VISIBILITY_SECRET) } + // TODO only for > lollipop + protected fun buildSummaryNotification(): NotificationCompat.Builder { + return buildNewNotification().apply { + // TODO Ic setSmallIcon(R.drawable.notification_ic_data_usage_24dp) + setGroupSummary(true) + } + } + + override fun onDestroy() { + notificationManager?.cancel(notificationId) + + super.onDestroy() + } + companion object { const val CHANNEL_ID_KEEPASS = "com.kunzisoft.keepass.notification.channel" const val CHANNEL_NAME_KEEPASS = "KeePass DX notification" const val GROUP_KEEPASS = "GROUP_KEEPASS" + const val SUMMARY_ID = 0 } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt b/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt new file mode 100644 index 000000000..ce9393d7e --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt @@ -0,0 +1,221 @@ +package com.kunzisoft.keepass.otp + +import com.kunzisoft.keepass.model.OtpModel +import org.apache.commons.codec.binary.Base32 +import org.apache.commons.codec.binary.Base64 +import org.apache.commons.codec.binary.Hex +import java.nio.charset.Charset +import java.util.* +import java.util.regex.Pattern + +data class OtpElement(var otpModel: OtpModel = OtpModel()) { + + var type + get() = otpModel.type + set(value) { + otpModel.type = value + if (type == OtpType.HOTP) { + if (!OtpTokenType.getHotpTokenTypeValues().contains(tokenType)) + tokenType = OtpTokenType.RFC4226 + } + if (type == OtpType.TOTP) { + if (!OtpTokenType.getTotpTokenTypeValues().contains(tokenType)) + tokenType = OtpTokenType.RFC6238 + } + } + + var tokenType + get() = otpModel.tokenType + set(value) { + otpModel.tokenType = value + when (tokenType) { + OtpTokenType.RFC4226 -> { + otpModel.algorithm = TokenCalculator.OTP_DEFAULT_ALGORITHM + otpModel.digits = TokenCalculator.OTP_DEFAULT_DIGITS + otpModel.counter = TokenCalculator.HOTP_INITIAL_COUNTER + } + OtpTokenType.RFC6238 -> { + otpModel.algorithm = TokenCalculator.OTP_DEFAULT_ALGORITHM + otpModel.digits = TokenCalculator.OTP_DEFAULT_DIGITS + otpModel.period = TokenCalculator.TOTP_DEFAULT_PERIOD + } + OtpTokenType.STEAM -> { + otpModel.algorithm = TokenCalculator.OTP_DEFAULT_ALGORITHM + otpModel.digits = TokenCalculator.STEAM_DEFAULT_DIGITS + otpModel.period = TokenCalculator.TOTP_DEFAULT_PERIOD + } + } + } + + var name + get() = otpModel.name + set(value) { + otpModel.name = value + } + + var issuer + get() = otpModel.issuer + set(value) { + otpModel.issuer = value + } + + var secret + get() = otpModel.secret + private set(value) { + otpModel.secret = value + } + + var counter + get() = otpModel.counter + @Throws(NumberFormatException::class) + set(value) { + otpModel.counter = if (value < MIN_HOTP_COUNTER || value > MAX_HOTP_COUNTER) { + TokenCalculator.HOTP_INITIAL_COUNTER + throw IllegalArgumentException() + } else value + } + + var period + get() = otpModel.period + @Throws(NumberFormatException::class) + set(value) { + otpModel.period = if (value < MIN_TOTP_PERIOD || value > MAX_TOTP_PERIOD) { + TokenCalculator.TOTP_DEFAULT_PERIOD + throw NumberFormatException() + } else value + } + + var digits + get() = otpModel.digits + @Throws(NumberFormatException::class) + set(value) { + otpModel.digits = if (value < MIN_OTP_DIGITS|| value > MAX_OTP_DIGITS) { + TokenCalculator.OTP_DEFAULT_DIGITS + throw NumberFormatException() + } else value + } + + var algorithm + get() = otpModel.algorithm + set(value) { + otpModel.algorithm = value + } + + @Throws(IllegalArgumentException::class) + fun setUTF8Secret(secret: String) { + if (secret.isNotEmpty()) + otpModel.secret = secret.toByteArray(Charset.forName("UTF-8")) + else + throw IllegalArgumentException() + } + + @Throws(IllegalArgumentException::class) + fun setHexSecret(secret: String) { + if (secret.isNotEmpty()) + otpModel.secret = Hex.decodeHex(secret) + else + throw IllegalArgumentException() + } + + fun getBase32Secret(): String { + return otpModel.secret?.let { + Base32().encodeAsString(it) + } ?: "" + } + + @Throws(IllegalArgumentException::class) + fun setBase32Secret(secret: String) { + if (secret.isNotEmpty() && checkBase32Secret(secret)) + otpModel.secret = Base32().decode(secret.toByteArray()) + else + throw IllegalArgumentException() + } + + @Throws(IllegalArgumentException::class) + fun setBase64Secret(secret: String) { + if (secret.isNotEmpty() && checkBase64Secret(secret)) + otpModel.secret = Base64().decode(secret.toByteArray()) + else + throw IllegalArgumentException() + } + + val token: String + get() { + if (secret == null) + return "" + return when (type) { + OtpType.HOTP -> TokenCalculator.HOTP(secret, counter, digits, algorithm) + OtpType.TOTP -> when (tokenType) { + OtpTokenType.STEAM -> TokenCalculator.TOTP_Steam(secret, period, digits, algorithm) + else -> TokenCalculator.TOTP_RFC6238(secret, period, digits, algorithm) + } + } + } + + val secondsRemaining: Int + get() = otpModel.period - (System.currentTimeMillis() / 1000 % otpModel.period).toInt() + + fun shouldRefreshToken(): Boolean { + return secondsRemaining == otpModel.period + } + + companion object { + const val MIN_HOTP_COUNTER = 1 + const val MAX_HOTP_COUNTER = Long.MAX_VALUE + + const val MIN_TOTP_PERIOD = 1 + const val MAX_TOTP_PERIOD = 60 + + const val MIN_OTP_DIGITS = 4 + const val MAX_OTP_DIGITS = 18 + + fun checkBase32Secret(secret: String): Boolean { + return (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$", secret)) + } + + fun checkBase64Secret(secret: String): Boolean { + return (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", secret)) + } + } +} + +enum class OtpType { + HOTP, // counter based + TOTP; // time based +} + +enum class OtpTokenType { + RFC4226, // HOTP + RFC6238, // TOTP + + // Proprietary + STEAM; // TOTP Steam + + override fun toString(): String { + return when (this) { + STEAM -> "steam" + else -> super.toString() + } + } + + companion object { + fun getFromString(tokenType: String): OtpTokenType { + return when (tokenType.toLowerCase(Locale.ENGLISH)) { + "s", "steam" -> STEAM + "hotp" -> RFC4226 + else -> RFC6238 + } + } + + fun getTotpTokenTypeValues(getProprietaryElements: Boolean = true): Array { + return if (getProprietaryElements) + arrayOf(RFC6238, STEAM) + else + arrayOf(RFC6238) + } + + fun getHotpTokenTypeValues(): Array { + return arrayOf(RFC4226) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt b/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt new file mode 100644 index 000000000..6af0f1028 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt @@ -0,0 +1,386 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with KeePass DX. If not, + * see . + * + * This code is based on KeePassXC code + * https://github.com/keepassxreboot/keepassxc/blob/master/src/totp/totp.cpp + * https://github.com/keepassxreboot/keepassxc/blob/master/src/core/Entry.cpp + */ +package com.kunzisoft.keepass.otp + +import android.net.Uri +import android.util.Log +import com.kunzisoft.keepass.database.element.security.ProtectedString +import com.kunzisoft.keepass.model.Field +import com.kunzisoft.keepass.otp.TokenCalculator.* +import java.lang.Exception +import java.lang.StringBuilder +import java.net.URLEncoder +import java.util.* +import java.util.regex.Pattern + +object OtpEntryFields { + + private val TAG = OtpEntryFields::class.java.name + + // Field from KeePassXC + private const val OTP_FIELD = "otp" + + // URL parameters (https://github.com/google/google-authenticator/wiki/Key-Uri-Format) + private const val OTP_SCHEME = "otpauth" + private const val TOTP_AUTHORITY = "totp" // time-based + private const val HOTP_AUTHORITY = "hotp" // counter-based + private const val ALGORITHM_URL_PARAM = "algorithm" + private const val ISSUER_URL_PARAM = "issuer" + private const val SECRET_URL_PARAM = "secret" + private const val DIGITS_URL_PARAM = "digits" + private const val PERIOD_URL_PARAM = "period" + private const val ENCODER_URL_PARAM = "encoder" + private const val COUNTER_URL_PARAM = "counter" + + // Key-values (maybe from plugin or old KeePassXC) + private const val SEED_KEY = "key" + private const val DIGITS_KEY = "size" + private const val STEP_KEY = "step" + + // HmacOtp KeePass2 values (https://keepass.info/help/base/placeholders.html#hmacotp) + private const val HMACOTP_SECRET_FIELD = "HmacOtp-Secret" + private const val HMACOTP_SECRET_HEX_FIELD = "HmacOtp-Secret-Hex" + private const val HMACOTP_SECRET_BASE32_FIELD = "HmacOtp-Secret-Base32" + private const val HMACOTP_SECRET_BASE64_FIELD = "HmacOtp-Secret-Base64" + private const val HMACOTP_SECRET_COUNTER_FIELD = "HmacOtp-Counter" + + // Custom fields (maybe from plugin) + private const val TOTP_SEED_FIELD = "TOTP Seed" + private const val TOTP_SETTING_FIELD = "TOTP Settings" + + // Token field, use dynamically to generate OTP token + const val OTP_TOKEN_FIELD = "OTP Token" + + // Logical breakdown of key=value regex. the final string is as follows: + // [^&=\s]+=[^&=\s]+(&[^&=\s]+=[^&=\s]+)* + private const val validKeyValue = "[^&=\\s]+" + private const val validKeyValuePair = "$validKeyValue=$validKeyValue" + private const val validKeyValueRegex = "$validKeyValuePair&($validKeyValuePair)*" + + /** + * Parse fields of an entry to retrieve an OtpElement + */ + fun parseFields(getField: (id: String) -> String?): OtpElement? { + val otpElement = OtpElement() + // OTP (HOTP/TOTP) from URL and field from KeePassXC + if (parseOTPUri(getField, otpElement)) + return otpElement + // TOTP from key values (maybe plugin or old KeePassXC) + if (parseTOTPKeyValues(getField, otpElement)) + return otpElement + // TOTP from custom field + if (parseTOTPFromField(getField, otpElement)) + return otpElement + // HOTP fields from KeePass 2 + if (parseHOTPFromField(getField, otpElement)) + return otpElement + + return null + } + + /** + * Parses a secret value from a URI. The format will be: + * + * otpauth://totp/user@example.com?secret=FFF... + * + * otpauth://hotp/user@example.com?secret=FFF...&counter=123 + */ + private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { + val otpPlainText = getField(OTP_FIELD) + if (otpPlainText != null && otpPlainText.isNotEmpty()) { + val uri = Uri.parse(replaceChars(otpPlainText)) + + if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) { + Log.e(TAG, "Invalid or missing scheme in uri") + return false + } + + val authority = uri.authority + if (TOTP_AUTHORITY == authority) { + otpElement.type = OtpType.TOTP + + } else if (HOTP_AUTHORITY == authority) { + otpElement.type = OtpType.HOTP + + val counterParameter = uri.getQueryParameter(COUNTER_URL_PARAM) + if (counterParameter != null) { + try { + otpElement.counter = counterParameter.toLongOrNull() ?: HOTP_INITIAL_COUNTER + } catch (e: NumberFormatException) { + Log.e(TAG, "Invalid counter in uri") + return false + } + } + + } else { + Log.e(TAG, "Invalid or missing authority in uri") + return false + } + + val nameParam = validateAndGetNameInPath(uri.path) + if (nameParam != null && nameParam.isNotEmpty()) + otpElement.name = nameParam + + val issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM) + if (issuerParam != null && issuerParam.isNotEmpty()) + otpElement.issuer = issuerParam + + val secretParam = uri.getQueryParameter(SECRET_URL_PARAM) + if (secretParam != null && secretParam.isNotEmpty()) { + try { + otpElement.setBase32Secret(secretParam) + } catch (exception: Exception) { + Log.e(TAG, "Unable to retrieve OTP secret.", exception) + } + } + + val encoderParam = uri.getQueryParameter(ENCODER_URL_PARAM) + if (encoderParam != null && encoderParam.isNotEmpty()) + otpElement.tokenType = OtpTokenType.getFromString(encoderParam) + + val digitsParam = uri.getQueryParameter(DIGITS_URL_PARAM) + if (digitsParam != null && digitsParam.isNotEmpty()) + try { + otpElement.digits = digitsParam.toIntOrNull() ?: OTP_DEFAULT_DIGITS + } catch (exception: Exception) { + Log.e(TAG, "Unable to retrieve OTP digits.", exception) + otpElement.digits = OTP_DEFAULT_DIGITS + } + + val counterParam = uri.getQueryParameter(COUNTER_URL_PARAM) + if (counterParam != null && counterParam.isNotEmpty()) + try { + otpElement.counter = counterParam.toLongOrNull() ?: HOTP_INITIAL_COUNTER + } catch (exception: Exception) { + Log.e(TAG, "Unable to retrieve HOTP counter.", exception) + otpElement.counter = HOTP_INITIAL_COUNTER + } + + val stepParam = uri.getQueryParameter(PERIOD_URL_PARAM) + if (stepParam != null && stepParam.isNotEmpty()) + try { + otpElement.period = stepParam.toIntOrNull() ?: TOTP_DEFAULT_PERIOD + } catch (exception: Exception) { + Log.e(TAG, "Unable to retrieve TOTP period.", exception) + otpElement.period = TOTP_DEFAULT_PERIOD + } + + val algorithmParam = uri.getQueryParameter(ALGORITHM_URL_PARAM) + if (algorithmParam != null && algorithmParam.isNotEmpty()) { + otpElement.algorithm = HashAlgorithm.fromString(algorithmParam) + } + + return true + } + return false + } + + private fun buildOtpUri(otpElement: OtpElement, title: String?, username: String?): Uri { + val counterOrPeriodLabel: String + val counterOrPeriodValue: String + val otpAuthority: String + + when (otpElement.type) { + OtpType.TOTP -> { + counterOrPeriodLabel = PERIOD_URL_PARAM + counterOrPeriodValue = otpElement.period.toString() + otpAuthority = TOTP_AUTHORITY + } + else -> { + counterOrPeriodLabel = COUNTER_URL_PARAM + counterOrPeriodValue = otpElement.counter.toString() + otpAuthority = HOTP_AUTHORITY + } + } + val issuer = + if (title != null && title.isNotEmpty()) + replaceCharsForUrl(title) + else + replaceCharsForUrl(otpElement.issuer) + val accountName = + if (username != null && username.isNotEmpty()) + replaceCharsForUrl(username) + else + replaceCharsForUrl(otpElement.name) + val uriString = StringBuilder("otpauth://$otpAuthority/$issuer:$accountName" + + "?$SECRET_URL_PARAM=${otpElement.getBase32Secret()}" + + "&$counterOrPeriodLabel=$counterOrPeriodValue" + + "&$DIGITS_URL_PARAM=${otpElement.digits}" + + "&$ISSUER_URL_PARAM=$issuer") + if (otpElement.tokenType == OtpTokenType.STEAM) { + uriString.append("&$ENCODER_URL_PARAM=${otpElement.tokenType}") + } else { + uriString.append("&$ALGORITHM_URL_PARAM=${otpElement.algorithm}") + } + + return Uri.parse(uriString.toString()) + } + + private fun replaceCharsForUrl(parameter: String): String { + return URLEncoder.encode(replaceChars(parameter), "UTF-8") + } + + private fun replaceChars(parameter: String): String { + return parameter.replace("([\\r|\\n|\\t|\\s|\\u00A0]+)", "") + } + + private fun parseTOTPKeyValues(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { + val plainText = getField(OTP_FIELD) + if (plainText != null && plainText.isNotEmpty()) { + if (Pattern.matches(validKeyValueRegex, plainText)) { + try { + // KeeOtp string format + val query = breakDownKeyValuePairs(plainText) + + var secretString = query[SEED_KEY] + if (secretString == null) + secretString = "" + otpElement.setBase32Secret(secretString) + otpElement.digits = query[DIGITS_KEY]?.toIntOrNull() ?: OTP_DEFAULT_DIGITS + otpElement.period = query[STEP_KEY]?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD + + otpElement.type = OtpType.TOTP + return true + } catch (exception: Exception) { + return false + } + } else { + // Malformed + return false + } + } + return false + } + + private fun parseTOTPFromField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { + val seedField = getField(TOTP_SEED_FIELD) ?: return false + try { + otpElement.setBase32Secret(seedField) + + val settingsField = getField(TOTP_SETTING_FIELD) + if (settingsField != null) { + // Regex match, sync with shortNameToEncoder + val pattern = Pattern.compile("(\\d+);((?:\\d+)|S)") + val matcher = pattern.matcher(settingsField) + if (!matcher.matches()) { + // malformed + return false + } + otpElement.period = matcher.group(1).toIntOrNull() ?: TOTP_DEFAULT_PERIOD + otpElement.tokenType = OtpTokenType.getFromString(matcher.group(2)) + } + } catch (exception: Exception) { + return false + } + otpElement.type = OtpType.TOTP + return true + } + + private fun parseHOTPFromField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { + val secretField = getField(HMACOTP_SECRET_FIELD) + val secretHexField = getField(HMACOTP_SECRET_HEX_FIELD) + val secretBase32Field = getField(HMACOTP_SECRET_BASE32_FIELD) + val secretBase64Field = getField(HMACOTP_SECRET_BASE64_FIELD) + try { + when { + secretField != null -> otpElement.setUTF8Secret(secretField) + secretHexField != null -> otpElement.setHexSecret(secretHexField) + secretBase32Field != null -> otpElement.setBase32Secret(secretBase32Field) + secretBase64Field != null -> otpElement.setBase64Secret(secretBase64Field) + else -> return false + } + + val secretCounterField = getField(HMACOTP_SECRET_COUNTER_FIELD) + if (secretCounterField != null) { + otpElement.counter = secretCounterField.toLongOrNull() ?: HOTP_INITIAL_COUNTER + } + } catch (exception: Exception) { + return false + } + + otpElement.type = OtpType.HOTP + return true + } + + private fun validateAndGetNameInPath(path: String?): String? { + if (path == null || !path.startsWith("/")) { + return null + } + // path is "/name", so remove leading "/", and trailing white spaces + val name = path.substring(1).trim { it <= ' ' } + return if (name.isEmpty()) { + null // only white spaces. + } else name + } + + private fun breakDownKeyValuePairs(pairs: String): HashMap { + val elements = pairs.split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val output = HashMap() + for (element in elements) { + val pair = element.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + output[pair[0]] = pair[1] + } + return output + } + + /** + * Build Otp field from an OtpElement + */ + fun buildOtpField(otpElement: OtpElement, title: String?, username: String?): Field { + return Field(OTP_FIELD, ProtectedString(true, + buildOtpUri(otpElement, title, username).toString())) + } + + /** + * Build new generated fields in a new list from [fieldsToParse] in parameter, + * Remove parameters fields use to generate auto fields + */ + fun generateAutoFields(fieldsToParse: MutableList): MutableList { + val newCustomFields: MutableList = ArrayList(fieldsToParse) + // Remove parameter fields + val otpField = Field(OTP_FIELD) + val totpSeedField = Field(TOTP_SEED_FIELD) + val totpSettingField = Field(TOTP_SETTING_FIELD) + val hmacOtpSecretField = Field(HMACOTP_SECRET_FIELD) + val hmacOtpSecretHewField = Field(HMACOTP_SECRET_HEX_FIELD) + val hmacOtpSecretBase32Field = Field(HMACOTP_SECRET_BASE32_FIELD) + val hmacOtpSecretBase64Field = Field(HMACOTP_SECRET_BASE64_FIELD) + val hmacOtpSecretCounterField = Field(HMACOTP_SECRET_COUNTER_FIELD) + newCustomFields.remove(otpField) + newCustomFields.remove(totpSeedField) + newCustomFields.remove(totpSettingField) + newCustomFields.remove(hmacOtpSecretField) + newCustomFields.remove(hmacOtpSecretHewField) + newCustomFields.remove(hmacOtpSecretBase32Field) + newCustomFields.remove(hmacOtpSecretBase64Field) + newCustomFields.remove(hmacOtpSecretCounterField) + // Empty auto generated OTP Token field + if (fieldsToParse.contains(otpField) + || fieldsToParse.contains(totpSeedField) + || fieldsToParse.contains(hmacOtpSecretField) + || fieldsToParse.contains(hmacOtpSecretHewField) + || fieldsToParse.contains(hmacOtpSecretBase32Field) + || fieldsToParse.contains(hmacOtpSecretBase64Field) + ) + newCustomFields.add(Field(OTP_TOKEN_FIELD)) + return newCustomFields + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/otp/TokenCalculator.java b/app/src/main/java/com/kunzisoft/keepass/otp/TokenCalculator.java new file mode 100644 index 000000000..a790710b0 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/otp/TokenCalculator.java @@ -0,0 +1,132 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with KeePass DX. If not, + * see . + * + * This code is based on andOTP code + * https://github.com/andOTP/andOTP/blob/master/app/src/main/java/org/shadowice/flocke/andotp/ + * Utilities/TokenCalculator.java + */ +package com.kunzisoft.keepass.otp; + +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.text.NumberFormat; +import java.util.Locale; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +public class TokenCalculator { + public static final int TOTP_DEFAULT_PERIOD = 30; + public static final long HOTP_INITIAL_COUNTER = 1; + public static final int OTP_DEFAULT_DIGITS = 6; + public static final int STEAM_DEFAULT_DIGITS = 5; + public static final HashAlgorithm OTP_DEFAULT_ALGORITHM = HashAlgorithm.SHA1; + + private static final char[] STEAMCHARS = new char[] { + '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', + 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', + 'R', 'T', 'V', 'W', 'X', 'Y' + }; + + public enum HashAlgorithm { + SHA1, SHA256, SHA512; + + static HashAlgorithm fromString(String hashString) { + String hash = hashString.replace("[^a-zA-Z0-9]", "").toUpperCase(); + try { + return valueOf(hash); + } catch (Exception e) { + return OTP_DEFAULT_ALGORITHM; + } + } + } + + private static byte[] generateHash(HashAlgorithm algorithm, byte[] key, byte[] data) + throws NoSuchAlgorithmException, InvalidKeyException { + String algo = "Hmac" + algorithm.toString(); + + Mac mac = Mac.getInstance(algo); + mac.init(new SecretKeySpec(key, algo)); + + return mac.doFinal(data); + } + + public static int TOTP_RFC6238(byte[] secret, int period, long time, int digits, HashAlgorithm algorithm) { + int fullToken = TOTP(secret, period, time, algorithm); + int div = (int) Math.pow(10, digits); + + return fullToken % div; + } + + public static String TOTP_RFC6238(byte[] secret, int period, int digits, HashAlgorithm algorithm) { + return formatTokenString(TOTP_RFC6238(secret, period, System.currentTimeMillis() / 1000, digits, algorithm), digits); + } + + public static String TOTP_Steam(byte[] secret, int period, int digits, HashAlgorithm algorithm) { + int fullToken = TOTP(secret, period, System.currentTimeMillis() / 1000, algorithm); + + StringBuilder tokenBuilder = new StringBuilder(); + + for (int i = 0; i < digits; i++) { + tokenBuilder.append(STEAMCHARS[fullToken % STEAMCHARS.length]); + fullToken /= STEAMCHARS.length; + } + + return tokenBuilder.toString(); + } + + public static String HOTP(byte[] secret, long counter, int digits, HashAlgorithm algorithm) { + int fullToken = HOTP(secret, counter, algorithm); + int div = (int) Math.pow(10, digits); + + return formatTokenString(fullToken % div, digits); + } + + private static int TOTP(byte[] key, int period, long time, HashAlgorithm algorithm) { + return HOTP(key, time / period, algorithm); + } + + private static int HOTP(byte[] key, long counter, HashAlgorithm algorithm) { + int r = 0; + + try { + byte[] data = ByteBuffer.allocate(8).putLong(counter).array(); + byte[] hash = generateHash(algorithm, key, data); + + int offset = hash[hash.length - 1] & 0xF; + + int binary = (hash[offset] & 0x7F) << 0x18; + binary |= (hash[offset + 1] & 0xFF) << 0x10; + binary |= (hash[offset + 2] & 0xFF) << 0x08; + binary |= (hash[offset + 3] & 0xFF); + + r = binary; + } catch (Exception e) { + e.printStackTrace(); + } + + return r; + } + + public static String formatTokenString(int token, int digits) { + NumberFormat numberFormat = NumberFormat.getInstance(Locale.ENGLISH); + numberFormat.setMinimumIntegerDigits(digits); + numberFormat.setGroupingUsed(false); + + return numberFormat.format(token); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt index 4c76edd0d..18ee9eae9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt @@ -83,11 +83,16 @@ class MainPreferenceFragment : PreferenceFragmentCompat() { } } - findPreference(getString(R.string.settings_database_change_credentials_key))?.apply { + findPreference(getString(R.string.settings_database_security_key))?.apply { onPreferenceClickListener = Preference.OnPreferenceClickListener { - fragmentManager?.let { fragmentManager -> - AssignMasterKeyDialogFragment().show(fragmentManager, "passwordDialog") - } + mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.DATABASE_SECURITY) + false + } + } + + findPreference(getString(R.string.settings_database_credentials_key))?.apply { + onPreferenceClickListener = Preference.OnPreferenceClickListener { + mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.DATABASE_MASTER_KEY) false } } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.kt index 444f84505..9e6837050 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.kt @@ -22,37 +22,56 @@ package com.kunzisoft.keepass.settings import android.content.ActivityNotFoundException import android.content.Intent import android.content.res.Resources +import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings -import androidx.annotation.RequiresApi -import androidx.fragment.app.DialogFragment -import androidx.appcompat.app.AlertDialog import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import android.view.autofill.AutofillManager import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AlertDialog import androidx.biometric.BiometricManager +import androidx.fragment.app.DialogFragment import androidx.preference.* +import com.kunzisoft.androidclearchroma.ChromaUtil import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.dialogs.KeyboardExplanationDialogFragment -import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment -import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment -import com.kunzisoft.keepass.activities.dialogs.UnderDevelopmentFeatureDialogFragment +import com.kunzisoft.keepass.activities.dialogs.* import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.stylish.Stylish import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction +import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper +import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm +import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm import com.kunzisoft.keepass.education.Education -import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper import com.kunzisoft.keepass.icons.IconPackChooser -import com.kunzisoft.keepass.settings.preference.DialogListExplanationPreference -import com.kunzisoft.keepass.settings.preference.IconPackListPreference -import com.kunzisoft.keepass.settings.preference.InputNumberPreference -import com.kunzisoft.keepass.settings.preference.InputTextPreference +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_COLOR_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_COMPRESSION_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_DEFAULT_USERNAME_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_DESCRIPTION_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_ENCRYPTION_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_ITERATIONS_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_KEY_DERIVATION_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_MAX_HISTORY_ITEMS_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_MAX_HISTORY_SIZE_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_NAME_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE_PARALLELISM_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_ELEMENT_KEY +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_ELEMENT_KEY +import com.kunzisoft.keepass.settings.preference.* +import com.kunzisoft.keepass.settings.preference.DialogColorPreference.Companion.DISABLE_COLOR import com.kunzisoft.keepass.settings.preferencedialogfragment.* +import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.utils.UriUtil class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener { @@ -61,12 +80,21 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen private var mCount = 0 - private var mRoundPref: InputNumberPreference? = null - private var mMemoryPref: InputNumberPreference? = null - private var mParallelismPref: InputNumberPreference? = null + private var dbNamePref: InputTextPreference? = null + private var dbDescriptionPref: InputTextPreference? = null + private var dbDefaultUsername: InputTextPreference? = null + private var dbCustomColorPref: DialogColorPreference? = null + private var dbDataCompressionPref: Preference? = null + private var dbMaxHistoryItemsPref: InputNumberPreference? = null + private var dbMaxHistorySizePref: InputNumberPreference? = null + private var mEncryptionAlgorithmPref: DialogListExplanationPreference? = null + private var mKeyDerivationPref: DialogListExplanationPreference? = null + private var mRoundPref: InputKdfNumberPreference? = null + private var mMemoryPref: InputKdfNumberPreference? = null + private var mParallelismPref: InputKdfNumberPreference? = null enum class Screen { - APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, DATABASE, APPEARANCE + APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY } override fun onResume() { @@ -74,7 +102,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen activity?.let { activity -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key)) + val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key)) if (autoFillEnablePreference != null) { val autofillManager = activity.getSystemService(AutofillManager::class.java) autoFillEnablePreference.isChecked = autofillManager != null @@ -102,7 +130,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen onCreateFormFillingPreference(rootKey) } Screen.ADVANCED_UNLOCK -> { - onCreateAdvancesUnlockPreferences(rootKey) + onCreateAdvancedUnlockPreferences(rootKey) } Screen.APPEARANCE -> { onCreateAppearancePreferences(rootKey) @@ -110,6 +138,12 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen Screen.DATABASE -> { onCreateDatabasePreference(rootKey) } + Screen.DATABASE_SECURITY -> { + onCreateDatabaseSecurityPreference(rootKey) + } + Screen.DATABASE_MASTER_KEY -> { + onCreateDatabaseMasterKeyPreference(rootKey) + } } } @@ -139,7 +173,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen setPreferencesFromResource(R.xml.preferences_form_filling, rootKey) activity?.let { activity -> - val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key)) + val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key)) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val autofillManager = activity.getSystemService(AutofillManager::class.java) if (autofillManager != null && autofillManager.hasEnabledAutofillServices()) @@ -197,10 +231,15 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen } } + findPreference(getString(R.string.magic_keyboard_explanation_key))?.setOnPreferenceClickListener { + UriUtil.gotoUrl(context!!, R.string.magic_keyboard_explanation_url) + false + } + findPreference(getString(R.string.magic_keyboard_key))?.setOnPreferenceClickListener { - if (fragmentManager != null) { - KeyboardExplanationDialogFragment().show(fragmentManager!!, "keyboardExplanationDialog") - } + startActivity(Intent(Settings.ACTION_INPUT_METHOD_SETTINGS).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }) false } @@ -209,15 +248,25 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen false } + findPreference(getString(R.string.clipboard_explanation_key))?.setOnPreferenceClickListener { + UriUtil.gotoUrl(context!!, R.string.clipboard_explanation_url) + false + } + + findPreference(getString(R.string.autofill_explanation_key))?.setOnPreferenceClickListener { + UriUtil.gotoUrl(context!!, R.string.autofill_explanation_url) + false + } + // Present in two places allowCopyPassword() } - private fun onCreateAdvancesUnlockPreferences(rootKey: String?) { + private fun onCreateAdvancedUnlockPreferences(rootKey: String?) { setPreferencesFromResource(R.xml.preferences_advanced_unlock, rootKey) activity?.let { activity -> - val biometricUnlockEnablePreference: SwitchPreference? = findPreference(getString(R.string.biometric_unlock_enable_key)) + val biometricUnlockEnablePreference: SwitchPreference? = findPreference(getString(R.string.biometric_unlock_enable_key)) // < M solve verifyError exception var biometricUnlockSupported = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { @@ -240,7 +289,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen } } - val deleteKeysFingerprints: Preference? = findPreference(getString(R.string.biometric_delete_all_key_key)) + val deleteKeysFingerprints: Preference? = findPreference(getString(R.string.biometric_delete_all_key_key)) if (!biometricUnlockSupported) { deleteKeysFingerprints?.isEnabled = false } else { @@ -273,6 +322,11 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen } } } + + findPreference(getString(R.string.advanced_unlock_explanation_key))?.setOnPreferenceClickListener { + UriUtil.gotoUrl(context!!, R.string.advanced_unlock_explanation_url) + false + } } private fun onCreateAppearancePreferences(rootKey: String?) { @@ -338,66 +392,148 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen if (mDatabase.loaded) { - val dbGeneralPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_general_key)) + val dbGeneralPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_general_key)) - // Db name - val dbNamePref: InputTextPreference? = findPreference(getString(R.string.database_name_key)) - if (mDatabase.containsName()) { + // Database name + dbNamePref = findPreference(getString(R.string.database_name_key)) + if (mDatabase.allowName) { dbNamePref?.summary = mDatabase.name } else { dbGeneralPrefCategory?.removePreference(dbNamePref) } - // Db description - val dbDescriptionPref: InputTextPreference? = findPreference(getString(R.string.database_description_key)) - if (mDatabase.containsDescription()) { + // Database description + dbDescriptionPref = findPreference(getString(R.string.database_description_key)) + if (mDatabase.allowDescription) { dbDescriptionPref?.summary = mDatabase.description } else { dbGeneralPrefCategory?.removePreference(dbDescriptionPref) } + // Database default username + dbDefaultUsername = findPreference(getString(R.string.database_default_username_key)) + if (mDatabase.allowDefaultUsername) { + dbDefaultUsername?.summary = mDatabase.defaultUsername + } else { + dbDefaultUsername?.isEnabled = false + // TODO dbGeneralPrefCategory?.removePreference(dbDefaultUsername) + } + + // Database custom color + dbCustomColorPref = findPreference(getString(R.string.database_custom_color_key)) + if (mDatabase.allowCustomColor) { + dbCustomColorPref?.apply { + try { + color = Color.parseColor(mDatabase.customColor) + summary = mDatabase.customColor + } catch (e: Exception) { + color = DISABLE_COLOR + summary = "" + } + } + } else { + dbCustomColorPref?.isEnabled = false + // TODO dbGeneralPrefCategory?.removePreference(dbCustomColorPref) + } + + // Version + findPreference(getString(R.string.database_version_key)) + ?.summary = mDatabase.version + + val dbCompressionPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_compression_key)) + + // Database compression + dbDataCompressionPref = findPreference(getString(R.string.database_data_compression_key)) + if (mDatabase.allowDataCompression) { + dbDataCompressionPref?.summary = (mDatabase.compressionAlgorithm + ?: PwCompressionAlgorithm.None).getName(resources) + } else { + dbCompressionPrefCategory?.isVisible = false + } + + val dbRecycleBinPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_recycle_bin_key)) + // Recycle bin - val recycleBinPref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_key)) - // TODO Recycle - dbGeneralPrefCategory?.removePreference(recycleBinPref) // To delete - if (mDatabase.isRecycleBinAvailable) { + val recycleBinPref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_key)) + if (mDatabase.allowRecycleBin) { recycleBinPref?.isChecked = mDatabase.isRecycleBinEnabled + // TODO Recycle Bin recycleBinPref?.isEnabled = false } else { - dbGeneralPrefCategory?.removePreference(recycleBinPref) + dbRecycleBinPrefCategory?.isVisible = false } - // Version - findPreference(getString(R.string.database_version_key)) - ?.summary = mDatabase.getVersion() + findPreference(getString(R.string.database_category_history_key)) + ?.isVisible = mDatabase.manageHistory == true + // Max history items + dbMaxHistoryItemsPref = findPreference(getString(R.string.max_history_items_key))?.apply { + summary = mDatabase.historyMaxItems.toString() + } + + // Max history size + dbMaxHistorySizePref = findPreference(getString(R.string.max_history_size_key))?.apply { + summary = mDatabase.historyMaxSize.toString() + } + + } else { + Log.e(javaClass.name, "Database isn't ready") + } + } + + private fun onCreateDatabaseSecurityPreference(rootKey: String?) { + setPreferencesFromResource(R.xml.preferences_database_security, rootKey) + + if (mDatabase.loaded) { // Encryption Algorithm - findPreference(getString(R.string.encryption_algorithm_key)) - ?.summary = mDatabase.getEncryptionAlgorithmName(resources) + mEncryptionAlgorithmPref = findPreference(getString(R.string.encryption_algorithm_key))?.apply { + summary = mDatabase.getEncryptionAlgorithmName(resources) + } // Key derivation function - findPreference(getString(R.string.key_derivation_function_key)) - ?.summary = mDatabase.getKeyDerivationName(resources) + mKeyDerivationPref = findPreference(getString(R.string.key_derivation_function_key))?.apply { + summary = mDatabase.getKeyDerivationName(resources) + } // Round encryption - mRoundPref = findPreference(getString(R.string.transform_rounds_key)) - mRoundPref?.summary = mDatabase.numberKeyEncryptionRoundsAsString + mRoundPref = findPreference(getString(R.string.transform_rounds_key))?.apply { + summary = mDatabase.numberKeyEncryptionRounds.toString() + } // Memory Usage - mMemoryPref = findPreference(getString(R.string.memory_usage_key)) - mMemoryPref?.summary = mDatabase.memoryUsageAsString + mMemoryPref = findPreference(getString(R.string.memory_usage_key))?.apply { + summary = mDatabase.memoryUsage.toString() + } // Parallelism - mParallelismPref = findPreference(getString(R.string.parallelism_key)) - mParallelismPref?.summary = mDatabase.parallelismAsString + mParallelismPref = findPreference(getString(R.string.parallelism_key))?.apply { + summary = mDatabase.parallelism.toString() + } + } else { + Log.e(javaClass.name, "Database isn't ready") + } + } + + private fun onCreateDatabaseMasterKeyPreference(rootKey: String?) { + setPreferencesFromResource(R.xml.preferences_database_master_key, rootKey) + if (mDatabase.loaded) { + findPreference(getString(R.string.settings_database_change_credentials_key))?.apply { + onPreferenceClickListener = Preference.OnPreferenceClickListener { + fragmentManager?.let { fragmentManager -> + AssignMasterKeyDialogFragment.getInstance(mDatabase.allowNoMasterKey) + .show(fragmentManager, "passwordDialog") + } + false + } + } } else { Log.e(javaClass.name, "Database isn't ready") } } private fun allowCopyPassword() { - val copyPasswordPreference: SwitchPreference? = findPreference(getString(R.string.allow_copy_password_key)) + val copyPasswordPreference: SwitchPreference? = findPreference(getString(R.string.allow_copy_password_key)) copyPasswordPreference?.setOnPreferenceChangeListener { _, newValue -> if (newValue as Boolean && context != null) { val message = getString(R.string.allow_copy_password_warning) + @@ -447,6 +583,199 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen } } + private val colorSelectedListener: ((Boolean, Int)-> Unit)? = { enable, color -> + dbCustomColorPref?.summary = ChromaUtil.getFormattedColorString(color, false) + if (enable) { + dbCustomColorPref?.color = color + } else { + dbCustomColorPref?.color = DISABLE_COLOR + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = super.onCreateView(inflater, container, savedInstanceState) + + try { + // To reassign color listener after orientation change + val chromaDialog = fragmentManager?.findFragmentByTag(TAG_PREF_FRAGMENT) as DatabaseColorPreferenceDialogFragmentCompat? + chromaDialog?.onColorSelectedListener = colorSelectedListener + } catch (e: Exception) {} + + return view + } + + fun onProgressDialogThreadResult(actionTask: String, + result: ActionRunnable.Result) { + result.data?.let { data -> + if (data.containsKey(OLD_ELEMENT_KEY) + && data.containsKey(NEW_ELEMENT_KEY)) { + when (actionTask) { + /* + -------- + Main preferences + -------- + */ + ACTION_DATABASE_SAVE_NAME_TASK -> { + val oldName = data.getString(OLD_ELEMENT_KEY)!! + val newName = data.getString(NEW_ELEMENT_KEY)!! + val nameToShow = + if (result.isSuccess) { + newName + } else { + mDatabase.name = oldName + oldName + } + dbNamePref?.summary = nameToShow + } + ACTION_DATABASE_SAVE_DESCRIPTION_TASK -> { + val oldDescription = data.getString(OLD_ELEMENT_KEY)!! + val newDescription = data.getString(NEW_ELEMENT_KEY)!! + val descriptionToShow = + if (result.isSuccess) { + newDescription + } else { + mDatabase.description = oldDescription + oldDescription + } + dbDescriptionPref?.summary = descriptionToShow + } + ACTION_DATABASE_SAVE_DEFAULT_USERNAME_TASK -> { + val oldDefaultUsername = data.getString(OLD_ELEMENT_KEY)!! + val newDefaultUsername = data.getString(NEW_ELEMENT_KEY)!! + val defaultUsernameToShow = + if (result.isSuccess) { + newDefaultUsername + } else { + mDatabase.defaultUsername = oldDefaultUsername + oldDefaultUsername + } + dbDefaultUsername?.summary = defaultUsernameToShow + } + ACTION_DATABASE_SAVE_COLOR_TASK -> { + val oldColor = data.getString(OLD_ELEMENT_KEY)!! + val newColor = data.getString(NEW_ELEMENT_KEY)!! + + val defaultColorToShow = + if (result.isSuccess) { + newColor + } else { + mDatabase.customColor = oldColor + oldColor + } + dbCustomColorPref?.summary = defaultColorToShow + } + ACTION_DATABASE_SAVE_COMPRESSION_TASK -> { + val oldCompression = data.getSerializable(OLD_ELEMENT_KEY) as PwCompressionAlgorithm + val newCompression = data.getSerializable(NEW_ELEMENT_KEY) as PwCompressionAlgorithm + val algorithmToShow = + if (result.isSuccess) { + newCompression + } else { + mDatabase.compressionAlgorithm = oldCompression + oldCompression + } + dbDataCompressionPref?.summary = algorithmToShow.getName(resources) + } + ACTION_DATABASE_SAVE_MAX_HISTORY_ITEMS_TASK -> { + val oldMaxHistoryItems = data.getInt(OLD_ELEMENT_KEY) + val newMaxHistoryItems = data.getInt(NEW_ELEMENT_KEY) + val maxHistoryItemsToShow = + if (result.isSuccess) { + newMaxHistoryItems + } else { + mDatabase.historyMaxItems = oldMaxHistoryItems + oldMaxHistoryItems + } + dbMaxHistoryItemsPref?.summary = maxHistoryItemsToShow.toString() + } + ACTION_DATABASE_SAVE_MAX_HISTORY_SIZE_TASK -> { + val oldMaxHistorySize = data.getLong(OLD_ELEMENT_KEY) + val newMaxHistorySize = data.getLong(NEW_ELEMENT_KEY) + val maxHistorySizeToShow = + if (result.isSuccess) { + newMaxHistorySize + } else { + mDatabase.historyMaxSize = oldMaxHistorySize + oldMaxHistorySize + } + dbMaxHistorySizePref?.summary = maxHistorySizeToShow.toString() + } + + /* + -------- + Security + -------- + */ + ACTION_DATABASE_SAVE_ENCRYPTION_TASK -> { + val oldEncryption = data.getSerializable(OLD_ELEMENT_KEY) as PwEncryptionAlgorithm + val newEncryption = data.getSerializable(NEW_ELEMENT_KEY) as PwEncryptionAlgorithm + val algorithmToShow = + if (result.isSuccess) { + newEncryption + } else { + mDatabase.encryptionAlgorithm = oldEncryption + oldEncryption + } + mEncryptionAlgorithmPref?.summary = algorithmToShow.getName(resources) + } + ACTION_DATABASE_SAVE_KEY_DERIVATION_TASK -> { + val oldKeyDerivationEngine = data.getSerializable(OLD_ELEMENT_KEY) as KdfEngine + val newKeyDerivationEngine = data.getSerializable(NEW_ELEMENT_KEY) as KdfEngine + val kdfEngineToShow = + if (result.isSuccess) { + newKeyDerivationEngine + } else { + mDatabase.kdfEngine = oldKeyDerivationEngine + oldKeyDerivationEngine + } + mKeyDerivationPref?.summary = kdfEngineToShow.getName(resources) + + mRoundPref?.summary = kdfEngineToShow.defaultKeyRounds.toString() + // Disable memory and parallelism if not available + mMemoryPref?.summary = kdfEngineToShow.defaultMemoryUsage.toString() + mParallelismPref?.summary = kdfEngineToShow.defaultParallelism.toString() + } + ACTION_DATABASE_SAVE_ITERATIONS_TASK -> { + val oldIterations = data.getLong(OLD_ELEMENT_KEY) + val newIterations = data.getLong(NEW_ELEMENT_KEY) + val roundsToShow = + if (result.isSuccess) { + newIterations + } else { + mDatabase.numberKeyEncryptionRounds = oldIterations + oldIterations + } + mRoundPref?.summary = roundsToShow.toString() + } + ACTION_DATABASE_SAVE_MEMORY_USAGE_TASK -> { + val oldMemoryUsage = data.getLong(OLD_ELEMENT_KEY) + val newMemoryUsage = data.getLong(NEW_ELEMENT_KEY) + val memoryToShow = + if (result.isSuccess) { + newMemoryUsage + } else { + mDatabase.memoryUsage = oldMemoryUsage + oldMemoryUsage + } + mMemoryPref?.summary = memoryToShow.toString() + } + ACTION_DATABASE_SAVE_PARALLELISM_TASK -> { + val oldParallelism = data.getInt(OLD_ELEMENT_KEY) + val newParallelism = data.getInt(NEW_ELEMENT_KEY) + val parallelismToShow = + if (result.isSuccess) { + newParallelism + } else { + mDatabase.parallelism = oldParallelism + oldParallelism + } + mParallelismPref?.summary = parallelismToShow.toString() + } + } + } + } + } + override fun onDisplayPreferenceDialog(preference: Preference?) { var otherDialogFragment = false @@ -455,12 +784,32 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen preference?.let { preference -> var dialogFragment: DialogFragment? = null when { + // Main Preferences preference.key == getString(R.string.database_name_key) -> { dialogFragment = DatabaseNamePreferenceDialogFragmentCompat.newInstance(preference.key) } preference.key == getString(R.string.database_description_key) -> { dialogFragment = DatabaseDescriptionPreferenceDialogFragmentCompat.newInstance(preference.key) } + preference.key == getString(R.string.database_default_username_key) -> { + dialogFragment = DatabaseDefaultUsernamePreferenceDialogFragmentCompat.newInstance(preference.key) + } + preference.key == getString(R.string.database_custom_color_key) -> { + dialogFragment = DatabaseColorPreferenceDialogFragmentCompat.newInstance(preference.key).apply { + onColorSelectedListener = colorSelectedListener + } + } + preference.key == getString(R.string.database_data_compression_key) -> { + dialogFragment = DatabaseDataCompressionPreferenceDialogFragmentCompat.newInstance(preference.key) + } + preference.key == getString(R.string.max_history_items_key) -> { + dialogFragment = MaxHistoryItemsPreferenceDialogFragmentCompat.newInstance(preference.key) + } + preference.key == getString(R.string.max_history_size_key) -> { + dialogFragment = MaxHistorySizePreferenceDialogFragmentCompat.newInstance(preference.key) + } + + // Security preference.key == getString(R.string.encryption_algorithm_key) -> { dialogFragment = DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.newInstance(preference.key) } @@ -486,7 +835,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen if (dialogFragment != null && !mDatabaseReadOnly) { dialogFragment.setTargetFragment(this, 0) - dialogFragment.show(fragmentManager, null) + dialogFragment.show(fragmentManager, TAG_PREF_FRAGMENT) } // Could not be handled here. Try with the super method. else if (otherDialogFragment) { @@ -510,6 +859,8 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen private const val TAG_KEY = "NESTED_KEY" + private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT" + private const val REQUEST_CODE_AUTOFILL = 5201 @JvmOverloads @@ -529,8 +880,10 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen Screen.APPLICATION -> resources.getString(R.string.menu_app_settings) Screen.FORM_FILLING -> resources.getString(R.string.menu_form_filling_settings) Screen.ADVANCED_UNLOCK -> resources.getString(R.string.menu_advanced_unlock_settings) - Screen.DATABASE -> resources.getString(R.string.menu_database_settings) Screen.APPEARANCE -> resources.getString(R.string.menu_appearance_settings) + Screen.DATABASE -> resources.getString(R.string.menu_database_settings) + Screen.DATABASE_SECURITY -> resources.getString(R.string.menu_security_settings) + Screen.DATABASE_MASTER_KEY -> resources.getString(R.string.menu_master_key_settings) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt index b49bd139f..4a74f62eb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt @@ -33,6 +33,12 @@ object PreferencesUtil { return prefs.getBoolean(context.getString(R.string.show_read_only_warning), true) } + fun rememberKeyFiles(context: Context): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.keyfile_key), + context.resources.getBoolean(R.bool.keyfile_default)) + } + fun omitBackup(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getBoolean(context.getString(R.string.omitbackup_key), @@ -109,8 +115,8 @@ object PreferencesUtil { fun getAppTimeout(context: Context): Long { return try { val prefs = PreferenceManager.getDefaultSharedPreferences(context) - java.lang.Long.parseLong(prefs.getString(context.getString(R.string.app_timeout_key), - context.getString(R.string.clipboard_timeout_default)) ?: "60000") + (prefs.getString(context.getString(R.string.app_timeout_key), + context.getString(R.string.clipboard_timeout_default)) ?: "300000").toLong() } catch (e: NumberFormatException) { TimeoutHelper.DEFAULT_TIMEOUT } @@ -128,6 +134,12 @@ object PreferencesUtil { context.resources.getBoolean(R.bool.lock_database_back_root_default)) } + fun isPersistentNotificationEnable(context: Context): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.persistent_notification_key), + context.resources.getBoolean(R.bool.persistent_notification_default)) + } + fun isBiometricUnlockEnable(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key), @@ -185,12 +197,6 @@ object PreferencesUtil { context.resources.getBoolean(R.bool.monospace_font_fields_enable_default)) } - fun autoOpenSelectedFile(context: Context): Boolean { - val prefs = PreferenceManager.getDefaultSharedPreferences(context) - return prefs.getBoolean(context.getString(R.string.auto_open_file_uri_key), - context.resources.getBoolean(R.bool.auto_open_file_uri_default)) - } - fun isFirstTimeAskAllowCopyPasswordAndProtectedFields(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getBoolean(context.getString(R.string.allow_copy_password_first_time_key), @@ -209,6 +215,12 @@ object PreferencesUtil { context.resources.getBoolean(R.bool.clear_clipboard_notification_default)) } + fun isClearKeyboardNotificationEnable(context: Context): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.keyboard_notification_entry_clear_close_key), + context.resources.getBoolean(R.bool.keyboard_notification_entry_clear_close_default)) + } + fun setAllowCopyPasswordAndProtectedFields(context: Context, allowCopy: Boolean) { val prefs = PreferenceManager.getDefaultSharedPreferences(context) prefs.edit() diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt index 5f8897212..d8300bb26 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt @@ -25,16 +25,15 @@ import android.content.DialogInterface import android.content.Intent import android.net.Uri import android.os.Bundle -import androidx.fragment.app.Fragment -import androidx.appcompat.widget.Toolbar import android.view.MenuItem +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.lock.LockingActivity -import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable -import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread +import com.kunzisoft.keepass.database.action.ProgressDialogThread import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.timeout.TimeoutHelper @@ -47,6 +46,8 @@ open class SettingsActivity private var toolbar: Toolbar? = null + var progressDialogThread: ProgressDialogThread? = null + companion object { private const val TAG_NESTED = "TAG_NESTED" @@ -88,6 +89,26 @@ open class SettingsActivity } backupManager = BackupManager(this) + + progressDialogThread = ProgressDialogThread(this) { actionTask, result -> + // Call result in fragment + (supportFragmentManager + .findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?) + ?.onProgressDialogThreadResult(actionTask, result) + } + } + + override fun onResume() { + super.onResume() + + progressDialogThread?.registerProgressTask() + } + + override fun onPause() { + + progressDialogThread?.unregisterProgressTask() + + super.onPause() } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -103,33 +124,43 @@ open class SettingsActivity super.onStop() } - override fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?, keyFileChecked: Boolean, keyFile: Uri?) { + override fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, + masterPassword: String?, + keyFileChecked: Boolean, + keyFile: Uri?) { Database.getInstance().let { database -> - val progressDialogThread = ProgressDialogSaveDatabaseThread(this) { - AssignPasswordInDatabaseRunnable(this, - database, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile, - true) - } - // Show the progress dialog now or after dialog confirmation - if (database.validatePasswordEncoding(masterPassword)) { - progressDialogThread.start() - } else { - PasswordEncodingDialogFragment().apply { - positiveButtonClickListener = DialogInterface.OnClickListener { _, _ -> - progressDialogThread.start() + database.fileUri?.let { databaseUri -> + // Show the progress dialog now or after dialog confirmation + if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) { + progressDialogThread?.startDatabaseAssignPassword( + databaseUri, + masterPasswordChecked, + masterPassword, + keyFileChecked, + keyFile + ) + } else { + PasswordEncodingDialogFragment().apply { + positiveButtonClickListener = DialogInterface.OnClickListener { _, _ -> + progressDialogThread?.startDatabaseAssignPassword( + databaseUri, + masterPasswordChecked, + masterPassword, + keyFileChecked, + keyFile + ) + } + show(supportFragmentManager, "passwordEncodingTag") } - show(supportFragmentManager, "passwordEncodingTag") } } } } - override fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?, keyFileChecked: Boolean, keyFile: Uri?) { - + override fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, + masterPassword: String?, + keyFileChecked: Boolean, + keyFile: Uri?) { } override fun onBackPressed() { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogColorPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogColorPreference.kt new file mode 100644 index 000000000..a94fc8912 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogColorPreference.kt @@ -0,0 +1,33 @@ +package com.kunzisoft.keepass.settings.preference + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import androidx.annotation.ColorInt +import com.kunzisoft.androidclearchroma.ChromaPreferenceCompat + +import com.kunzisoft.keepass.R + +class DialogColorPreference @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleRes: Int = defStyleAttr) + : ChromaPreferenceCompat(context, attrs, defStyleAttr, defStyleRes) { + + override fun setSummary(summary: CharSequence?) { + if (color == DISABLE_COLOR) + super.setSummary("") + else + super.setSummary(summary) + } + + override fun getDialogLayoutResource(): Int { + return R.layout.pref_dialog_input_color + } + + companion object { + + @ColorInt + const val DISABLE_COLOR: Int = Color.TRANSPARENT + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt new file mode 100644 index 000000000..14516d174 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.settings.preference + +import android.content.Context +import android.util.AttributeSet +import androidx.preference.DialogPreference +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine + +class InputKdfNumberPreference @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleRes: Int = defStyleAttr) + : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { + + override fun getDialogLayoutResource(): Int { + return R.layout.pref_dialog_input_numbers + } + + override fun setSummary(summary: CharSequence) { + if (summary == UNKNOWN_VALUE_STRING) { + isEnabled = false + super.setSummary("") + } else { + isEnabled = true + super.setSummary(summary) + } + } + + companion object { + const val UNKNOWN_VALUE_STRING = KdfEngine.UNKNOWN_VALUE.toString() + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt index dff5ea0a6..866912fd8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt @@ -20,66 +20,29 @@ package com.kunzisoft.keepass.settings.preference import android.content.Context -import android.content.res.TypedArray import android.util.AttributeSet - +import androidx.preference.DialogPreference import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine -class InputNumberPreference @JvmOverloads constructor(context: Context, +open class InputNumberPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleRes: Int = defStyleAttr) - : InputTextExplanationPreference(context, attrs, defStyleAttr, defStyleRes) { - - // Save to Shared Preferences - var number: Long = 0 - set(number) { - field = number - persistLong(number) - } + : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { override fun getDialogLayoutResource(): Int { - return R.layout.pref_dialog_numbers + return R.layout.pref_dialog_input_numbers } override fun setSummary(summary: CharSequence) { - if (summary == KdfEngine.UNKNOWN_VALUE_STRING) { - isEnabled = false + if (summary == INFINITE_VALUE_STRING) { super.setSummary("") } else { - isEnabled = true super.setSummary(summary) } } - override fun onGetDefaultValue(a: TypedArray?, index: Int): Any { - // Default value from attribute. Fallback value is set to 0. - return a?.getInt(index, 0) ?: 0 + companion object { + const val INFINITE_VALUE_STRING = "-1" } - - override fun onSetInitialValue(restorePersistedValue: Boolean, - defaultValue: Any?) { - // Read the value. Use the default value if it is not possible. - var numberValue: Long - if (!restorePersistedValue) { - numberValue = 100000 - if (defaultValue is String) { - numberValue = java.lang.Long.parseLong(defaultValue) - } - if (defaultValue is Int) { - numberValue = defaultValue.toLong() - } - try { - numberValue = defaultValue as Long - } catch (e: Exception) { - e.printStackTrace() - } - } else { - numberValue = getPersistedLong(this.number) - } - - number = numberValue - } - } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputTextExplanationPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputTextExplanationPreference.kt deleted file mode 100644 index d022cfe1f..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputTextExplanationPreference.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.kunzisoft.keepass.settings.preference - -import android.content.Context -import androidx.preference.DialogPreference -import android.util.AttributeSet - -import com.kunzisoft.keepass.R - -open class InputTextExplanationPreference @JvmOverloads constructor(context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.dialogPreferenceStyle, - defStyleRes: Int = defStyleAttr) - : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { - - var explanation: String? = null - - init { - val styleAttributes = context.theme.obtainStyledAttributes( - attrs, - R.styleable.explanationDialog, - 0, 0) - try { - explanation = styleAttributes.getString(R.styleable.explanationDialog_explanations) - } finally { - styleAttributes.recycle() - } - } - - override fun getDialogLayoutResource(): Int { - return R.layout.pref_dialog_input_text_explanation - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputTextPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputTextPreference.kt index 9c6f6ea1d..71a21642d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputTextPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputTextPreference.kt @@ -6,10 +6,10 @@ import android.util.AttributeSet import com.kunzisoft.keepass.R -class InputTextPreference @JvmOverloads constructor(context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.dialogPreferenceStyle, - defStyleRes: Int = defStyleAttr) +open class InputTextPreference @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleRes: Int = defStyleAttr) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { override fun getDialogLayoutResource(): Int { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseColorPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseColorPreferenceDialogFragmentCompat.kt new file mode 100644 index 000000000..2cd54591c --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseColorPreferenceDialogFragmentCompat.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.settings.preferencedialogfragment + +import android.app.Dialog +import android.graphics.Color +import android.os.Bundle +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.widget.CompoundButton +import androidx.annotation.ColorInt +import androidx.appcompat.app.AlertDialog +import com.kunzisoft.androidclearchroma.ChromaUtil +import com.kunzisoft.androidclearchroma.IndicatorMode +import com.kunzisoft.androidclearchroma.colormode.ColorMode +import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment +import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment.* +import com.kunzisoft.keepass.R +import java.lang.Exception + +class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { + + private lateinit var rootView: View + private lateinit var enableSwitchView: CompoundButton + private var chromaColorFragment: ChromaColorFragment? = null + + var onColorSelectedListener: ((enable: Boolean, color: Int) -> Unit)? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + + val alertDialogBuilder = AlertDialog.Builder(activity!!) + + rootView = activity!!.layoutInflater.inflate(R.layout.pref_dialog_input_color, null) + enableSwitchView = rootView.findViewById(R.id.switch_element) + + val fragmentManager = childFragmentManager + chromaColorFragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_COLORS) as ChromaColorFragment? + val fragmentTransaction = fragmentManager.beginTransaction() + + database?.let { database -> + val initColor = try { + enableSwitchView.isChecked = true + Color.parseColor(database.customColor) + } catch (e: Exception) { + enableSwitchView.isChecked = false + DEFAULT_COLOR + } + arguments?.putInt(ARG_INITIAL_COLOR, initColor) + } + + if (chromaColorFragment == null) { + chromaColorFragment = newInstance(arguments) + fragmentTransaction.add(com.kunzisoft.androidclearchroma.R.id.color_dialog_container, chromaColorFragment!!, TAG_FRAGMENT_COLORS).commit() + } + + alertDialogBuilder.setPositiveButton(android.R.string.ok) { _, _ -> + val currentColor = chromaColorFragment!!.currentColor + val customColorEnable = enableSwitchView.isChecked + + onColorSelectedListener?.invoke(customColorEnable, currentColor) + + database?.let { database -> + val newColor = if (customColorEnable) { + ChromaUtil.getFormattedColorString(currentColor, false) + } else { + "" + } + val oldColor = database.customColor + database.customColor = newColor + progressDialogThread?.startDatabaseSaveColor(oldColor, newColor) + } + + onDialogClosed(true) + dismiss() + } + + alertDialogBuilder.setNegativeButton(android.R.string.cancel) { _, _ -> + onDialogClosed(false) + dismiss() + } + + alertDialogBuilder.setView(rootView) + + val dialog = alertDialogBuilder.create() + // request a window without the title + dialog.window?.requestFeature(Window.FEATURE_NO_TITLE) + + dialog.setOnShowListener { measureLayout(it as Dialog) } + + return dialog + } + + override fun onDialogClosed(positiveResult: Boolean) { + // Nothing here + } + + /** + * Set new dimensions to dialog + * @param ad dialog + */ + private fun measureLayout(ad: Dialog) { + val typedValue = TypedValue() + resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_height_multiplier, typedValue, true) + val heightMultiplier = typedValue.float + val height = (ad.context.resources.displayMetrics.heightPixels * heightMultiplier).toInt() + + resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_width_multiplier, typedValue, true) + val widthMultiplier = typedValue.float + val width = (ad.context.resources.displayMetrics.widthPixels * widthMultiplier).toInt() + + ad.window?.setLayout(width, height) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return rootView + } + + companion object { + private const val TAG_FRAGMENT_COLORS = "TAG_FRAGMENT_COLORS" + + @ColorInt + const val DEFAULT_COLOR: Int = Color.WHITE + + fun newInstance(key: String): DatabaseColorPreferenceDialogFragmentCompat { + val fragment = DatabaseColorPreferenceDialogFragmentCompat() + val bundle = Bundle(1) + bundle.putString(ARG_KEY, key) + bundle.putInt(ARG_INITIAL_COLOR, Color.BLACK) + bundle.putInt(ARG_COLOR_MODE, ColorMode.RGB.ordinal) + bundle.putInt(ARG_INDICATOR_MODE, IndicatorMode.HEX.ordinal) + fragment.arguments = bundle + + return fragment + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDataCompressionPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDataCompressionPreferenceDialogFragmentCompat.kt new file mode 100644 index 000000000..f20873115 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDataCompressionPreferenceDialogFragmentCompat.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.settings.preferencedialogfragment + +import android.os.Bundle +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm +import com.kunzisoft.keepass.settings.preferencedialogfragment.adapter.ListRadioItemAdapter + +class DatabaseDataCompressionPreferenceDialogFragmentCompat + : DatabaseSavePreferenceDialogFragmentCompat(), + ListRadioItemAdapter.RadioItemSelectedCallback { + + private var compressionSelected: PwCompressionAlgorithm? = null + + override fun onBindDialogView(view: View) { + super.onBindDialogView(view) + + setExplanationText(R.string.database_data_compression_summary) + + val recyclerView = view.findViewById(R.id.pref_dialog_list) + recyclerView.layoutManager = LinearLayoutManager(context) + + activity?.let { activity -> + val compressionAdapter = ListRadioItemAdapter(activity) + compressionAdapter.setRadioItemSelectedCallback(this) + recyclerView.adapter = compressionAdapter + + database?.let { database -> + compressionSelected = database.compressionAlgorithm?.apply { + compressionAdapter.setItems(database.availableCompressionAlgorithms, this) + } + } + } + } + + override fun onDialogClosed(positiveResult: Boolean) { + + if (positiveResult) { + database?.let { database -> + if (compressionSelected != null) { + val newCompression = compressionSelected + val oldCompression = database.compressionAlgorithm + database.compressionAlgorithm = newCompression + + if (oldCompression != null && newCompression != null) + progressDialogThread?.startDatabaseSaveCompression(oldCompression, newCompression) + } + } + } + } + + override fun onItemSelected(item: PwCompressionAlgorithm) { + this.compressionSelected = item + } + + companion object { + + fun newInstance(key: String): DatabaseDataCompressionPreferenceDialogFragmentCompat { + val fragment = DatabaseDataCompressionPreferenceDialogFragmentCompat() + val bundle = Bundle(1) + bundle.putString(ARG_KEY, key) + fragment.arguments = bundle + + return fragment + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDefaultUsernamePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDefaultUsernamePreferenceDialogFragmentCompat.kt new file mode 100644 index 000000000..2430616b4 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDefaultUsernamePreferenceDialogFragmentCompat.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.settings.preferencedialogfragment + +import android.os.Bundle +import android.view.View + +class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { + + override fun onBindDialogView(view: View) { + super.onBindDialogView(view) + + inputText = database?.defaultUsername?: "" + } + + override fun onDialogClosed(positiveResult: Boolean) { + database?.let { database -> + if (positiveResult) { + val newDefaultUsername = inputText + val oldDefaultUsername = database.defaultUsername + database.defaultUsername = newDefaultUsername + + progressDialogThread?.startDatabaseSaveDefaultUsername(oldDefaultUsername, newDefaultUsername) + } + } + } + + companion object { + + fun newInstance(key: String): DatabaseDefaultUsernamePreferenceDialogFragmentCompat { + val fragment = DatabaseDefaultUsernamePreferenceDialogFragmentCompat() + val bundle = Bundle(1) + bundle.putString(ARG_KEY, key) + fragment.arguments = bundle + + return fragment + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDescriptionPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDescriptionPreferenceDialogFragmentCompat.kt index 3cd23462e..199f42373 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDescriptionPreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDescriptionPreferenceDialogFragmentCompat.kt @@ -21,9 +21,8 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment import android.os.Bundle import android.view.View -import com.kunzisoft.keepass.tasks.ActionRunnable -class DatabaseDescriptionPreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() { +class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { override fun onBindDialogView(view: View) { super.onBindDialogView(view) @@ -32,27 +31,14 @@ class DatabaseDescriptionPreferenceDialogFragmentCompat : InputDatabaseSavePrefe } override fun onDialogClosed(positiveResult: Boolean) { - if (database != null && positiveResult) { - val newDescription = inputText - val oldDescription = database!!.description - database?.assignDescription(newDescription) + database?.let { database -> + if (positiveResult) { + val newDescription = inputText + val oldDescription = database.description + database.description = newDescription - actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newDescription, oldDescription) - } - - super.onDialogClosed(positiveResult) - } - - private inner class AfterDescriptionSave(private val mNewDescription: String, - private val mOldDescription: String) - : ActionRunnable() { - - override fun onFinishRun(result: Result) { - val descriptionToShow = mNewDescription - if (!result.isSuccess) { - database?.assignDescription(mOldDescription) + progressDialogThread?.startDatabaseSaveDescription(oldDescription, newDescription) } - preference.summary = descriptionToShow } } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt index fc16e6321..61c1f3edb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt @@ -26,7 +26,6 @@ import android.view.View import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm import com.kunzisoft.keepass.settings.preferencedialogfragment.adapter.ListRadioItemAdapter -import com.kunzisoft.keepass.tasks.ActionRunnable class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat(), @@ -56,41 +55,27 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat } override fun onDialogClosed(positiveResult: Boolean) { - if (database != null && positiveResult && database!!.allowEncryptionAlgorithmModification()) { - if (algorithmSelected != null) { - val newAlgorithm = algorithmSelected - val oldAlgorithm = database?.encryptionAlgorithm - newAlgorithm?.let { - database?.assignEncryptionAlgorithm(it) + if (positiveResult) { + database?.let { database -> + if (database.allowEncryptionAlgorithmModification) { + if (algorithmSelected != null) { + val newAlgorithm = algorithmSelected + val oldAlgorithm = database.encryptionAlgorithm + database.encryptionAlgorithm = newAlgorithm + + if (oldAlgorithm != null && newAlgorithm != null) + progressDialogThread?.startDatabaseSaveEncryption(oldAlgorithm, newAlgorithm) + } } - - if (oldAlgorithm != null && newAlgorithm != null) - actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newAlgorithm, oldAlgorithm) } } - - super.onDialogClosed(positiveResult) } override fun onItemSelected(item: PwEncryptionAlgorithm) { this.algorithmSelected = item } - private inner class AfterDescriptionSave(private val mNewAlgorithm: PwEncryptionAlgorithm, - private val mOldAlgorithm: PwEncryptionAlgorithm) - : ActionRunnable() { - - override fun onFinishRun(result: Result) { - var algorithmToShow = mNewAlgorithm - if (!result.isSuccess) { - database?.assignEncryptionAlgorithm(mOldAlgorithm) - algorithmToShow = mOldAlgorithm - } - preference.summary = algorithmToShow.getName(settingsResources) - } - } - companion object { fun newInstance(key: String): DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseKeyDerivationPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseKeyDerivationPreferenceDialogFragmentCompat.kt index f05b1bbc2..dc82d55cb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseKeyDerivationPreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseKeyDerivationPreferenceDialogFragmentCompat.kt @@ -27,7 +27,6 @@ import android.view.View import com.kunzisoft.keepass.R import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.settings.preferencedialogfragment.adapter.ListRadioItemAdapter -import com.kunzisoft.keepass.tasks.ActionRunnable class DatabaseKeyDerivationPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat(), @@ -52,25 +51,26 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat recyclerView.adapter = kdfAdapter database?.let { database -> - kdfEngineSelected = database.kdfEngine - if (kdfEngineSelected != null) - kdfAdapter.setItems(database.availableKdfEngines, kdfEngineSelected!!) + kdfEngineSelected = database.kdfEngine?.apply { + kdfAdapter.setItems(database.availableKdfEngines, this) + } } } } override fun onDialogClosed(positiveResult: Boolean) { - if (database != null && positiveResult && database!!.allowKdfModification()) { - if (kdfEngineSelected != null) { - val newKdfEngine = kdfEngineSelected!! - val oldKdfEngine = database!!.kdfEngine - database?.assignKdfEngine(newKdfEngine) - - actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newKdfEngine, oldKdfEngine) + if (positiveResult) { + database?.let { database -> + if (database.allowKdfModification) { + val newKdfEngine = kdfEngineSelected + val oldKdfEngine = database.kdfEngine + if (newKdfEngine != null && oldKdfEngine != null) { + database.kdfEngine = newKdfEngine + progressDialogThread?.startDatabaseSaveKeyDerivation(oldKdfEngine, newKdfEngine) + } + } } } - - super.onDialogClosed(positiveResult) } fun setRoundPreference(preference: Preference?) { @@ -89,25 +89,6 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat kdfEngineSelected = item } - private inner class AfterDescriptionSave(private val mNewKdfEngine: KdfEngine, - private val mOldKdfEngine: KdfEngine) - : ActionRunnable() { - - override fun onFinishRun(result: Result) { - val kdfEngineToShow = mNewKdfEngine - - if (!result.isSuccess) { - database?.assignKdfEngine(mOldKdfEngine) - } - preference.summary = kdfEngineToShow.getName(settingsResources) - - roundPreference?.summary = kdfEngineToShow.defaultKeyRounds.toString() - // Disable memory and parallelism if not available - memoryPreference?.summary = kdfEngineToShow.getDefaultMemoryUsage().toString() - parallelismPreference?.summary = kdfEngineToShow.getDefaultParallelism().toString() - } - } - companion object { fun newInstance(key: String): DatabaseKeyDerivationPreferenceDialogFragmentCompat { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseNamePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseNamePreferenceDialogFragmentCompat.kt index 3f10ff627..6eb74e17a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseNamePreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseNamePreferenceDialogFragmentCompat.kt @@ -21,9 +21,8 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment import android.os.Bundle import android.view.View -import com.kunzisoft.keepass.tasks.ActionRunnable -class DatabaseNamePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() { +class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { override fun onBindDialogView(view: View) { super.onBindDialogView(view) @@ -32,27 +31,14 @@ class DatabaseNamePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDi } override fun onDialogClosed(positiveResult: Boolean) { - if (database != null && positiveResult) { - val newName = inputText - val oldName = database!!.name - database?.assignName(newName) + if (positiveResult) { + database?.let { database -> + val newName = inputText + val oldName = database.name + database.name = newName - actionInUIThreadAfterSaveDatabase = AfterNameSave(newName, oldName) - } - - super.onDialogClosed(positiveResult) - } - - private inner class AfterNameSave(private val mNewName: String, - private val mOldName: String) - : ActionRunnable() { - - override fun onFinishRun(result: Result) { - val nameToShow = mNewName - if (!result.isSuccess) { - database?.assignName(mOldName) + progressDialogThread?.startDatabaseSaveName(oldName, newName) } - preference.summary = nameToShow } } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt index 0f6fe8888..2d4182679 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt @@ -19,59 +19,33 @@ */ package com.kunzisoft.keepass.settings.preferencedialogfragment -import android.content.res.Resources -import android.util.Log -import android.view.View -import android.widget.Toast -import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread -import com.kunzisoft.keepass.database.action.SaveDatabaseActionRunnable +import android.content.Context +import android.os.Bundle +import com.kunzisoft.keepass.database.action.ProgressDialogThread import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.settings.SettingsActivity abstract class DatabaseSavePreferenceDialogFragmentCompat : InputPreferenceDialogFragmentCompat() { protected var database: Database? = null - var actionInUIThreadAfterSaveDatabase: ActionRunnable? = null + protected var progressDialogThread: ProgressDialogThread? = null - protected lateinit var settingsResources: Resources - - override fun onBindDialogView(view: View) { - super.onBindDialogView(view) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) this.database = Database.getInstance() - - activity?.resources?.let { settingsResources = it } } - override fun onDialogClosed(positiveResult: Boolean) { - if (positiveResult) { - activity?.let { notNullActivity -> - database?.let { notNullDatabase -> - ProgressDialogSaveDatabaseThread(notNullActivity) { - SaveDatabaseActionRunnable( - notNullActivity, - notNullDatabase, - true) - }.apply { - actionFinishInUIThread = object:ActionRunnable() { - override fun onFinishRun(result: Result) { - if (!result.isSuccess) { - Log.e(TAG, result.message) - Toast.makeText(notNullActivity, result.message, Toast.LENGTH_SHORT).show() - } - actionInUIThreadAfterSaveDatabase?.onFinishRun(result) - } - } - start() - } - } - } + override fun onAttach(context: Context) { + super.onAttach(context) + // Attach dialog thread to start action + if (context is SettingsActivity) { + progressDialogThread = context.progressDialogThread } } companion object { - private const val TAG = "DbSavePrefDialog" } } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/InputDatabaseSavePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/InputDatabaseSavePreferenceDialogFragmentCompat.kt deleted file mode 100644 index 31b4fcbac..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/InputDatabaseSavePreferenceDialogFragmentCompat.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePass DX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.settings.preferencedialogfragment - -import android.view.View -import android.widget.EditText - -import com.kunzisoft.keepass.R - -open class InputDatabaseSavePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { - - private var inputTextView: EditText? = null - - var inputText: String - get() = this.inputTextView?.text?.toString() ?: "" - set(inputText) { - if (inputTextView != null) { - this.inputTextView?.setText(inputText) - this.inputTextView?.setSelection(this.inputTextView!!.text.length) - } - } - - override fun onBindDialogView(view: View) { - super.onBindDialogView(view) - - inputTextView = view.findViewById(R.id.input_text) - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/InputPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/InputPreferenceDialogFragmentCompat.kt index 2cc0bb982..bf60fd0ef 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/InputPreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/InputPreferenceDialogFragmentCompat.kt @@ -19,36 +19,87 @@ */ package com.kunzisoft.keepass.settings.preferencedialogfragment -import androidx.annotation.StringRes -import androidx.preference.PreferenceDialogFragmentCompat import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.CompoundButton +import android.widget.EditText import android.widget.TextView - +import androidx.annotation.StringRes +import androidx.preference.PreferenceDialogFragmentCompat import com.kunzisoft.keepass.R abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() { + private var inputTextView: EditText? = null private var textExplanationView: TextView? = null + private var switchElementView: CompoundButton? = null + + var inputText: String + get() = this.inputTextView?.text?.toString() ?: "" + set(inputText) { + if (inputTextView != null) { + this.inputTextView?.setText(inputText) + this.inputTextView?.setSelection(this.inputTextView!!.text.length) + } + } var explanationText: String? get() = textExplanationView?.text?.toString() ?: "" set(explanationText) { - if (explanationText != null && explanationText.isNotEmpty()) { - textExplanationView?.text = explanationText - textExplanationView?.visibility = View.VISIBLE - } else { - textExplanationView?.text = explanationText - textExplanationView?.visibility = View.VISIBLE + textExplanationView?.apply { + if (explanationText != null && explanationText.isNotEmpty()) { + text = explanationText + visibility = View.VISIBLE + } else { + text = "" + visibility = View.GONE + } } } override fun onBindDialogView(view: View) { super.onBindDialogView(view) + inputTextView = view.findViewById(R.id.input_text) + inputTextView?.apply { + imeOptions = EditorInfo.IME_ACTION_DONE + setOnEditorActionListener { _, actionId, _ -> + when (actionId) { + EditorInfo.IME_ACTION_DONE -> { + onDialogClosed(true) + dialog?.dismiss() + true + } + else -> { + false + } + } + } + } textExplanationView = view.findViewById(R.id.explanation_text) + textExplanationView?.visibility = View.GONE + switchElementView = view.findViewById(R.id.switch_element) + switchElementView?.visibility = View.GONE + } + + fun setInoutText(@StringRes inputTextId: Int) { + inputText = getString(inputTextId) + } + + fun showInputText(show: Boolean) { + inputTextView?.visibility = if (show) View.VISIBLE else View.GONE } fun setExplanationText(@StringRes explanationTextId: Int) { explanationText = getString(explanationTextId) } + + fun setSwitchAction(onCheckedChange: ((isChecked: Boolean)-> Unit)?, defaultChecked: Boolean) { + switchElementView?.visibility = if (onCheckedChange == null) View.GONE else View.VISIBLE + switchElementView?.isChecked = defaultChecked + inputTextView?.visibility = if (defaultChecked) View.VISIBLE else View.GONE + switchElementView?.setOnCheckedChangeListener { _, isChecked -> + onCheckedChange?.invoke(isChecked) + } + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MaxHistoryItemsPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MaxHistoryItemsPreferenceDialogFragmentCompat.kt new file mode 100644 index 000000000..f9f45731e --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MaxHistoryItemsPreferenceDialogFragmentCompat.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.settings.preferencedialogfragment + +import android.os.Bundle +import android.view.View +import com.kunzisoft.keepass.R + +class MaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { + + override fun onBindDialogView(view: View) { + super.onBindDialogView(view) + + setExplanationText(R.string.max_history_items_summary) + database?.historyMaxItems?.let { maxItemsDatabase -> + inputText = maxItemsDatabase.toString() + setSwitchAction({ isChecked -> + inputText = if (!isChecked) { + INFINITE_MAX_HISTORY_ITEMS.toString() + } else + DEFAULT_MAX_HISTORY_ITEMS.toString() + showInputText(isChecked) + }, maxItemsDatabase > INFINITE_MAX_HISTORY_ITEMS) + } + } + + override fun onDialogClosed(positiveResult: Boolean) { + if (positiveResult) { + database?.let { database -> + var maxHistoryItems: Int = try { + inputText.toInt() + } catch (e: NumberFormatException) { + DEFAULT_MAX_HISTORY_ITEMS + } + if (maxHistoryItems < INFINITE_MAX_HISTORY_ITEMS) { + maxHistoryItems = INFINITE_MAX_HISTORY_ITEMS + } + + val oldMaxHistoryItems = database.historyMaxItems + database.historyMaxItems = maxHistoryItems + + progressDialogThread?.startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems, maxHistoryItems) + } + } + } + + companion object { + + const val DEFAULT_MAX_HISTORY_ITEMS = 10 + const val INFINITE_MAX_HISTORY_ITEMS = -1 + + fun newInstance(key: String): MaxHistoryItemsPreferenceDialogFragmentCompat { + val fragment = MaxHistoryItemsPreferenceDialogFragmentCompat() + val bundle = Bundle(1) + bundle.putString(ARG_KEY, key) + fragment.arguments = bundle + + return fragment + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MaxHistorySizePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MaxHistorySizePreferenceDialogFragmentCompat.kt new file mode 100644 index 000000000..ca709a2b5 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MaxHistorySizePreferenceDialogFragmentCompat.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePass DX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.settings.preferencedialogfragment + +import android.os.Bundle +import android.view.View +import com.kunzisoft.keepass.R + +class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { + + override fun onBindDialogView(view: View) { + super.onBindDialogView(view) + + setExplanationText(R.string.max_history_size_summary) + database?.historyMaxSize?.let { maxItemsDatabase -> + inputText = maxItemsDatabase.toString() + setSwitchAction({ isChecked -> + inputText = if (!isChecked) { + INFINITE_MAX_HISTORY_SIZE.toString() + } else + DEFAULT_MAX_HISTORY_SIZE.toString() + showInputText(isChecked) + }, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE) + } + } + + override fun onDialogClosed(positiveResult: Boolean) { + if (positiveResult) { + database?.let { database -> + var maxHistorySize: Long = try { + inputText.toLong() + } catch (e: NumberFormatException) { + DEFAULT_MAX_HISTORY_SIZE + } + if (maxHistorySize < INFINITE_MAX_HISTORY_SIZE) { + maxHistorySize = INFINITE_MAX_HISTORY_SIZE + } + + val oldMaxHistorySize = database.historyMaxSize + database.historyMaxSize = maxHistorySize + + progressDialogThread?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, maxHistorySize) + } + } + } + + companion object { + + const val DEFAULT_MAX_HISTORY_SIZE = 134217728L + const val INFINITE_MAX_HISTORY_SIZE = -1L + + fun newInstance(key: String): MaxHistorySizePreferenceDialogFragmentCompat { + val fragment = MaxHistorySizePreferenceDialogFragmentCompat() + val bundle = Bundle(1) + bundle.putString(ARG_KEY, key) + fragment.arguments = bundle + + return fragment + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MemoryUsagePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MemoryUsagePreferenceDialogFragmentCompat.kt index 72efaaca7..c2f6e41a0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MemoryUsagePreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MemoryUsagePreferenceDialogFragmentCompat.kt @@ -21,58 +21,42 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment import android.os.Bundle import android.view.View -import android.widget.Toast import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.tasks.ActionRunnable -class MemoryUsagePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() { +class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { override fun onBindDialogView(view: View) { super.onBindDialogView(view) setExplanationText(R.string.memory_usage_explanation) - inputText = database?.memoryUsageAsString ?: "" + inputText = database?.memoryUsage?.toString()?: MIN_MEMORY_USAGE.toString() } override fun onDialogClosed(positiveResult: Boolean) { - if (database != null && positiveResult) { - var memoryUsage: Long - try { - val stringMemory = inputText - memoryUsage = java.lang.Long.parseLong(stringMemory) - } catch (e: NumberFormatException) { - Toast.makeText(context, R.string.error_rounds_not_number, Toast.LENGTH_LONG).show() // TODO change error - return - } - - if (memoryUsage < 1) { - memoryUsage = 1 - } + if (positiveResult) { + database?.let { database -> + var memoryUsage: Long = try { + inputText.toLong() + } catch (e: NumberFormatException) { + MIN_MEMORY_USAGE + } + if (memoryUsage < MIN_MEMORY_USAGE) { + memoryUsage = MIN_MEMORY_USAGE + } + // TODO Max Memory - val oldMemoryUsage = database!!.memoryUsage - database!!.memoryUsage = memoryUsage - - actionInUIThreadAfterSaveDatabase = AfterMemorySave(memoryUsage, oldMemoryUsage) - } + val oldMemoryUsage = database.memoryUsage + database.memoryUsage = memoryUsage - super.onDialogClosed(positiveResult) - } - - private inner class AfterMemorySave(private val mNewMemory: Long, - private val mOldMemory: Long) - : ActionRunnable() { - - override fun onFinishRun(result: Result) { - val memoryToShow = mNewMemory - if (!result.isSuccess) { - database?.memoryUsage = mOldMemory + progressDialogThread?.startDatabaseSaveMemoryUsage(oldMemoryUsage, memoryUsage) } - preference.summary = memoryToShow.toString() } } companion object { + const val MIN_MEMORY_USAGE = 1L + fun newInstance(key: String): MemoryUsagePreferenceDialogFragmentCompat { val fragment = MemoryUsagePreferenceDialogFragmentCompat() val bundle = Bundle(1) diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/ParallelismPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/ParallelismPreferenceDialogFragmentCompat.kt index 550ac18b1..2d675a54b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/ParallelismPreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/ParallelismPreferenceDialogFragmentCompat.kt @@ -21,58 +21,42 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment import android.os.Bundle import android.view.View -import android.widget.Toast import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.tasks.ActionRunnable -class ParallelismPreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() { +class ParallelismPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { override fun onBindDialogView(view: View) { super.onBindDialogView(view) setExplanationText(R.string.parallelism_explanation) - inputText = database?.parallelismAsString ?: "" + inputText = database?.parallelism?.toString() ?: MIN_PARALLELISM.toString() } override fun onDialogClosed(positiveResult: Boolean) { - if (database != null && positiveResult) { - var parallelism: Int - try { - val stringParallelism = inputText - parallelism = Integer.parseInt(stringParallelism) - } catch (e: NumberFormatException) { - Toast.makeText(context, R.string.error_rounds_not_number, Toast.LENGTH_LONG).show() // TODO change error - return - } - - if (parallelism < 1) { - parallelism = 1 - } + if (positiveResult) { + database?.let { database -> + var parallelism: Int = try { + inputText.toInt() + } catch (e: NumberFormatException) { + MIN_PARALLELISM + } + if (parallelism < MIN_PARALLELISM) { + parallelism = MIN_PARALLELISM + } + // TODO Max Parallelism - val oldParallelism = database!!.parallelism - database?.parallelism = parallelism - - actionInUIThreadAfterSaveDatabase = AfterParallelismSave(parallelism, oldParallelism) - } + val oldParallelism = database.parallelism + database.parallelism = parallelism - super.onDialogClosed(positiveResult) - } - - private inner class AfterParallelismSave(private val mNewParallelism: Int, - private val mOldParallelism: Int) - : ActionRunnable() { - - override fun onFinishRun(result: Result) { - val parallelismToShow = mNewParallelism - if (!result.isSuccess) { - database?.parallelism = mOldParallelism + progressDialogThread?.startDatabaseSaveParallelism(oldParallelism, parallelism) } - preference.summary = parallelismToShow.toString() } } companion object { + const val MIN_PARALLELISM = 1 + fun newInstance(key: String): ParallelismPreferenceDialogFragmentCompat { val fragment = ParallelismPreferenceDialogFragmentCompat() val bundle = Bundle(1) diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/RoundsPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/RoundsPreferenceDialogFragmentCompat.kt index a793f708e..7faa0d370 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/RoundsPreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/RoundsPreferenceDialogFragmentCompat.kt @@ -23,61 +23,46 @@ import android.os.Bundle import android.view.View import android.widget.Toast import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.tasks.ActionRunnable -class RoundsPreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() { +class RoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { override fun onBindDialogView(view: View) { super.onBindDialogView(view) explanationText = getString(R.string.rounds_explanation) - inputText = database?.numberKeyEncryptionRoundsAsString ?: "" + inputText = database?.numberKeyEncryptionRounds?.toString() ?: MIN_ITERATIONS.toString() } override fun onDialogClosed(positiveResult: Boolean) { - if (database != null && positiveResult) { - var rounds: Long - try { - val strRounds = inputText - rounds = java.lang.Long.parseLong(strRounds) - } catch (e: NumberFormatException) { - Toast.makeText(context, R.string.error_rounds_not_number, Toast.LENGTH_LONG).show() - return - } + if (positiveResult) { + database?.let { database -> + var rounds: Long = try { + inputText.toLong() + } catch (e: NumberFormatException) { + MIN_ITERATIONS + } + if (rounds < MIN_ITERATIONS) { + rounds = MIN_ITERATIONS + } + // TODO Max iterations - if (rounds < 1) { - rounds = 1 - } + val oldRounds = database.numberKeyEncryptionRounds + try { + database.numberKeyEncryptionRounds = rounds + } catch (e: NumberFormatException) { + Toast.makeText(context, R.string.error_rounds_too_large, Toast.LENGTH_LONG).show() + database.numberKeyEncryptionRounds = Long.MAX_VALUE + } - val oldRounds = database!!.numberKeyEncryptionRounds - try { - database?.numberKeyEncryptionRounds = rounds - } catch (e: NumberFormatException) { - Toast.makeText(context, R.string.error_rounds_too_large, Toast.LENGTH_LONG).show() - database?.numberKeyEncryptionRounds = Integer.MAX_VALUE.toLong() + progressDialogThread?.startDatabaseSaveIterations(oldRounds, rounds) } - - actionInUIThreadAfterSaveDatabase = AfterRoundSave(rounds, oldRounds) - } - - super.onDialogClosed(positiveResult) - } - - private inner class AfterRoundSave(private val mNewRounds: Long, - private val mOldRounds: Long) : ActionRunnable() { - - override fun onFinishRun(result: Result) { - val roundsToShow = mNewRounds - if (!result.isSuccess) { - database?.numberKeyEncryptionRounds = mOldRounds - } - - preference.summary = roundsToShow.toString() } } companion object { + const val MIN_ITERATIONS = 1L + fun newInstance(key: String): RoundsPreferenceDialogFragmentCompat { val fragment = RoundsPreferenceDialogFragmentCompat() val bundle = Bundle(1) diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/adapter/ListRadioItemAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/adapter/ListRadioItemAdapter.kt index db67c4e78..00ece0107 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/adapter/ListRadioItemAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/adapter/ListRadioItemAdapter.kt @@ -27,7 +27,7 @@ import android.view.ViewGroup import android.widget.RadioButton import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.database.ObjectNameResource +import com.kunzisoft.keepass.utils.ObjectNameResource import java.util.ArrayList diff --git a/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt index d75e1ac1e..a0eba5888 100644 --- a/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt @@ -19,75 +19,49 @@ */ package com.kunzisoft.keepass.tasks -import android.app.Activity -import android.content.Context import android.os.Bundle -import android.util.Log -import android.widget.Toast +import com.kunzisoft.keepass.database.exception.LoadDatabaseException /** * Callback after a task is completed. */ -abstract class ActionRunnable(private var nestedActionRunnable: ActionRunnable? = null, - private var executeNestedActionIfResultFalse: Boolean = false) - : Runnable { +abstract class ActionRunnable: Runnable { var result: Result = Result() - private fun execute() { - nestedActionRunnable?.let { - // Pass on result on call finish - it.result = result - it.run() - } - onFinishRun(result) - } - override fun run() { - execute() + onStartRun() + onActionRun() + onFinishRun() } - /** - * If [success] or [executeNestedActionIfResultFalse] true, - * launch the nested action runnable if exists and finish, - * else directly finish - */ - protected fun finishRun(isSuccess: Boolean, message: String? = null) { - result.isSuccess = isSuccess - result.message = message - if (isSuccess || executeNestedActionIfResultFalse) { - execute() - } - else - onFinishRun(result) - } + abstract fun onStartRun() + + abstract fun onActionRun() /** * Method called when the action is finished - * @param result 'true' if success action, 'false' elsewhere, with message */ - abstract fun onFinishRun(result: Result) + abstract fun onFinishRun() - /** - * Display a message as a Toast only if [context] is an Activity - * @param context Context to show the message - */ - protected fun displayMessage(context: Context) { - val message = result.message - Log.i(ActionRunnable::class.java.name, message) - try { - (context as Activity).runOnUiThread { - message?.let { - if (it.isNotEmpty()) { - Toast.makeText(context, message, Toast.LENGTH_LONG).show() - } - } - } - } catch (exception: ClassCastException) {} + protected fun setError(message: String? = null) { + setError(null, message) } + protected fun setError(exception: LoadDatabaseException?, + message: String? = null) { + result.isSuccess = false + result.exception = exception + result.message = message + } + + + /** * Class to manage result from ActionRunnable */ - data class Result(var isSuccess: Boolean = true, var message: String? = null, var data: Bundle? = null) + data class Result(var isSuccess: Boolean = true, + var message: String? = null, + var exception: LoadDatabaseException? = null, + var data: Bundle? = null) } diff --git a/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskDialogFragment.kt index 32eee39f5..583ff7f71 100644 --- a/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskDialogFragment.kt @@ -21,7 +21,6 @@ package com.kunzisoft.keepass.tasks import android.annotation.SuppressLint import android.app.Dialog -import android.content.DialogInterface import android.os.Bundle import androidx.annotation.StringRes import androidx.fragment.app.DialogFragment @@ -31,8 +30,6 @@ import android.view.View import android.widget.ProgressBar import android.widget.TextView import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.view.lockScreenOrientation -import com.kunzisoft.keepass.view.unlockScreenOrientation open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater { @@ -77,11 +74,6 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater { return super.onCreateDialog(savedInstanceState) } - override fun onDismiss(dialog: DialogInterface) { - activity?.unlockScreenOrientation() - super.onDismiss(dialog) - } - fun setTitle(@StringRes titleId: Int) { this.title = titleId } @@ -116,36 +108,27 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater { private const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment" - private const val UNDEFINED = -1 + const val UNDEFINED = -1 - fun build(@StringRes titleId: Int, - @StringRes messageId: Int? = null, - @StringRes warningId: Int? = null): ProgressTaskDialogFragment { - // Create an instance of the dialog fragment and show it - val dialog = ProgressTaskDialogFragment() - dialog.updateTitle(titleId) - messageId?.let { - dialog.updateMessage(it) - } - warningId?.let { - dialog.updateWarning(it) - } - return dialog + fun build(): ProgressTaskDialogFragment { + // Create an instance of the dialog fragment + return ProgressTaskDialogFragment() } fun start(activity: FragmentActivity, dialog: ProgressTaskDialogFragment) { - activity.lockScreenOrientation() - dialog.show(activity.supportFragmentManager, PROGRESS_TASK_DIALOG_TAG) + activity.runOnUiThread { + dialog.show(activity.supportFragmentManager, PROGRESS_TASK_DIALOG_TAG) + } + } + + fun retrieveProgressDialog(activity: FragmentActivity): ProgressTaskDialogFragment? { + return activity.supportFragmentManager + .findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment? } fun stop(activity: FragmentActivity) { - val fragmentTask = activity.supportFragmentManager.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) - if (fragmentTask != null) { - val loadingDatabaseDialog = fragmentTask as ProgressTaskDialogFragment - loadingDatabaseDialog.dismissAllowingStateLoss() - activity.unlockScreenOrientation() - } + retrieveProgressDialog(activity)?.dismissAllowingStateLoss() } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/timeout/ClipboardHelper.kt b/app/src/main/java/com/kunzisoft/keepass/timeout/ClipboardHelper.kt index a799645b2..646b47601 100644 --- a/app/src/main/java/com/kunzisoft/keepass/timeout/ClipboardHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/timeout/ClipboardHelper.kt @@ -60,8 +60,7 @@ class ClipboardHelper(private val context: Context) { val sClipClear = prefs.getString(context.getString(R.string.clipboard_timeout_key), context.getString(R.string.clipboard_timeout_default)) - val clipClearTime = java.lang.Long.parseLong(sClipClear ?: "60000") - + val clipClearTime = (sClipClear ?: "300000").toLong() if (clipClearTime > 0) { mTimer.schedule(ClearClipboardTask(context, text), clipClearTime) } diff --git a/app/src/main/java/com/kunzisoft/keepass/timeout/TimeoutHelper.kt b/app/src/main/java/com/kunzisoft/keepass/timeout/TimeoutHelper.kt index 0aae1c763..26aaa412f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/timeout/TimeoutHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/timeout/TimeoutHelper.kt @@ -29,6 +29,7 @@ import android.util.Log import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.lock import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.LOCK_ACTION @@ -141,8 +142,11 @@ object TimeoutHelper { /** * Temporarily disable timeout, checkTime() function always return true */ - fun temporarilyDisableTimeout() { + fun temporarilyDisableTimeout(context: Context) { temporarilyDisableTimeout = true + + // Stop the opening notification + DatabaseOpenNotificationService.stop(context) } /** @@ -150,9 +154,15 @@ object TimeoutHelper { */ fun releaseTemporarilyDisableTimeoutAndLockIfTimeout(context: Context): Boolean { temporarilyDisableTimeout = false - return if (context is LockingActivity) + val inTime = if (context is LockingActivity) { checkTimeAndLockIfTimeout(context) - else + } else { checkTime(context) + } + if (inTime) { + // Start the opening notification + DatabaseOpenNotificationService.startIfAllowed(context) + } + return inTime } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt index f75dd8290..8b8f9d8b2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt @@ -1,5 +1,9 @@ package com.kunzisoft.keepass.utils +const val DATABASE_START_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_START_TASK_ACTION" +const val DATABASE_STOP_TASK_ACTION = "com.kunzisoft.keepass.DATABASE_STOP_TASK_ACTION" + const val LOCK_ACTION = "com.kunzisoft.keepass.LOCK" + const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY_MAGIKEYBOARD" diff --git a/app/src/main/java/com/kunzisoft/keepass/database/ObjectNameResource.kt b/app/src/main/java/com/kunzisoft/keepass/utils/ObjectNameResource.kt similarity index 96% rename from app/src/main/java/com/kunzisoft/keepass/database/ObjectNameResource.kt rename to app/src/main/java/com/kunzisoft/keepass/utils/ObjectNameResource.kt index c2c0cb9f7..180568fd5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/ObjectNameResource.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/ObjectNameResource.kt @@ -17,7 +17,7 @@ * along with KeePass DX. If not, see . * */ -package com.kunzisoft.keepass.database +package com.kunzisoft.keepass.utils import android.content.res.Resources diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt index 92f0dba58..c70b6630f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt @@ -19,7 +19,6 @@ */ package com.kunzisoft.keepass.utils -import android.content.ActivityNotFoundException import android.content.ContentResolver import android.content.Context import android.content.Intent @@ -27,7 +26,6 @@ import android.net.Uri import android.os.Build import android.widget.Toast import com.kunzisoft.keepass.R -import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException import java.io.InputStream @@ -35,89 +33,6 @@ import java.io.InputStream object UriUtil { - /** - * Many android apps respond with non-writeable content URIs that correspond to files. - * This will attempt to translate the content URIs to file URIs when possible/appropriate - * @param uri - * @return - */ - fun translateUri(ctx: Context, uri: Uri): Uri { - var currentUri = uri - // StorageAF provides nice URIs - if (hasWritableContentUri(currentUri)) { - return currentUri - } - - val scheme = currentUri.scheme - if (scheme == null || scheme.isEmpty()) { - return currentUri - } - - var filepath: String? = null - - try { - // Use content resolver to try and find the file - if (scheme.equals("content", ignoreCase = true)) { - val cursor = ctx.contentResolver.query(currentUri, arrayOf(android.provider.MediaStore.Images.ImageColumns.DATA), null, null, null) - if (cursor != null) { - cursor.moveToFirst() - filepath = cursor.getString(0) - cursor.close() - if (!isValidFilePath(filepath)) { - filepath = null - } - } - } - - // Try using the URI path as a straight file - if (filepath == null || filepath.isEmpty()) { - filepath = currentUri.encodedPath - if (!isValidFilePath(filepath)) { - filepath = null - } - } - } catch (e: Exception) { - filepath = null - } - // Fall back to URI if this fails. - - // Update the file to a file URI - if (filepath != null && filepath.isNotEmpty()) { - val b = Uri.Builder() - currentUri = b.scheme("file").authority("").path(filepath).build() - } - - return currentUri - } - - private fun isValidFilePath(filepath: String?): Boolean { - if (filepath == null || filepath.isEmpty()) { - return false - } - val file = File(filepath) - return file.exists() && file.canRead() - } - - /** - * Whitelist for known content providers that support writing - * @param uri - * @return - */ - private fun hasWritableContentUri(uri: Uri): Boolean { - val scheme = uri.scheme - if (scheme == null || scheme.isEmpty()) { - return false - } - if (!scheme.equals("content", ignoreCase = true)) { - return false - } - when (uri.authority) { - "com.google.android.apps.docs.storage" -> return true - } - - return false - } - @Throws(FileNotFoundException::class) fun getUriInputStream(contentResolver: ContentResolver, uri: Uri?): InputStream? { if (uri == null) @@ -132,38 +47,9 @@ object UriUtil { } } - fun verifyFileUri(fileUri: Uri?): Boolean { - - if (fileUri == null || fileUri == Uri.EMPTY) - return false - - val scheme = fileUri.scheme - return when { - scheme == null || scheme.isEmpty() -> { - false - } - scheme.equals("file", ignoreCase = true) -> { - val filePath = fileUri.path - if (filePath == null || filePath.isEmpty()) - false - else { - File(filePath).exists() - } - } - scheme.equals("content", ignoreCase = true) -> { - true - } - else -> false - } - } - fun parse(stringUri: String?): Uri? { return if (stringUri?.isNotEmpty() == true) { - val uriParsed = Uri.parse(stringUri) - if (verifyFileUri(uriParsed)) - uriParsed - else - null + Uri.parse(stringUri) } else null } @@ -193,18 +79,16 @@ object UriUtil { return null } - @Throws(ActivityNotFoundException::class) fun gotoUrl(context: Context, url: String?) { try { if (url != null && url.isNotEmpty()) { - context.startActivity(Intent(Intent.ACTION_VIEW, parse(url))) + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } - } catch (e: ActivityNotFoundException) { + } catch (e: Exception) { Toast.makeText(context, R.string.no_url_handler, Toast.LENGTH_LONG).show() } } - @Throws(ActivityNotFoundException::class) fun gotoUrl(context: Context, resId: Int) { gotoUrl(context, context.getString(resId)) } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt index d2d36a523..d4da8e9ff 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt @@ -20,18 +20,24 @@ package com.kunzisoft.keepass.view import android.content.Context import android.graphics.Color -import androidx.core.content.ContextCompat -import android.text.method.PasswordTransformationMethod import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.LinearLayout +import android.widget.ProgressBar import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.adapters.EntryHistoryAdapter +import com.kunzisoft.keepass.database.element.EntryVersioned +import com.kunzisoft.keepass.database.element.PwDate import com.kunzisoft.keepass.database.element.security.ProtectedString -import java.text.DateFormat +import com.kunzisoft.keepass.otp.OtpElement +import com.kunzisoft.keepass.otp.OtpType import java.util.* class EntryContentsView @JvmOverloads constructor(context: Context, @@ -50,6 +56,13 @@ class EntryContentsView @JvmOverloads constructor(context: Context, private val passwordView: TextView private val passwordActionView: ImageView + private val otpContainerView: View + private val otpLabelView: TextView + private val otpView: TextView + private val otpActionView: ImageView + + private var otpRunnable: Runnable? = null + private val urlContainerView: View private val urlView: TextView @@ -59,16 +72,18 @@ class EntryContentsView @JvmOverloads constructor(context: Context, private val extrasContainerView: View private val extrasView: ViewGroup - private val dateFormat: DateFormat = android.text.format.DateFormat.getDateFormat(context) - private val timeFormat: DateFormat = android.text.format.DateFormat.getTimeFormat(context) - private val creationDateView: TextView private val modificationDateView: TextView private val lastAccessDateView: TextView + private val expiresImageView: ImageView private val expiresDateView: TextView private val uuidView: TextView + private val historyContainerView: View + private val historyListView: RecyclerView + private val historyAdapter = EntryHistoryAdapter(context) + val isUserNamePresent: Boolean get() = userNameContainerView.visibility == View.VISIBLE @@ -87,6 +102,11 @@ class EntryContentsView @JvmOverloads constructor(context: Context, passwordView = findViewById(R.id.entry_password) passwordActionView = findViewById(R.id.entry_password_action_image) + otpContainerView = findViewById(R.id.entry_otp_container) + otpLabelView = findViewById(R.id.entry_otp_label) + otpView = findViewById(R.id.entry_otp) + otpActionView = findViewById(R.id.entry_otp_action_image) + urlContainerView = findViewById(R.id.entry_url_container) urlView = findViewById(R.id.entry_url) @@ -99,10 +119,18 @@ class EntryContentsView @JvmOverloads constructor(context: Context, creationDateView = findViewById(R.id.entry_created) modificationDateView = findViewById(R.id.entry_modified) lastAccessDateView = findViewById(R.id.entry_accessed) - expiresDateView = findViewById(R.id.entry_expires) + expiresImageView = findViewById(R.id.entry_expires_image) + expiresDateView = findViewById(R.id.entry_expires_date) uuidView = findViewById(R.id.entry_UUID) + historyContainerView = findViewById(R.id.entry_history_container) + historyListView = findViewById(R.id.entry_history_list) + historyListView?.apply { + layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) + adapter = historyAdapter + } + val attrColorAccent = intArrayOf(R.attr.colorAccent) val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent) colorAccent = taColorAccent.getColor(0, Color.BLACK) @@ -170,11 +198,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context, } fun setHiddenPasswordStyle(hiddenStyle: Boolean) { - if (!hiddenStyle) { - passwordView.transformationMethod = null - } else { - passwordView.transformationMethod = PasswordTransformationMethod.getInstance() - } + passwordView.applyHiddenStyle(hiddenStyle) // Hidden style for custom fields extrasView.let { for (i in 0 until it.childCount) { @@ -185,6 +209,56 @@ class EntryContentsView @JvmOverloads constructor(context: Context, } } + fun assignOtp(otpElement: OtpElement?, + otpProgressView: ProgressBar?, + onClickListener: OnClickListener) { + otpContainerView.removeCallbacks(otpRunnable) + + if (otpElement != null) { + otpContainerView.visibility = View.VISIBLE + + if (otpElement.token.isEmpty()) { + otpView.text = context.getString(R.string.error_invalid_OTP) + otpActionView.setColorFilter(ContextCompat.getColor(context, R.color.grey_dark)) + assignOtpCopyListener(null) + } else { + assignOtpCopyListener(onClickListener) + otpView.text = otpElement.token + otpLabelView.text = otpElement.type.name + + when (otpElement.type) { + // Only add token if HOTP + OtpType.HOTP -> { + otpProgressView?.visibility = View.GONE + } + // Refresh view if TOTP + OtpType.TOTP -> { + otpProgressView?.apply { + max = otpElement.period + progress = otpElement.secondsRemaining + visibility = View.VISIBLE + } + otpRunnable = Runnable { + if (otpElement.shouldRefreshToken()) { + otpView.text = otpElement.token + } + otpProgressView?.progress = otpElement.secondsRemaining + otpContainerView.postDelayed(otpRunnable, 1000) + } + otpContainerView.post(otpRunnable) + } + } + } + } else { + otpContainerView.visibility = View.GONE + otpProgressView?.visibility = View.GONE + } + } + + fun assignOtpCopyListener(onClickListener: OnClickListener?) { + otpActionView.setOnClickListener(onClickListener) + } + fun assignURL(url: String?) { if (url != null && url.isNotEmpty()) { urlContainerView.visibility = View.VISIBLE @@ -230,24 +304,24 @@ class EntryContentsView @JvmOverloads constructor(context: Context, extrasContainerView.visibility = View.GONE } - private fun getDateTime(date: Date): String { - return dateFormat.format(date) + " " + timeFormat.format(date) + fun assignCreationDate(date: PwDate) { + creationDateView.text = date.getDateTimeString(resources) } - fun assignCreationDate(date: Date) { - creationDateView.text = getDateTime(date) + fun assignModificationDate(date: PwDate) { + modificationDateView.text = date.getDateTimeString(resources) } - fun assignModificationDate(date: Date) { - modificationDateView.text = getDateTime(date) + fun assignLastAccessDate(date: PwDate) { + lastAccessDateView.text = date.getDateTimeString(resources) } - fun assignLastAccessDate(date: Date) { - lastAccessDateView.text = getDateTime(date) + fun setExpires(isExpires: Boolean) { + expiresImageView.visibility = if (isExpires) View.VISIBLE else View.GONE } - fun assignExpiresDate(date: Date) { - expiresDateView.text = getDateTime(date) + fun assignExpiresDate(date: PwDate) { + assignExpiresDate(date.getDateTimeString(resources)) } fun assignExpiresDate(constString: String) { @@ -258,6 +332,21 @@ class EntryContentsView @JvmOverloads constructor(context: Context, uuidView.text = uuid.toString() } + fun showHistory(show: Boolean) { + historyContainerView.visibility = if (show) View.VISIBLE else View.GONE + } + + fun assignHistory(history: ArrayList) { + historyAdapter.clear() + historyAdapter.entryHistoryList.addAll(history) + } + + fun onHistoryClick(action: (historyItem: EntryVersioned, position: Int)->Unit) { + historyAdapter.onItemClickListener = { item, position -> + action.invoke(item, position) + } + } + override fun generateDefaultLayoutParams(): LayoutParams { return LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryCustomField.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryCustomField.kt index 6935972ba..992660b5e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryCustomField.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryCustomField.kt @@ -21,13 +21,12 @@ package com.kunzisoft.keepass.view import android.content.Context import android.graphics.Color -import android.text.method.PasswordTransformationMethod -import androidx.core.content.ContextCompat import android.util.AttributeSet import android.view.LayoutInflater import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.core.content.ContextCompat import com.kunzisoft.keepass.R open class EntryCustomField @JvmOverloads constructor(context: Context, @@ -72,11 +71,7 @@ open class EntryCustomField @JvmOverloads constructor(context: Context, } fun setHiddenPasswordStyle(hiddenStyle: Boolean) { - if (isProtected && hiddenStyle) { - valueView.transformationMethod = PasswordTransformationMethod.getInstance() - } else { - valueView.transformationMethod = null - } + valueView.applyHiddenStyle(isProtected && hiddenStyle) } fun enableActionButton(enable: Boolean) { diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryEditContentsView.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryEditContentsView.kt index 70e62d6a3..d3354da34 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryEditContentsView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryEditContentsView.kt @@ -36,7 +36,7 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context, val generatePasswordView: View private val entryCommentView: EditText private val entryExtraFieldsContainer: ViewGroup - val addNewFieldView: View + val addNewFieldButton: View private var iconColor: Int = 0 @@ -55,7 +55,7 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context, generatePasswordView = findViewById(R.id.entry_edit_generate_button) entryCommentView = findViewById(R.id.entry_edit_notes) entryExtraFieldsContainer = findViewById(R.id.entry_edit_advanced_container) - addNewFieldView = findViewById(R.id.entry_edit_add_new_field) + addNewFieldButton = findViewById(R.id.entry_edit_add_new_field) // Retrieve the textColor to tint the icon val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) @@ -137,7 +137,7 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context, } fun allowCustomField(allow: Boolean, action: () -> Unit) { - addNewFieldView.apply { + addNewFieldButton.apply { if (allow) { visibility = View.VISIBLE setOnClickListener { action.invoke() } @@ -165,15 +165,42 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context, } /** - * Add a new view to fill in the information of the customized field + * Add a new view to fill in the information of the customized field and focus it */ - fun addNewCustomField(name: String = "", value:ProtectedString = ProtectedString(false, "")) { - val entryEditCustomField = EntryEditCustomField(context) - entryEditCustomField.setData(name, value) - entryEditCustomField.setFontVisibility(fontInVisibility) + fun addEmptyCustomField() { + val entryEditCustomField = EntryEditCustomField(context).apply { + setFontVisibility(fontInVisibility) + requestFocus() + } entryExtraFieldsContainer.addView(entryEditCustomField) } + /** + * Update a custom field or create a new one if doesn't exists + */ + fun putCustomField(name: String, + value: ProtectedString = ProtectedString()) { + var updateField = false + for (i in 0..entryExtraFieldsContainer.childCount) { + try { + val extraFieldView = entryExtraFieldsContainer.getChildAt(i) as EntryEditCustomField? + if (extraFieldView?.label == name) { + extraFieldView.setData(name, value, fontInVisibility) + updateField = true + break + } + } catch(e: Exception) { + // Simply ignore when child view is not a custom field + } + } + if (!updateField) { + val entryEditCustomField = EntryEditCustomField(context).apply { + setData(name, value, fontInVisibility) + } + entryExtraFieldsContainer.addView(entryEditCustomField) + } + } + /** * Validate or not the entry form * diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryEditCustomField.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryEditCustomField.kt index 50ef8a702..a023ff52e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryEditCustomField.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryEditCustomField.kt @@ -66,13 +66,14 @@ class EntryEditCustomField @JvmOverloads constructor(context: Context, protectionCheckView = findViewById(R.id.protection) } - fun setData(label: String?, value: ProtectedString?) { + fun setData(label: String?, value: ProtectedString?, fontInVisibility: Boolean) { if (label != null) labelView.text = label if (value != null) { valueView.setText(value.toString()) protectionCheckView.isChecked = value.isProtected } + setFontVisibility(fontInVisibility) } /** diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt b/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt new file mode 100644 index 000000000..c55391f87 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt @@ -0,0 +1,124 @@ +package com.kunzisoft.keepass.view + +import android.content.Context +import android.util.AttributeSet +import android.view.Menu +import android.view.MenuInflater +import android.view.View +import androidx.appcompat.view.ActionMode +import androidx.appcompat.view.SupportMenuInflater +import androidx.appcompat.widget.Toolbar +import com.kunzisoft.keepass.R + +class ToolbarAction @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyle: Int = androidx.appcompat.R.attr.toolbarStyle) + : Toolbar(context, attrs, defStyle) { + + private var mActionModeCallback: ActionMode.Callback? = null + private val actionMode = NodeActionMode(this) + private var isOpen = false + + init { + visibility = View.GONE + } + + fun startSupportActionMode(actionModeCallback: ActionMode.Callback): ActionMode { + mActionModeCallback?.onDestroyActionMode(actionMode) + mActionModeCallback = actionModeCallback + mActionModeCallback?.onCreateActionMode(actionMode, menu) + mActionModeCallback?.onPrepareActionMode(actionMode, menu) + + setOnMenuItemClickListener { + mActionModeCallback?.onActionItemClicked(actionMode, it) ?: false + } + setNavigationOnClickListener{ + actionMode.finish() + } + + setNavigationIcon(R.drawable.ic_close_white_24dp) + + open() + + return actionMode + } + + fun getSupportActionModeCallback(): ActionMode.Callback? { + return mActionModeCallback + } + + fun removeSupportActionModeCallback() { + mActionModeCallback = null + } + + fun invalidateMenu() { + open() + mActionModeCallback?.onPrepareActionMode(actionMode, menu) + } + + fun open() { + if (!isOpen) { + isOpen = true + expand() + } + } + + fun close() { + if (isOpen) { + isOpen = false + collapse() + } + mActionModeCallback?.onDestroyActionMode(actionMode) + } + + private class NodeActionMode(var toolbarAction: ToolbarAction): ActionMode() { + + override fun finish() { + menu.clear() + toolbarAction.close() + toolbarAction.removeSupportActionModeCallback() + } + + override fun getMenu(): Menu { + return toolbarAction.menu + } + + override fun getCustomView(): View { + return toolbarAction + } + + override fun setCustomView(view: View?) {} + + override fun getMenuInflater(): MenuInflater { + return SupportMenuInflater(toolbarAction.context) + } + + override fun invalidate() { + toolbarAction.invalidateMenu() + } + + override fun getSubtitle(): CharSequence { + return toolbarAction.subtitle + } + + override fun setTitle(title: CharSequence?) { + toolbarAction.title = title + } + + override fun setTitle(resId: Int) { + toolbarAction.setTitle(resId) + } + + override fun getTitle(): CharSequence { + return toolbarAction.title + } + + override fun setSubtitle(subtitle: CharSequence?) { + toolbarAction.subtitle = subtitle + } + + override fun setSubtitle(resId: Int) { + toolbarAction.setSubtitle(resId) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt index 50fd64c2a..610f531d1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt @@ -19,13 +19,16 @@ */ package com.kunzisoft.keepass.view -import android.app.Activity -import android.content.pm.ActivityInfo -import android.content.res.Configuration +import android.animation.AnimatorSet +import android.animation.ValueAnimator import android.graphics.Color import android.graphics.Typeface -import com.google.android.material.snackbar.Snackbar +import android.text.method.PasswordTransformationMethod +import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator import android.widget.TextView +import androidx.appcompat.widget.Toolbar +import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R /** @@ -36,6 +39,16 @@ fun TextView.applyFontVisibility() { typeface = typeFace } +fun TextView.applyHiddenStyle(hide: Boolean) { + if (hide) { + transformationMethod = PasswordTransformationMethod.getInstance() + maxLines = 1 + } else { + transformationMethod = null + maxLines = 800 + } +} + fun Snackbar.asError(): Snackbar { this.view.apply { setBackgroundColor(Color.RED) @@ -44,15 +57,38 @@ fun Snackbar.asError(): Snackbar { return this } -fun Activity.lockScreenOrientation() { - val currentOrientation = resources.configuration.orientation - requestedOrientation = if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) { - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } else { - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE +fun Toolbar.collapse(animate: Boolean = true) { + val recordBarHeight = layoutParams.height + val slideAnimator = ValueAnimator.ofInt(height, 0) + if (animate) + slideAnimator.duration = 300L + slideAnimator.addUpdateListener { animation -> + layoutParams.height = animation.animatedValue as Int + if (layoutParams.height <= 1) { + visibility = View.GONE + layoutParams.height = recordBarHeight + } + requestLayout() } + AnimatorSet().apply { + play(slideAnimator) + interpolator = AccelerateDecelerateInterpolator() + }.start() } -fun Activity.unlockScreenOrientation() { - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED -} +fun Toolbar.expand(animate: Boolean = true) { + visibility = View.VISIBLE + val actionBarHeight = layoutParams.height + val slideAnimator = ValueAnimator + .ofInt(0, actionBarHeight) + if (animate) + slideAnimator.duration = 300L + slideAnimator.addUpdateListener { animation -> + layoutParams.height = animation.animatedValue as Int + requestLayout() + } + AnimatorSet().apply { + play(slideAnimator) + interpolator = AccelerateDecelerateInterpolator() + }.start() +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/notification_ic_clipboard_key_24dp.png b/app/src/main/res/drawable-hdpi/notification_ic_clipboard_key_24dp.png new file mode 100644 index 000000000..52609ed68 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/notification_ic_clipboard_key_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/notification_ic_database_load.png b/app/src/main/res/drawable-hdpi/notification_ic_database_load.png new file mode 100644 index 000000000..9dccaba9f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/notification_ic_database_load.png differ diff --git a/app/src/main/res/drawable-hdpi/notification_ic_database_open.png b/app/src/main/res/drawable-hdpi/notification_ic_database_open.png new file mode 100644 index 000000000..85ef02d78 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/notification_ic_database_open.png differ diff --git a/app/src/main/res/drawable-hdpi/notification_ic_keyboard_key_24dp.png b/app/src/main/res/drawable-hdpi/notification_ic_keyboard_key_24dp.png new file mode 100644 index 000000000..c4e0172c6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/notification_ic_keyboard_key_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/notification_ic_clipboard_key_24dp.png b/app/src/main/res/drawable-mdpi/notification_ic_clipboard_key_24dp.png new file mode 100644 index 000000000..c6c81f523 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/notification_ic_clipboard_key_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/notification_ic_database_load.png b/app/src/main/res/drawable-mdpi/notification_ic_database_load.png new file mode 100644 index 000000000..90e30dd4e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/notification_ic_database_load.png differ diff --git a/app/src/main/res/drawable-mdpi/notification_ic_database_open.png b/app/src/main/res/drawable-mdpi/notification_ic_database_open.png new file mode 100644 index 000000000..902ceed10 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/notification_ic_database_open.png differ diff --git a/app/src/main/res/drawable-mdpi/notification_ic_keyboard_key_24dp.png b/app/src/main/res/drawable-mdpi/notification_ic_keyboard_key_24dp.png new file mode 100644 index 000000000..bfe25a520 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/notification_ic_keyboard_key_24dp.png differ diff --git a/app/src/main/res/drawable-v21/background_item_selection.xml b/app/src/main/res/drawable-v21/background_item_selection.xml new file mode 100644 index 000000000..4f4b3a3fc --- /dev/null +++ b/app/src/main/res/drawable-v21/background_item_selection.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/notification_ic_clipboard_key_24dp.png b/app/src/main/res/drawable-xhdpi/notification_ic_clipboard_key_24dp.png new file mode 100644 index 000000000..182b6bf18 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/notification_ic_clipboard_key_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/notification_ic_database_load.png b/app/src/main/res/drawable-xhdpi/notification_ic_database_load.png new file mode 100644 index 000000000..596c44869 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/notification_ic_database_load.png differ diff --git a/app/src/main/res/drawable-xhdpi/notification_ic_database_open.png b/app/src/main/res/drawable-xhdpi/notification_ic_database_open.png new file mode 100644 index 000000000..d9f7a89de Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/notification_ic_database_open.png differ diff --git a/app/src/main/res/drawable-xhdpi/notification_ic_keyboard_key_24dp.png b/app/src/main/res/drawable-xhdpi/notification_ic_keyboard_key_24dp.png new file mode 100644 index 000000000..89dc37718 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/notification_ic_keyboard_key_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/notification_ic_clipboard_key_24dp.png b/app/src/main/res/drawable-xxhdpi/notification_ic_clipboard_key_24dp.png new file mode 100644 index 000000000..671b015ee Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/notification_ic_clipboard_key_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/notification_ic_database_load.png b/app/src/main/res/drawable-xxhdpi/notification_ic_database_load.png new file mode 100644 index 000000000..8dd589805 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/notification_ic_database_load.png differ diff --git a/app/src/main/res/drawable-xxhdpi/notification_ic_database_open.png b/app/src/main/res/drawable-xxhdpi/notification_ic_database_open.png new file mode 100644 index 000000000..94a0a01ec Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/notification_ic_database_open.png differ diff --git a/app/src/main/res/drawable-xxhdpi/notification_ic_keyboard_key_24dp.png b/app/src/main/res/drawable-xxhdpi/notification_ic_keyboard_key_24dp.png new file mode 100644 index 000000000..561c78f3c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/notification_ic_keyboard_key_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/notification_ic_clipboard_key_24dp.png b/app/src/main/res/drawable-xxxhdpi/notification_ic_clipboard_key_24dp.png new file mode 100644 index 000000000..2c60a8139 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/notification_ic_clipboard_key_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/notification_ic_database_load.png b/app/src/main/res/drawable-xxxhdpi/notification_ic_database_load.png new file mode 100644 index 000000000..6bfaaf61e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/notification_ic_database_load.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/notification_ic_database_open.png b/app/src/main/res/drawable-xxxhdpi/notification_ic_database_open.png new file mode 100644 index 000000000..78aa6939b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/notification_ic_database_open.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/notification_ic_keyboard_key_24dp.png b/app/src/main/res/drawable-xxxhdpi/notification_ic_keyboard_key_24dp.png new file mode 100644 index 000000000..de004664f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/notification_ic_keyboard_key_24dp.png differ diff --git a/app/src/main/res/drawable/background_item_selection.xml b/app/src/main/res/drawable/background_item_selection.xml new file mode 100644 index 000000000..8d6ff044a --- /dev/null +++ b/app/src/main/res/drawable/background_item_selection.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/capture_keyboard_entry_key.png b/app/src/main/res/drawable/capture_keyboard_entry_key.png deleted file mode 100644 index 0f934700e..000000000 Binary files a/app/src/main/res/drawable/capture_keyboard_entry_key.png and /dev/null differ diff --git a/app/src/main/res/drawable/capture_keyboard_fields_keys.png b/app/src/main/res/drawable/capture_keyboard_fields_keys.png deleted file mode 100644 index c3604dca1..000000000 Binary files a/app/src/main/res/drawable/capture_keyboard_fields_keys.png and /dev/null differ diff --git a/app/src/main/res/drawable/capture_keyboard_lock_key.png b/app/src/main/res/drawable/capture_keyboard_lock_key.png deleted file mode 100644 index 0deb6f745..000000000 Binary files a/app/src/main/res/drawable/capture_keyboard_lock_key.png and /dev/null differ diff --git a/app/src/main/res/drawable/capture_keyboard_switch_key.png b/app/src/main/res/drawable/capture_keyboard_switch_key.png deleted file mode 100644 index a19f4049d..000000000 Binary files a/app/src/main/res/drawable/capture_keyboard_switch_key.png and /dev/null differ diff --git a/app/src/main/res/drawable/capture_keyboard_switcher_logo.png b/app/src/main/res/drawable/capture_keyboard_switcher_logo.png deleted file mode 100644 index ba0cd6af9..000000000 Binary files a/app/src/main/res/drawable/capture_keyboard_switcher_logo.png and /dev/null differ diff --git a/app/src/main/res/drawable/capture_type_password.png b/app/src/main/res/drawable/capture_type_password.png deleted file mode 100644 index 7a6c072a8..000000000 Binary files a/app/src/main/res/drawable/capture_type_password.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_av_timer_white_24dp.xml b/app/src/main/res/drawable/ic_av_timer_white_24dp.xml new file mode 100644 index 000000000..36f3f9f6d --- /dev/null +++ b/app/src/main/res/drawable/ic_av_timer_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_clipboard_key_white_24dp.xml b/app/src/main/res/drawable/ic_clipboard_key_white_24dp.xml deleted file mode 100644 index 778c118e7..000000000 --- a/app/src/main/res/drawable/ic_clipboard_key_white_24dp.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_content_paste_white_24dp.xml b/app/src/main/res/drawable/ic_content_paste_white_24dp.xml new file mode 100644 index 000000000..7d2f624c9 --- /dev/null +++ b/app/src/main/res/drawable/ic_content_paste_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_data_usage_white_24dp.xml b/app/src/main/res/drawable/ic_data_usage_white_24dp.xml deleted file mode 100644 index b8c205af5..000000000 --- a/app/src/main/res/drawable/ic_data_usage_white_24dp.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_send_white_24dp.xml b/app/src/main/res/drawable/ic_send_white_24dp.xml deleted file mode 100644 index 795b2ba45..000000000 --- a/app/src/main/res/drawable/ic_send_white_24dp.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/prefs_autorenew_24dp.xml b/app/src/main/res/drawable/prefs_autorenew_24dp.xml new file mode 100644 index 000000000..9f8858186 --- /dev/null +++ b/app/src/main/res/drawable/prefs_autorenew_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/prefs_info_24dp.xml b/app/src/main/res/drawable/prefs_info_24dp.xml new file mode 100644 index 000000000..69963122c --- /dev/null +++ b/app/src/main/res/drawable/prefs_info_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/prefs_key_white_24dp.xml b/app/src/main/res/drawable/prefs_key_24dp.xml similarity index 100% rename from app/src/main/res/drawable/prefs_key_white_24dp.xml rename to app/src/main/res/drawable/prefs_key_24dp.xml diff --git a/app/src/main/res/drawable/prefs_security_24dp.xml b/app/src/main/res/drawable/prefs_security_24dp.xml new file mode 100644 index 000000000..baf924f91 --- /dev/null +++ b/app/src/main/res/drawable/prefs_security_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout-v23/fragment_fingerprint_explanation.xml b/app/src/main/res/layout-v23/fragment_fingerprint_explanation.xml deleted file mode 100644 index 5bf5c6e21..000000000 --- a/app/src/main/res/layout-v23/fragment_fingerprint_explanation.xml +++ /dev/null @@ -1,217 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_entry.xml b/app/src/main/res/layout/activity_entry.xml index 5a78631be..88df31be7 100644 --- a/app/src/main/res/layout/activity_entry.xml +++ b/app/src/main/res/layout/activity_entry.xml @@ -72,10 +72,20 @@ tools:targetApi="lollipop"> + + - + + + + + diff --git a/app/src/main/res/layout/activity_file_selection.xml b/app/src/main/res/layout/activity_file_selection.xml index 48c3f3885..dd9639e86 100644 --- a/app/src/main/res/layout/activity_file_selection.xml +++ b/app/src/main/res/layout/activity_file_selection.xml @@ -182,7 +182,7 @@ app:layout_constraintBottom_toBottomOf="parent"> - - - - - - - - - - - - - + app:layout_constraintBottom_toTopOf="@+id/create_database_button"/> + android:layout_above="@+id/toolbar_action"> - + android:orientation="horizontal" + android:gravity="center_vertical"> + + + + - - - - - + android:elevation="4dp" + android:theme="?attr/toolbarBottomAppearance" + android:background="?attr/colorAccent" + tools:targetApi="lollipop" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_password.xml b/app/src/main/res/layout/activity_password.xml index aaefd9481..1feb81dc5 100644 --- a/app/src/main/res/layout/activity_password.xml +++ b/app/src/main/res/layout/activity_password.xml @@ -68,7 +68,7 @@ android:layout_marginEnd="12dp" android:text="@string/default_checkbox"/> @@ -186,8 +186,8 @@ android:importantForAutofill="no" android:layout_toEndOf="@+id/keyfile_checkox" android:layout_toRightOf="@+id/keyfile_checkox" - android:layout_toLeftOf="@+id/browse_button" - android:layout_toStartOf="@+id/browse_button"> + android:layout_toLeftOf="@+id/open_database_button" + android:layout_toStartOf="@+id/open_database_button"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_set_otp.xml b/app/src/main/res/layout/fragment_set_otp.xml new file mode 100644 index 000000000..59674d439 --- /dev/null +++ b/app/src/main/res/layout/fragment_set_otp.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_set_password.xml b/app/src/main/res/layout/fragment_set_password.xml index e67ee9e29..26d165322 100644 --- a/app/src/main/res/layout/fragment_set_password.xml +++ b/app/src/main/res/layout/fragment_set_password.xml @@ -114,7 +114,7 @@ android:layout_height="wrap_content"> . --> - @@ -25,7 +27,8 @@ android:id="@+id/title" android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" /> + tools:text="title" + style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" /> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_list_nodes_entry.xml b/app/src/main/res/layout/item_list_nodes_entry.xml index 56a0bbdec..cbe7b3b46 100644 --- a/app/src/main/res/layout/item_list_nodes_entry.xml +++ b/app/src/main/res/layout/item_list_nodes_entry.xml @@ -23,7 +23,8 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/node_container" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + style="@style/KeepassDXStyle.Selectable.Item"> + android:layout_height="wrap_content" + style="@style/KeepassDXStyle.Selectable.Item"> - - + - - \ No newline at end of file + android:layout_height="wrap_content" + android:layout_margin="20dp" + android:text="@string/enable" + android:background="@drawable/background_button_small" + android:textColor="?attr/textColorInverse" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + android:minHeight="48dp"/> + \ No newline at end of file diff --git a/app/src/main/res/layout/pref_dialog_numbers.xml b/app/src/main/res/layout/pref_dialog_input_numbers.xml similarity index 59% rename from app/src/main/res/layout/pref_dialog_numbers.xml rename to app/src/main/res/layout/pref_dialog_input_numbers.xml index c32d52783..5082fd234 100644 --- a/app/src/main/res/layout/pref_dialog_numbers.xml +++ b/app/src/main/res/layout/pref_dialog_input_numbers.xml @@ -17,11 +17,13 @@ You should have received a copy of the GNU General Public License along with KeePass DX. If not, see . --> - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/pref_dialog_input_text.xml b/app/src/main/res/layout/pref_dialog_input_text.xml index 830bf43f1..a62c2ed43 100644 --- a/app/src/main/res/layout/pref_dialog_input_text.xml +++ b/app/src/main/res/layout/pref_dialog_input_text.xml @@ -1,6 +1,6 @@ - + + - \ No newline at end of file + android:id="@+id/input_text" + android:layout_height="wrap_content" + android:layout_width="0dp" + app:layout_constraintTop_toBottomOf="@+id/switch_element" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:minHeight="48dp"/> + \ No newline at end of file diff --git a/app/src/main/res/layout/view_button_add_node.xml b/app/src/main/res/layout/view_button_add_node.xml index 177df25c8..14b0f83a1 100644 --- a/app/src/main/res/layout/view_button_add_node.xml +++ b/app/src/main/res/layout/view_button_add_node.xml @@ -27,6 +27,7 @@ android:layout_alignEnd="@+id/add_button" android:layout_alignRight="@+id/add_button" android:layout_marginBottom="-12dp" + android:descendantFocusability="blocksDescendants" android:visibility="gone"> + + + + + + + - - + android:orientation="horizontal"> + + + @@ -254,7 +297,7 @@ android:layout_margin="@dimen/default_margin" android:orientation="vertical"> - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_entry_edit_contents.xml b/app/src/main/res/layout/view_entry_edit_contents.xml index 609a40ad3..9db6f41ff 100644 --- a/app/src/main/res/layout/view_entry_edit_contents.xml +++ b/app/src/main/res/layout/view_entry_edit_contents.xml @@ -165,8 +165,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="start" - android:lines="4" - android:maxLines="10" + android:maxLines="20" android:importantForAccessibility="no" android:importantForAutofill="no" android:inputType="textMultiLine" diff --git a/app/src/main/res/menu/advanced_unlock.xml b/app/src/main/res/menu/advanced_unlock.xml index 3a71f9213..184f2fb94 100644 --- a/app/src/main/res/menu/advanced_unlock.xml +++ b/app/src/main/res/menu/advanced_unlock.xml @@ -19,9 +19,9 @@ --> - \ No newline at end of file diff --git a/app/src/main/res/menu/edit_entry.xml b/app/src/main/res/menu/edit_entry.xml new file mode 100644 index 000000000..0eb1f795a --- /dev/null +++ b/app/src/main/res/menu/edit_entry.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/app/src/main/res/menu/node_menu.xml b/app/src/main/res/menu/node_menu.xml index 247a14d6f..bf46c2797 100644 --- a/app/src/main/res/menu/node_menu.xml +++ b/app/src/main/res/menu/node_menu.xml @@ -17,31 +17,32 @@ You should have received a copy of the GNU General Public License along with KeePass DX. If not, see . --> - - - - - - + + + + + + diff --git a/app/src/main/res/menu/node_paste_menu.xml b/app/src/main/res/menu/node_paste_menu.xml index 3f51ca3ee..6e3642080 100644 --- a/app/src/main/res/menu/node_paste_menu.xml +++ b/app/src/main/res/menu/node_paste_menu.xml @@ -20,6 +20,7 @@ diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a3cfa9fb1..6220a324c 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -18,7 +18,7 @@ along with KeePass DX. If not, see . --> - الصفحة الرئيسية : + الصفحة الرئيسية قبول إضافة مجموعة التعمية @@ -27,7 +27,6 @@ لا تظهر مرة أخرى أقواس تمديد ASCII - إلغاء السماح مُسِحت الحافظة خطأ في الحافظة @@ -54,7 +53,6 @@ اكتب عنوانًا. اسم الحقل قيمة الحقل - تعذر إيجاد الملف. توليد كلمة سر تأكيد كلمة السر اسم المجموعة @@ -78,7 +76,7 @@ حذف التبرع تعديل - قفل قاعدة البيانات + اقفل قاعدة البيانات فتح البحث إزالة بصمة المفتاح @@ -101,8 +99,8 @@ تصاعدياً العنوان اسم المستخدِم - حسب تاريخ الإنشاء - حسب تاريخ آخر تعديل + تاريخ الإنشاء + تاريخ آخر تعديل خاصّ البحث نتائج البحث @@ -113,11 +111,11 @@ هل أنت متأكد من أنك لا تريد استخدام أي مفتاح تشفير ؟ الإصدار %1$s أضف عناصر جديدة إلى قاعدتك - قم بفتح قاعدة بياناتك ببصمتك + قم بفتح قاعدة بياناتك ببصمتك إضافة حقول مخصصة نسخ حقل تأمين قاعدة البيانات - الملاحظات : + الأصداء تنفيذ أندرويد لمدير كلمات السر «كي‌باس» إضافة مدخلة تحرير مدخلة @@ -130,7 +128,7 @@ مدة التخزين في الحافظة اختر لنسخ %1$s إلى الحافظة يجلب مفتاح قاعدة البيانات… - استخدام هذا كقاعدة البيانات الافتراضية + استخدامها كقاعدة بيانات افتراضية KeePass DX \u00A9 %1$d د كونزيسوفت تأتي مع الضمان لا على الإطلاق؛ هذا هو البرمجيات الحرة، وكنت أهلا إعادة توزيعه تحت شروط إصدار الترخيص 3 أو في وقت لاحق. نُفذ إليه تنتهي صلاحيته في @@ -143,7 +141,6 @@ تعذر تحميل قاعدة البيانات. غير قادر على تحميل المفتاح، في محاولة لتقليل الذاكرة المستخدمة من قبل KDF. يجب تحديد كلمة مرور واحد على الأقل نوع الجيل - يجب أن تكون \"جولات\" عددا. \"جولات\" كبيرة جداً. الإعداد إلى 2147483648. يجب أن يكون لكل سلسلة اسم حقل. أدخل عددًا صحيحًا موجبًا في حقل «الطول». @@ -151,14 +148,14 @@ غير قادر على نقل مجموعة إلى نفسها. تعذر إيجاد الملف. جرِّب فتحه من متصفح ملفات. متصفح الملفات - تعذرت قراءة كلمة السر أو ملف المفتاح. + تعذرت قراءة كلمة السر أو ملف المفتاح. تعذر تمييز نسق قاعدة البيانات. لا يوجد ملف مفتاح. ملف المفتاح فارغ. أظهر أسماء المستخدمين أظهر أسماء المستخدمين في قوائم المدخلات كلمةالسر المشفرة - ملف قفل + الملف المفتاحي اخفاء كلمات السر نُسخ %1$s نسخ @@ -177,8 +174,8 @@ قيد العمل… KeePass DX يحتاج صلاحية الكتابة من اجل تعديل قاعدة البيانات. ابتداءا من اندرويد كيت كات بعض الاجهزة لا تسمح بالكتابة على قاعدة البيانات. - تذكر موقع ملف قفل قاعدة البيانات - حفظ ملف القفل + تذكر موقع ملف المفتاح قاعدة البيانات + حفظ الملف المفتاحي خوارزمية تشفير جميع البيانات. قاعدة بيانات غير مدعومة. اسمح بالكتابة على بطاقة الذاكرة لحفظ التغيرات. @@ -197,11 +194,9 @@ تعيين المحارف المسموح بها لتوليد كلمة السر حافظة اشعارات الحافظة - اذا فشل الحذف التلقائي من الحافظة ,احذف تأريخه يدويا + اذا فشل الحذف التلقائي من الحافظة ,احذف تأريخه يدويا. قفل الشاشة اقفل قاعدة البيانات عند انغلاق الشاشة - ادخل كلمة سر قاعدة البيانات - استخدام حذف مفاتيح التشفير لا يمكن بدأ هذه الميزة . نسخة الاندرويد %1$s لا تحقق ادنى متطلبات السنخة %2$s. @@ -216,23 +211,15 @@ \nاستعد كلمة السر. لم يتعرّف على البصمة استخدم البصمة لحفظ كلمة السر - تأريخ + تأريخ مكن اشعارات الحافظة لنسخ الحقول - كيف أعد فحص البصمة للفتح السريع \? - "احفظ البصمات في " - افحص البصمة لتخزين كلمة سر قاعدة البيانات بأمان. - افحص البصمة لفتح قاعدة البيانات عند تعطيل كلمة السر. البصمة فحص البصمة يسمح بفحص البصمة لفتح قاعدة البيانات غير خط الحقول لتوضيح المحارف - افتح الملفات بالتحديد -\n - افتح الملفات تلقائيا عند تحديدها في متصفح الملفات الوثوق بالحافظة اسمح لكلمة السر والحقول المحمية بالدخول للحافظة تحذير: الحافظة مشتركة بين التطبيقات وستتمكن التطبيقات الاخرى من الوصول الى المعلومات الحساسة. - رابط قاعدة البيانات اسم قاعدة البيانات وصف قاعدة البيانات نسخة قاعدة البيانات @@ -240,15 +227,11 @@ تطبيق أخرى لوحة مفاتيح - تحديد المدخل مع المفتاح. - إملأ الحقول باستخدام عناصر الادخال. - أغلق قاعدة البيانات. - العودة للوحة المفاتيح الافتراضية. إعدادات Magikeyboard مدخل انتهت المهلة انتهاء المهلة لحذف مدخل الحافظة - معلومات الاشعار + معلومات الإشعار أظهر إشعار عند توفر مدخل مدخل إمسح عند الخروج @@ -271,16 +254,42 @@ قاعده بيانات طبيعية الوصول إقفال - \"الإعدادات\" ← \"الأمان\" ← \"البصمة\" تعيين مفتاح رئيسي استخدم سلة المحذوفات Magikeyboard - إعدادات Magikeyboard - \"الإعدادات\" ← \"اللغة والإدخال\" ← \"لوحة المفاتيح الحالية\" ثم اختر واحدا. Magikeyboard Magikeyboard (KeePass DX) %1$s متوفر على Magikeyboard %1$s إعادة تعيين الشاشات التعليمية البحث من خلال الإدخالات - + افتح الملف + إضافة إدخال + إضافة مجموعة + معلومات الملف + مولد كلمة السر + الخلفية + دورات التحويل + توفر الدورات الاضافية ضد هجوم توليد التركيبات ،لكنها تبطئ التحميل والحفظ. + دورات التحميل + مقدار الذاكرة (بالبايت) لاستخدامها في دالة اشتقاق المفتاح. + درجة التوازي (عدد العمليات) لدالة اشتقاق المفتاح. + مجموعات قبل + نمط التحديد + لا تقتل التطبيق… + العقد الفرعية + أضف عقدة + ايقونة المدخل + حفظ المدخل + طول كلمة السر + أضف حقل + أزل حقل + لا يمكنك نقل مدخل هنا. + لا يمكنك نسخ مدخل هنا. + عرض عدد المدخلات + عرض عدد المدخلات في المجموعة + تحديث + أغلق الحقول + لا يمكن انشاء قاعدة بيانات بكلمة السر وملف المفتاح الحاليين. + إلغاء القفل المتقدم + \ No newline at end of file diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 6fe43faae..a5a113977 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -34,7 +34,6 @@ Paràmetres de l\'aplicació Parèntesis L\'exploració d\'arxius necessita l\'aplicació Open Intents File Manager, clica a sota per instal·lar-la. Degut a peculiaritats de l\'explorador d\'arxius pot ser que no funcioni correctament la primera execució. - Cancel·la Porta-retalls netejat. Temps d\'espera del porta-retalls Temps abans de netejar el porta-retalls després de copiar un usuari o contrasenya @@ -69,11 +68,9 @@ El telèfon sha quedat sense memòria processant la teva base de dades. Potser és massa gran pel teu telèfon. Has de seleccionar almenys un tipus de generador de contrasenyes Les contrasenyes no coincideixen. - Les passades han de ser un número. Massa passades. Establint a 2147483648. És necessari un títol. Insereix un enter positiu al camp longitud - Arxiu no trobat. Explorador d\'arxius Generar contrasenya confirma contrasenya @@ -85,7 +82,7 @@ Contrasenya Instal·la de la Play Store Instal·la de la F-Droid - Contrasenya o arxiu clau invàlids. + Contrasenya o arxiu clau invàlids. Format de base de dades desconegut. Longitud Mida de la llista de grups diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 9c3490669..dabf34100 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -34,7 +34,6 @@ Znovu neukázat Závorky Instalace správce souborů OpenIntents k procházení souborů - Storno Schránka vyčištěna Chyba schránky Některé Android telefony od Samsungu nedovolují aplikacím používat schránku. @@ -73,14 +72,12 @@ Nedostatek paměti k otevření databáze. Je třeba zvolit alespoň jeden způsob vytváření hesla. Zadání hesla se neshodují. - „Počet průchodů“ musí být číslo. Příliš vysoký „Počet průchodů“. Nastavuji na 2147483648. Je třeba, aby každý řetězec měl název kolonky. Přidejte název. Do nastavení „Délka“ zadejte celé kladné číslo. Název pole Hodnota pole - Soubor nenalezen. Správce souborů Vytvoř heslo potvrď heslo @@ -92,7 +89,7 @@ Heslo Instalovat z katalogu Play Store Instalovat z katalogu F-Droid - Nesprávné heslo nebo soubor s klíčem. + Nesprávné heslo nebo soubor s klíčem. Nesprávný algoritmus. Nedaří se rozpoznat formát databáze. Soubor s klíčem neexistuje. @@ -214,22 +211,22 @@ Nepoužívejte v hesle pro databázový soubor znaky mimo znakovou sadu Latin-1 (nepoužívejte znaky s diakritikou). Opravdu chcete ponechat databázi nechráněnou (bez hesla)? Opravdu nechcete používat šifrovací klíč? - Otisk prstu je zařízením podporován, ale není nastavený. - Snímání otisku prstu + Biometrická pobídka je zařízením podporována, ale není nastavena. + Otevři biometrickou pobídku k otevření databáze Šifrované heslo uloženo - Problém s neplatným otiskem prstu. Obnovte své heslo. - Otisk prstu nerozpoznán - Problém s otiskem prstu: %1$s - Použít pro uložení tohoto hesla otisk prstu - Tato databáze zatím není chráněna heslem. - Historie + Nelze načíst biometrický klíč. Obnovte své heslo. + Biometrický prvek nerozpoznán + Chyba s biometrickým prvkem: %1$s + Otevři biometrickou pobídku k uložení hesel + Tato databáze zatím nemá uložené heslo. + Historie Vzhled Obecné Automatické vyplnění KeePass DX automatické vyplňování formulářů Přihlásit se pomocí KeePass DX Nastavit výchozí službu automatického vyplňování - Povolit rychlé automatické vyplňování formulářů v ostatních aplikacích + Povolit rychlé automatické vyplňování formulářů v ostatních aplikacích Délka generovaného hesla Nastavení výchozí délky generovaných hesel Znaky hesla @@ -241,19 +238,12 @@ Zamknout Zámek obrazovky Při zhasnutí obrazovky uzamknout databázi - Jak nastavit rychlé odemykání otiskem prstu? - Uložte svůj otisk prstu pro své zařízení v - „Nastavení“ → „Zabezpečení“ → „Otisk prstu“ - Zadejte heslo pro zamčení databáze - Přiložte prst na snímač otisku prstu a zabezpečte tak heslo k databázi otiskem. - Pro otevření databáze s vypnutým heslem přiložte prst na snímač otisku prstu. - Použítí - Otisk - Snímání otisku prstu + Pokročilé odemčení + Biometrické odemčení Nechá otevřít databázi snímáním otisku prstu Smazat šifrovací klíče - Smazat všechny šifrovací klíče související s rozpoznáváním otisku prstu - Opravdu chcete smazat všechny klíče přiřazené k otiskům prstů\? + Smazat všechny šifrovací klíče související s biometrickým rozlišením + Opravdu chcete smazat všechny klíče související s biometrickým rozlišením\? Tuto funkci se nedaří spustit. Verze %1$s vámi používaného systému Android nevyhovuje minimální verzi %2$s. Hardware nebyl rozpoznán. @@ -268,12 +258,9 @@ Přesune skupiny a položky do „Koše“ před smazáním Písmo položek Čitelnost znaků v položkách můžete přizpůsobit změnou písma - Otevírat soubory vybráním - Otevírat soubory automaticky vybráním ve správci souborů Důvěřovat schránce - Povolit kopírovat heslo a chráněné položky do schránky - VAROVÁNÍ: Schránka je sdílena všemi aplikacemi. Pokud jsou do ní zkopírovány citlivé údaje, může se k nim dostat další software. - Odkaz na otevíranou databázi + Povolit vložit heslo a chráněné položky do schránky + VAROVÁNÍ: Schránka je sdílena všemi aplikacemi. Pokud jsou do ní zkopírovány citlivé údaje, mohl by se k nim dostat další software. Název databáze Popis databáze Verze databáze @@ -282,17 +269,7 @@ Ostatní Klávesnice Magikeyboard - Aktivovat vlastní klávesnici, která snadno vyplní hesla a další položky - Nastavení Magikeyboard - Nastavit klávesnici pro bezpečné vyplňování formulářů. - Zapnout \"Magikeyboard\" v nastavení zařízení. - „Nastavení“ → „Jazyk a vstup“ → „Stávající klávesnice“ a zvolte některou. - Když potřebujete vyplnit formulář, zvolte Magikeyboard. - "Přepínat klávesnice dlouhým stisknutím mezerníku na klávesnici nebo pokud nejsou k dispozici tak pomocí:" - Vyberte položku klávesou. - Vyplnit hodnoty pomocí prvků položky. - Zamknout databázi. - Vrátit se zpět k obvyklé klávesnici. + Aktivovat vlastní klávesnici, která snadno vyplní hesla a další položky identity Umožnit bez hlavního klíče Povolit tlačítko \"Otevřít\", i když není vybráno žádné heslo Chráněno před zápisem @@ -306,16 +283,14 @@ Vytvořte svůj první soubor pro správu hesel. Otevřít existující databázi Otevřete svou dříve používanou databázi ze správce souborů a pokračujte v jejím používání. - Postačí odkaz na umístění souboru - Databázi také můžete otevírat pomocí fyzického odkazu (například s file:// a content://). Přidejte položky do databáze Položky pomáhají se správou vašich digitálních identit. \n \nSkupiny (ekvivalent složek) organizují záznamy v databázi. Hledejte v položkách Zadejte název, uživatelské jméno nebo jiné položky k nalezení svých hesel. - Odemykání databáze otiskem prstu - Propojte své heslo a otisk prstu pro rychlé odemykání databáze. + Odemykání databáze otiskem prstu + Propojte své heslo a otisk prstu pro rychlé odemykání databáze. Upravit položku Přidejte ke své položce vlastní kolonky. Společná data mohou být sdílena mezi více různými kolonkami. Vytvořte k záznamu silné heslo. @@ -386,15 +361,13 @@ K uzamknutí stiskněte Zpět v hlavním panelu Zamknout obrazovku, pokud uživatel stiskne tlačítko Zpět v hlavním panelu Vymazat při ukončení - Uzavři databázi při uzavření oznámení + Uzavřít databázi při uzavření oznámení Koš Výběr položky - Při prohlížení záznamu ukaž na Magikeyboard pole položek + Při prohlížení záznamu ukázat na Magikeyboard pole položek Smazat heslo Smaže heslo zadané po pokusu o připojení Otevři soubor - Zobraz odkaz na soubor - Otevři odkaz na soubor Potomci uzlu Přidej uzel Přidej záznam @@ -414,4 +387,18 @@ Sem záznam zkopírovat nelze. Ukaž počet záznamů Ukaž počet záznamů ve skupině + Pozadí + Aktualizovat + Zavři kolonky + Nelze vytvořit databázi s tímto heslem a klíčem ze souboru. + Pokročilé odemčení + Uložit biometrické rozlišení + Uložit hesla databáze s biometrickými daty + Otevřít databázi skrze biometrické rozlišení + Vytáhnout heslo databáze biometrickými daty + Biometrika + Automaticky otevřít biometrickou pobídku + Automaticky otevřít biometrickou pobídku, je-li biometrický klíč pro databázi definován + Zapnout + Vypnout \ No newline at end of file diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 3bbf73586..0a920e5e2 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -34,7 +34,6 @@ Vis ikke igen Parenteser Installer OpenIntents Fil Manager for at gennemse filer - Annuller Udklipsholder ryddet Udklipsfejl Nogle Samsung Android-telefoner, vil ikke lade programmer bruge udklipsholderen. @@ -72,14 +71,12 @@ Ikke nok hukommelse til at indlæse hele databasen. Der skal vælges mindst én kode for kodeordsgenerering. Adgangskoderne er ikke ens. - \"Transformationsrunder\" skal være en talværdi. \"Transformation Runder\" er for stor. Sættes til 2147483648. Hver streng skal have et feltnavn. Tilføj en titel. Angiv et positivt heltal i feltet \"Længde\". Feltnavn Feltværdi - Kunne ikke finde filen. Filhåndtering Generer adgangskode bekræft adgangskode @@ -91,7 +88,7 @@ Adgangskode Installer fra Google Play Installer fra F-Droid - Ugyldig adgangskode eller nøglefil. + Ugyldig adgangskode eller nøglefil. Forkert algoritme. Kunne ikke genkende databaseformat. Nøglefil eksisterer ikke. @@ -213,22 +210,22 @@ Undgå adgangskodetegn uden for tekstkodningsformatet i databasefilen (ukendte tegn konverteres til samme bogstav). Bekræft brug af ingen adgangskode til beskyttelse mod oplåsning\? Bekræft ingen brug af en krypteringsnøgle? - Fingeraftryksscanning understøttes, men er ikke sat op. - Fingeraftryks-scanning + Biometrisk prompt understøttes, men er ikke konfigureret. + Åbn den biometriske prompt for at låse databasen op Krypteret adgangskode er gemt Kunne ikke læse fingeraftryksnøgle. Gendan adgangskode. Kunne ikke genkende fingeraftryk Problem med fingeraftryk: %1$s - Brug fingeraftryk til at gemme adgangskoden + Åbn den biometriske prompt for at gemme legitimationsoplysninger Databasen har endnu ikke en adgangskode. - Historik + Historik Udseende Generelt Autoudfyld KeePass DX formularudfyldning Log ind med KeePass DX Indstil standard autoudfyldservice - Aktiver autofyldning for hurtigt at udfylde formularer i andre programmer + Aktiver autofyldning for hurtigt at udfylde formularer i andre programmer Genereret kodeordslængde Angiver standardlængden for genererede adgangskoder Adgangskodetegn @@ -240,13 +237,6 @@ Lås Skærmlås Lås databasen, når skærmen er slukket - Hvordan konfigureres fingeraftryksscanning til hurtig oplåsning\? - Gem scannede fingeraftryk for enhed i - \"Indstillinger\" → \"Sikkerhed\" → \"Fingeraftryk\" - Indtast adgangskoden til at låse databasen - Scan fingeraftryk for at gemme dataseadgangskode sikkert. - Scan fingeraftryk til at åbne databasen, når adgangskoden er slået fra. - Brug Fingeraftryk Fingeraftryksscanning Scan fingeraftryk for at åbne databasen @@ -267,12 +257,9 @@ Flyt grupper og poster til \"Papirkurven\" før den slettes Feltskrifttype Skift skrifttypen, der anvendes i felter, for at forbedre tegnsynlighed - Åbn filer ved at vælge - Åbn filer automatisk efter valg i filhåndtering Udklipsholder tillid Tillad at adgangskoden og beskyttede felter kopieres til udklipsholderen ADVARSEL: Udklipsholder deles af alle apps. Hvis følsomme data er kopieret, kan andet software gendanne den. - Link til database-filen der skal åbnes Databasenavn Database beskrivelse Databaseversion @@ -281,19 +268,7 @@ Øvrige Tastatur Magikeyboard - Aktiver et brugerdefineret tastatur, der udfylder adgangskoder og alle identitetsfelter - Magikeyboard indstillinger - Hvordan konfigureres tastaturet til sikker formularudfyldning\? -\n -\nOpsæt tastaturet til autofyld af formularerne sikkert. - Aktiver Magikeyboard\" i enhedens indstillinger. - \"Indstillinger\" → \"Sprog & input\" → \"Aktuelt tastatur\" og vælg et. - Vælg Magikeyboard, når der er brug for at udfylde en formular. - Skift tastatur med et langt tryk på mellemrumstasten på tastaturet eller -hvis det ikke er tilgængelig - med: - Vælg post med nøglen. - Udfyld felterne ved hjælp af elementerne i posten. - Lås databasen. - Brug standard tastaturet igen. + Aktiver et brugerdefineret tastatur, der udfylder adgangskoder og alle identitetsfelter Tillad ingen hovednøgle Aktiver knappen \"Åbn\", hvis der ikke er valgt nogen legitimationsoplysninger Skrivebeskyttet @@ -307,16 +282,14 @@ Opret den første adgangskodeadministrationsfil. Åbn en eksisterende database Åbn den tidligere database fil fra filhåndtering for at fortsætte med at bruge den. - Et link til filen er tilstrækkelig - Databasen kan også åbnes med en fysisk forbindelse (med file:// og content:// for eksempel). Tilføj elementer til databasen Tilføje poster til at styre digitale identiteter. \n \nTilføje grupper (svarende til mapper) for at organisere indtastninger og database. Søg i poster Indtast titel, brugernavn eller indhold af andre felter for at hente adgangskoder. - Database oplåsning med fingeraftryk - Link adgangskoden til det scannede fingeraftryk for hurtigt at låse databasen op. + Database oplåsning med fingeraftryk + Link adgangskoden til det scannede fingeraftryk for hurtigt at låse databasen op. Rediger posten Rediger post med brugerdefinerede felter. Pool data kan refereres mellem forskellige indtastningsfelter. Opret en stærk adgangskode til posten. @@ -394,8 +367,6 @@ Slet adgangskode Sletter adgangskoden som er indtastet efter et forbindelsesforsøg Åbn fil - Vis fillink - Åbn fillink Underknude Tilføj knude Tilføj post @@ -405,7 +376,7 @@ Afkrydsningsfelt for nøglefil Gentag for at skifte synlighed for adgangskode Indtastningsikon - Gem indlæg + Gem indtastning Adgangskodegenerator Længde på adgangskode Tilføj felt @@ -413,4 +384,20 @@ UUID Vis antal poster Vise antallet af poster i en gruppe + Post kan ikke flyttes her til. + Post kan ikke kopieres her til. + Baggrund + Opdater + Luk felter + Kan ikke oprette database med denne adgangskode og nøglefil. + Avanceret oplåsning + Gem biometrisk genkendelse + Gem databasens legitimationsoplysninger med biometriske data + Åbn database med biometrisk genkendelse + Uddrag databasens legitimationsoplysninger med biometriske data + Biometrisk + Åbn automatisk biometrisk prompt + Åbn automatisk biometrisk prompt, når der er defineret en biometrisk nøgle for en database + Aktiver + Deaktiver \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a40f60e45..964b2aa82 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -36,7 +36,6 @@ Nicht mehr anzeigen Klammern Durchsuchen Sie Ihre Dateien, indem Sie den OpenIntents File Manager installieren - Abbrechen Zwischenablage geleert Zwischenablagefehler Einige Samsung Android-Smartphones lassen keine Nutzung der Zwischenablage durch Apps zu. @@ -75,14 +74,12 @@ Zu wenig Speicherplatz, um die ganze Datenbank zu laden. Mindestens eine Art der Passwortgenerierung muss ausgewählt werden. Die Passwörter stimmen nicht überein. - „Transformationsrunden“ zu einer Zahl machen. „Transformationsrunden“ zu hoch. Wird auf 214748364848 eingestellt. Für jede Zeichenfolge ist ein Feldname notwendig. Titel hinzufügen. Eine positive ganze Zahl in das Feld „Länge“ eingeben. Feldname Feldwert - Datei nicht gefunden. Datei nicht gefunden. Bitte versuchen, sie über den Dateimanager zu öffnen. Dateimanager Passwort generieren @@ -95,7 +92,7 @@ Passwort Google Play F-Droid - Passwort oder Schlüsseldatei ist falsch. + Passwort oder Schlüsseldatei ist falsch. Falscher Algorithmus. Datenbankformat nicht erkannt. Es existiert keine Schlüsseldatei. @@ -194,11 +191,8 @@ Dateiname Dieses Feature konnte nicht gestartet werden. Ermöglicht die Datenbanköffnung mit dem Fingerabdruck - Fingerabdruck + Erweiterte Entsperrung Fingerabdruckscanner - Fingerabdruck scannen, um die Datenbank zu öffnen, wenn die Passworteingabe ausgeschaltet ist. - Fingerabdruck scannen, um das Datenbank-Passwort sicher zu speichern. - Datenbank-Passwort eingeben Sperre Erlaubte Zeichen für Passwortgenerator festlegen Passwortzeichen @@ -207,11 +201,7 @@ Verschlüsseltes Passwort wurde gespeichert Fingerabdruckschlüssel nicht lesbar. Bitte das Passwort wiederherstellen. Problem mit dem Fingerabdruck: %1$s - Verlauf - Wie richte ich den Fingerabdruckscanner für schnelles Entsperren ein? - Eingelesenen Fingerabdruck für das Gerät speichern in - „Einstellungen“ → „Sicherheit“ → „Fingerabdruck“ - Verwendung + Verlauf Allgemein Fingerabdruck verwenden, um dieses Passwort zu speichern Diese Datenbank hat noch kein Passwort. @@ -245,7 +235,7 @@ KeePass DX autom. Formularausfüllung Mit KeePass DX anmelden Standard Autofill-Dienst auswählen - Autofill aktivieren, um automatisch Eingabefelder in anderen Apps auszufüllen + Autofill aktivieren, um automatisch Eingabefelder in anderen Apps auszufüllen Zwischenablage Verschlüsselungsschlüssel löschen Alle Verschlüsselungsschlüssel für Fingerabdruckerkennung löschen @@ -259,11 +249,8 @@ Verschiebt Gruppen oder Einträge in den Papierkorb, bevor sie gelöscht werden. Feldschriftart Schriftart in Feldern ändern, um Lesbarkeit zu verbessern - Gewählte Datei automatisch öffnen - Dateien nach Auswahl im Dateimanager automatisch öffnen Zwischenablage vertrauen Kopieren des Passworts und der geschützten Felder in die Zwischenablage erlauben - Link zu der zu öffnenden Datenbank Datenbankname Datenbankbeschreibung Datenbankversion @@ -272,7 +259,7 @@ Andere Tastatur Magikeyboard - Eine eigene Tastatur zum einfachen Ausfüllen aller Passwort- und Identitätsfelder aktivieren + Eine eigene Tastatur zum einfachen Ausfüllen aller Passwort- und Identitätsfelder aktivieren Hilfe-Anzeige wiederholen Alle Hilfethemen noch einmal anzeigen Hilfe-Anzeige zurückgesetzt @@ -280,16 +267,14 @@ Die erste Datei zur Passwortverwaltung erstellen. Existierende Datenbank öffnen Eine frühere Datenbankdatei über den Dateimanager öffnen, um sie weiter zu verwenden. - Ein Link zum Speicherort der Datei ist ausreichend - Die Datenbank lässt sich auch mit einem physischen Link öffnen (z. B. mit file:// und content://). Datenbankelemente hinzufügen Einträge helfen, die digitalen Identitäten zu verwalten. \n \nGruppen (wie Ordner) helfen, Einträge in der Datenbank zu ordnen. Einträge durchsuchen Titel, Benutzernamen oder Inhalte anderer Feldern eingeben, um die Passwörter wiederzufinden. - Datenbank mit Fingerabdruck entsperren - Passwort und eigenen Fingerabdruck verknüpfen, um die Datenbank schnell zu entsperren. + Datenbank mit Fingerabdruck entsperren + Passwort und eigenen Fingerabdruck verknüpfen, um die Datenbank schnell zu entsperren. Eintrag bearbeiten Einträge mit benutzerdefinierten Feldern bearbeiten. Pooldaten können zwischen verschiedenen Eingabefeldern referenziert werden. Ein starkes Passwort für den Eintrag erstellen. @@ -334,16 +319,6 @@ Abbrechen Wenn das automatische Löschen der Zwischenablage fehlschlägt, bitte den Verlauf manuell löschen. WARNUNG: Alle Apps teilen sich die Zwischenablage. Wenn sensible Daten kopiert werden, kann andere Software darauf zugreifen. - Magikeyboard-Einstellungen - Tastatur zum sicheren Ausfüllen von Formularen einrichten. - Das „Magikeyboard“ in den Geräteeinstellungen aktivieren. - „Einstellungen“ → „Sprache & Eingabe“ → „Aktuelle Tastatur“ und auswählen. - Das Magikeyboard auswählen, wenn ein Formular ausgefüllt werden soll. - Tastaturen durch langes Drücken auf die Leertaste wechseln oder, wenn das nicht zur Verfügung steht, mit: - Den Eintrag mit dem Schlüssel auswählen. - Die Felder mit den Elementen des Eintrags ausfüllen. - Die Datenbank sperren. - Zur Standardtastatur zurückkehren. Zulassen, dass kein Master-Passwort verwendet wird. „Öffnen“-Taste aktivieren, wenn keine Passwort-Identifikation festgelegt ist Hilfe-Anzeige @@ -391,8 +366,6 @@ Passwort löschen Löscht das eingegebene Passwort nach einem Verbindungsversuch Datei öffnen - Datei-Verknüpfung anzeigen - Datei-Verknüpfung öffnen Eintrag hinzufügen Gruppe hinzufügen Datei-Informationen @@ -413,4 +386,19 @@ Eintrag kann nicht hierher verschoben werden. Eintrag kann nicht hierher kopiert werden. Passwort-Kontrollkästchen + Umschalten der Kennwortanzeige wiederholen + Hintergrund + Aktualisieren + Fehler schließen + Es ist nicht möglich, eine Datenbank mit diesem Passwort und dieser Schlüsseldatei zu erstellen. + Erweiterte Entsperrung + Speichern der Fingerabdruck-Erkennung + Datenbank mit Fingerabdruck-Erkennung öffnen + Fingerabdruck + Aktivieren + Deaktivieren + Datenbank-Anmeldeinformationen biometrisch speichern + Datenbank-Anmeldeinformationen mit biometrischen Daten extrahieren + Biometrische Eingabeaufforderung automatisch öffnen + Biometrische Eingabeaufforderung automatisch öffnen, wenn ein biometrischer Schlüssel für eine Datenbank definiert ist \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 5619c6224..1369fdc88 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -18,90 +18,87 @@ along with KeePass DX. If not, see . --> - Ανατροφοδότηση: - Αρχική Σελίδα: - Το KeePass DX είναι μία Android εφαρμογή του διαχειριστή συνθηματικών KeePass + Σχόλια + Αρχική Σελίδα + Το KeePass DX είναι μία εφαρμογή Android του διαχειριστή κωδικών KeePass Αποδοχή - Προσθήκη εγγραφής + Προσθήκη καταχώρησης Προσθήκη ομάδας - Αλγόριθμος + Αλγόριθμος κρυπτογράφησης Χρονικό όριο εφαρμογής - Ο χρόνος προτού κλειδωθεί η βάση δεδομένων όταν η εφαρμογή είναι ανενεργή. + Αδράνεια πριν κλείσει η εφαρμογή Εφαρμογή Ρυθμίσεις εφαρμογής - Να μην ερωτηθώ ξανά + Να μην εμφανιστεί ξανά Αγκύλες - Η αναζήτηση αρχείων απαιτεί τον Διαχειριστή Αρχείων Open Intents, πατήστε παρακάτω για να τον εγκαταστήσετε. Λόγω μερικών ιδιορρυθμιών στον διαχειριστή αρχείων, η περιήγηση μπορεί να μην λειτουργεί σωστά την πρώτη φορά που θα περιηγηθείτε. - Ακύρωση - Το πρόχειρο καθαρίστηκε. + H Δημιουργία, το Άνοιγμα και η Αποθήκευση αρχείου βάσης δεδομένων απαιτεί την εγκατάσταση ενός διαχειριστή αρχείων που δέχεται την ενεργό δράση ACTION_CREATE_DOCUMENT και ACTION_OPEN_DOCUMENT + Το πρόχειρο καθαρίστηκε Σφάλμα προχείρου - Μερικά Android κινητά τηλέφωνα της Samsung έχουν ένα σφάλμα στην εφαρμογή του προχείρου που προκαλεί την αντιγραφή από εφαρμογές να αποτυγχάνει. Για περισσότερες πληροφορίες πηγαίνετε: - Η εκκαθάριση του προχείρου απέτυχε - Λήξη χρονικού ορίου προχείρου - Χρόνος προτού γίνει εκκαθάριση του προχείρου μετά από αντιγραφή ονόματος χρήστη ή κωδικού πρόσβασης + Ορισμένα τηλέφωνα Android της Samsung δεν θα επιτρέψουν στις εφαρμογές να χρησιμοποιούν το πρόχειρο. + Δεν ήταν δυνατή η διαγραφή του προχείρου + Χρονικό όριο του προχείρου + Διάρκεια αποθήκευσης στο πρόχειρο Επιλέξτε για αντιγραφή %1$s στο πρόχειρο - Δημιουργία κλειδιού βάσης δεδομένων… + Ανάκτηση κλειδιού βάσης δεδομένων… Βάση Δεδομένων - Αποκρυπτογράφηση περιεχομένων βάσης δεδομένων… - Χρήση αυτής της βάσης ως προεπιλεγμένη + Αποκρυπτογράφηση περιεχομένου βάσης δεδομένων … + Χρήση ως προεπιλεγμένης βάσης δεδομένων Ψηφία - KeePass DX \u00A9 %1$d Kunzisoft χωρίς καμια απολυτως εγγυηση. Το παρόν είναι δωρεάν λογισμικό και είστε ευπρόσδεκτοι να το διαμοιράσετε υπό τις συνθήκες της ΙΕΛ έκδοσης 3 ή μεταγενέστερης. - Εισαγωγή ονόματος βάσης δεδομένων - Προσπελάσθηκε + Το KeePass DX ©% 1 $ d Η Kunzisoft έρχεται χωρίς απολύτως καμία εγγύηση. Αυτό είναι ελεύθερο λογισμικό και είστε ευπρόσδεκτοι να το διανείμετε εκ νέου υπό τις συνθήκες της έκδοσης GPL 3 ή νεότερης έκδοσης. + Ανοίξτε την υπάρχουσα βάση δεδομένων + Πρόσβαση Ακύρωση - Σχόλια - Επιβεβαίωση κωδικού πρόσβασης + Σημειώσεις + Επιβεβαίωση κωδικού Δημιουργήθηκε Λήγει - Αρχείο Κλειδιού + Αρχείο-Κλειδί Τροποποιήθηκε - Δεν βρέθηκαν δεδομένα εγγραφών. + Δεν ήταν δυνατή η εύρεση δεδομένων εισόδου. Κωδικός Πρόσβασης Αποθήκευση - Όνομα + Τίτλος Διεύθυνση URL Όνομα Χρήστη - Η ροή κρυπτογράφησης ArcFour δεν υποστηρίζεται. - Το KeePass DX δε μπορεί να χειριστεί αυτή τη διεύθυνση uri. - Δεν ήταν δυνατή η δημιουργία του αρχείου: - Μη έγκυρη βάση δεδομένων. - Μη έγκυρη διαδρομή. - Απαιτείται ένα όνομα. - Απαιτείται ο κωδικός πρόσβασης ή ένα αρχείο κλειδιού. - Το τηλέφωνο ξέμεινε από μνήμη κατά τη διάρκεια προσπέλασης της βάσης δεδομένων σας. Μπορεί να είναι πολύ μεγάλη για το τηλέφωνο σας. - Πρέπει να επιλεγεί τουλάχιστον ένας τύπος δημιουργίας κωδικού πρόσβασης + Η ροή κρυπτογράφησης ARCFOUR δεν υποστηρίζεται. + Το KeePass DX δε μπορεί να χειριστεί αυτή τη διεύθυνση URI. + Δεν ήταν δυνατή η δημιουργία αρχείου: + Δεν ήταν δυνατή η ανάγνωση της βάσης δεδομένων. + Βεβαιωθείτε ότι η διαδρομή είναι σωστή. + Εισαγάγετε ένα όνομα. + Επιλέξτε ένα αρχείο-κλειδί. + Δεν υπάρχει μνήμη για να φορτώσετε ολόκληρη τη βάση δεδομένων σας. + Πρέπει να επιλεγεί τουλάχιστον ένας τύπος δημιουργίας κωδικού πρόσβασης. Οι κωδικοί δεν ταιριάζουν. - Οι κύκλοι πρέπει να είναι αριθμός. - Οι κύκλοι είναι υπερβολικά πολλοί. Ορισμός σε 2147483648. - Ένα όνομα πεδίου απαιτείται για κάθε σειρά. - Απαιτείται ένας τίτλος. - Εισάγετε έναν θετικό ακέραιο αριθμό στο πεδίο μήκους - Όνομα Πεδίου + Οι \"κύκλοι μετασχηματισμού\" είναι πολύ υψηλοί. Ρύθμιση στο 2147483648. + Κάθε σειρά πρέπει να έχει όνομα πεδίου. + Προσθέστε έναν τίτλο. + Εισάγετε ένα θετικό ακέραιο αριθμό στο πεδίο \"Μήκος\". + Όνομα πεδίου Τιμή πεδίου - Το αρχείο δεν βρέθηκε. - Διαχείριση Αρχείων - Δημιουργία Κωδικού + Πρόγραμμα περιήγησης Αρχείων + Δημιουργία κωδικού πρόσβασης επιβεβαίωση κωδικού - παραγόμενος κωδικός + παραγόμενος κωδικός πρόσβασης Όνομα ομάδας - αρχείο κλειδιού + αρχείο-κλειδί μήκος κωδικός Κωδικός Πρόσβασης Εγκατάσταση από το Play Store Εγκατάσταση από το F-Droid - Εσφαλμένος κωδικός ή αρχείο κλειδιού. - Εσφαλμένος αλγόριθμος. - Η μορφή της Βάσης Δεδομένων δεν αναγνωρίζεται. - Δεν υπάρχει αρχείο κλειδιού. - Το αρχείο κλειδιού είναι κενό. + Εσφαλμένος κωδικός ή αρχείο κλειδιού. + Λάθος αλγόριθμος. + Δεν ήταν δυνατή η αναγνώριση της μορφής της βάσης δεδομένων. + Δεν υπάρχει αρχείο-κλειδϊ. + Το αρχείο-κλειδί είναι κενό. Μήκος - Μέγεθος λίστας ομάδας - Μέγεθος κειμένου μέσα στη λίστα ομάδας - Φόρτωση βάσης δεδομένων… - Πεζά - Απόκρυψη κωδικού - Απόκρυψη κωδικών από προεπιλογή + Μέγεθος στοιχείου λίστας + Μέγεθος κειμένου στη λίστα στοιχείων + Φόρτωση βάσης δεδομένων… + Μικρά + Απόκρυψη κωδικών πρόσβασης + Μάσκα κωδικούς πρόσβασης (***) από προεπιλογή Σχετικά με Αλλαγή Κύριου Κλειδιού Ρυθμίσεις @@ -109,64 +106,290 @@ Διαγραφή Δωρεά Επεξεργασία - Απόκρυψη Κωδικού - Κλείδωμα Βάσης Δεδομένων + Απόκρυψη κωδικού + Κλείδωμα βάσης δεδομένων Άνοιγμα Αναζήτηση Εμφάνιση κωδικού - Πηγαίνετε στη διεύθυνση URL - Πλην + Μεταβείτε στη διεύθυνση URL + Μείον Ποτέ - Δε βρέθηκαν αποτελέσματα αναζήτησης - Χωρίς χειριστή για τη διεύθυνση url. - Άνοιγμα πρόσφατης βάσης : - Να μην γίνει αναζήτηση σε αντίγραφο ασφαλείας εγγραφών - Παράληψη ομάδας \'Αντίγραφο Ασφαλείας\' από τα αποτελέσματα αναζήτησης (εφαρμόζεται μόνο σε αρχεία .kdb) - Δημιουργία νέας βάσης δεδομένων… - Επεξεργασία… + Δεν βρέθηκαν αποτελέσματα αναζήτησης + Εγκαταστήστε ένα πρόγραμμα περιήγησης για να ανοίξετε αυτήν τη διεύθυνση URL. + Πρόσφατες βάσεις δεδομένων + Να μην γίνει αναζήτηση μέσα από τις καταχωρήσεις αντιγραφών ασφαλείας + Παράληψη ομάδας \"Αντίγραφο Ασφαλείας\" και \"Κάδος Ανακύκλωσης\" από τα αποτελέσματα αναζήτησης + Δημιουργία νέας βάσης δεδομένων… + Επεξεργασία… Προστασία - Το KeePass DX δεν έχει δικαίωμα εγγραφής στη τοποθεσία της βάσης, οπότε η βάση σας θ ανοίξει μόνο για ανάγνωση. + Το KeePass DX χρειάζεται άδεια εγγραφής για να αλλάξει οτιδήποτε στη βάση δεδομένων σας. Ξεκινώντας από το Android KitKat, μερικές συσκευές δεν επιτρέπουν πλέον στις εφαρμογές να γράψουν στην κάρτα SD. Πρόσφατο ιστορικό αρχείων - Να θυμάσαι πρόσφατα χρησιμοποιημένα ονόματα αρχείων - Να θυμάσαι την τοποθεσία των αρχείων κλειδιών - Αποθήκευση αρχείου κλειδιού - Απομάκρυνση + Απομνημόνευση πρόσφατων ονομάτων αρχείων + Απομνημόνευση της τοποθεσίας αρχείων-κλειδιών των βάσεων δεδομένων + Αποθήκευση αρχείου-κλειδιού + Αφαίρεση Rijndael (AES) Ριζικός Κατάλογος - Κύκλοι Κρυπτογράφησης - Μεγαλύτεροι κύκλοι κρυπτογράφησης παρέχουν πρόσθετη προστασία ενάντια σε επιθέσεις brute force, αλλά μπορεί να επιβραδύνει πολύ την φόρτωση και την αποθήκευση. - κύκλοι - Αποθήκευση βάσης… + Κύκλοι μετασχηματισμού Κρυπτογράφησης + Επιπλέον κύκλοι κρυπτογράφησης παρέχουν πρόσθετη προστασία ενάντια σε επιθέσεις brute force, αλλά μπορεί να επιβραδύνει πολύ την φόρτωση και την αποθήκευση. + κύκλοι μετασχηματισμού + Αποθήκευση βάσης δεδομένων… Κενό Αναζήτηση - Σειρά ταξινόμησης βάσης + Κανονική ταξινόμηση Ειδικοί - Τίτλος/περιγραφή εγγραφής + Αναζήτηση Αποτελέσματα αναζήτησης Twofish Υπογράμμιση Μη υποστηριζόμενη έκδοση βάσης δεδομένων. Κεφαλαία - Η κάρτα SD δεν είναι προσαρμοσμένη αυτή τη στιγμή στη συσκευή σας. Δεν θα μπορείτε να φορτώσετε ή να δημιουργήσετε τη βάση δεδομένων σας. + Μοντάρετε την κάρτα SD για να δημιουργήσετε ή να φορτώσετε μια βάση δεδομένων. Έκδοση %1$s - - Εισάγετε έναν κωδικό πρόσβασης και/ή αρχείο κλειδιού για να ξεκλειδώσετε τη βάση δεδομένων σας. - + Καταχωρίστε τον κωδικό πρόσβασης και /ή το αρχείο-κλειδί για να ξεκλειδώσετε τη βάση δεδομένων σας. +\n +\nΔημιουργήστε αντίγραφα ασφαλείας του αρχείου βάσης δεδομένων σας, σε ασφαλές μέρος μετά από κάθε αλλαγή. - 5 δευτερόλεπτα - 10 δευτερόλεπτα - 20 δευτερόλεπτα - 30 δευτερόλεπτα - 1 λεπτό - 5 λεπτά - 15 λεπτά - 30 λεπτά - Ποτέ + 5 δευτερόλεπτα + 10 δευτερόλεπτα + 20 δευτερόλεπτα + 30 δευτερόλεπτα + 1 λεπτό + 5 λεπτά + 15 λεπτά + 30 λεπτά + Ποτέ - Μικρά - Μεσαία - Μεγάλα + Μικρά + Μεσαία + Μεγάλα - + Κρυπτογράφηση + Λειτουργία εξαγωγής κλειδιών + Extended ASCII + Δέχομαι + Σύρετε για να διαγράψετε το πρόχειρο τώρα + Δεν ήταν δυνατή η ενεργοποίηση της υπηρεσίας αυτόματης συμπλήρωσης. + Δεν ήταν δυνατή η εύρεση αρχείου. Δοκιμάστε να το ανοίξετε ξανά από το πρόγραμμα περιήγησης αρχείων σας. + Αντιγραφή του %1$s + Γέμισμα Φόρμας + Προστασία εγγραφής + Αλγόριθμος κρυπτογράφησης βάσης δεδομένων που χρησιμοποιείται για όλα τα δεδομένα. + Για να δημιουργηθεί το κλειδί για τον αλγόριθμο κρυπτογράφησης, το κύριο κλειδί μετασχηματίζεται χρησιμοποιώντας μια τυχαία αλατισμένη λειτουργία εξαγωγής κλειδιών. + Χρήση μνήμης + Ποσότητα μνήμης (σε δυαδικά bytes) που θα χρησιμοποιηθεί από τη λειτουργία εξαγωγής κλειδιών. + Παραλληλισμός + Βαθμός παραλληλισμού (δηλ. Αριθμός νημάτων) που χρησιμοποιείται από τη συνάρτηση εξαγωγής κλειδιών. + Ταξινόμηση + Χαμηλότερα πρώτα ↓ + Ομάδες πριν + Κάδος ανακύκλωσης στο τέλος + Τίτλος + Όνομα χρήστη + Δημιουργία + Τροποποίηση + Προσπέλαση + Προειδοποίηση + Αποφύγετε τους χαρακτήρες κωδικού πρόσβασης έξω από τη μορφή κωδικοποίησης κειμένου στο αρχείο βάσης δεδομένων (οι μη αναγνωρισμένοι χαρακτήρες μετατρέπονται στο ίδιο γράμμα). + Δώστε πρόσβαση εγγραφής στην κάρτα SD για να αποθηκεύσετε τις αλλαγές της βάσης δεδομένων. + Θέλετε πραγματικά να μην έχετε κανέναν κωδικό προστασίας ξεκλειδώματος; + Είστε βέβαιοι ότι δεν θέλετε να χρησιμοποιήσετε κάποιο κλειδί κρυπτογράφησης; + Ο Κρυπτογραφημένος κωδικός έχει αποθηκευτεί + Γενικά + Αυτόματη συμπλήρωση + Μορφή KeePass DX αυτόματης συμπλήρωσης + Συνδεθείτε με το KeePass DX + Ορίστε την προεπιλεγμένη υπηρεσία αυτόματης συμπλήρωσης + Παραγόμενο μέγεθος κωδικού πρόσβασης + Ορίστε το προεπιλεγμένο μέγεθος των παραγόμενων κωδικών πρόσβασης + Χαρακτήρες Κωδικού Πρόσβασης + Ορίστε τους χαρακτήρες της γεννήτριας κωδικών πρόσβασης + Ειδοποιήσεις Προχείρου + Ενεργοποιήστε τις ειδοποιήσεις του προχείρου για την αντιγραφή πεδίων κατά την προβολή μιας καταχώρησης + Κλείδωμα + Κλείδωμα Οθόνης + Κλείδωμα της βάσης δεδομένων όταν η οθόνη είναι απενεργοποιημένη + Δεν ήταν δυνατή η εκκίνηση αυτής της λειτουργίας. + Η έκδοση Android %1$s σας δεν πληροί την ελάχιστη απαιτούμενη έκδοση %2$s. + Δεν ήταν δυνατή η εύρεση του αντίστοιχου υλικού. + Όνομα αρχείου + Διαδρομή + Ορίστε ένα κύριο κλειδί + Δημιουργία νέας βάσης δεδομένων + Bytes + Διαδρομή αρχείου + Προβολή ολόκληρης της διαδρομής αρχείου + Χρησιμοποιήστε κάδο ανακύκλωσης + Μετακίνηση ομάδων και καταχωρίσεων στο \"Κάδο ανακύκλωσης\" πριν την διαγραφή + Γραμματοσειρά πεδίου + Αλλαγή γραμματοσειράς που χρησιμοποιείται σε πεδία για καλύτερη ορατότητα χαρακτήρων + Πρόχειρο εμπιστοσύνης + Επιτρέψτε τον κωδικό εισόδου και τα προστατευμένα πεδία να εισέλθουν στο πρόχειρο + Όνομα Βάσης Δεδομένων + Περιγραφή Βάσης Δεδομένων + Έκδοση Βάσης Δεδομένων + Κείμενο + Εφαρμογή + Άλλα + Πληκτρολόγιο + Magikeyboard + Επαναφορά οθονών εκμάθησης + Δείτε όλα τα στοιχεία εκμάθησης ξανά + Επαναφορά οθονών εκμάθησης + Δημιουργήστε το αρχείο της βάσης δεδομένων σας + Δημιουργήστε το πρώτο αρχείο διαχείρισης κωδικού πρόσβασης. + Ανοίξτε μια υπάρχουσα βάση δεδομένων + Ανοίξτε το πρόσφατο αρχείο βάσης δεδομένων από το πρόγραμμα περιήγησης αρχείων για να συνεχίσετε να το χρησιμοποιείτε. + Προσθέστε στοιχεία στη βάση δεδομένων σας + Οι καταχωρίσεις σας βοηθούν να διαχειριστείτε τις ψηφιακές σας ταυτότητες. +\n +\nΟι ομάδες (~ φάκελοι) οργανώνουν καταχωρήσεις στη βάση δεδομένων σας. + Αναζήτηση μέσα στις καταχωρήσεις + Εισαγάγετε τον τίτλο, το όνομα χρήστη ή το περιεχόμενο άλλων πεδίων για να ανακτήσετε τους κωδικούς πρόσβασής σας. + Επεξεργαστείτε την καταχώρηση + Επεξεργαστείτε την καταχώρηση σας με προσαρμοσμένα πεδία. Τα Pool data μπορούν να αναφέρονται μεταξύ διαφορετικών πεδίων εισαγωγής. + Δημιουργήστε έναν ισχυρό κωδικό πρόσβασης για την καταχώρησή σας. + Δημιουργήστε έναν ισχυρό κωδικό πρόσβασης για να συσχετιστεί με την καταχώρισή σας, ορίστε τον εύκολα σύμφωνα με τα κριτήρια της φόρμας και μην ξεχνάτε τον ασφαλή κωδικό πρόσβασης. + Προσθέστε προσαρμοσμένα πεδία + Καταχωρίστε ένα βασικό μη παρεχόμενο πεδίο συμπληρώνοντας ένα νέο που μπορείτε επίσης να προστατεύσετε. + Ξεκλειδώστε τη βάση δεδομένων σας + Αντιγράψτε ένα πεδίο + Τα πεδία αντιγραφής μπορούν να επικολληθούν οπουδήποτε. +\n +\nΧρησιμοποιήστε τη μέθοδο συμπλήρωσης φόρμας που προτιμάτε. + Κλειδώστε τη βάση δεδομένων + Κλείδώστε γρήγορα τη βάση δεδομένων σας, μπορείτε να ρυθμίσετε την εφαρμογή να την κλειδώνει μετά από λίγο και όταν η οθόνη σβήσει. + Ταξινόμηση Στοιχείων + Επιλέξτε τον τρόπο ταξινόμησης καταχωρήσεων και ομάδων. + Συμμετοχή + Βοηθήστε να αυξήσετε τη σταθερότητα, την ασφάλεια και την προσθήκη περισσότερων λειτουργιών. + Σε αντίθεση με πολλές άλλες εφαρμογές διαχείρισης κωδικών πρόσβασης, αυτό είναι χωρίς διαφημίσεις, copylefted libre λογισμικό και δεν συλλέγει προσωπικά δεδομένα στους διακομιστές του, ανεξάρτητα από την έκδοση που χρησιμοποιείτε. + Με την αγορά της επαγγελματικής έκδοσης, θα έχετε πρόσβαση σε αυτό το οπτικό στοιχείο και θα βοηθήσετε ιδιαίτερα την υλοποίηση ιδεών της κοινότητας. + + Αυτή η οπτική λειτουργία είναι διαθέσιμη χάρη στη γενναιοδωρία σας. + Για να διατηρήσουμε την ελεύθερη έκδοση και να είμαστε πάντα ενεργοί, υπολογίζουμε στην συνεισφορά σας. + + Αυτή η λειτουργία είναι υπό ανάπτυξη και απαιτεί την συνεισφοράς σας για να είναι σύντομα διαθέσιμη. + Με την αγορά της έκδοσης pro, + Με την συνεισφορά σας, + ενθαρρύνετε τους προγραμματιστές να δημιουργούν νέες λειτουργίες και να διορθώνουν σφάλματα σύμφωνα με τις παρατηρήσεις σας. + Ευχαριστούμε πολύ για τη συνεισφορά σας. + Εργαζόμαστε σκληρά για να διαθέσουμε αυτό το χαρακτηριστικό γρήγορα. + Μην ξεχνάτε να ενημερώνετε την εφαρμογή σας, εγκαθιστώντας νέες εκδόσεις. + Download + Συνεισφορά + ChaCha20 + AES KDF + Argon2 + Θέμα Εφαρμογής + Θέμα που χρησιμοποιείται στην εφαρμογή + Πακέτο Εικονιδίων + Πακέτο εικονιδίων που χρησιμοποιείται στην εφαρμογή + Δεν μπορείτε να μετακινήσετε μια ομάδα μέσα στον εαυτό της. + Αντιγραφή + Μετακίνηση + Επικόλληση + Ακύρωση + Εάν αποτύχει η αυτόματη διαγραφή του προχείρου, διαγράψτε το ιστορικό του χειροκίνητα. + ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Το πρόχειρο μοιράζεται από όλες τις εφαρμογές. Αν αντιγράφονται ευαίσθητα δεδομένα, άλλο λογισμικό μπορεί να το ανακτήσει. + Να μην επιτρέπεται κανένα κύριο κλειδί + Ενεργοποίηση του κουμπιού \"Άνοιγμα\" αν δεν έχουν επιλεγεί τα διαπιστευτήρια + Οθόνες Εκμάθησης + Επισήμανση των στοιχείων για να μάθετε πώς λειτουργεί η εφαρμογή + Προστασία εγγραφής + Τροποποιήσιμο + Προστασία Εγγραφής + Ανοίξτε τη βάση δεδομένων μόνο για ανάγνωση από προεπιλογή + Προστασία Εγγραφής της βάσης δεδομένων σας + Αλλάξτε τη λειτουργία ανοίγματος για το session. +\n +\nΤο \"Προστατευμένο από εγγραφή\" αποτρέπει τυχόν μη επιθυμητές αλλαγές στη βάση δεδομένων. +\nΤο \"Τροποποιητικό\" σάς επιτρέπει να προσθέσετε, να διαγράψετε ή να τροποποιήσετε όλα τα στοιχεία. + Επεξεργασία καταχώρησης + Δεν ήταν δυνατή η φόρτωση της βάσης δεδομένων σας. + Δεν ήταν δυνατή η φόρτωση του κλειδιού. Προσπαθήστε να μειώσετε την KDF \"Χρήση μνήμης\". + Εμφάνιση ονομάτων χρηστών + Εμφάνιση ονομάτων χρηστών σε λίστες καταχώρησης + Πρόχειρο + Build %1$s + Magikeyboard + Magikeyboard (KeePass DX) + Ρυθμίσεις Magikeyboard + Καταχώριση + Τέλος χρόνου + Τέλος χρόνου για καθαρισμό της καταχώρησης πληκτρολογίου + Πληροφορίες Ειδοποίησης + Εμφάνιση ειδοποίησης όταν είναι διαθέσιμη μια καταχώρηση + Καταχώριση + %1$s είναι διαθέσιμο στο Magikeyboard + %1$s + Καθαρισμός στο κλείσιμο + Κλείσιμο της βάση δεδομένων κατά το κλείσιμο της ειδοποίησης + Εμφάνιση + Θέμα Πληκτρολογίου + Κλειδιά + Δόνηση κατά το πάτημα πλήκτρου + Ήχος στο πάτημα πλήκτρων + Λειτουργία επιλογής + Μη κλείσιμο της εφαρμογής … + Πατήστε Πίσω στη ρίζα καταλόγου για κλείδωμα + Κλείδωμα της βάσης δεδομένων όταν ο χρήστης κάνει κλικ στο κουμπί \"πίσω\" στη αρχική οθόνη + Καθαρισμός στο κλείσιμο + Κλείσιμο της βάση δεδομένων κατά το κλείσιμο της ειδοποίησης + Κάδος Aνακύκλωσης + Επιλογή καταχώρισης + Εμφάνιση πεδίων εισαγωγής στο Magikeyboard κατά την προβολή μιας καταχώρησης + Διαγραφή κωδικού πρόσβασης + Διαγράφει τον κωδικό πρόσβασης που εισάγεται μετά από μια προσπάθεια σύνδεσης + Άνοιγμα αρχείου + Κόμβος + Προσθήκη κόμβου + Προσθήκη καταχώρησης + Προσθήκη ομάδας + Πληροφορίες αρχείου + Έλεγχος κωδικού πρόσβασης + Έλεγχος κλειδιού-αρχείου + Επανάληψη της ορατότητας του κωδικού πρόσβασης + Εικονίδιο καταχώρησης + Αποθήκευση καταχώρησης + Γεννήτρια κωδικού πρόσβασης + Μήκος κωδικού πρόσβασης + Προσθήκη πεδίου + Αφαίρεση πεδίου + UUID + Δεν μπορείτε να μετακινήσετε μια καταχώρηση εδώ. + Δεν μπορείτε να αντιγράψετε μια καταχώρηση εδώ. + Εμφάνιση αριθμού καταχωρήσεων + Εμφάνιση του αριθμού καταχωρήσεων σε μια ομάδα + Υπόβαθρο + Ενημέρωση + Κλείσιμο πεδίων + Δεν είναι δυνατή η δημιουργία βάσης δεδομένων με αυτόν τον κωδικό πρόσβασης και το αρχείο-κλειδί. + Προηγμένο ξεκλείδωμα + Διαγράψτε το αποθηκευμένο βιομετρικό κλειδί + Η Βιομετρική προτροπή υποστηρίζεται αλλά δεν έχει ρυθμιστεί. + Ανοίξτε τη βιομετρική προτροπή για να ξεκλειδώσετε τη βάση δεδομένων + Ανοίξτε τη βιομετρική προτροπή για την αποθήκευση διαπιστευτηρίων + Αποθήκευση βιομετρικής αναγνώρισης + Αποθήκευση διαπιστευτηρίων βάσης δεδομένων με βιομετρικά δεδομένα + Άνοιγμα βάσης δεδομένων με βιομετρική αναγνώριση + Εξαγωγή της πιστοποίησης βάσης δεδομένων με βιομετρικά δεδομένα + Δεν ήταν δυνατή η ανάγνωση του βιομετρικού κλειδιού. Επαναφέρετε τα διαπιστευτήριά σας. + Δεν ήταν δυνατή η αναγνώριση βιομετρικών στοιχείων + Βιομετρικό σφάλμα: %1$s + Αυτή η βάση δεδομένων δεν έχει αποθηκευμένα διαπιστευτήρια ακόμα. + Εμφάνιση + Βιομετρία + Προηγμένο ξεκλείδωμα + Βιομετρικό ξεκλείδωμα + Σας επιτρέπει να σαρώσετε το δακτυλικό σας αποτύπωμα ή άλλο βιομετρικό για να ανοίξετε τη βάση δεδομένων + Αυτόματο άνοιγμα βιομετρικής προτροπής + Αυτόματο άνοιγμα βιομετρικής προτροπής όταν ορίζεται ένα βιομετρικό κλειδί για μια βάση δεδομένων + Διαγράψτε τα κλειδιά κρυπτογράφησης + Διαγράψτε όλα τα κλειδιά κρυπτογράφησης που σχετίζονται με τη βιομετρική αναγνώριση + Είστε βέβαιοι ότι θέλετε να διαγράψετε όλα τα κλειδιά που σχετίζονται με τη βιομετρική αναγνώριση; + Ενεργοποίηση + Απενεργοποίηση + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index fb4818fe8..1a9bcb4d3 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -18,8 +18,7 @@ along with KeePass DX. If not, see . Spanish translation by José I. Paños. Updated by David García-Abad (23-09-2013) ---> - +--> Commentario Página de inicio Implementación para Android del gestor de contraseñas KeePass @@ -33,21 +32,20 @@ Configuración de la aplicación Paréntesis Explora ficheros con OpenIntents File Manager - Cancelar Portapapeles limpiado Portapapeles caducado Duración de almacemiento en el portapapeles - Seleccionar para copiar %1$s al portapapeles + Seleccionar para copiar %1$s en el portapapeles Creando clave de la base de datos… Base de datos Descifrando el contenido de la base de datos… Utilice como base de datos por defecto Dígitos - KeePass DX \u00A9 %1$d Kunzisoft no tiene total garantía. Este es software libre, y puedes redristribuirlo bajo las condiciones de la licencia GPL version 3 o posterior. - Introduzca el nombre del archivo de base de datos + KeePass DX, © %1$d de Kunzisoft, no incluye garantías. Es un «software» libre y puede redistribuirlo en virtud de las condiciones de la GPL, en su versión 3 o posterior. + Abrir base de datos existente Acceso Cancelar - Comentario + Notas Confirmar contraseña Creación Caducidad @@ -58,41 +56,39 @@ Nombre URL Nombre de usuario - Secuencia de cifrado ARCFOUR no es soportada. + No se admite el cifrador de flujo ARCFOUR. KeePass DX no puede manejar este URI. No se pudo crear el archivo: - Impossible de leer la base de datos. - Asegúranse que la ruta es correcta. - Se necesita un nombre. - Se necesita un archivo de clave. - El dispositivo se quedó sin memory mientras analizada la base de datos. Puede ser demasiado grande para este dispositivo. - Debe seleccionar al menos un tipo de generación de contraseñas + No se pudo leer la base de datos. + Asegúrese de que la ruta sea correcta. + Proporcione un nombre. + Seleccione un archivo de clave. + No queda memoria para cargar toda la base de datos. + Debe seleccionar al menos un tipo de generación de contraseñas. Las contraseñas no coinciden. - Las pasadas deben ser un número. Pasadas demasiado grande. Establecido a 2147483648. - Se necesita un título. - Introduzca un entero positivo en el campo longitud - Archivo no encontrado. - Explorador de Archivos - Generar Contraseña + Añada un título. + Proporcione un número entero positivo en el campo «Longitud». + Explorador de archivos + Generar contraseña confirmar contraseña contraseña generada Nombre de grupo - archivo clave + archivo de clave longitud Contraseña contraseña Instalar desde Play Store Instalar desde F-Droid - Contraseña o archivo de clave no válido. - Formato de base de datos no reconocido. + Contraseña o archivo de clave no válido. + No se pudo reconocer el formato de la base de datos. Longitud Tamaño de la lista de Grupo Tamaño del texto de la lista de grupo Cargando base de datos… Minúsculas - Ocultar contraseña - Ocultar contraseñas por defecto + Ocultar contraseñas + Enmascarar contraseñas (***) de manera predeterminada Acerca de Cambiar Contraseña Maestra Configuración @@ -100,8 +96,8 @@ Eliminar Donar Editar - Ocultar Contraseña - Bloquear Base de datos + Ocultar contraseña + Bloquear base de datos Abrir Buscar Mostrar contraseña @@ -109,33 +105,33 @@ Menos Nunca Sin resultado de búsqueda - Sin manejador para esta url. + Instale un navegador web para abrir este URL. Historial de archivos recientes - Recordar nombres de archivos usados recientemente - Abrir base de datos reciente : - No buscar en las entradas de copia de seguridad o papelera de reciclaje - Omitir \'Backup\' y grupo de Papelera de Reciclaje en los resultados de búsqueda + Recordar nombres de archivos recientes + Bases de datos recientes + No buscar en las entradas de respaldo + Omite los grupos «Respaldo» y «Papelera de reciclaje» de los resultados de búsqueda Creando nueva base de datos… Trabajando… - Recordar la ubicación de archivos de clave + Recuerda la ubicación de los archivos de clave de bases de datos Guardando archivo de clave - Borrar + Quitar Rijndael (AES) Raíz - Pasadas de cifrado + Pasadas de transformación Un alto número de pasadas de cifrado proporciona protección adicional contra ataques de fuerza bruta, pero puede ralentizar mucho el cargado y el guardado. - pasadas + pasadas de transformación Guardando base de datos… Espacio Buscar - Orden de BD + Orden natural Especial - Título/descripción de entrada + Búsqueda Twofish Subrayado - Versión de base de datos no soportada. + No se admite esta versión de la base de datos. Mayúsculas - Actualmente su tarjeta SD no está montada en el dispositivo. No podrá cargar o crear su base de datos. + Monte la tarjeta SD para crear o cargar una base de datos. Versión %1$s Introduzca una contraseña y/o un archivo de clave para desbloquear su base de datos. @@ -163,149 +159,135 @@ Algunos teléfonos Samsung con Android tienen un error en la implementación del portapapeles que provoca fallos al copiar desde las aplicaciones. Falló la limpieza del portapapeles Deslizar ahora para limpiar el portapapeles - Cada cadena requiere un nombre de campo. - El servicio de autocompletar no puede habilitarse. + Cada cadena debe tener un nombre de campo. + No se pudo activar el servicio de compleción automática. Nombre del campo Valor del campo - Archivo no encontrado. Intente volver a abrirlo desde su proveedor de contenido. - Algoritmo no válido. - El archivo clave no existe. - El archivo clave está vacío. + No se pudo encontrar el archivo. Intente volver a abrirlo en el explorador de archivos. + El algoritmo es incorrecto. + No existe ningún archivo de clave. + El archivo de clave está vacío. Copia de %1$s Llenado de formulario Quite la clave de huella dactilar Protección - Solo lectura - KeePass DX no tiene permiso de escritura en la ubicación de la base de datos, la base de datos se abrirá como solo lectura. - Comenzando con Android KitKat, algunos dispositivos ya no permiten a las aplicaciones escribir en la tarjeta SD. - Algoritmo para cifrar la base de datos. (Las contraseñas, usuarios, notas y todos los datos en la base de datos son cifrados con el algoritmo seleccionado) - Para generar la clave del algoritmo de cifrado, la clave maestra comprimida (SHA-256) se transforma usando una función de derivación de clave (con una sal aleatoria). + Protegida contra escritura + KeePass DX necesita permiso de escritura para modificar la base de datos. + A partir de Android KitKat, algunos dispositivos ya no permiten a las aplicaciones escribir en la tarjeta SD. + Algoritmo de cifrado utilizado en todos los datos. + Para generar la clave del algoritmo de cifrado, la clave maestra se transforma mediante una función de derivación de claves con una sal aleatoria. Uso de memoria Cantidad de memoria (en bytes binarios) a usar por la función de derivación de clave. Paralelismo Grado de paralelismo (p. ej. número de hilos) usados por la función de derivación de clave. Ordenar - Ascendente + Inferiores primero ↓ Agrupar antes Papelera debajo - Ordenar por Título - Ordenar por Usuario - Ordenar por Hora de creado - Ordenar por Hora de última modificación - Ordenar por Hora de último acceso - Resultados de la búsqueda - Advertencia + Título + Nombre de usuario + Creación + Modificación + Acceso + Resultados de búsqueda + Atención Datos de entrada no encontrados. - El formato .kdb solo admite el juego de caracteres Latin1. Su contraseña puede contener caracteres fuera de este juego de caracteres. Todos los caracteres no-Latin1 se convierten al mismo carácter, lo que reduce la seguridad de su contraseña. Se recomienda cambiar su contraseña. - ¿De verdad quieres usar una cadena vacía como contraseña? - ¿Estás seguro que no quieres usar clave de cifrado? - Huella digital compatible pero no configurada para el dispositivo - Monitoreando huellas digitales - Contraseña encriptada almacenada + Evite emplear en la base de datos caracteres que no pertenezcan al formato de codificación del texto (los caracteres no reconocidos se convierten a la misma letra). + ¿Confirma que no quiere utilizar la protección ante desbloqueo de contraseñas\? + ¿Confirma que no quiere utilizar alguna clave de cifrado\? + Se admite la petición de datos biométricos pero no se han configurado. + Abra la petición de datos biométricos para desbloquear la base de datos + Contraseña cifrada almacenada Problema clave de huella digital no válida. Restaure su contraseña. Huella digital no reconocida Problema de huella digital: %1$s - Usa la huella digital para almacenar esta contraseña - Aún sin contraseña almacenada para esta base de datos - Historial + Historial + Habilite el servicio para completar formularios fácilmente desde otras aplicaciones + Abra la petición de datos biométricos para almacenar credenciales + Esta base de datos aún no tiene credenciales almacenadas. Apariencia General - Autocompletar - Servicio de Autocompletar KeePass DX - Inicia sesión con KeePass DX - Establecer predeterminado el servicio de Autocompletar - Habilite el servicio para completar formularios fácilmente desde otras aplicaciones - Tamaño de la contraseña - Establecer el tamaño predeterminado de la contraseña generada + Compleción automática + Compleción automática de formularios de KeePass DX + Acceder con KeePass DX + Establecer servicio de compleción automática predeterminado + Tamaño de la contraseña generada + Establece el tamaño predeterminado de las contraseñas generadas Caracteres de contraseña - Establecer los caracteres predeterminados del generador de contraseñas + Establecer los caracteres permitidos del generador de contraseñas Portapapeles - Notificaciones del Portapapeles + Notificaciones del portapapeles Habilitar las notificaciones del portapapeles para copiar el nombre de usuario y la contraseña Bloquear Bloqueo de pantalla Bloquear la base de datos cuando la pantalla esté apagada - ¿Cómo configurar la huella digital para un desbloqueo rápido? - Establezca su huella digital personal para su dispositivo en - Configuración -> Seguridad -> Huella digital - Escriba su contraseña en Keepass DX - Escanee su huella digital para almacenar su contraseña maestra de forma segura - Escanee su huella digital cuando la casilla de verificación de contraseña esté desmarcada para abrir la base de datos - Uso - Huella digital - Monitoreando Huella digital - Habilitar la apertura de base de datos por huella digital + Desbloqueo avanzado + Desbloqueo biométrico + Le permite escanear su huella dactilar u otro dato biométrico para abrir la base de datos Eliminar claves de cifrado Eliminar todas las claves de cifrado relacionadas con el reconocimiento de huellas digitales - ¿Está seguro de que desea borrar todas las claves relacionadas con las huellas digitales? - No se puede iniciar esta característica. - Su versión de Android %1$s no es la versión mínima, se requiere %2$s. - El hardware no es detectado. + ¿Confirma que quiere eliminar todas las claves relativas al reconocimiento biométrico\? + No se pudo iniciar esta funcionalidad. + Su versión de Android %1$s no es la versión mínima; se necesita %2$s. + No se encontró el hardware correspondiente. Nombre del archivo Ruta Asignar una clave maestra - Crear un archivo de KeePass + Crear base de datos nueva Bytes Ruta de archivo Ver la ruta completa del archivo - Usar la Papelera de reciclaje - Mueva un grupo o una entrada a la Papelera de reciclaje antes de eliminar - Fuente de los Campos + Usar la papelera de reciclaje + Mueva un grupo o una entrada a la papelera de reciclaje antes de eliminar + Fuente de los campos Cambiar la fuente de los campos para una mejor visibilidad del carácter - Abrir automáticamente el archivo seleccionado - Abrir automáticamente un archivo desde la pantalla de selección después de una selección en el explorador de archivos - Copia de contraseña - Permitir la copia de la contraseña al portapapeles. - Enlace del archivo Kdbx para abrir + Portapapeles de confianza + Permitir la copia de la contraseña al portapapeles Nombre de la base de datos Descripción de la base de datos Versión de la base de datos - Apariencia del texto - Apariencia de la aplicación + Texto + Aplicación Otro Teclado Teclado mágico - Active un teclado personalizado que llene sus contraseñas y todos los campos de identidad fácilmente. - Restablecer pantallas de educación - Resalta los elementos para aprender cómo funciona la aplicación - Pantallas de educación reiniciadas - Crear tu archivo de base de datos - Aún no conoce KeePass DX, cree su primer archivo de administración de contraseñas. + Active un teclado personalizado que llene sus contraseñas y todos los campos de identidad fácilmente. + Restablecer pantallas didácticas + Mostrar de nuevo todas las pantallas didácticas + Se restablecieron las pantallas didácticas + Cree su archivo de base de datos + Cree su primer archivo de gestión de contraseñas. Abrir una base de datos existente - Ya has usado un administrador de KeePass. Simplemente abre tu archivo Kdbx desde el buscador de archivos. - Un enlace a la ubicación de su archivo es suficiente - También puede abrir su base con un enlace físico (Con file:// y content:// por ejemplo). - Añadir nuevos elementos a su base + Abra su archivo de base de datos anterior desde el explorador de archivos para seguir utilizándolo. + Añada elementos a su base de datos Agregar entradas para administrar tus identidades digitales. \n \nAgregar grupos (el equivalente de carpetas) para organizar tus entradas y tu base de datos. - Busque fácilmente tus entradas + Busque registros fácilmente Busque entradas por título, nombre de usuario u otros campos para recuperar fácilmente sus contraseñas. - Desbloquee su base de datos con su huella digital - Haga el enlace entre su contraseña y su huella digital para desbloquear fácilmente su base de datos. + Desbloquee su base de datos con su huella digital + Haga el enlace entre su contraseña y su huella digital para desbloquear fácilmente su base de datos. Editar la entrada Edite la entrada con campos personalizados, puede agregar referencias a los datos de la agrupación entre campos de diferentes entradas. - Crear una contraseña segura - Genere una contraseña segura para asociarla con su entrada, defínala fácilmente según los criterios del formulario y no olvides una contraseña difícil pero segura. + Crear una contraseña segura. + Genere una contraseña segura para asociarla con su entrada. Defínala fácilmente según los criterios del formulario y no olvides una contraseña poner una contraseña difícil y segura. Agregar campos personalizados Si desea registrar un campo básico no suministrado, simplemente complete uno nuevo que también pueda proteger visualmente. Desbloquee su base de datos Copia un campo - Copie un campo fácilmente para pegarlo donde quiera -\n -\n¡Puede usar varios métodos de llenado de formularios. ¡Use el que prefieras! - Bloquear la base de datos - Bloquee su base de datos rápidamente, puede parametrizar la aplicación para bloquearla después de un tiempo y cuando la pantalla se apague. + Los campos copiados pueden pegarse en cualquier sitio. +\n +\nUtilice el método de relleno de formularios que prefiera. + Bloquear base de datos + Bloquee su base de datos rápidamente, puede parametrizar la aplicación para bloquearla después de un tiempo o cuando la pantalla se apague. Ordenar elementos - Ordenar entradas y grupos de acuerdo a parámetros específicos. + Ordenar registros y grupos de acuerdo a parámetros específicos. Participar Participe para aumentar la estabilidad, la seguridad y agregar más funciones. A diferencia de muchas aplicaciones de administración de contraseñas, esta aplicación es sin publicidad , fuente abierta y no recupera datos personales en sus servidores, ni siquiera en su versión gratuita. - Al comprar la versión pro, tendrá acceso a la característica visual y usted ayudará especialmente a la realización de proyectos comunitarios. - + Al comprar la versión pro, tendrá acceso a la característica visual y usted ayudará especialmente a la realización de proyectos comunitarios. Esta característica visual está disponible gracias a tu generosidad. - Para mantener nuestra libertad y estar siempre vigente, contamos con tu contribución. - + Para mantener nuestra libertad y estar siempre vigente, contamos con tu contribución. Esta función está en desarrollo y requiere de tu contribución para estar disponible dentro de poco. Al comprar la versión pro, Al contribuir, @@ -318,13 +300,13 @@ ChaCha20 AES-KDF Argon2 - Seleccione un tema - Cambie el tema de la aplicación cambiando los colores + Tema de aplicación + Tema utilizado en la aplicación Seleccione un paquete de iconos - Cambiar el paquete de íconos de la aplicación + Cambiar el paquete de iconos en la aplicación Editar entrada - Impossible de cargar su banco de datos. - Impossible de cargar la clave. Intentan de diminuir la memoria utilizada para la función de derivación de clave. + No se pudo cargar la base de datos. + No se pudo cargar la clave. Intente disminuir el uso de memoria de KDF. Usted no pueden mover un grupo en sí mismo. Enseña nombres de usuario Enseña nombres de usuador en las listras de entradas @@ -334,7 +316,82 @@ Cancelar Protegido contra escritura Modificable - Compila %1$s + Compilación %1$s Si la supresión automática del portapapeles falla, borran el historial manualmente. - Ajustes de Magikeyboard + Otorgue permiso de escritura a la tarjeta SD para guardar los cambios en la base de datos. + ATENCIÓN: todas las aplicaciones comparten el portapapeles. Si copia datos confidenciales, otros programas podrían recuperarlos. + No permitir claves maestras + Activar el botón «Abrir» si no se selecciona ninguna credencial + Pantallas didácticas + Resaltar los elementos para conocer cómo funciona la aplicación + Protegida contra escritura + Abrir su base de datos protegida contra escritura de manera predeterminada + Proteja la base de datos contra escritura + Teclado mágico + Teclado mágico (KeePass DX) + Configuración del Teclado mágico + Entrada + Tiempo límite + Tiempo límite para vaciar la entrada del teclado + Información sobre notificación + Mostrar una notificación cuando esté disponible una entrada + Entrada + %1$s disponible en Teclado mágico + %1$s + Vaciar al cerrar + Cerrar la base de datos al cerrar la notificación + Apariencia + Tema del teclado + Teclas + Vibrar al pulsar + Emitir sonido al pulsar + Modo de selección + No cierre la aplicación… + Bloquear la base de datos cuando se pulsa el botón Atrás en la pantalla inicial + Vaciar al cerrar + Cerrar la base de datos al cerrar la notificación + Papelera de reciclaje + Selección de entrada + Mostrar campos de entrada en el Teclado mágico al ver una entrada + Eliminar contraseña + Elimina la contraseña proporcionada tras un intento de conexión + Abrir archivo + Elementos secundarios del nodo + Añadir nodo + Añadir entrada + Añadir grupo + Información de archivo + Casilla de contraseña + Casilla de archivo de clave + Icono de entrada + Generador de contraseñas + Longitud de contraseña + Añadir campo + Quitar campo + UUID + No puede desplazar entradas aquí. + No puede copiar entradas aquí. + Mostrar cantidad de entradas + Mostrar la cantidad de entradas en un grupo + Fondo + Actualizar + Cerrar campos + No se puede crear la base de datos con esta contraseña y este archivo de clave. + Desbloqueo avanzado + Guardar reconocimiento biométrico + Almacenar credenciales de base de datos con datos biométricos + Abrir base de datos con reconocimiento biométrico + Extraer credencial de base de datos con datos biométricos + Biometría + Abrir petición de datos biométricos automáticamente + Abrir automáticamente la petición de datos biométricos cuando se define una clave biométrica para una base de datos + Activar + Desactivar + Cambiar el modo de apertura de la sesión. +\n +\n\"Protegido contra escritura\" evita cambios no deseados en la base de datos. +\n\"Modificable\" le permite agregar, eliminar o modificar todos los elementos. + Presione hacia atrás en la raíz para bloquear + Repetir la visibilidad de la contraseña + Guardar entrada \ No newline at end of file diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index b6765176a..4dfb63050 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -33,7 +33,6 @@ Ez erakutsi berriro Brackets Fitxategietan nabigatzeak Open Intents Fitxategi Kudeatzailea behar du. Klik egin azpian instalatzeko. Fitxategien kudeatzailaren arazo batzuk direla eta, izan daiteke nabigazioak ondo ez funtzionatzea lehenengo aldian. - Utzi Arbela ezabatuta. Arbelean errorea Samsung Android telefono batzuek akats bat daukate arbelaren inplementazioan, eta honen ondorioz aplikazioetatik kopiatzeak huts egiten du. Xehetasun gehiagotarako ondokora joan: @@ -72,14 +71,12 @@ Telefonoa memoriarik gabe gelditu da zure datubasea arakatzean. Handiegia izan daiteke zure telefonorako. Pasahitza sortzeko mota bat gutxienez aukeratu behar da Pasahitzak ez datoz bat. - Rondak zenbaki bat izan behar dira. Rondak handiegiak. 2147483648 balorean jarrita. Eremu izen bat behar da testu kate bakoitzerako. Izenburu bat behar da. Eremuaren luzeran entero positibo bat sartu Eremuaren izena Eremuaren balorea - Fitxategi ez aurkitua. Fitxategien nabigatzailea Pasahitza sortu pasahitza berretsi @@ -91,7 +88,7 @@ Pasahitza Play store-etik Instalatu F-Droid-etik Instalatu - Pasahitz edo gako fitxategi baliogabea. + Pasahitz edo gako fitxategi baliogabea. Algoritmo baliogabea. Datubase formato ez ezaguna. Gako fitxategia ez da esistitzen. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index a6ed13b8e..17d93f582 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -32,7 +32,6 @@ Älä näytä enää uudelleen Hakasulkeet Tiedostojen selaus vaatii Open Intents File Manager -tiedostonhallintaohjelman, klikkaa alla olevaa linkkiä asentaaksesi sen. Joidenkin ominaisuuksien takia se ei ehkä toimi oikein ensimmäisellä käynnistyksellä. - Peruuta Leikepöytä tyhjennetty. Leikepöytävirhe Joissakin Android-puhelimissa on virhe leikepöydän toteutuksessa, mikä aiheuttaa kopioinnin epäonnistumisen. Lisätietoa: @@ -71,14 +70,12 @@ Puhelimesta loppui muisti salasanatietokantaa avatessa. Tietokanta voi olla liian suuri tälle puhelinmallille. Vähintään yksi salasanagenerointitapa täytyy olla valittuna. Salasanat eivät täsmää. - Kierroksia täytyy olla numero. Kierroksia on liian paljon. Asetetaan se arvoon 2147483648. Kentän nimi on pakollinen joka tekstille. Otsikko on pakollinen. Syötä positiivinen kokonaisluku pituus-kenttään Kentän nimi Kentän arvo - Tiedostoa ei löydetty. Tiedostoselain Generoi salasana vahvista salasana @@ -90,7 +87,7 @@ Salasana Asenna Play Storesta Asenna F-Droid - Väärä salasana tai avaintiedosto. + Väärä salasana tai avaintiedosto. Epäkelpo algoritmi. Salasanatietokannan tyyppiä ei tunnistettu. Avaintiedostoa ei ole olemassa. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d4c171572..c3f4f2b60 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -36,7 +36,6 @@ Crochets ASCII étendu Parcourir les fichiers en installant le gestionnaire de fichiers OpenIntents - Annuler Autoriser Presse-papier vidé Erreur de presse-papier @@ -76,7 +75,6 @@ Mémoire insuffisante pour charger l’ensemble de votre base de données. Au moins un type de génération de mot de passe doit être sélectionné. Les mots de passe ne correspondent pas. - « Tours de transformation » doit être un nombre. « Tours de transformation » trop grand. Défini à 2147483648. Chaque chaîne doit avoir un nom de champ. Ajoutez un titre. @@ -84,7 +82,6 @@ Impossible d’activer le service de remplissage automatique. Nom du champ Valeur du champ - Impossible de trouver le fichier. Impossible de trouver le fichier. Essayer de le rouvrir depuis votre gestionnaire de fichiers. Gestionnaire de fichiers Générer un mot de passe @@ -97,7 +94,7 @@ Mot de passe Installer depuis Google Play Installer depuis F-Droid - Impossible de lire le mot de passe ou le fichier clé. + Impossible de lire le mot de passe ou le fichier clé. Mauvais algorithme. Impossible de reconnaître le format de la base de données. Aucun fichier de clé n’existe. @@ -179,22 +176,22 @@ Ne voulez-vous vraiment aucune protection de déverrouillage par mot de passe \? Êtes-vous sûr de ne vouloir utiliser aucune clé de chiffrement \? Version %1$s - La reconnaissance d’empreinte digitale est prise en charge mais pas configurée. - Reconnaissance d’empreinte digitale + La reconnaissance biométrique est prise en charge mais n’est pas configurée. + Ouvrir la boite de dialogue biométrique pour déverrouiller la base de données Mot de passe chiffré stocké - Impossible de lire la clé de l’empreinte digitale. Restaurer votre mot de passe. - Impossible de reconnaître l’empreinte digitale - Problème d’empreinte digitale : %1$s - Utiliser l’empreinte digitale pour stocker ce mot de passe - Cette base de données n’a pas encore de mot de passe. - Historique + Historique + Impossible de lire la clé biométrique. Restaurer vos identifiants de connexion. + Impossible de reconnaître l’empreinte biométique + Erreur biométrique : %1$s + Ouvrir la boite de dialogue biométrique pour stocker les identifiants + Cette base de données n’a pas encore enregistré d\'identifiant de connexion. Apparence Général Remplissage automatique Remplissage automatique des formulaires KeePass DX Se connecter avec KeePass DX Définir le service de remplissage automatique par défaut - Activer le remplissage automatique pour remplir rapidement des formulaires dans d’autres applications + Activer le remplissage automatique pour remplir rapidement des formulaires dans d’autres applications Taille du mot de passe généré Définir la taille par défaut des mots de passe générés Caractères de mot de passe @@ -206,13 +203,6 @@ Verrouiller Verrouillage d’écran Verrouiller la base de données lorsque l’écran est éteint - Comment configurer la reconaissance de l’empreinte digitale pour un déverrouillage rapide \? - Enregistrer votre empreinte digitale pour votre appareil dans - « Paramètres » → « Sécurité » → « Empreinte digitale » - Saisir le mot de passe de verrouillage de la base de données - Numériser votre empreinte digitale pour stocker le mot de passe de verrouillage de votre base de données en sécurité. - Numériser votre empreinte digitale pour ouvrir la base de données lorsque le mot de passe est désactivé. - Utilisation Empreinte digitale Reconnaissance d’empreinte digitale Vous permet de numériser votre empreinte digitale pour ouvrir la base de données @@ -233,12 +223,9 @@ Déplace les groupes et les entrées dans la « Corbeille » avant suppression Police de champ Changer la police utilisée dans les champs pour une meilleure visibilité des caractères - Ouvrir les fichiers par sélection - Ouvrir automatiquement les fichiers lorsqu’ils sont sélectionnés dans le gestionnaire de fichiers Confiance dans le presse-papier Autoriser le mot de passe de l’entrée et les champs protégés à entrer dans le presse-papier ATTENTION : le presse-papier est partagé par toutes les applications. Si des données sensibles sont copiées, d’autres logiciels peuvent les récupérer. - Lien du fichier de base de données à ouvrir Nom de la base de données Description de la base de données Version de la base de données @@ -247,7 +234,7 @@ Autres Clavier Magikeyboard - Activer un clavier personnalisé pour remplir vos mots de passe et tous les champs d’identité + Activer un clavier personnalisé pour remplir vos mots de passe et tous les champs d’identité Écrans éducatifs Met en surbrillance les éléments pour apprendre le fonctionnement de l’application Réinitialiser les écrans éducatifs @@ -257,16 +244,14 @@ Créer votre premier fichier de gestion de mots de passe. Ouvrir une base de données existante Ouvrir votre ancien fichier de base de données depuis votre gestionnaire de fichiers pour continuer à l’utiliser. - Un lien vers l’emplacement de votre fichier suffit - Vous pouvez aussi ouvrir votre base de données avec un lien physique (avec file:// et content:// par exemple). Ajouter des éléments à votre base de données Les entrées aident à gérer vos identités numériques. \n \nLes groupes (~ dossiers) organisent les entrées dans votre base de données. Rechercher dans les entrées Entrer le titre, le nom d’utilisateur ou le contenu des autres champs pour récupérer vos mots de passe. - Déverrouillage de la base de données par empreinte digitale - Associer votre mot de passe à votre empreinte digitale numérisée pour déverrouiller rapidement votre base de données. + Déverrouillage de la base de données par empreinte digitale + Associer votre mot de passe à votre empreinte digitale numérisée pour déverrouiller rapidement votre base de données. Modifier l’entrée Modifier votre entrée avec des champs personnalisés. La collection de données peut être référencée entre différents champs de l’entrée. Créer un mot de passe fort pour votre entrée. @@ -290,7 +275,7 @@ Contrairement à beaucoup d’applications de gestion de mots de passe, cette application est sans publicité, libre sous licence copyleft et ne collecte pas de données personnelles sur ses serveurs, peu importe la version que vous utilisez. En achetant la version pro, vous accéderez à cette <strong>fonctionnalité visuelle</strong> et vous aiderez en particulier à <strong>la réalisation de projets communautaires.</strong> Cette <strong>fonctionnalité visuelle</strong> est disponible grâce à votre générosité. - Afin de garder notre liberté et de toujours être actifs, nous comptons sur votre contribution. + Afin de garder notre liberté et de rester toujours actifs, nous comptons sur votre contribution. Cette fonctionnalité est en cours de développement et nécessite votre contribution pour être bientôt disponible. En achetant la version <strong>pro</strong>, @@ -336,21 +321,11 @@ Ensemble d’icônes Ensemble d’icônes utilisés dans l’application - Vous ne pouvez pas déplacer un groupe dans lui-même. + Vous ne pouvez pas déplacer un groupe en lui-même. Copier Déplacer Coller Annuler - Paramètres du Magikeyboard - Configurer le clavier pour le remplissage automatique des formulaires en sécurité. - Activer le « Magikeyboard » dans les paramètres de l’appareil. - « Paramètres » → « Langues et saisie » → « Clavier actuel » et en choisir un. - Choisir le Magikeyboard lorsque vous avez besoin de remplir un formulaire. - Basculer de clavier en appuyant longuement sur la barre d’espace de votre clavier ou, si ce n’est pas disponible, avec : - Sélectionner votre entrée avec la clé. - Remplisser vos champs avec les éléments de l’entrée. - Verrouiller la base de données. - Utiliser à nouveau le clavier par défaut. Autoriser aucune clé maîtresse Activer le bouton « Ouvrir » si aucune identification n’est sélectionnée Protégé en écriture @@ -411,8 +386,6 @@ UUID Afficher le nombre d\'entrées Afficher le nombre d\'entrées du groupe - Afficher le lien de fichier - Ouvrir le lien de fichier Enfants de noeud Information de fichier Case à cocher mot de passe @@ -420,4 +393,18 @@ Répéter le basculement de visibilité du mot de passe Vous ne pouvez pas déplacer une entrée ici. Vous ne pouvez pas copier une entrée ici. + Arrière-plan + Mise à jour + Fermer les champs + Impossible de créer une base avec ce mot de passe et ce fichier de clé. + Déverrouillage avancé + Activer + Désactiver + Enregistrer la reconnaissance biométrique + Stocker les informations d\'identification de la base de données avec des données biométriques + Ouvrir la base de données avec la reconnaissance biométrique + Extraire les informations d\'identification de la base de données avec des données biométriques + Biométrique + Ouvrir automatiquement la boite de dialogue biométrique + Ouvrir automatiquement la boite de dialogue biométrique lorsqu\'une clé biométrique est définie pour une base de données \ No newline at end of file diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 03d105380..3d1a2f4f9 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -14,7 +14,6 @@ Non amosar de novo Parénteses ASCII extendido - Cancelar Permitir Portapapeis limpo Erro do portapapeis diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 277e2d203..1d3d2b3c3 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -32,7 +32,6 @@ "Ne mutassa többet" Zárójelek Fájlok böngészése az OpenIntents fájlkezelő telepítésével - Mégse Vágólap törölve Vágólap hiba Egyes androidos Samsung telefonok nem engedik, hogy az alkalmazások használják a vágólapot. @@ -70,14 +69,12 @@ Nincs elég memória a teljes adatbázis betöltéséhez. Legalább egy jelszóelőállítási típust kell választania. A jelszavak nem egyeznek meg. - A „Transzformációs körök” szám kell legyen. A „Transzformációs körök” száma túl nagy. Beállítás 2147483648-ra. Minden karakterlánchoz szükséges egy mezőnév. Adjon hozzá egy címet. Írjon be egy pozitív egész számot a „Hossz” mezőbe. Mezőnév Mezőérték - A fájl nem található. A fájl nem található. Próbálja meg újra megnyitni a fájlkezelőben. Fájlkezelő Jelszó előállítása @@ -90,7 +87,7 @@ Jelszó Telepítés a Play Áruházból Telepítés az F-Droidból - A jelszó vagy kulcsfájl nem olvasható. + A jelszó vagy kulcsfájl nem olvasható. Hibás algoritmus. Az adatbázis formátuma nem ismerhető fel. Nem létezik kulcsfájl. @@ -223,14 +220,14 @@ Biztos, hogy nem akar semmilyen titkosítási kulcsot használni\? Összeállítás: %1$s Az ujjlenyomat nem ismerhető fel - Előzmények + Előzmények Megjelenés Általános Automatikus kitöltés KeePass DX űrlapkitöltés Bejelentkezés a KeePass DX-szel Alapértelmezett automatikus kitöltési szolgáltatás beállítása - Automatikus kitöltés engedélyezése az űrlapok gyors kitöltéséhez más alkalmazásokban + Automatikus kitöltés engedélyezése az űrlapok gyors kitöltéséhez más alkalmazásokban Előállított jelszó mérete Beállítja az előállított jelszavak alapértelmezett méretét Jelszó karakterek @@ -242,13 +239,6 @@ Zárolás Képernyőzár Az adatbázis zárolása, ha a képernyő kikapcsol - Hogyan állítsa be az ujjlenyomat-olvasást a gyors feloldáshoz\? - Mentse a leolvasott ujjlenyomatát az eszközén a - „Beállítások” → „Biztonság” → „Ujjlenyomat” menüpontban - Adja meg az adatbázis zárolási jelszavát - Olvassa le az ujjlenyomatát, hogy biztonságosan tárolja az adatbázist zároló jelszót. - Olvassa le az ujjlenyomatát, hogy kinyissa az adatbázist, ha a jelszó ki van kapcsolva. - Használat Ujjlenyomat Ujjlenyomat-leolvasás Lehetővé teszi, hogy leolvassa az ujjlenyomatát az adatbázis feloldásához @@ -268,12 +258,9 @@ A csoportok és bejegyzések „Kukába” helyezése törlés előtt Mező betűkészlete A mezőkben használt betűkészlet módosítása a karakterek jobb láthatósága érdekében - Fájlok megnyitása kiválasztással - Fájlok automatikus megnyitása, ha kiválasztásra kerülnek a fájlböngészőben Megbízás a vágólapban Engedélyezés, hogy a jelszó és a védett mezők a vágólapra kerüljenek FIGYELMEZTETÉS: A vágólapon osztozik az összes alkalmazás. Ha érzékeny adatokat másol, akkor a többi szoftver is hozzáférhet. - A megnyitandó adatbázisfájl hivatkozása Adatbázis neve Adatbázis leírása Adatbázis verzió @@ -282,17 +269,7 @@ Egyéb Billentyűzet Mágikus billentyűzet - Egyéni billentyűzet aktiválása, amely kitölti a jelszavakat és az összes azonosító mezőt - Mágikus billentyűzet beállításai - Billentyűzet beállítása az űrlapok biztonságos automatikus kitöltéséhez. - Aktiválja a „Mágikus billentyűzetet” az eszközbeállításokban. - „Beállítások” → „Nyelvek és bevitel” → „Jelenlegi billentyűzet” és válasszon egyet. - Válassza a Mágikus billentyűzetet, ha egy űrlapot kell kitöltenie. - Válasszon a a billentyűzetek közt a billentyűzet szóközének hosszú nyomásával, vagy ha nem érhető el, akkor így: - Válassza ki a bejegyzést a gombbal. - Töltse ki a mezőket a bejegyzés elemeivel. - Zárolja az adatbázist. - Használja újra az alapértelmezett billentyűzetet. + Egyéni billentyűzet aktiválása, amely kitölti a jelszavakat és az összes azonosító mezőt Mágikus billentyűzet Mágikus billentyűzet (KeePass DX) Mágikus billentyűzet beállításai @@ -324,16 +301,14 @@ Hozza létre az első jelszókezelő-fájlját. Létező adatbázis megnyitása Nyisson meg egy korábbi adatbázisfájlt a fájlböngészőből, hogy folytassa a használatát. - Egy a fájl helyére mutató hivatkozás is megfelelő - Egy fizikai hivatkozással is megnyithatja az adatbázist (például egy file:// vagy content:// hivatkozással). Elemek hozzáadása az adatbázishoz A bejegyzések segítenek a digitális személyazonosságai kezelésében. \n \nA csoportok (~ mappák) rendszerezik a bejegyzéseket az adatbázisban. Keresés a bejegyzések között Adja meg a címet, felhasználónevet vagy más mezők tartalmát, hogy lekérje a jelszavát. - Adatbázis feloldása ujjlenyomattal - Kösse össze a jelszavát a mentett ujjlenyomatával, hogy gyorsan fel tudja oldani az adatbázist. + Adatbázis feloldása ujjlenyomattal + Kösse össze a jelszavát a mentett ujjlenyomatával, hogy gyorsan fel tudja oldani az adatbázist. Bejegyzés szerkesztése Hozzon létre erős jelszót a bejegyzéshez. Állítson elő egy erős jelszót, és rendelje hozzá a bejegyzéshez, adja meg egyszerűen az űrlap feltételeinek megfelelően, és ne felejtse el biztonságosan tárolni. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c3b0b8c56..10ffc0aa1 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -18,8 +18,7 @@ along with KeePass DX. If not, see . Italian translation by Diego Pierotto. Updated by anthologist on April 2018. ---> - +--> Commenti Pagina web Implementazione Android del gestore password KeePass @@ -33,7 +32,6 @@ Impostazioni app Parentesi Sfoglia i file installando il Gestore File di OpenIntents - Annulla Appunti eliminati Errore negli appunti Alcuni telefoni Android di Samsung non permettono alle app di usare gli appunti. @@ -71,14 +69,12 @@ Memoria insufficiente per caricare l\'intero database. Deve essere selezionato almeno un tipo di generazione password. Le password non corrispondono. - Rendi il \"livello\" un numero. \"Livello\" troppo alto. Impostato a 2147483648. Ogni stringa deve avere un nome. Aggiungi un titolo. Inserisci un numero naturale positivo nel campo \"lunghezza\". Nome campo Valore campo - File non trovato. File non trovato. Prova a riaprirlo dal tuo gestore di file. Gestore file Genera password @@ -91,7 +87,7 @@ password Installa dal Play Store Installa dal F-Droid - Password o file chiave non validi. + Password o file chiave non validi. Algoritmo errato. Formato database non riconosciuto. Non esiste alcun file chiave. @@ -215,14 +211,14 @@ Vuoi veramente che non ci sia una password di sblocco\? Sei sicuro di non volere usare una chiave di cifratura? Impronta non riconosciuta - Cronologia + Cronologia Aspetto Generale Autocompletamento Autocompletamento di KeePass DX Accedi con KeePass DX Imposta servizio predefinito di autocompletamento - Attiva l\'autocompletamento per compilare velocemente i moduli in altre app + Attiva l\'autocompletamento per compilare velocemente i moduli in altre app Dimensione password generata Imposta la dimensione predefinita delle password generate Caratteri password @@ -233,13 +229,6 @@ Blocca Blocco schermo Blocca il database quando lo schermo è spento - Come impostare la scansione di impronte digitali per sbloccare velocemente\? - Salva la tua impronta per il dispositivo in - \"Impostazioni\" → \"Sicurezza\" → \"Impronta digitale\" - Digita la password di blocco database - Scansiona la tua impronta per salvare la password di blocco database in modo sicuro. - Scansiona la tua impronta per aprire il database quando la password è disattivata. - Utilizzo Impronta digitale Scansione di impronte Consente la scansione di impronte per aprire il database @@ -260,12 +249,9 @@ Sposta gruppi ed elementi nel cestino prima di eliminare Carattere campi Cambia il carattere usato nei campi per una migliore visibilità - Apri i file selezionandoli - Apri automaticamente i file selezionandoli nel gestore di file Fiducia appunti Permetti la copia della password e dei campi protetti negli appunti ATTENZIONE: gli appunti sono condivisi da tutte le app. Se vengono copiati dati sensibili, altri software possono recuperarli. - Link del file database da aprire Nome database Descrizione database Versione database @@ -274,15 +260,7 @@ Altro Tastiera Magitastiera - Attiva una tastiera personale che popola le tue password e i campi di identità - Impostazioni Magitastiera - Imposta la tastiera per un\'autocompletamento sicuro dei moduli. - Attiva la \"Magitastiera\" nelle impostazioni del dispositivo. - \"Impostazioni\" → \"Lingue e immissione\" → \"Tastiera attuale\" e scegline una. - Scegli la Magitastiera quando devi compilare un modulo. - Cambia la tastiera premendo a lungo la barra spaziatrice, oppure, se non disponibile, con: - Blocca il database. - Usa di nuovo la tastiera predefinita. + Attiva una tastiera personale che popola le tue password e i campi di identità Non consentire nessuna chiave principale Abilita il pulsante «Apri» se le credenziali non sono selezionate Protetto da scrittura @@ -296,16 +274,14 @@ Crea il tuo primo file di gestione password. Apri un database esistente Apri il tuo file database precedente dal tuo gestore di file per continuare ad usarlo. - Un link al percorso del tuo file è sufficiente - Puoi anche aprire il tuo database con un link fisico (con file:// e content:// ad esempio). Aggiungi elementi al tuo database Gli elementi aiutano a gestire le tue identità digitali. \n \nI gruppi (~ cartelle) organizzano gli elementi nel database. Cerca tra gli elementi Inserisci il titolo, il nome utente o il contenuto di altri campi per recuperare le tue password. - Sblocco del database tramite impronta digitale - Collega la password alla tua impronta digitale per sbloccare velocemente il database. + Sblocco del database tramite impronta digitale + Collega la password alla tua impronta digitale per sbloccare velocemente il database. Modifica l\'elemento Modifica l\'elemento con campi personalizzati. I dati possono fare riferimento ad altri campi. Crea una password robusta per l\'elemento. @@ -353,8 +329,6 @@ Tema usato nell\'app Pacchetto icone Pacchetto icone usato nell\'app - Seleziona un elemento con la chiave. - Riempi i campi usando gli elementi giusti. Modifica elemento Caricamento del database fallito. Caricamento della chiave fallito. Prova a diminuire l\' \"Utilizzo memoria\" del KDF. @@ -392,8 +366,6 @@ Elimina password Elimina la password immessa dopo un tentativo di connessione Apri il file - Mostra il link del file - Apri il link del file Figli del nodo Aggiungi un nodo Aggiungi una voce @@ -402,7 +374,7 @@ Casella di controllo della password Casella di controllo Keyfile "Ripeti attivare / disattivare la visibilità della password" - Icona d\'ingresso + Icona Generatore di password Lunghezza della password Aggiungi un campo @@ -411,4 +383,7 @@ Non è possibile copiare una voce qui. Mostra il numero di voci Mostra il numero di voci in un gruppo + Salva + Aggiorna + Chiudi campi \ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 23f8332bf..c5cffcf61 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -31,7 +31,6 @@ אל תציג שוב סוגריים סייר הקבצים דורש את סייר הקבצים Open Intents, לחץ למטע כדי להתקין. בגלל מספר בעיות בסייר, ייתכן ויהיו בעיות בהפעלה הראשונה. - בטל לוח ההעתקה נוקה. שגיאת לוח ההעתקה במספר מכשירי אנרואיד מסמסונג קיים באג במימוש לוח ההעתקה שיכול לגרום לבעיות בהעתקה מהיישום. לעוד מידע עבור אל: @@ -68,14 +67,12 @@ זיכרון המכשיר אזל בזמן ניתוח מסד הנתונים. יתכן והוא גדול מדי למכשירך. לפחות סוג אחד ליצירת סיסמה צריך להיבחר הסיסמאות לא תואמות. - סיבובים חייב להיות מספר. מספר סיבובים גדול מדי. מגדיר ל-2147483648. שדה שם נדרש לכל מחרוזת. כותרת נדרשת. הזן מספר חיובי בשדה האורך שם השדה ערך השדה - קובץ לא נמצא. סייר קבצים צור סיסמה אשר סיסמה @@ -87,7 +84,7 @@ סיסמה התקן מחנות Play התקן מהאינטרנט - סיסמה או קובץ מפתח לא מתאימים. + סיסמה או קובץ מפתח לא מתאימים. אלגוריתם לא חוקי. תבנית מסד הנתונים אינה מזוהה. קובץ המפתח לא קיים. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index d0de44bee..79db6b054 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -30,7 +30,6 @@ アプリケーション設定 カッコ ファイルを検索するには OI File Manager が必要です。 - キャンセル クリップボードを消去しました。 クリップボード タイムアウト コピーした情報をクリップボードから消去する時間 @@ -65,11 +64,9 @@ データベース解析中にメモリ不足になりました。 少なくとも1つ以上のパスワード生成タイプを選択する必要があります。 パスワードが一致しません - 数値を入力してください。 値が大きすぎます。 2147483648にセットしました。 タイトルは必須入力です。 \"長さ\"欄には正の整数を入力してください。 - ファイルが見つかりません。 ファイルブラウザ パスワードを生成する パスワードをもう一度入力 @@ -81,7 +78,7 @@ パスワード Play Storeで入手 F-Droidで入手 - パスワード/キーファイルが違います。 + パスワード/キーファイルが違います。 データベースフォーマットが認識できません。 生成するパスワードの長さ グループ一覧サイズ diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index a82edbcac..50f09a5c4 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -35,7 +35,6 @@ 브라켓 확장 ASCII OpenIntents File Manager를 설치하여 파일 찾아보기 - 취소 허가 클립보드 비워짐 클립보드 오류 @@ -77,7 +76,6 @@ 키를 로드할 수 없습니다. KDF \"메모리 사용량\"을 줄여 보세요. 최소 한 가지의 비밀번호 생성 방식이 선택되어야 합니다. 비밀번호가 일치하지 않습니다. - \"Transformation rounds\"는 숫자로 입력해 주세요. \"Transformation rounds\" 가 너무 높습니다. 2147483648로 설정합니다. 각 항목은 필드 이름을 가져야 합니다. 제목을 입력하십시오. @@ -86,7 +84,6 @@ 그룹을 자신에게 옮길 수 없습니다. 필드 이름 필드 값 - 파일을 찾을 수 없습니다. 파일을 찾을 수 없습니다. 파일 탐색기에서 열리는지 확인해 주세요. 파일 탐색기 비밀번호 생성 @@ -99,7 +96,7 @@ 비밀번호 F-Droid에서 설치 플레이 스토어에서 설치 - 비밀번호나 키 파일을 읽을 수 없습니다. + 비밀번호나 키 파일을 읽을 수 없습니다. 잘못된 알고리즘입니다. 데이터베이스 형식을 인식할 수 없습니다. 키 파일이 없습니다. @@ -165,8 +162,6 @@ 정렬 오름차순 ↓ 파일 열기 - 파일 링크 보기 - 파일 링크 열기 자식 노드 노드 추가 항목 추가 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 6a733d516..0b7b627f7 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -2,8 +2,7 @@ KeePass DX yra KeePass slaptažodžių tvarkyklės realizacija Android platformai Iškarpinė išvalyta. - Failas nerastas. - Neteisingas slaptažodis arba rakto failas. + Neteisingas slaptažodis arba rakto failas. Atsiliepimai: Pagrindinis puslapis: Priimti @@ -15,7 +14,6 @@ Programėlės nustatymai Daugiau neberodyti Skliaustai - Atšaukti Duomenų bazė Skaitmenys Atšaukti diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index cfff4c1b9..2a58ed703 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -14,7 +14,6 @@ Turpmāk nerādīt Iekavas Failu pārlūkošanai nepieciešams pārlūks. - Atcelt Starpliktuve notīrīta Starpliktuves kļūda Dažiem Samsung tālruņiem ir problēmas ar starpliktuves lietošanu. Lai saņemtu sīkāku informāciju, dodieties uz: @@ -51,14 +50,12 @@ Darbam ar datu bāzi, tālrunī nepietiek atmiņas. Ir jāatlasa vismaz viens paroles ģenerēšanas tips Paroles nesakrīt. - Ievadiet līmeni no 1 līdz 2147483648 Līmenis pārāk liels. Maksimālais 2147483648 A field name is required for each string. Nepieciešams nosaukums. Norādiet garumu lielāku par nulli Lauka nosaukums Lauka vērtība - Fails nav atrasts. Failu pārlūks Ģenerēt Paroli apstipriniet paroli @@ -70,7 +67,7 @@ Parole Instalēt no Play veikala Instalēt no F-Droid - Nederīga parole vai atslēgas fails. + Nederīga parole vai atslēgas fails. Nederīgs algoritms. Datu bāzes formāts nav atpazīts. Atslēgas fails nepastāv. diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 7b9b545ef..72fec885e 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -35,7 +35,6 @@ Parenteser Utvidet ASCII Utforsk filer ved å installere OpenIntents-filbehandleren - Avbryt Tillat Utklippstavle tømt Utklippstavlefeil @@ -77,7 +76,6 @@ Kunne ikke laste nøkkelen, prøv å senke minnet brukt av KDF. Minst én passordgenereringstype må velges. Passordene samsvarer ikke. - \"Omganger\" må være et tall. \"Omganger\" er for stort. Setter til 2147483648. Hver streng må ha et feltnavn. En tittel er påkrevd. @@ -86,7 +84,6 @@ Kan ikke flytte gruppe inn i seg selv. Feltnavn Feltverdi - Fant ikke filen. Fant ikke filen. Prøv å åpne den fra din innholdsleverandør. Filutforsker Opprett passord @@ -99,7 +96,7 @@ Passord Installer fra Play-butikken Installer fra F-Droid - Ugyldig passord eller nøkkelfil. + Ugyldig passord eller nøkkelfil. Ugyldig algoritme. Fremmed databaseformat. Ingen nøkkelfil finnes. @@ -115,7 +112,7 @@ Skjul passord som forvalg Om Endre hovednøkkel - Kopierte %1$s + Kopi av%1$s Innstillinger Programinnstillinger Skjemautfylling @@ -172,7 +169,7 @@ Laveste først ↓ Grupper før Papirkurv nederst - Databasesorteringsrekkefølge + Naturlig orden Tittel Brukernavn Opprettelsestidspunkt @@ -185,7 +182,7 @@ Ustøttet databaseversjon. Store bokstaver Advarsel - Passordet dit kan inneholde bokstaver som ikke støttes av Latin-1 -tegnsettet brukt av .kdb filer. Siden disse alle konverteres til samme bokstav, anbefales det å endre passordet ditt for å gjøre det sikrere. + Unngå passordtegn utenfor tekstkodingsformat i databasefil (ukjente tegn blir konvertert til samme bokstav). SD-kortet ditt er i øyeblikket skrivebeskyttet. Det kan hende du ikke kan lagre endringer i databasen din. SD-kortet ditt er ikke montert på enheten din. Du vil ikke kunne laste inne eller opprette din database. Ønsker du virkelig å bruke en tom streng som ditt passord? @@ -199,32 +196,25 @@ Fingeravtrykksproblem: %1$s Bruk fingeravtrykk til å lagre dette passordet Denne databasen har ikke et passord enda. - Historikk + Historikk Utseende Generelt Autofyll KeePass DX autofyll-tjeneste Logg inn med KeePass DX Sett forvalgt autofyll-tjeneste - Skru på tjenesten for å fylle ut skjema fra andre programmer + Skru på tjenesten for å fylle ut skjema fra andre programmer Passordsstørrelse Sett forvalgt størrelse for generert passord Passordtegn Sett forvalgte passordgenereringstegn Utklippstavle Utklippstavlemerknader - Skru på utklipstavlemerknader for å kopiering av oppføringsfelter + Aktiver varsler om utklippstavle for å kopiere felt når du viser en oppføring Hvis din enhet ikke er i stand til å slette ting fra utklippstavlen automatisk, slett det kopierte elementet fra din utklippshistorikk manuelt. Lås Skjermlås Lås databasen når skjermen er av - Hvordan sette opp fingeravtrykk for rask opplåsing? - Sett ditt personlige fingeravtrykk for din enhet i - \"Innstillinger\" → \"Sikkerhet\" → \"Fingeravtrykk\" - Skriv passordet ditt i KeePass DX - Skann ditt fingeravtrykk for å lagre hovedpassordet ditt sikkert. - Skann fingeravtrykket ditt for å åpne databasen når passord er avskrudd. - Bruk Fingeravtrykk Skanner etter fingeravtrykk Skru på fingeravtrykksåpning av database @@ -245,12 +235,9 @@ Flytt en gruppe eller oppføring til \"Papirkurv\" før sletting Feltskrift Endre skriften brukt i felter for bedre tegngjengivelse - Åpne valgt fil automatisk - Åpne en fil fra utvalgsskjermen automatisk etter at et valg er gjort i filutforskeren Kopi av passord Tillat kopiering av adgangspassordet og beskyttede felter til utklippstavlen ADVARSEL: Utklippstavlen deles av alle programmer. Hvis sensitiv data kopieres, kan annen programvare gjenopprette den. - Lenke til KDBD-filen å åpne Databasenavn Databasebeskrivelse Databaseversjon @@ -259,18 +246,8 @@ Annet Tastatur Magikeyboard - Aktiver et egendefinert tastatur som fyller inn passordene og alle identitetsfelter - Magikeyboard-innstillinger - Sett opp tastaturet for sikker skjemautfylling. - Aktiver Magikeyboard i enhetsinnstillingene. - \"Innstillinger\" → \"Språk og inndata\" → \"Nåværende tastatur\" og velg ett. - Velg Magikeyboard når du trenger å fylle inn et skjema. - Du kan enkelt bytte fra ditt hovedtastatur til Magikeyboard med språkknappen på tastaturet ditt, et langt trykk på ditt tastaturs mellomromstast, eller, hvis det ikke er tilgjengelig, med: - Velg din oppføring med nøkkelen. - Fyll inn feltene ved bruk av elementene i oppføringen. - Lås databasen. - Gå tilbake til ditt hovedtastatur. - Tillat inget passord + Aktiver et egendefinert tastatur som fyller inn passordene og alle identitetsfelter + Tillat ingen hovednøkkel Skru på \"Åpne\"-tasten hvis ingen passordidentifikasjon er valgt Skrivebeskyttet Åpne din database skrivebeskyttet som forvalg @@ -283,16 +260,14 @@ Du kjenner ikke KeePass DX enda, opprett din første passordsaministrasjonsfil. Åpne en eksisterende database Du har allerede brukt en KeePass-behandler. Bare åpne KDBX-filen fra din filbehandler. - En lenke til plasseringen av filen din er nok - Du kan også åpne din database med en fysisk lenke (med file:// og content:// for eksempel). Legg til nye elementer i databasen din Legg til oppføringer for å behandle dine digitale identiteter. \n \nLegg til grupper (tilsvarende mapper) for å organisere dine oppføringer og databasen din. Søk i dine oppføringer Søk etter oppføringer etter tittel, brukernavn eller andre felter for å hente passordene dine. - Lås opp databasen din med fingeravtrykket ditt - Lenk passordet og fingeravtrykket ditt for å låse opp databasen din enkelt. + Lås opp databasen din med fingeravtrykket ditt + Lenk passordet og fingeravtrykket ditt for å låse opp databasen din enkelt. Rediger oppføringen Rediger din oppføring med egendefinerte felter, referanser til pooldata kan legges til mellom felter av forskjellige oppføringer. Opprett et sterkt passord. @@ -355,7 +330,7 @@ %1$s tilgjengelig på Magikeyboard %1$s Tøm ved lukking - Tøm tastaturoppføringen ved lukking av merknaden + Lukk databasen når du lukker varselet Utseende Tastaturdrakt Taster @@ -366,15 +341,13 @@ Tilbakelås Lås databasen når brukeren klikker tilbakeknappen på root-skjermen Tøm ved lukking - Lukk databasen ved lukking av merknaden + Lukk databasen når du lukker varselet Papirkurv Oppføringsvalg Vis inndatafelter i Magikeyboard når en oppføring vises Slett passord Sletter passord innskrevet etter et tilkoblingsforsøk Åpne fil - Vis fillenke - Åpne fillenke Legg til node Legg til oppføring Legg til gruppe @@ -388,4 +361,13 @@ Du kan ikke kopiere en oppføring hit. Vis antall oppføringer Vis antall oppføringer i en gruppe + Bakgrunn + Oppdater + Lukk felt + Kan ikke opprette database med dette passordet og nøkkelfilen. + Avansert opplåsing + Lagre biometrisk gjenkjennelse + Lagre databaseopplysninger med biometriske data + Aktiver + Skru av \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 4851a9e48..a3a0fa60d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -33,7 +33,6 @@ App-instellingen Haakjes Zoek een bestand op door het installeren van de OpenIntents File Manager - Annuleren Klembord gewist Klembordtime-out Tijd van opslag op het klembord @@ -68,11 +67,9 @@ Onvoldoende vrij geheugen om de gehele databank te laden. Je moet minimaal één soort wachtwoordgenerering kiezen. De wachtwoorden komen niet overeen. - \"Cycli-waarde\" moet een getal zijn. \"Cycli-waarde\" te groot. Wordt ingesteld op 2147483648. Voeg een titel toe. Voer een positief geheel getal in in het veld \"Lengte\". - Bestand niet gevonden. Bestandsverkenner Wachtwoord genereren wachtwoord bevestigen @@ -84,7 +81,7 @@ wachtwoord Installeren via Play Store Installeren via F-Droid - Ongeldig wachtwoord of sleutelbestand. + Ongeldig wachtwoord of sleutelbestand. Databankformaat kan niet worden herkend. Lengte Grootte van itemlijst @@ -222,14 +219,14 @@ Vingerafdrukprobleem: %1$s Vingerafdruk gebruiken om dit wachtwoord op te slaan Deze databank heeft nog geen wachtwoord. - Geschiedenis + Geschiedenis Uiterlijk Algemeen Auto-aanvullen KeePass DX auto-aanvullendienst Inloggen met KeePass DX Standaard aanvuldienst instellen - Schakel de dienst in om formulieren in andere apps snel in te vullen + Schakel de dienst in om formulieren in andere apps snel in te vullen Gegenereerde wachtwoordgrootte Standaardgrootte instellen van gegenereerd wachtwoord Wachtwoordtekens @@ -241,13 +238,6 @@ Vergrendelen Schermvergrendeling Databank vergrendelen als het scherm uitgaat - Hoe stel ik mijn vingerafdruk in voor snelle ontgrendeling? - Stel je persoonlijke vingerafdruk voor je apparaat in via - \"Instellingen\" → \"Beveiliging\" → \"Vingerafdruk\" - Voer het databankwachtwoord in - Scan je vingerafdruk om je databankwachtwoord veilig op te slaan. - Scan je vingerafdruk om de databank te openen wanneer het wachtwoord is uitgeschakeld. - Gebruik Vingerafdruk Vingerafdrukherkenning Hiermee wordt je vingerafdruk gebruikt om de databank te openen @@ -268,12 +258,9 @@ Verplaatst groepen en items naar \"Prullenbak\" voordat ze worden verwijderd Veldlettertype Wijzig het lettertype dat in velden wordt gebruikt voor een betere leesbaarheid - Open bestanden door te selecteren - Bestanden automatisch openen wanneer deze worden geselecteerd in de bestandsverkenner Klembord vertrouwen Toestaan dat het wachtwoord en beveiligde velden worden gekopieerd naar het klembord WAARSCHUWING: het klembord wordt gedeeld door alle apps. Als gevoelige gegevens worden gekopieerd, kan andere software deze opvragen. - Koppeling naar het te openen databasebestand Databanknaam Databankomschrijving Databankversie @@ -282,17 +269,7 @@ Overig Toetsenbord Magikeyboard - Aangepast toetsenbord met je wachtwoorden en alle identiteitsvelden activeren - Magikeyboard-instellingen - Stel het toetsenbord in om formulieren veilig automatisch in te vullen. - Activeer Magikeyboard in de apparaatinstellingen. - \"Instellingen\" → \"Taal en invoer\" → \"Huidig toetsenbord\" en kies er een. - Kies Magikeyboard als je een formulier moet invullen. - Schakel tussen toetsenborden door lang op de spatiebalk te drukken of, als deze niet beschikbaar is, met: - Kies je iteminvoer met de toets. - Voer je velden in met de elementen van het item. - Databank vergrendelen. - Standaard-toetsenbord gebruiken. + Aangepast toetsenbord met je wachtwoorden en alle identiteitsvelden activeren Geen hoofdwachtwoord toestaan Schakel de knop \"Openen\" in als er geen referenties zijn geselecteerd Alleen-lezen @@ -306,16 +283,14 @@ Maak je eerste wachtwoordbeheerbestand aan. Open een bestaande databank Open vanuit je bestandsverkenner een bestaande databank om dit verder te gebruiken. - Een link naar de bestandslocatie is voldoende - Je kunt je databank ook openen middels een fysieke link (bijv. met file:// en content://). Voeg items toe aan de databank Voeg items toe om je digitale identiteiten te beheren. \n \nVoeg groepen toe (groepen zijn gelijk aan mappen) om items in je databank te organiseren. "Doorzoek al je items" Doorzoek items op titel, gebruikersnaam of andere velden om wachtwoorden te vinden. - Ontgrendel de databank met je vingerafdruk - Koppel je wachtwoord en vingerafdruk om de databank snel te ontgrendelen. + Ontgrendel de databank met je vingerafdruk + Koppel je wachtwoord en vingerafdruk om de databank snel te ontgrendelen. Item bewerken Bewerk het item met aangepaste velden. Referenties kunnen worden toegevoegd tussen velden van verschillende items. Genereer een sterk wachtwoord voor het item. @@ -392,8 +367,6 @@ Wachtwoord wissen Wist het ingevoerde wachtwoord na een verbindingspoging Bestand openen - Bestandskoppeling weergeven - Bestandskoppeling openen Items onder het knooppunt Knooppunt toevoegen Item toevoegen diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 854dad3d9..0d3ddb9ae 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -31,7 +31,6 @@ Programinnstillingar Parentesar Du må ha Open Intents filbehandlar for å kunna bla i filer. Klikk nedanfor for å installera han. Grunna nokre småfeil i programmet kan det vera at det ikkje fungerer heilt den første gongen du bruker det. - Avbryt Utklippstavla er tømt. Tidsavbrot på utklippstavla Tid før utklippstavla blir tømt etter at brukarnamnet eller passordet er kopiert. @@ -66,11 +65,9 @@ Telefonen gjekk tom for minne ved lesinga av databasen din. Databasen er kanskje for stor. Du må velja minst éin passordlagingstype Passorda samsvarer ikkje. - Omgangar må vera eit tal. For mange omgangar. Bruker 2147483648. Treng ein tittel. Bruk eit positivt heiltal i lengdfeltet - Fann ikkje fila. Filbehandlar Lag passord stadfest passordet @@ -82,7 +79,7 @@ passord Installer frå Play Store Installer frå F-Droid - Ugyldig passord eller nøkkelfil. + Ugyldig passord eller nøkkelfil. Ukjent databaseformat. Lengd Gruppelistestorleik diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d5f65001b..4870a7144 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -31,7 +31,6 @@ Ustawienia aplikacji Nawiasy Przeglądaj pliki, instalując Menedżera plików OpenIntents - Anuluj Schowek został wyczyszczony Czas wygaśnięcia schowka Czas przechowywania w schowku @@ -65,11 +64,9 @@ W urządzeniu zabrakło pamięci do załadowania całej bazy danych. Należy wybrać co najmniej jeden rodzaj generowania hasła. Hasła nie pasują do siebie. - \"Rundy szyfrowania\" muszą być liczbą. \"Rundy szyfrowania\" są zbyt wysokie. Ustaw na 2147483648. Dodaj tytuł. Wprowadź dodatnią liczbę całkowitą w polu \"Długość\". - Nie znaleziono pliku. Przeglądarka plików Generuj hasło potwierdź hasło @@ -81,7 +78,7 @@ hasło Zainstaluj z Play-Store Zainstaluj z F-Droid - Nieprawidłowe hasło lub plik klucza. + Nieprawidłowe hasło lub plik klucza. Nie można rozpoznać formatu bazy danych. Długość Wielkość listy grup @@ -109,7 +106,7 @@ Zainstaluj przeglądarkę internetową, aby otworzyć ten adres URL. Ostatnio używana baza danych Nie wyszukuj wpisów kopii zapasowej - Pomiń grupę \"Kopia zapasowa\" z wyników wyszukiwania (dotyczy tylko plików .kdb) + Pomija grupy „Kopia zapasowa” i „Kosz” w wynikach wyszukiwania Tworzenie nowej bazy danych… Pracuję… Najnowsza historia plików @@ -220,14 +217,14 @@ Problem z odciskiem palca: %1$s Użyj odcisku palca, aby zapisać to hasło Baza danych nie ma jeszcze hasła. - Historia + Historia Wygląd Ogólne Wypełnij automatycznie Autouzupełnianie formularzy KeePass DX Zaloguj się za pomocą KeePass DX Ustaw domyślną usługę autouzupełniania - Włącz autouzupełnianie, aby móc szybko wypełniać formularze w innych aplikacjach + Włącz autouzupełnianie, aby móc szybko wypełniać formularze w innych aplikacjach Wygenerowany rozmiar hasła Ustawia domyślny rozmiar wygenerowanych haseł Znaki hasła @@ -239,13 +236,6 @@ Blokada Blokada ekranu Zablokuj bazę danych, gdy ekran jest wyłączony - Jak skonfigurować skanowanie linii papilarnych do szybkiego odblokowania\? - Zapisz zeskanowany odcisk palca w urządzeniu - \"Ustawienia\" → \"Bezpieczeństwo\" → \"Odcisk palca\" - Wprowadź hasło blokady bazy danych - Zeskanuj swój odcisk palca, aby bezpiecznie zapisać hasło blokady bazy danych. - Zeskanuj swój odcisk palca, aby otworzyć bazę danych po wyłączeniu hasła. - Użycie Odcisk palca Skanowanie odcisków palców Umożliwia skanowanie odcisku palca w celu otwarcia bazy danych @@ -266,12 +256,9 @@ Przenosi grupy i wpisy do \"Kosza\" przed usunięciem Pole czcionka Zmień czcionkę użytą w polach, aby poprawić widoczność postaci - Otwórz pliki, wybierając - Automatyczne otwieranie plików po wybraniu w przeglądarce plików Zaufanie do schowka Zezwalaj na zapisywanie hasła dostepu i chronionych pól do schowka OSTRZEŻENIE: schowek jest udostępniany przez wszystkie aplikacje. Jeśli dane wrażliwe zostaną skopiowane, inne oprogramowanie może je odzyskać. - Link do otwarcia pliku bazy danych Nazwa bazy danych Opis bazy danych Wersja bazy danych @@ -280,17 +267,7 @@ Inne Klawiatura Magikeyboard - Aktywuj niestandardową klawiaturę wypełniającą hasła i wszystkie pola tożsamości - Ustawienia Magikeyboard - Skonfiguruj klawiaturę, aby bezpiecznie wypełniać formularze. - Aktywuj \"Magikeyboard\" w ustawieniach urządzenia. - \"Ustawienia\" → \"Język i wprowadzanie\" → \"Aktualna klawiatura\" i wybierz jedną. - Wybierz Magikeyboard, gdy potrzebujesz wypełnić formularz. - Przełącz klawiaturę naciskając klawisz spacji na klawiaturze lub, jeśli nie jest dostępny, z: - Wybierz wpis za pomocą klawisza. - Wypełnij swoje pola, używając elementów pozycji. - Zablokuj bazę danych. - Użyj ponownie domyślnej klawiatury. + Aktywuj niestandardową klawiaturę wypełniającą hasła i wszystkie pola tożsamości Zezwalaj na brak klucza głównego Włącz przycisk \"Otwórz\", jeśli nie wybrano uwierzytelnień Ochrona przed zapisem @@ -304,20 +281,18 @@ Utwórz swój pierwszy plik zarządzania hasłami. Otwórz istniejącą bazę danych Otwórz starszy plik bazy danych w przeglądarce plików, aby nadal z niego korzystać. - Link do lokalizacji pliku jest wystarczający - Możesz także otworzyć swoją bazę danych za pomocą fizycznego linku (with file:// and content:// for example). Dodaj elementy do swojej bazy danych "Dodaj wpisy, aby zarządzać swoimi cyfrowymi tożsamościami. \n \nDodaj grupy (odpowiednik folderów), aby uporządkować swoje wpisy i bazę danych." Przeszukuj wpisy Wprowadź tytuł, nazwę użytkownika lub zawartość innych pól, aby odzyskać swoje hasła. - Odblokuj bazę danych za pomocą odcisku palca - Połącz swoje hasło z zeskanowanym odciskiem palca, aby szybko odblokować bazę danych. + Odblokuj bazę danych za pomocą odcisku palca + Połącz swoje hasło z zeskanowanym odciskiem palca, aby szybko odblokować bazę danych. Edytuj wpis Edytuj swój wpis za pomocą pól niestandardowych. Dane puli mogą być przywoływane między różnymi polami wprowadzania. Utwórz silne hasło do swojego wpisu. - Wygeneruj silne hasło do powiązania z wpisem, z łatwością określ je zgodnie z kryteriami formularza i nie zapomnij o bezpiecznym haśle. + Wygeneruj silne hasło, które będzie kojarzyć się z Twoim wpisem, łatwo zdefiniuj je zgodnie z kryteriami formularza i nie zapomnij o bezpiecznym haśle. Dodaj niestandardowe pola Zarejestruj podstawowe pole niedostarczone, wypełniając nowe pole, które możesz również chronić. Odblokuj swoją bazę danych @@ -338,7 +313,7 @@ Weź udział Pomóż zwiększyć stabilność, bezpieczeństwo i dodawanie kolejnych funkcji. W przeciwieństwie do wielu aplikacji do zarządzania hasłami, ta jest wolna od reklam, jest oprogramowaniem darmowym typu copylefted libre i nie zbiera danych osobowych na swoich serwerach, bez względu na to, jakiej wersji używasz. - Kupując wersję pro, będziesz mieć dostęp do tej funkcja wizualna a szczególnie pomożesz zrealizować projekty społecznościowe. + Kupując wersję pro, będziesz mieć dostęp do tej funkcji wizualnej a szczególnie pomożesz zrealizować projekty społecznościowe. Ta funkcja wizualna jest dostępna dzięki Twojej hojności. Aby zachować naszą wolność i być zawsze aktywnym, liczymy na Twój wkład. @@ -372,14 +347,14 @@ %1$s dostępne na Magikeyboard %1$s Wyczyść przy zamykaniu - Zamknij bazę danych po zamknięciu powiadomienia. + Zamknij bazę danych podczas zamykania powiadomienia Wygląd Motyw klawiatury Klawiatura Wibracja po naciśnięciu klawisza Dźwięk przy naciśnięciu Nie zabijaj aplikacji… - Naciśnij Wstecz na głównym, aby zablokować + Naciśnij Wstecz na stronie głównej, aby zablokować Zablokuj bazę danych, gdy użytkownik kliknie przycisk \"Wstecz\" na ekranie głównym Wyczyść po zamknięciu Zamknij bazę danych podczas zamykania powiadomienia @@ -389,8 +364,6 @@ Usuń hasło Usuwa hasło wprowadzone po próbie połączenia Otwórz plik - Pokaż link do pliku - Otwórz link do pliku Dodaj wpis Dodaj grupę Informacje o pliku @@ -405,4 +378,24 @@ Pokaż liczbę wpisów w grupie Tryb wyboru Dodaj węzeł + Powtórz przełączanie widoczności hasła + UUID + W to miejsce nie można przenieść wpisu. + Pole hasła + Pole pliku klucza + Tło + Aktualizuj + Zamknij pola + Nie można utworzyć bazy danych przy użyciu tego hasła i pliku klucza. + Zaawansowane odblokowywanie + Zapisz rozpoznawanie biometryczne + Przechowuj dane biometryczne w bazie danych + Otwarta baza danych z rozpoznawaniem biometrycznym + Wyodrębnij poświadczenia bazy danych z danymi biometrycznymi + Biometryczne + Automatyczne otwieranie monitu biometrycznego + Włącz + Wyłącz + Automatycznie otwieraj monit biometryczny, gdy klucz biometryczny jest zdefiniowany dla bazy danych + Węzły podrzędne \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 49b8ab630..bbbc7ff16 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -16,8 +16,6 @@ You should have received a copy of the GNU General Public License along with KeePass DX. If not, see . - - Portuguese translation by Carlos Schlyter. --> Comentários @@ -33,7 +31,6 @@ Configurações do aplicativo Parênteses Procure arquivos instalando o OpenIntents File Manager - Cancelar Área de transferência limpa Tempo limite para o clipboard Duração do armazenamento na área de transferência @@ -68,11 +65,9 @@ Falta de memória para abrir todo o banco. Pelo menos um tipo de geração de senhas deve ser selecionado. As senhas não combinam. - \"Número de rodadas\" deve ser um número. \"Número de rodadas\" é muito grande. Modificado para 2147483648. Insira um título. Digite um número inteiro positivo no campo \"Tamanho\". - Não pôde encontrar o arquivo. Localizador de arquivos Gerar senha confirmar senha @@ -84,7 +79,7 @@ senha Instalar a partir do Google Play Instalar a partir do F-Droid - Senha ou arquivo de chaves inválidos. + Senha ou arquivo de chaves inválidos. Não pôde reconhecer formato do banco de dados. Tamanho Tamanho da lista de grupos @@ -216,14 +211,14 @@ Problema de Impressão digital: %1$s Use Impressão digital para armazenar esta senha Ainda não há nenhuma senha armazenada nesse banco de dados. - Histórico + Histórico Aparência Geral Preenchimento automático Preenchimento Automático KeePass DX Entre com o KeePass DX Definir serviço padrão de preenchimento automático - Habilite o serviço para rapidamente preencher formulários em outros aplicativos + Habilite o serviço para rapidamente preencher formulários em outros aplicativos Comprimento da senha gerada Define o tamanho padrão para senhas geradas Caracteres da senha @@ -234,13 +229,6 @@ Bloquear Bloqueio de tela Bloqueie o banco de dados quando a tela estiver desligada - Como configurar impressões digitais para desbloqueio rápido? - Salvar sua impressão digital esconeada para seu dispositivo em - \"Configurações\" → \"Segurança\" → \"Impressão Digital\" - Entre com sua senha no banco - Escaneie sua impressão digital para armazenar sua senha da base com segurança. - Escaneie sua impressão digital para abrir o banco de dados quando a senha estiber desativada. - Uso Impressão Digital Escaneamento de impressão digital Permite que você escaneie sua impressão digital para a abertura do banco de dados @@ -261,12 +249,9 @@ Move grupos e entradas para a \"Lixeira\" antes de apagar Fonte do Campo Muda a fonte usada nos campos para melhor visibilidade dos caracteres - Abrir arquivos ao selecionar - Abrir automaticamente arquivos selecionados no gerenciador de arquivos Confiança da área de transferência Permite a cópia da senha e de campos protegidos para a área de transferência AVISO: A área de transferência é compartilhada por todos os aplicativos. Se dados sensíveis forem copiados, outros programas podem recuperá-lo. - Link do arquivo da base de dados a ser aberto Nome do banco de dados Descrição do banco de dados Versão do banco de dados @@ -275,17 +260,7 @@ Outros Teclado Magikeyboard - Ative um teclado customizado, populando suas senhas e todos os campos de identidade - Configurações do Magikeyboard - Configure o teclado para automaticamente preencher formulários seguramente. - Ative \"Magikeyboard\" nas configurações do dispositivo. - \"Configurações\" → \"Idioma e Entrada\" → \"Teclado Atual\" → \"Selecionar Teclado\" e escolha um. - Escolha o Magikeyboard quando precisar preencher um formulário. - Troque teclados ao pressionar e segurar a barra de espaço de seu teclado, ou, se não estiver disponível, com: - Selecione uma entrada com a chave. - Preencha seus campos usando os elementos da entrada. - Bloqueie o banco de dados. - Voltar ao seu teclado principal. + Ative um teclado customizado, populando suas senhas e todos os campos de identidade Não permitir chave mestra Ativar o botão \"Abrir\" se nenhuma credencial for selecionada Apenas leitura @@ -299,16 +274,14 @@ Crie seu primeiro arquivo de gerenciamento de senhas. Abra um banco de dados existente Abra seu banco de dados de mais cedo pelo seu navegador de arquivos. - Um link para a localização do seu arquivo é suficiente - Você pode também abrir o seu banco com um link físico (com file:// e content:// por exemplo). Adicione itens ao seu banco Entradas ajudam a gerenciar suas identidades digitais. \n \nGrupos (~ pastas) organizam suas entradas e seu banco de dados. Pesquise suas entradas Entre com título, nome de usuário ou outros campos para recuperar facilmente suas senhas. - Destrave do banco de dados por Impressão digital - Faça o link entre sua senha e sua impressão digital para rapidamente desbloquear seu banco de dados. + Destrave do banco de dados por Impressão digital + Faça o link entre sua senha e sua impressão digital para rapidamente desbloquear seu banco de dados. Modifique a entrada Edite a sua entrada com campos personalizados. Os conjuntos de dados podem ser referenciados entre campos de entradas diferentes. Crie uma senha forte para sua entrada. @@ -333,15 +306,12 @@ Participar Ajude a aumentar a estabilidade, segurança e na adição de mais recursos. Ao contrário de muitos aplicativos de gerenciamento de senhas, este aplicativo é livre de anúncios, software livre e não recupera dados pessoais em seus servidores, mesmo em sua versão gratuita. - Ao comprar a versão pro, você terá acesso a este recurso visual e ajudará especialmente a realização de projetos comunitários. - + Ao comprar a versão pro, você terá acesso a este recurso visual e ajudará especialmente a realização de projetos comunitários. Este recurso visual está disponível graças à sua generosidade. - Para manter a nossa liberdade e estarmos sempre ativos, nós contamos com a sua contribuição. - + Para manter a nossa liberdade e estarmos sempre ativos, nós contamos com a sua contribuição. Esse recurso está em desenvolvimento e exige que sua contribuição para que esteja disponível em breve. Ao comprar a versão pro, - - Contribuindo , + Contribuindo , Você está incentivando os desenvolvedores a criar novos recursos e a corrigir erros de acordo com suas observações. Obrigado por sua contribuição. Estamos trabalhando duro para lançar esse recurso o mais rápido possível. @@ -385,7 +355,7 @@ Não feche o aplicativo… Pressione Voltar na raiz para trancar Tranca a base de dados quando o usuário pressiona o botão Voltar na tela inicial - Limpar a Área de Transferência ao fechar + Limpar a área de transferência ao fechar Fecha a base de dados ao dispensar a notificação Lixeira Seleção de entrada @@ -393,8 +363,6 @@ Deletar senha Deleta a senha inserida após uma tentativa de conexão Abrir arquivo - Exibir link do arquivo - Abrir link do arquivo Inserir nó Adicionar entrada Adicionar grupo @@ -410,4 +378,22 @@ Mostrar número de entradas Mostrar o número de entradas dentro de um grupo Salvar entrada + Nó filho + Caixa de seleção de senha + Caixa de seleção do arquivo-chave + Repetir mudança de visibilidade da senha + Plano de fundo + Atualizar + Campos fechados + Impossibilitado de criar um banco de dados com essa senha e arquivo-chave. + Desbloqueio avançado + Salvar reconhecimento biométrico + Armazenar credenciais do banco de dados com biometria + Abrir banco de dados com biometria + Extrair credenciais do banco de dados com biometria + Biometria + Abrir automaticamente o prompt de biometria + Abrir automaticamente o prompt de biometria quando a chave biométrica for definida para o banco de dados + Habilitado + Desabilitado \ No newline at end of file diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 7e11de7f2..162d35749 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -32,7 +32,6 @@ Não mostrar novamente Parênteses Explore ficheiros instalando o gestor de ficheiros OpenIntents - Cancelar Área de transferência limpa Erro na área de transferência Alguns dispositivos Samsung Android não deixam as apps usarem a área de transferência. @@ -71,14 +70,12 @@ Sem memória para carregar toda a bases de dados. Pelo menos um tipo de geração de palavra-chave deve ser selecionado. As palavras-passe não coincidem. - \"Número de rodadas\" deve ser um número. \"Número de rodadas\" é muito grande. Modificado para 2147483648. Um nome do campo é necessário para cada string. Adicione um título. Digite um número inteiro positivo no campo \"Tamanho\". Nome do campo Valor do campo - Não pôde encontrar o ficheiro. Arquivo não encontrado. Tente reabrí-lo de seu provedor de conteúdo. Localizador de ficheiros Gerar palavra-chave @@ -91,7 +88,7 @@ Palavra-chave Instalar pela Play Store Instalar pela F-Droid - Palavra-chave ou ficheiro chave inválidos. + Palavra-chave ou ficheiro chave inválidos. Algoritmo errado. Não pôde reconhecer formato do banco de dados. Não existem ficheiros-chave. @@ -212,14 +209,14 @@ Problema da Impressão digital: %1$s Use a impressão digital para armazenar esta palavra-chave Ainda não há nenhuma palavra-chave armazenada nesta base de dados. - Histórico + Histórico Aparência Geral Preenchimento automático Serviço de Preenchimento Automático do KeePass DX Entrar com KeePass DX Definir como serviço de preenchimento automático padrão - Ativar o serviço para preencher formulários em outras aplicações + Ativar o serviço para preencher formulários em outras aplicações Tamanho da palavra-chave gerada Editar entrada Encriptação @@ -236,10 +233,6 @@ Bloquear Bloqueio de tela Bloqueie o banco de dados quando a tela estiver desligada - Salvar sua impressão digital esconeada para seu dispositivo em - Escaneie sua impressão digital para armazenar sua palavra-passe da base com segurança. - Escaneie sua impressão digital para abrir o banco de dados quando a palavra-passe estiber desativada. - Uso Não foi possível iniciar esse recurso. Sua versão do Android %1$s não corresponde a versão mínima %2$s necessária. Não foi possível encontrar o hardware correspondente. @@ -253,11 +246,8 @@ Move grupos e entradas para a \"Lixeira\" antes de apagar Fonte do Campo Muda a fonte usada nos campos para melhor visibilidade dos caracteres - Abrir ficheiros ao selecionar - Abrir automaticamente ficheiros selecionados no gerenciador de ficheiros Confiança da área de transferência Permite a cópia da palavra-passe e de campos protegidos para a área de transferência - Link do ficheiro da base de dados a ser aberto Nome do banco de dados Descrição do banco de dados Versão do banco de dados @@ -266,7 +256,7 @@ Outros Teclado Magikeyboard - Ative um teclado customizado, populando suas palavras-passe e todos os campos de identidade + Ative um teclado customizado, populando suas palavras-passe e todos os campos de identidade Reiniciar telas educacionais Exibir todos os itens educacionais denovo Telas educacionais redefinidas @@ -274,16 +264,14 @@ Crie seu primeiro ficheiro de gerenciamento de palavras-passe. Abra um banco de dados existente Abra seu banco de dados de mais cedo pelo seu navegador de ficheiros. - Um link para a localização do seu ficheiro é suficiente - Você pode também abrir o seu banco com um link físico (com file:// e content:// por exemplo). Adicione itens ao seu banco Entradas ajudam a gerenciar suas identidades digitais. \n \nGrupos (~ pastas) organizam suas entradas e seu banco de dados. Pesquise suas entradas Entre com título, nome de usuário ou outros campos para recuperar facilmente suas palavras-passe. - Destrave do banco de dados por Impressão digital - Faça o link entre sua palavra-passe e sua impressão digital para rapidamente desbloquear seu banco de dados. + Destrave do banco de dados por Impressão digital + Faça o link entre sua palavra-passe e sua impressão digital para rapidamente desbloquear seu banco de dados. Modifique a entrada Edite a sua entrada com campos personalizados. Os conjuntos de dados podem ser referenciados entre campos de entradas diferentes. Crie uma palavra-passe forte para sua entrada. @@ -326,15 +314,6 @@ Pacote de ícones usado no aplicativo Se a limpeza da área de transferência falhar, limpe seu histórico manualmente. AVISO: A área de transferência é compartilhada por todos os aplicativos. Se dados sensíveis forem copiados, outros programas podem recuperá-lo. - Configurações do Magikeyboard - Configure o teclado para automaticamente preencher formulários seguramente. - Ative \"Magikeyboard\" nas configurações do dispositivo. - Escolha o Magikeyboard quando precisar preencher um formulário. - Troque teclados ao pressionar e segurar a barra de espaço de seu teclado, ou, se não estiver disponível, com: - Selecione uma entrada com a chave. - Preencha seus campos usando os elementos da entrada. - Bloqueie o banco de dados. - Voltar ao seu teclado principal. Não permitir chave mestra Ativar o botão \"Abrir\" se nenhuma credencial for selecionada Telas educacionais @@ -379,8 +358,6 @@ Deletar palavra-passe Deleta a palavra-passe inserida após uma tentativa de conexão Abrir ficheiro - Exibir link do ficheiro - Abrir link do ficheiro Crianças do nó Inserir nó Adicionar entrada diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0d1503ee5..d153afd78 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -16,8 +16,7 @@ You should have received a copy of the GNU General Public License along with KeePass DX. If not, see . ---> - +--> Обратная связь Сайт Android-версия программы управления паролями KeePass @@ -28,29 +27,28 @@ Задержка Задержка блокировки при бездействии Приложение - Настройки KeePass DX + Настройки приложения Не показывать снова {[(Скобки)]} - Для обзора файлов установите OpenIntents File Manager - Отмена + Создание, открытие и сохранение файла базы требует установки файлового менеджера, который принимает действия Intent ACTION_CREATE_DOCUMENT и ACTION_OPEN_DOCUMENT Буфер обмена очищен Ошибка буфера обмена Некоторые устройства Samsung не дают приложению использовать буфер обмена. Не удалось очистить буфер обмена Задержка очистки буфера обмена Продолжительность хранения в буфере обмена - Выберите %1$s для копирования в буфер обмена + Выберите %1$s для копирования в буфер обмена Создание ключа базы… База Расшифровка базы… База по умолчанию Цифры - KeePass DX \u00A9 %1$d Kunzisoft Программа предоставляется без каких-либо гарантий. Распространяется свободно по лицензии GPL v3 или новее. + Приложение KeePass DX © %1$d Kunzisoft предоставляется без каких-либо гарантий. Распространяется свободно по лицензии GPL v3 или новее. Открыть существующую базу Доступ Отмена Заметки - Подтверждение + Подтверждение пароля Создано Истекает Файл ключа @@ -60,30 +58,28 @@ Сохранить Название Ссылка - Логин - Потоковый шифр RC4 не поддерживается. - Не удалось обработать указанный URI в KeePass DX. - Не удалось создать файл: + Имя + Потоковый шифр ARCFOUR не поддерживается. + Невозможно обработать указанный URI в KeePass DX. + Невозможно создать файл: Невозможно прочитать базу. - Убедитесь что путь указан верно. + Убедитесь, что путь указан правильно. Введите название. Выберите файл ключа. Недостаточно памяти для работы с базой. Выберите один или несколько типов символов. Пароли не совпадают. - Введите число. Предельное значение 2147483648. Каждое поле должно иметь название. Введите название. Поле \"Длина\" должно быть положительным целым числом. Название поля Значение поля - Файл не найден. - Файл не найден. Попробуйте повторно открыть через встроенный поставщик содержимого. + Файл не найден. Попробуйте открыть его через файловый менеджер. Обзор файлов - Генерация пароля - подтвердите пароль - сгенерированный пароль + Создание пароля + подтверждение пароля + созданный пароль Название группы файл ключа длина @@ -91,58 +87,58 @@ Пароль Google Play F-Droid - Неверный пароль или файл ключа. - Неверный алгоритм. - Не удалось определить формат базы. + Неверный пароль или файл ключа. + Неправильный алгоритм. + Невозможно определить формат базы. Файл ключа не найден. Файл ключа пуст. - Длина пароля + Длина Размер списка Размер шрифта элементов списка Загрузка базы… Строчные - Скрыть пароли - Скрывать пароли по умолчанию + Скрывать пароли + Скрывать пароли за (***) по умолчанию Сведения - Изменить мастер-ключ + Изменить пароль базы Настройки Настройки базы Удалить Помочь - Правка - Скрыть + Изменить + Скрыть пароль Заблокировать базу Открыть Поиск - Показать + Показать пароль Перейти -Дефис- Никогда Совпадения не найдены - Не удалось открыть ссылку. + Установите браузер, чтобы открыть этот URL. Недавно открытые Не искать в резервных копиях - Не искать в Резервировании (.kdb) + Не искать в \"Резервировании\" и \"Корзине\" Создание новой базы… Обработка… Защита Только чтение - KeePass DX не имеет разрешений на запись по указанному пути. База будет открыта только для чтения. + KeePass DX необходимо разрешение на запись, чтобы иметь возможность изменить что-либо в вашей базе. Начиная с Android KitKat, на некоторых устройствах приложениям запрещена запись на SD-карту. Недавно открытые - Хранить имена открытых файлов + Хранить имена недавно открытых файлов Хранить пути к файлам ключей Хранить файлы ключей Убрать из списка Rijndael (AES) База Раунды шифрования - Больше раундов шифрования – выше стойкость базы к подбору пароля, но медленнее открытие и сохранение. - раунды + Дополнительные раунды шифрования – выше стойкость базы к подбору пароля, но медленнее открытие и сохранение. + раундов шифрования Сохранение базы… П р о б е л Поиск - Сортировка баз + Естественный порядок $пеци@льные Поиск Результаты поиска @@ -151,20 +147,20 @@ Неподдерживаемая версия базы. ЗАГЛАВНЫЕ Внимание - Избегайте использования в пароле символов вне кодировки Latin-1 в .kbd файлах, так как эти символы будут преобразованы в одинаковый символ. - Карта помять в режиме только для чтения. Изменения не будут сохранены. - Карта памяти не подключена. Работа с базой невозможна. + Избегайте использования в пароле символов вне кодировки текста в файле базы, так как эти символы будут преобразованы в одинаковый символ. + Предоставьте доступ к SD-карте на запись для сохранения изменений в базе. + Подключите SD-карту для создания или загрузки базы. Версия %1$s Отпечатки пальцев поддерживаются, но не настроены. Ожидание отпечатка пальца Зашифрованный пароль сохранён - Неверный ключ отпечатка пальца. Восстановите пароль. - Проблема с отпечатком пальца : %1$s + Неправильный отпечаток пальца. Восстановите пароль. + Проблема с отпечатком пальца: %1$s Используйте отпечаток пальца, чтобы сохранить пароль - Для этой базы пароль ещё не сохранён + Для этой базы пароль ещё не сохранён. Введите пароль и/или файл ключа, чтобы разблокировать базу. \n -\nНе забудьте сохранить копию .kdbx файла в безопасном месте после каждого изменения. +\nНе забывайте сохранять копию файла базы в безопасном месте после каждого изменения. 5 секунд 10 секунд @@ -184,29 +180,29 @@ Шифрование Функция формирования ключа Расширенный набор ASCII - "Сервис автозаполнения не может быть включен." - Копия %1$s - Заполнение формы + Сервис автозаполнения не может быть включён. + %1$s скопировано + Заполнение форм Алгоритм шифрования базы для всех данных. - При генерации ключа для алгоритма шифрования, мастер-ключ преобразуется при помощи функции формирования ключа со случайной солью. + При создании ключа для алгоритма шифрования, пароль базы преобразуется при помощи функции формирования ключа со случайной солью. Использование памяти - Количество памяти (в байтах), которое будет использоваться функцией формирования ключа. + Объём памяти (в байтах), которое будет использоваться функцией формирования ключа. Уровень параллелизма Уровень параллелизма (т.е. количество потоков), используемый функцией формирования ключа. Сортировка По возрастанию ↓ - Логину - Создание - Правка - Дате использования + Имя пользователя + Время создания + Время изменения + Время последнего доступа Редактировать запись Разрешить Смахните, чтобы очистить буфер обмена сейчас Невозможно загрузить базу. Невозможно загрузить ключ. Попробуйте уменьшить размер памяти, используемой функцией формирования ключа (KDF). Нельзя переместить группу в саму себя. - Показывать логин - Показывать логин в списке записей + Показывать имя + Показывать имя пользователя в списке записей Копировать Переместить Вставить @@ -215,37 +211,30 @@ Только чтение Чтение и запись Сначала группы - Корзина внизу - Название - Вы действительно хотите использовать пустой пароль? + \"Корзина\" внизу + Название записи + Вы действительно не хотите использовать пароль для защиты базы\? Вы действительно не хотите использовать ключ шифрования? Отпечаток пальца не распознан - История + История Внешний вид Общие Автозаполнение Сервис автозаполнения KeePass DX Войти с помощью KeePass DX - Установить Сервис автозаполнения по умолчанию - Включить сервис для быстрого заполнения форм в других приложениях - Длина пароля - Установить длину сгенерированных паролей по умолчанию + Включить сервис для быстрого заполнения форм в других приложениях + Сервис автозаполнения по умолчанию + Длина создаваемого пароля + Установить длину создаваемых паролей по умолчанию Символы пароля - Установить символы для генерации пароля по умолчанию + Установить набор разрешённых символов для создания пароля Буфер обмена Уведомления буфера обмена - Включить уведомления буфера обмен ля копирования полей записи - Если автоматическая очистка буфера обмена недоступна на вашем устройстве, очистите историю буфера обмена вручную. + Включить уведомления буфера обмена для копирования полей при просмотре записи + Если автоматическая очистка буфера обмена недоступна, очистите историю буфера обмена вручную. Блокировка Блокировка экрана Блокировать базу при отключении экрана - Как настроить отпечаток пальца для быстрой разблокировки? - Установите ваш личный отпечаток пальца для вашего устройства - \"Настройки\" → \"Безопасность\" → \"Отпечаток пальца\" - Введите ваш пароль в Keepass DX - Сканируйте ваш отпечаток пальца для безопасного хранения вашего мастер-пароля - Сканируйте ваш отпечаток пальца, когда поле \"Пароль\" не отмечено, для разблокировки базы - Использование Отпечаток пальца Сканирование отпечатка пальца Включить разблокировку базы с помощью отпечатка пальца @@ -254,113 +243,158 @@ Вы уверены, что хотите удалить все ключи, связанные с отпечатком пальца? Невозможно запустить эту функцию. Ваша версия Android %1$s ниже минимально необходимой %2$s. - Оборудование не найдено. - Название файла + Соответствующее оборудование не найдено. + Имя файла Путь - Установить мастер-ключ + Установить пароль базы Создать новую базу Байт Путь к файлу Показывать полный путь к файлу - Использовать Корзину - Перемещать группу или запись в Корзину перед удалением + Использовать \"Корзину\" + Перемещать группу или запись в \"Корзину\" вместо удаления Шрифт полей - Изменить шрифт, используемый в полях, для лучшей видимости - Автоматически открывать выбранный файл - Открывать файл автоматически после выбора в файловом менеджере - Копирование пароля - Разрешить копирование пароля и защищенных полей в буфер обмена - ПРЕДУПРЕЖДЕНИЕ: Буфер обмена доступен всем приложениям. Если чувствительные данные копируются, другие программы могут его перехватить. - Ссылка на файл KDBX + Изменить используемый в полях шрифт для лучшей читаемости + Доверять буферу обмена + Разрешить копирование пароля и защищённых полей в буфер обмена + ПРЕДУПРЕЖДЕНИЕ: буфер обмена доступен всем приложениям. Если копируются чувствительные данные, другие программы могут их перехватить. Название базы Описание базы Версия базы - Отображение текста - Отображение приложения + Текст + Приложение Прочее Клавиатура Magikeyboard - Активировать пользовательскую клавиатуру для простого заполнения паролей и всех ваших идентификаторов - Настройки Magikeyboard - Как настроить клавиатуру для безопасного заполнения форм? - Активировать Magikeyboard в настройках устройства. - \"Настройки\" → \"Язык и ввод\" → \"Текущая клавиатура\" и выбрать. - Выбрать Magikeyboard, когда вам необходимо заполнить форму. - Вы можете легко переключаться с основной клавиатуры на Magikeyboard с помощью кнопки языка клавиатуры, длительного нажатия на пробел клавиатуры или, если она недоступна, с помощью: - Выберите запись с помощью кнопки. - Заполните поля, используя элементы записи. - Заблокируйте базу. - Вернутесь к основной клавиатуре. + Активировать пользовательскую клавиатуру для простого заполнения паролей и всех ваших идентификаторов Разрешить без пароля - Включить кнопку открыть, если не выбрана идентификация паролем + Включить кнопку \"Открыть\", если пароль не указан Только чтение По умолчанию открывать базу только для чтения Экраны обучения - Выделять элементы, чтобы узнать, как работает приложение + Выделять элементы, чтобы показать, как работает приложение Сбросить экраны обучения - Сбрасывает отображение обучающих элементов + Будет снова включено отображение всех обучающих элементов Экраны обучения сброшены Создайте файл базы - Вы ещё не знаете KeePass DX, создайте свой первый файл управления паролями. + Создайте свой первый файл управления паролями. Откройте существующую базу - Вы уже использовали KeePass менеджер. Просто откройте ваш файл KDBX из вашего файлового менеджера. - Ссылки на местоположение файла достаточно - Вы также можете открыть базу с помощью физической ссылки (например, с file:// или content://). + Откройте ранее созданный файл базы из файлового менеджера, чтобы продолжить его использование. Добавляйте новые элементы в базу Добавляйте записи для управления цифровыми идентификаторами. \n \nДобавляйте группы (аналог папок) для организации записей и баз. Легко находите ваши записи - Ищите записи по названию, логину или другим полям для быстрого доступа к вашим паролям. - Разблокируйте базу с помощью отпечатка пальца - Установите связь между паролем и отпечатком пальца для быстрой разблокировки базы. + Разблокируйте базу с помощью отпечатка пальца + Установите связь между паролем и отпечатком пальца для быстрой разблокировки базы. + Ищите записи по названию, имени или другим полям для быстрого доступа к своим паролям. Редактируйте записи - Редактируйте записи с настраиваемыми полями. Ссылки на пул данных могут быть добавлены между полями разных записей. - Создайте надежный пароль - Создайте надежный пароль, чтобы связать с записью, легко определить его по критериям формы и не забывайте, что пароль можно запомнить. + Редактируйте записи с настраиваемыми полями. Возможны перекрёстные ссылки между полями разных записей. + Создайте надёжный пароль для записи. + Создайте надёжный пароль, связанный с записью, легко настраиваемый под критерии формы. И не забудьте пароль от базы. Добавляйте настраиваемые поля - Если хотите зарегистрировать поле, которого ещё нет, просто введите в новое, которое также можно защитить визуально. + Зарегистрируйте дополнительное поле, просто заполнив его, добавьте значение и при необходимости защитите. Разблокируйте базу - Включите только для чтения - Изменяйте режим открытия для сессии. + База только для чтения + Изменяйте режим открытия в сессии. \n -\nВ режиме только для чтения, можно предотвратить непреднамеренные изменения в базе. -\n -\nВ режиме записи, вы можете добавлять, удалять или изменять все элементы, как вы хотите. +\nВ \"режиме только для чтения\" можно предотвратить непреднамеренные изменения в базе. +\nВ \"режиме записи\" вы можете добавлять, удалять или изменять любые элементы. Копируйте поля - Легко скопируйте поле, чтобы вставить его там, где вы хотите + Скопированные поля можно вставить в любом месте. \n -\nВы можете использовать несколько методов заполнения форм. Используйте тот, который вы предпочитаете. +\nИспользуйте метод заполнения формы, который вам наиболее удобен. Заблокируйте базу - Быстро заблокируйте вашу базу. Вы можете настроить приложение так, чтобы заблокировать её через некоторое время и при выключении экрана. - Сортируйте элементы - Сортируйте записи и группы по определенным параметрам. + Быстро блокируйте вашу базу. Вы можете настроить приложение так, чтобы заблокировать её через некоторое время и при выключении экрана. + Сортировка записей + Выберите критерий сортировки записей и групп. Участвуйте Примите участие в проекте для повышения стабильности, безопасности и добавления новых возможностей. - В отличие от многих приложений управления паролями, это без рекламы, свободное программное обеспечение (copyleft) и не хранит ваши личные данные на своих серверах, даже в бесплатной версии. - При покупке Pro версии, вы будете иметь доступ к этим визуальным функциям и особенно поможете реализации общественных проектов. + В отличие от многих приложений управления паролями, это без рекламысвободное программное обеспечение (copyleft) и не хранит ваши личные данные на своих серверах независимо от того, какую версию вы используете. + При покупке Pro-версии вы будете иметь доступ к этим визуальным функциям и особенно поможете реализации общественных проектов. - Эти визуальные функции доступны благодаря вашей щедрости. - Для того, чтобы сохранить нашу свободу и быть всегда активными, мы рассчитываем на ваш вклад. + Эти визуальные функции доступны благодаря вашей щедрости. + Для того, чтобы сохранить нашу независимость и быть всегда активными, мы рассчитываем на ваш вклад. - Эта функция на в разработке и требует вашего участия, чтобы быть доступной в ближайшее время. - Покупая Pro версию, + Эта функция находится в разработке и требует вашего участия, чтобы стать доступной в ближайшее время. + Покупая Pro-версию, Участвуя в проекте, - вы поощряете разработчиков создавать новые возможности и исправлять ошибки в соответствии с вашими замечаниями. + вы поощряете разработчиков добавлять новые возможности и исправлять ошибки в соответствии с вашими замечаниями. Спасибо большое за ваш вклад. Мы прилагаем все усилия, чтобы быстро выпустить эту функцию. Не забывайте обновлять приложение. Скачать Помощь проекту ChaCha20 - Функция формирования ключа AES + AES KDF Argon2 - Выбрать тему - Изменить тему приложения, изменив цвета - "Выбрать набор значков " - Изменить набор значков приложения + Тема приложения + Тема, используемая в приложении + Набор значков + Набор значков, используемый в приложении Magikeyboard Magikeyboard (KeePass DX) Настройки Magikeyboard + Сборка %1$s + Запись + Задержка + Время задержки перед очисткой вводимой записи + Информационное уведомление + Показывать уведомление, когда запись доступна + Запись + %1$s доступно в Magikeyboard + %1$s + Очищать при закрытии + Закрывать базу при закрытии уведомления + Внешний вид + Тема клавиатуры + Кнопки + Вибрация при нажатии + Звук при нажатии + Не убивать приложение… + \"Назад\" для блокировки + Блокировка базы при нажатии кнопки \"Назад\" на начальном экране + Очищать при закрытии + Закрывать базу при закрытии уведомления + Корзина + Выбор записи + Показывать поля ввода в Magikeyboard при просмотре записи + Удалять пароль + Забывать введённый пароль + Открыть файл + Дочерний узел + Добавить узел + Добавить запись + Добавить группу + Информация о файле + Флажок пароля + Флажок ключевого файла + Повтор переключения видимости пароля + Значок записи + Сохранение записи + Генератор паролей + Длина пароля + Добавить поле + Удалить поле + UUID + Вы не можете переместить запись сюда. + Вы не можете скопировать запись сюда. + Показывать количество записей + Показывать количество записей в группе + Фон + Обновить + Закрыть поля + Невозможно создать базу с этим паролем и ключевым файлом. + Дополнительная разблокировка + Сохранение отпечатка пальца + Сохранять пароль базы отпечатком пальца + Открывать базу отпечатком пальца + Извлекать пароль отпечатком пальца + Отпечаток пальца + Автозапрос отпечатка пальца + Автоматически открывать запрос отпечатка пальца, если отпечаток установлен для базы + Включить + Отключить + Режим выбора \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index b4d44415f..aaa0fd1fe 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -30,7 +30,6 @@ Nastavenia aplikácie Konzoly Prezeranie súborov vyžaduje otvorenie Správcu súborov, kliknite nižšie pre inštalovanie. Kôli chybám v správcovi súborov, prehľadávanie nemusí pracovať správne, ak prehľadávate prvý krát. - Zrušiť Schránka vyčistená. Timeout Schránky Čas uchovania v schránke @@ -65,11 +64,9 @@ Telefón vyčerpal pamäť pri analýze databázy. Možno je to príliš na Váš telefón. Musí byť vybraý najmenej jeden typ generovania hesla Heslá sa nezhodujú. - Opakovanie musí byť číslo. Príliš veľa opakovaní. Nastavujem na 2147483648. Vyžaduje sa názov. Zadajte celé kladné číslo na dĺžku poľa - Súbor nenájdený. Správca Súborov Generovať Heslo potvrdiť heslo @@ -81,7 +78,7 @@ heslo Inštalovať z Play Store Inštalovať z F-Droid - Chybné heslo, alebo súbor keyfile. + Chybné heslo, alebo súbor keyfile. Formát Databázy nerozpoznaný. Dĺžka Dĺžka zoznamu skupiny diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index e6a9f5129..12edf7f84 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -33,7 +33,6 @@ Visa inte igen Parenteser Filhantering kräver Open Intents File Manager, klicka nedan för att installera. Filhanteraren kanske inte fungerar korrekt vid första användningen. - Avbryt Urklippet är rensat. Urklippsfel Vissa Samsung-telefoner har en bugg som gör att applikationer inte kan kopiera till urklipp. För mer detaljer, gå till: @@ -71,14 +70,12 @@ The phone ran out of memory while parsing your database. It may be too large for your phone. At least one password generation type must be selected Lösenorden matchar inte. - Antalet rundor måste vara en siffra. Antalet rundor är för stort. Sätter värdet till 2147483648. Ett fältnamn krävs för varje sträng. En titel krävs. Ange ett positivt heltal i fältet för längd Fältnamn Fältvärde - Filen hittades inte. Filhanterare Generera lösenord bekräfta lösenord @@ -90,7 +87,7 @@ Lösenord Installera från Play Store Installera från F-Droid - Ogiltigt lösenord eller nyckelfil. + Ogiltigt lösenord eller nyckelfil. Ogiltig algoritm. Databasformatet är okänt. Nyckelfilen existerar inte. diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index b63d20695..61267754e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -22,9 +22,9 @@ Ana sayfa KeePass parola yöneticisinin Android uygulaması Kabul et - "Girdi Ekle " - "Girdi Düzenle " - "Grup Ekle " + Girdi Ekle + Girdi Düzenle + Grup Ekle Şifreleme Şifreleme algoritması Anahtar üretme fonksiyonu @@ -35,7 +35,6 @@ Parantez Genişletilmiş ASCII OpenIntents Dosya Yöneticisi\'ni yükleyerek dosyalara göz atın - İptal İzin ver Pano temizlendi Pano hatası @@ -77,7 +76,6 @@ Anahtar yüklenemedi. KDF \"Bellek Kullanımı\" nı azaltmaya çalışın. En az bir parola oluşturma türü seçilmelidir. Parolalar uyuşmuyor. - \"Dönüşüm turları\"nı bir sayı yapın. \"Dönüşüm turları\" çok yüksek. 2147483648\'e ayarlayın. Her dizenin bir alan adı olmalıdır. Bir başlık ekle. @@ -86,7 +84,6 @@ Bir grubu kendine taşıyamazsın. Alan adı Alan değeri - Dosya bulunamadı. Dosya bulunamadı. Dosya tarayıcınızda yeniden açmayı deneyin. Dosya tarayıcı Parola üret @@ -99,7 +96,7 @@ Parola F-Droid\'den yükleyin "Play Store\'dan yükleyin " - Parola veya anahtar dosya okunamadı. + Parola veya anahtar dosya okunamadı. Yanlış algoritma. Veritabanı biçimi tanımlanamadı. Hiç anahtar dosya yok. @@ -114,7 +111,7 @@ "Parolaları gizle " Parola maskesi. Varsayılan (***) Hakkında - Ana anahtarı değitir + Ana anahtarı değiştir %1$s kopyalandı Ayarlar Uygulama ayarları @@ -152,7 +149,7 @@ Android KitKat ile başlayan bazı cihazlar artık uygulamaların SD karta yazmasına izin vermiyor. Son dosya geçmişi Son dosya adlarını hatırla - Veritaban anahtar dosyaların yerini hatırlar + Veritabanı anahtar dosyaların yerini hatırlar Anahtar dosya kaydet Kaldır Kök @@ -202,14 +199,14 @@ Parmak izi sorunu: %1$s Bu şifreyi saklamak için parmak izini kullanın Bu veritabanının henüz bir parolası yok. - Geçmiş + Geçmiş Görünüm Genel Otomatik Doldurma KeePass DX formu otomatik doldurma KeePass DX ile giriş yap Varsayılan otomatik doldurma hizmetini ayarla - Diğer uygulamalardaki formları hızlı doldurmak için otomatik doldurmayı etkinleştirin + Diğer uygulamalardaki formları hızlı doldurmak için otomatik doldurmayı etkinleştirin Oluşturulan parola boyutu Oluşturulan parolaların varsayılan boyutunu ayarlar Parola karakterleri @@ -221,13 +218,6 @@ Kilit Ekran kilidi Ekran kapalıyken veritabanını kilitle - Hızlı kilit açmak için parmak izi taraması nasıl ayarlanır\? - Cihazınız için taranmış parmak izinizi kaydet - \"Ayarlar\" → \"Güvenlik\" → \"Parmak İzi\" - Veritabanı kilitleme parolasını girin - Veritabanı kilit parolanızı güvenli bir şekilde saklamak için parmak izinizi tarayın. - Parola kapatıldığında veritabanını açmak için parmak izinizi tarayın. - Kullanım Parmakizi Parmak izi tarama Veritabanını açmak için parmak izinizi taramanızı sağlar @@ -247,12 +237,9 @@ Silmeden önce grupları ve girdileri \"Geri Dönüşüm Kutusu\"na taşır Yazı tipi alanı Daha iyi karakter görünürlüğü için alanlarda kullanılan yazı tipini değiştirin - Seçerek dosyaları aç - Dosya tarayıcısında seçildiğinde dosyaları otomatik aç Pano güveni Giriş parolası ve korunan alanların panoya aktarılmasına izin ver UYARI: Pano tüm uygulamalar tarafından paylaşılmaktadır. Hassas veriler kopyalanırsa, diğer yazılımlar onu alabilir. - Açılacak veritabanı dosyasının bağlantısı Veritabanı adı Veritabanı açıklaması Veritabanı sürümü @@ -261,17 +248,7 @@ Diğer Klavye Magikeyboard - Parolalarınızı ve tüm kimlik alanlarınızı içeren özel bir klavye etkinleştirin - Magikeyboard ayarları - Formları güvenli bir şekilde otomatik doldurmak için klavyeyi ayarlayın. - Cihaz ayarlarında \"Magikeyboard\"u etkinleştirin. - \"Ayarlar\" → \"Dil & giriş\" → \"Geçerli Klavye\" ve birini seçin. - Bir formu doldurmanız gerektiğinde Magikeyboard\'u seçin. - Klavyenizin boşluk çubuğuna uzun basarak veya yoksa şununla klavyeleri değiştirin: - Anahtarlı girişinizi seçin. - Giriş öğelerini kullanarak alanlarınızı doldurun. - Veritabanını kilitle. - Varsayılan klavyeyi tekrar kullan. + Parolalarınızı ve tüm kimlik alanlarınızı içeren özel bir klavye etkinleştirin Magikeyboard Magikeyboard (KeePass DX) Magikeyboard ayarları @@ -303,16 +280,14 @@ İlk parola yönetim dosyanızı oluşturun. Mevcut bir veritabanını aç Kullanmaya devam etmek için önceki veritabanı dosyanızı dosya tarayıcınızdan açın. - Dosyanızın konumuna bir bağlantı yeterlidir - Veritabanınızı fiziksel bir bağlantıyla da açabilirsiniz (örneğin file:// ve content:// ile). Veritabanınıza öğe ekleyin Girdiler dijital kimliğinizi yönetmenize yardımcı olur. \n \nGruplar (~ klasörler) veritabanınızdaki girdileri düzenler. Girişlerde ara Parolanızı kurtarmak için başlık, kullanıcı adı veya diğer alanların içeriğini girin. - Parmak iziyle veritabanı kilidini açma - Veritabanınızı hızlıca açmak için parolanızı taranan parmak izinize bağlayın. + Parmak iziyle veritabanı kilidini açma + Veritabanınızı hızlıca açmak için parolanızı taranan parmak izinize bağlayın. Girdiyi düzenle Girdinizi özel alanlarla düzenleyin. Havuz verileri farklı giriş alanları arasında referans alınabilir. Girdiniz için güçlü bir parola oluşturun. @@ -344,7 +319,7 @@ Özgürlüğümüzü korumak ve daima aktif olmak için katkılarınıza güveniyoruz Bu özellik geliştirme aşamasındadır ve katkılarınızın yakında kullanıma sunulmasını gerektirir. - pro sürümünü satın alarak, + pro sürümü satın alarak, katkıda bulunarak, Geliştiricilerin yeni özellikler oluşturmasını ve söz konusu hatalara göre hataları düzeltmesini teşvik ediyorsunuz. @@ -368,14 +343,12 @@ Kapanışta temizle Bildirimi kapatırken veritabanını kapatın Dosya aç - Dosya bağlantısını göster - Dosya bağlantısını aç Düğüm ekle Girdi Ekle Grup ekle Dosya bilgileri Parola onay kutusu - content_description_keyfile_checkbox + Anahtar dosyası onay kutusu Giriş simgesi Parola üreteci Parola uzunluğu @@ -393,6 +366,20 @@ Parolayı sil Bir bağlantı denemesinden sonra girilen parolayı siler Alt düğüm - Geçiş şifre görünürlüğünü tekrarlayın + Geçiş şifresi görünürlüğünü tekrarlayın Giriş kaydet + Arka plan + Güncelleme + Alanları kapat + Bu parola ve anahtar dosyası ile veritabanı oluşturulamıyor. + Gelişmiş kilit açma + Biyometrik tanımayı kaydedin + Biyolojik verilerle veritabanı kimlik bilgilerini saklayın + Biyometrik tanıma ile veritabanı aç + Biyometrik verilerle veritabanı kimlik bilgilerini çıkar + Biyometrik + Biyometrik istemi otomatik aç + Bir veritabanı için bir biyometrik anahtar tanımlandığında biyometrik komut istemini otomatik olarak aç + Etkinleştir + Devre dışı \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ae2983f07..52a7816c0 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -31,7 +31,6 @@ Налаштування програми Дужки Для перегляду файла необхідно Open Intents File Manager, натисніть нижче для його інсталяції. У зв’язку з деякими недоробками у менеджері файлів перегляд може працювати некоректно при запуску перший раз. - Відміна Буфер обміну очищено. Тайм-аут буфера обміну Час через який буде очищено буфер обміну після копіювання ім’я користувача чи пароля @@ -66,11 +65,9 @@ Телефону не вистачило пам’яті при аналізі вашої бази даних. Можливо база надто велика для вашог телефона. Принаймні один тип генерації пароля необхідно вибрати. Паролі не співпадають. - Кількість циклів має бути числом. Надто багато циклів. Установлено 2147483648. Необхідно вказати заголовок. Введіть ціле число на усю довжину поля - Файл не знайдено. Перегляд файлів Згенерувати пароль підтвердження пароля @@ -82,7 +79,7 @@ пароль Інсталювати із Google Play Інсталювати із F-Droid - Невірний пароль або файл ключа. + Невірний пароль або файл ключа. Формат бази даних не розпізнано. Довжина Розмір списку груп diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml index 03ffed2c6..99ee8c6df 100644 --- a/app/src/main/res/values-v21/styles.xml +++ b/app/src/main/res/values-v21/styles.xml @@ -15,6 +15,7 @@ center 4dp + \ No newline at end of file diff --git a/app/src/main/res/values/style_red.xml b/app/src/main/res/values/style_red.xml index 5bae67bb6..2314fc58f 100644 --- a/app/src/main/res/values/style_red.xml +++ b/app/src/main/res/values/style_red.xml @@ -43,6 +43,6 @@ \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9827953ee..0d35b2f19 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -180,7 +180,7 @@ @@ -287,6 +287,11 @@ @color/background_button_color_accent + + + +